@ybotok/loam 0.2.0-alpha.7 → 0.2.0-alpha.9

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 (648) hide show
  1. package/CHANGELOG.md +1315 -1
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +40 -22
  5. package/QUICKSTART.md +181 -9
  6. package/README.md +256 -57
  7. package/ROADMAP.md +83 -7
  8. package/SCHEMA.md +569 -98
  9. package/WORKFLOW.md +272 -62
  10. package/dist/cli.js +26 -8
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt/adopt.js +5 -10
  13. package/dist/commands/adopt/adopt.js.map +1 -1
  14. package/dist/commands/archive/archive.js +15 -18
  15. package/dist/commands/archive/archive.js.map +1 -1
  16. package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
  17. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  18. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  19. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/openapi.js +25 -42
  21. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
  23. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  24. package/dist/commands/archive/plan/gate.js +3 -7
  25. package/dist/commands/archive/plan/gate.js.map +1 -1
  26. package/dist/commands/archive/plan/landscape.js +4 -4
  27. package/dist/commands/archive/plan/landscape.js.map +1 -1
  28. package/dist/commands/archive/plan/refusal.js +8 -18
  29. package/dist/commands/archive/plan/refusal.js.map +1 -1
  30. package/dist/commands/archive/plan/specs.js +134 -25
  31. package/dist/commands/archive/plan/specs.js.map +1 -1
  32. package/dist/commands/archive/plan/state.js +1 -0
  33. package/dist/commands/archive/plan/state.js.map +1 -1
  34. package/dist/commands/archive/run.js +33 -11
  35. package/dist/commands/archive/run.js.map +1 -1
  36. package/dist/commands/archive/shipped.js +49 -0
  37. package/dist/commands/archive/shipped.js.map +1 -0
  38. package/dist/commands/context/context.js +4 -7
  39. package/dist/commands/context/context.js.map +1 -1
  40. package/dist/commands/context/print.js +2 -2
  41. package/dist/commands/context/print.js.map +1 -1
  42. package/dist/commands/delta/delta.js +44 -12
  43. package/dist/commands/delta/delta.js.map +1 -1
  44. package/dist/commands/delta/print.js +1 -1
  45. package/dist/commands/delta/print.js.map +1 -1
  46. package/dist/commands/dependencies.js +3 -7
  47. package/dist/commands/dependencies.js.map +1 -1
  48. package/dist/commands/diff/diff.js +20 -27
  49. package/dist/commands/diff/diff.js.map +1 -1
  50. package/dist/commands/explore.js +4 -8
  51. package/dist/commands/explore.js.map +1 -1
  52. package/dist/commands/gate/gate.js +45 -8
  53. package/dist/commands/gate/gate.js.map +1 -1
  54. package/dist/commands/gate/print.js +15 -3
  55. package/dist/commands/gate/print.js.map +1 -1
  56. package/dist/commands/gherkin/commit.js +13 -54
  57. package/dist/commands/gherkin/commit.js.map +1 -1
  58. package/dist/commands/gherkin/gherkin.js +36 -20
  59. package/dist/commands/gherkin/gherkin.js.map +1 -1
  60. package/dist/commands/gherkin/reconcile.js +10 -13
  61. package/dist/commands/gherkin/reconcile.js.map +1 -1
  62. package/dist/commands/init/first-hour.js +44 -0
  63. package/dist/commands/init/first-hour.js.map +1 -1
  64. package/dist/commands/init/init.js +18 -22
  65. package/dist/commands/init/init.js.map +1 -1
  66. package/dist/commands/list/campaign/campaign.js +4 -6
  67. package/dist/commands/list/campaign/campaign.js.map +1 -1
  68. package/dist/commands/list/json.js +0 -4
  69. package/dist/commands/list/json.js.map +1 -1
  70. package/dist/commands/list/list.js +38 -23
  71. package/dist/commands/list/list.js.map +1 -1
  72. package/dist/commands/list/print.js +38 -6
  73. package/dist/commands/list/print.js.map +1 -1
  74. package/dist/commands/list/suites/suites.js +140 -0
  75. package/dist/commands/list/suites/suites.js.map +1 -0
  76. package/dist/commands/list/views.js +28 -33
  77. package/dist/commands/list/views.js.map +1 -1
  78. package/dist/commands/mcp/dispatch.js +91 -36
  79. package/dist/commands/mcp/dispatch.js.map +1 -1
  80. package/dist/commands/mcp/mcp.js +56 -15
  81. package/dist/commands/mcp/mcp.js.map +1 -1
  82. package/dist/commands/mcp/serve.js.map +1 -1
  83. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  84. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  85. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  86. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  87. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  88. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  89. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  90. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  91. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  92. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  93. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  94. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  95. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  96. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  97. package/dist/commands/new/capability-template.js +118 -0
  98. package/dist/commands/new/capability-template.js.map +1 -0
  99. package/dist/commands/new/commit.js +8 -22
  100. package/dist/commands/new/commit.js.map +1 -1
  101. package/dist/commands/new/modifies.js +206 -0
  102. package/dist/commands/new/modifies.js.map +1 -0
  103. package/dist/commands/new/new.js +59 -14
  104. package/dist/commands/new/new.js.map +1 -1
  105. package/dist/commands/new/templates.js +48 -124
  106. package/dist/commands/new/templates.js.map +1 -1
  107. package/dist/commands/open.js +3 -7
  108. package/dist/commands/open.js.map +1 -1
  109. package/dist/commands/policy/gate.js +27 -0
  110. package/dist/commands/policy/gate.js.map +1 -1
  111. package/dist/commands/rebase/plan.js +2 -2
  112. package/dist/commands/rebase/plan.js.map +1 -1
  113. package/dist/commands/rebase/rebase.js +60 -35
  114. package/dist/commands/rebase/rebase.js.map +1 -1
  115. package/dist/commands/seed/commit.js +21 -25
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/seed.js +6 -9
  118. package/dist/commands/seed/seed.js.map +1 -1
  119. package/dist/commands/show/show.js +3 -7
  120. package/dist/commands/show/show.js.map +1 -1
  121. package/dist/commands/status/print.js +13 -4
  122. package/dist/commands/status/print.js.map +1 -1
  123. package/dist/commands/status/status.js +3 -7
  124. package/dist/commands/status/status.js.map +1 -1
  125. package/dist/commands/steps/steps.js +3 -7
  126. package/dist/commands/steps/steps.js.map +1 -1
  127. package/dist/commands/subsystem/edit.js +3 -5
  128. package/dist/commands/subsystem/edit.js.map +1 -1
  129. package/dist/commands/subsystem/move.js +5 -8
  130. package/dist/commands/subsystem/move.js.map +1 -1
  131. package/dist/commands/subsystem/query.js +2 -4
  132. package/dist/commands/subsystem/query.js.map +1 -1
  133. package/dist/commands/subsystem/subsystem.js +4 -7
  134. package/dist/commands/subsystem/subsystem.js.map +1 -1
  135. package/dist/commands/subsystem/sync.js +14 -41
  136. package/dist/commands/subsystem/sync.js.map +1 -1
  137. package/dist/commands/subsystem/txn/txn.js +17 -38
  138. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  139. package/dist/commands/subsystem/txn/views.js +32 -5
  140. package/dist/commands/subsystem/txn/views.js.map +1 -1
  141. package/dist/commands/tasks/render.js +116 -0
  142. package/dist/commands/tasks/render.js.map +1 -0
  143. package/dist/commands/tasks/tasks.js +113 -0
  144. package/dist/commands/tasks/tasks.js.map +1 -0
  145. package/dist/commands/unarchive/manifest.js +6 -9
  146. package/dist/commands/unarchive/manifest.js.map +1 -1
  147. package/dist/commands/unarchive/restore.js +17 -28
  148. package/dist/commands/unarchive/restore.js.map +1 -1
  149. package/dist/commands/unarchive/unarchive.js +22 -30
  150. package/dist/commands/unarchive/unarchive.js.map +1 -1
  151. package/dist/commands/validate/arch-coverage.js +6 -15
  152. package/dist/commands/validate/arch-coverage.js.map +1 -1
  153. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  154. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  155. package/dist/commands/validate/checks/requirements.js +18 -8
  156. package/dist/commands/validate/checks/requirements.js.map +1 -1
  157. package/dist/commands/validate/checks/sources.js +6 -2
  158. package/dist/commands/validate/checks/sources.js.map +1 -1
  159. package/dist/commands/validate/feature.js +23 -19
  160. package/dist/commands/validate/feature.js.map +1 -1
  161. package/dist/commands/validate/fleet/landscape.js +23 -23
  162. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  163. package/dist/commands/validate/fleet/map/attest.js +6 -10
  164. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  165. package/dist/commands/validate/fleet/map/bindings.js +1 -3
  166. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  167. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  168. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  169. package/dist/commands/validate/fleet/obligations.js +1 -5
  170. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  171. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  172. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  173. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  174. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  175. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  176. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  177. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  178. package/dist/commands/validate/fleet/views/root-documents.js +8 -28
  179. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  180. package/dist/commands/validate/links/corpus.js +1 -4
  181. package/dist/commands/validate/links/corpus.js.map +1 -1
  182. package/dist/commands/validate/links/glossary.js.map +1 -1
  183. package/dist/commands/validate/report.js +5 -4
  184. package/dist/commands/validate/report.js.map +1 -1
  185. package/dist/commands/validate/scope.js +113 -0
  186. package/dist/commands/validate/scope.js.map +1 -0
  187. package/dist/commands/validate/service/api.js +16 -104
  188. package/dist/commands/validate/service/api.js.map +1 -1
  189. package/dist/commands/validate/service/contracts/generated.js +134 -0
  190. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  191. package/dist/commands/validate/service/events/events.js +1 -1
  192. package/dist/commands/validate/service/events/events.js.map +1 -1
  193. package/dist/commands/validate/service/events/message-producers.js +1 -1
  194. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  195. package/dist/commands/validate/service/evidence-pins.js +56 -10
  196. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  197. package/dist/commands/validate/service/service.js +16 -26
  198. package/dist/commands/validate/service/service.js.map +1 -1
  199. package/dist/commands/validate/service/specs.js +31 -38
  200. package/dist/commands/validate/service/specs.js.map +1 -1
  201. package/dist/commands/validate/service/spine.js +8 -11
  202. package/dist/commands/validate/service/spine.js.map +1 -1
  203. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  204. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  205. package/dist/commands/validate/validate.js +44 -26
  206. package/dist/commands/validate/validate.js.map +1 -1
  207. package/dist/commands/verify/cross/diff.js +2 -6
  208. package/dist/commands/verify/cross/diff.js.map +1 -1
  209. package/dist/commands/verify/evidence/contract.js +1 -1
  210. package/dist/commands/verify/evidence/contract.js.map +1 -1
  211. package/dist/commands/verify/evidence/read.js +12 -3
  212. package/dist/commands/verify/evidence/read.js.map +1 -1
  213. package/dist/commands/verify/frozen.js +45 -23
  214. package/dist/commands/verify/frozen.js.map +1 -1
  215. package/dist/commands/verify/record.js +69 -17
  216. package/dist/commands/verify/record.js.map +1 -1
  217. package/dist/commands/verify/report.js +27 -44
  218. package/dist/commands/verify/report.js.map +1 -1
  219. package/dist/commands/verify/results.js +112 -30
  220. package/dist/commands/verify/results.js.map +1 -1
  221. package/dist/commands/verify/verify.js +54 -51
  222. package/dist/commands/verify/verify.js.map +1 -1
  223. package/dist/commands/vouch/pack/pack.js +2 -2
  224. package/dist/commands/vouch/pack/pack.js.map +1 -1
  225. package/dist/commands/vouch/pack/print.js +1 -6
  226. package/dist/commands/vouch/pack/print.js.map +1 -1
  227. package/dist/commands/vouch/pack/sections.js +1 -2
  228. package/dist/commands/vouch/pack/sections.js.map +1 -1
  229. package/dist/commands/vouch/run.js +26 -81
  230. package/dist/commands/vouch/run.js.map +1 -1
  231. package/dist/commands/vouch/sample/plan.js +20 -34
  232. package/dist/commands/vouch/sample/plan.js.map +1 -1
  233. package/dist/commands/vouch/vet/verify.js +39 -7
  234. package/dist/commands/vouch/vet/verify.js.map +1 -1
  235. package/dist/commands/vouch/vouch.js +10 -17
  236. package/dist/commands/vouch/vouch.js.map +1 -1
  237. package/dist/core/agent/agents-md/command-map.js +56 -101
  238. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  239. package/dist/core/agent/agents-md/map/explain.js +4 -3
  240. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  241. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  242. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  243. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  244. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  245. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  246. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  247. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  248. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  249. package/dist/core/agent/agents-md/map/status.js +9 -3
  250. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  251. package/dist/core/agent/agents-md/map/subsystem.js +4 -3
  252. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  253. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  254. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  255. package/dist/core/agent/agents-md/refusals.js +32 -18
  256. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  257. package/dist/core/agent/agents-md.js +9 -3
  258. package/dist/core/agent/agents-md.js.map +1 -1
  259. package/dist/core/agent/agents-stamp.js +10 -0
  260. package/dist/core/agent/agents-stamp.js.map +1 -1
  261. package/dist/core/agent/scaffold.js +10 -15
  262. package/dist/core/agent/scaffold.js.map +1 -1
  263. package/dist/core/agent/tools/dialects.js +1 -0
  264. package/dist/core/agent/tools/dialects.js.map +1 -1
  265. package/dist/core/agent/tools/registry.js +41 -76
  266. package/dist/core/agent/tools/registry.js.map +1 -1
  267. package/dist/core/agent/workflows/adopt.js +5 -3
  268. package/dist/core/agent/workflows/adopt.js.map +1 -1
  269. package/dist/core/agent/workflows/check.js +20 -16
  270. package/dist/core/agent/workflows/check.js.map +1 -1
  271. package/dist/core/agent/workflows/closing.js +54 -21
  272. package/dist/core/agent/workflows/closing.js.map +1 -1
  273. package/dist/core/agent/workflows/feature.js +79 -11
  274. package/dist/core/agent/workflows/feature.js.map +1 -1
  275. package/dist/core/agent/workflows/implement.js +91 -13
  276. package/dist/core/agent/workflows/implement.js.map +1 -1
  277. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  278. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  279. package/dist/core/agent/workflows/reference/codes.js +8 -6
  280. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  281. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  282. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  283. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  284. package/dist/core/agent/workflows/reference/spine.js +50 -18
  285. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  286. package/dist/core/asyncapi/baseline/plan.js +14 -81
  287. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  288. package/dist/core/asyncapi/digest.js +30 -25
  289. package/dist/core/asyncapi/digest.js.map +1 -1
  290. package/dist/core/asyncapi/merge/components.js +5 -4
  291. package/dist/core/asyncapi/merge/components.js.map +1 -1
  292. package/dist/core/asyncapi/merge/error.js +7 -2
  293. package/dist/core/asyncapi/merge/error.js.map +1 -1
  294. package/dist/core/asyncapi/merge/markers.js +30 -50
  295. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  296. package/dist/core/asyncapi/merge/merge.js +31 -53
  297. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  298. package/dist/core/asyncapi/merge/pin.js +10 -36
  299. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  300. package/dist/core/asyncapi/model.js.map +1 -1
  301. package/dist/core/asyncapi/read.js +36 -12
  302. package/dist/core/asyncapi/read.js.map +1 -1
  303. package/dist/core/brief/brief.js +6 -18
  304. package/dist/core/brief/brief.js.map +1 -1
  305. package/dist/core/brief/landscape.js +8 -12
  306. package/dist/core/brief/landscape.js.map +1 -1
  307. package/dist/core/brief/map/extend.js +8 -5
  308. package/dist/core/brief/map/extend.js.map +1 -1
  309. package/dist/core/brief/map/owed.js +8 -26
  310. package/dist/core/brief/map/owed.js.map +1 -1
  311. package/dist/core/brief/targets.js +15 -5
  312. package/dist/core/brief/targets.js.map +1 -1
  313. package/dist/core/c4/likec4.js +147 -16
  314. package/dist/core/c4/likec4.js.map +1 -1
  315. package/dist/core/c4/locate/unlocated.js +150 -0
  316. package/dist/core/c4/locate/unlocated.js.map +1 -0
  317. package/dist/core/c4/parsed/values.js +54 -1
  318. package/dist/core/c4/parsed/values.js.map +1 -1
  319. package/dist/core/c4/prefetch/fleet.js +1 -9
  320. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  321. package/dist/core/c4/project/documents.js +31 -6
  322. package/dist/core/c4/project/documents.js.map +1 -1
  323. package/dist/core/c4/project/load.js +6 -17
  324. package/dist/core/c4/project/load.js.map +1 -1
  325. package/dist/core/c4/resolve/external.js +51 -0
  326. package/dist/core/c4/resolve/external.js.map +1 -0
  327. package/dist/core/c4/resolve/service.js +25 -1
  328. package/dist/core/c4/resolve/service.js.map +1 -1
  329. package/dist/core/c4/seed/template.js +15 -1
  330. package/dist/core/c4/seed/template.js.map +1 -1
  331. package/dist/core/c4/service-model/shape.js +2 -22
  332. package/dist/core/c4/service-model/shape.js.map +1 -1
  333. package/dist/core/c4/source-mask.js +288 -4
  334. package/dist/core/c4/source-mask.js.map +1 -1
  335. package/dist/core/c4/source-scan.js +203 -147
  336. package/dist/core/c4/source-scan.js.map +1 -1
  337. package/dist/core/c4/splice/authored-source.js +19 -0
  338. package/dist/core/c4/splice/authored-source.js.map +1 -1
  339. package/dist/core/c4/splice/delta-blocks.js +5 -18
  340. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  341. package/dist/core/c4/splice/landscape-merge.js +13 -8
  342. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  343. package/dist/core/c4/splice/model/merge.js +10 -6
  344. package/dist/core/c4/splice/model/merge.js.map +1 -1
  345. package/dist/core/c4/splice/placement.js +108 -18
  346. package/dist/core/c4/splice/placement.js.map +1 -1
  347. package/dist/core/c4/workspace.js +13 -22
  348. package/dist/core/c4/workspace.js.map +1 -1
  349. package/dist/core/capabilities/capabilities.js +6 -6
  350. package/dist/core/capabilities/capabilities.js.map +1 -1
  351. package/dist/core/capabilities/delta/doc.js +7 -10
  352. package/dist/core/capabilities/delta/doc.js.map +1 -1
  353. package/dist/core/capabilities/findings.js +15 -20
  354. package/dist/core/capabilities/findings.js.map +1 -1
  355. package/dist/core/capabilities/realizes/findings.js +12 -19
  356. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  357. package/dist/core/capabilities/realizes/join.js +0 -20
  358. package/dist/core/capabilities/realizes/join.js.map +1 -1
  359. package/dist/core/capabilities/rollup.js +6 -0
  360. package/dist/core/capabilities/rollup.js.map +1 -1
  361. package/dist/core/capabilities/tree.js +6 -28
  362. package/dist/core/capabilities/tree.js.map +1 -1
  363. package/dist/core/capabilities/usecase-join.js +3 -13
  364. package/dist/core/capabilities/usecase-join.js.map +1 -1
  365. package/dist/core/coherence/authoring/scaffold.js +11 -3
  366. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  367. package/dist/core/coherence/authoring/sentinels.js +2 -1
  368. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  369. package/dist/core/coherence/coherence.js +30 -16
  370. package/dist/core/coherence/coherence.js.map +1 -1
  371. package/dist/core/coherence/consumed/operations.js +70 -0
  372. package/dist/core/coherence/consumed/operations.js.map +1 -0
  373. package/dist/core/coherence/consumed/refusal.js +38 -0
  374. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  375. package/dist/core/coherence/declared.js +60 -34
  376. package/dist/core/coherence/declared.js.map +1 -1
  377. package/dist/core/coherence/events/declared.js +44 -59
  378. package/dist/core/coherence/events/declared.js.map +1 -1
  379. package/dist/core/coherence/events/events.js +119 -8
  380. package/dist/core/coherence/events/events.js.map +1 -1
  381. package/dist/core/coherence/events/grades.js +31 -8
  382. package/dist/core/coherence/events/grades.js.map +1 -1
  383. package/dist/core/coherence/events/lookups.js +14 -82
  384. package/dist/core/coherence/events/lookups.js.map +1 -1
  385. package/dist/core/coherence/lookups.js +18 -78
  386. package/dist/core/coherence/lookups.js.map +1 -1
  387. package/dist/core/coherence/retired/archived.js +285 -0
  388. package/dist/core/coherence/retired/archived.js.map +1 -0
  389. package/dist/core/coherence/retired/event-pair.js +62 -0
  390. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  391. package/dist/core/coherence/retired/merged.js +79 -0
  392. package/dist/core/coherence/retired/merged.js.map +1 -0
  393. package/dist/core/coherence/retired/retirement.js +49 -0
  394. package/dist/core/coherence/retired/retirement.js.map +1 -0
  395. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  396. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  397. package/dist/core/coherence/retired/revival/revived.js +297 -0
  398. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  399. package/dist/core/consumers/after.js +113 -0
  400. package/dist/core/consumers/after.js.map +1 -0
  401. package/dist/core/consumers/consumers.js +296 -0
  402. package/dist/core/consumers/consumers.js.map +1 -0
  403. package/dist/core/consumers/current.js +211 -0
  404. package/dist/core/consumers/current.js.map +1 -0
  405. package/dist/core/consumers/scan.js +46 -0
  406. package/dist/core/consumers/scan.js.map +1 -0
  407. package/dist/core/consumers/state.js +27 -0
  408. package/dist/core/consumers/state.js.map +1 -0
  409. package/dist/core/delta/delta.js +1 -1
  410. package/dist/core/delta/delta.js.map +1 -1
  411. package/dist/core/deployment/delta.js +3 -6
  412. package/dist/core/deployment/delta.js.map +1 -1
  413. package/dist/core/diff/base-git.js +94 -12
  414. package/dist/core/diff/base-git.js.map +1 -1
  415. package/dist/core/diff/base-state.js +28 -16
  416. package/dist/core/diff/base-state.js.map +1 -1
  417. package/dist/core/diff/joins.js +136 -0
  418. package/dist/core/diff/joins.js.map +1 -0
  419. package/dist/core/diff/scope/changed-paths.js +22 -6
  420. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  421. package/dist/core/diff/scope/edges.js +82 -0
  422. package/dist/core/diff/scope/edges.js.map +1 -0
  423. package/dist/core/diff/semantic.js +100 -31
  424. package/dist/core/diff/semantic.js.map +1 -1
  425. package/dist/core/doctor/agents.js +34 -18
  426. package/dist/core/doctor/agents.js.map +1 -1
  427. package/dist/core/doctor/report.js.map +1 -1
  428. package/dist/core/document/apply.js +10 -2
  429. package/dist/core/document/apply.js.map +1 -1
  430. package/dist/core/document/frontmatter.js +55 -0
  431. package/dist/core/document/frontmatter.js.map +1 -1
  432. package/dist/core/document/parse.js +29 -6
  433. package/dist/core/document/parse.js.map +1 -1
  434. package/dist/core/document/scenarios.js +17 -0
  435. package/dist/core/document/scenarios.js.map +1 -1
  436. package/dist/core/document/spec.js +4 -11
  437. package/dist/core/document/spec.js.map +1 -1
  438. package/dist/core/envelope/config.js +10 -0
  439. package/dist/core/envelope/config.js.map +1 -1
  440. package/dist/core/envelope/json.js +34 -0
  441. package/dist/core/envelope/json.js.map +1 -1
  442. package/dist/core/explain/families.js +17 -12
  443. package/dist/core/explain/families.js.map +1 -1
  444. package/dist/core/explain/refusals.js +3 -3
  445. package/dist/core/explain/refusals.js.map +1 -1
  446. package/dist/core/explain/terms.js +18 -9
  447. package/dist/core/explain/terms.js.map +1 -1
  448. package/dist/core/explore/explore.js +2 -7
  449. package/dist/core/explore/explore.js.map +1 -1
  450. package/dist/core/fleet-context.js +34 -95
  451. package/dist/core/fleet-context.js.map +1 -1
  452. package/dist/core/gate/checks.js +5 -2
  453. package/dist/core/gate/checks.js.map +1 -1
  454. package/dist/core/gate/partners.js +4 -2
  455. package/dist/core/gate/partners.js.map +1 -1
  456. package/dist/core/gate/report.js.map +1 -1
  457. package/dist/core/gate/verification.js +75 -12
  458. package/dist/core/gate/verification.js.map +1 -1
  459. package/dist/core/gherkin/stale.js +13 -37
  460. package/dist/core/gherkin/stale.js.map +1 -1
  461. package/dist/core/glossary/tree.js +6 -19
  462. package/dist/core/glossary/tree.js.map +1 -1
  463. package/dist/core/kernel/concurrency.js +19 -0
  464. package/dist/core/kernel/concurrency.js.map +1 -1
  465. package/dist/core/kernel/records.js +11 -0
  466. package/dist/core/kernel/records.js.map +1 -1
  467. package/dist/core/links/backlinks.js +1 -3
  468. package/dist/core/links/backlinks.js.map +1 -1
  469. package/dist/core/links/corpus.js +5 -36
  470. package/dist/core/links/corpus.js.map +1 -1
  471. package/dist/core/mcp/argv.js +101 -0
  472. package/dist/core/mcp/argv.js.map +1 -1
  473. package/dist/core/mcp/author-tools.js +2 -1
  474. package/dist/core/mcp/author-tools.js.map +1 -1
  475. package/dist/core/mcp/protocol.js +2 -2
  476. package/dist/core/mcp/protocol.js.map +1 -1
  477. package/dist/core/mcp/tools.js +43 -33
  478. package/dist/core/mcp/tools.js.map +1 -1
  479. package/dist/core/openapi/baseline/gate.js +53 -12
  480. package/dist/core/openapi/baseline/gate.js.map +1 -1
  481. package/dist/core/openapi/baseline/plan.js +22 -8
  482. package/dist/core/openapi/baseline/plan.js.map +1 -1
  483. package/dist/core/openapi/baseline/record.js +0 -1
  484. package/dist/core/openapi/baseline/record.js.map +1 -1
  485. package/dist/core/openapi/contract-text.js +33 -0
  486. package/dist/core/openapi/contract-text.js.map +1 -0
  487. package/dist/core/openapi/contract-tree.js +56 -0
  488. package/dist/core/openapi/contract-tree.js.map +1 -0
  489. package/dist/core/openapi/depth.js +38 -24
  490. package/dist/core/openapi/depth.js.map +1 -1
  491. package/dist/core/openapi/digest.js +52 -2
  492. package/dist/core/openapi/digest.js.map +1 -1
  493. package/dist/core/openapi/doc.js +12 -4
  494. package/dist/core/openapi/doc.js.map +1 -1
  495. package/dist/core/openapi/merge/components.js +25 -60
  496. package/dist/core/openapi/merge/components.js.map +1 -1
  497. package/dist/core/openapi/merge/error.js +3 -13
  498. package/dist/core/openapi/merge/error.js.map +1 -1
  499. package/dist/core/openapi/merge/markers.js +26 -30
  500. package/dist/core/openapi/merge/markers.js.map +1 -1
  501. package/dist/core/openapi/merge/merge.js +26 -52
  502. package/dist/core/openapi/merge/merge.js.map +1 -1
  503. package/dist/core/openapi/merge/pin.js +15 -35
  504. package/dist/core/openapi/merge/pin.js.map +1 -1
  505. package/dist/core/openspec/decide/verdict.js.map +1 -1
  506. package/dist/core/openspec/inventory.js +4 -5
  507. package/dist/core/openspec/inventory.js.map +1 -1
  508. package/dist/core/openspec/model/mapping.js +30 -28
  509. package/dist/core/openspec/model/mapping.js.map +1 -1
  510. package/dist/core/openspec/model/model.js +12 -7
  511. package/dist/core/openspec/model/model.js.map +1 -1
  512. package/dist/core/openspec/scan/changes.js +10 -4
  513. package/dist/core/openspec/scan/changes.js.map +1 -1
  514. package/dist/core/openspec/scan/config.js +7 -16
  515. package/dist/core/openspec/scan/config.js.map +1 -1
  516. package/dist/core/openspec/scan/shape.js +63 -19
  517. package/dist/core/openspec/scan/shape.js.map +1 -1
  518. package/dist/core/openspec/scan/walk.js +21 -38
  519. package/dist/core/openspec/scan/walk.js.map +1 -1
  520. package/dist/core/pack/living.js +2 -16
  521. package/dist/core/pack/living.js.map +1 -1
  522. package/dist/core/provenance/findings.js +48 -12
  523. package/dist/core/provenance/findings.js.map +1 -1
  524. package/dist/core/provenance/git.js +33 -57
  525. package/dist/core/provenance/git.js.map +1 -1
  526. package/dist/core/provenance/gitq/moves.js +71 -114
  527. package/dist/core/provenance/gitq/moves.js.map +1 -1
  528. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  529. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  530. package/dist/core/provenance/sources.js +23 -6
  531. package/dist/core/provenance/sources.js.map +1 -1
  532. package/dist/core/provenance/stamp.js +5 -17
  533. package/dist/core/provenance/stamp.js.map +1 -1
  534. package/dist/core/repo/entries.js.map +1 -1
  535. package/dist/core/repo/repo.js +24 -14
  536. package/dist/core/repo/repo.js.map +1 -1
  537. package/dist/core/repo/tree/find.js +12 -3
  538. package/dist/core/repo/tree/find.js.map +1 -1
  539. package/dist/core/repo/tree/fs.js +27 -1
  540. package/dist/core/repo/tree/fs.js.map +1 -1
  541. package/dist/core/repo/tree/walk.js +8 -30
  542. package/dist/core/repo/tree/walk.js.map +1 -1
  543. package/dist/core/results.js +47 -12
  544. package/dist/core/results.js.map +1 -1
  545. package/dist/core/staging/commit.js +10 -12
  546. package/dist/core/staging/commit.js.map +1 -1
  547. package/dist/core/staging/lock.js +74 -12
  548. package/dist/core/staging/lock.js.map +1 -1
  549. package/dist/core/staging/recovery/intent.js +2 -5
  550. package/dist/core/staging/recovery/intent.js.map +1 -1
  551. package/dist/core/staging/recovery/recover.js +60 -1
  552. package/dist/core/staging/recovery/recover.js.map +1 -1
  553. package/dist/core/staging/snapshot/manifest.js +152 -0
  554. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  555. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  556. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  557. package/dist/core/status/actions/execution.js +11 -0
  558. package/dist/core/status/actions/execution.js.map +1 -1
  559. package/dist/core/status/feature/artifacts.js +2 -2
  560. package/dist/core/status/feature/artifacts.js.map +1 -1
  561. package/dist/core/status/feature/feature.js +5 -11
  562. package/dist/core/status/feature/feature.js.map +1 -1
  563. package/dist/core/status/fleet/fleet.js +6 -2
  564. package/dist/core/status/fleet/fleet.js.map +1 -1
  565. package/dist/core/status/fleet/next.js +26 -3
  566. package/dist/core/status/fleet/next.js.map +1 -1
  567. package/dist/core/status/report.js.map +1 -1
  568. package/dist/core/status/verification.js +30 -15
  569. package/dist/core/status/verification.js.map +1 -1
  570. package/dist/core/tasks/packet.js +174 -0
  571. package/dist/core/tasks/packet.js.map +1 -0
  572. package/dist/core/tasks/record.js +43 -0
  573. package/dist/core/tasks/record.js.map +1 -0
  574. package/dist/core/tasks/sources.js +181 -0
  575. package/dist/core/tasks/sources.js.map +1 -0
  576. package/dist/core/tasks/stamps.js +76 -0
  577. package/dist/core/tasks/stamps.js.map +1 -0
  578. package/dist/core/tasks/unit.js +0 -0
  579. package/dist/core/tasks/unit.js.map +1 -0
  580. package/dist/core/usecases/capability.js +11 -14
  581. package/dist/core/usecases/capability.js.map +1 -1
  582. package/dist/core/usecases/delta/flows.js +5 -7
  583. package/dist/core/usecases/delta/flows.js.map +1 -1
  584. package/dist/core/usecases/fleet.js +178 -31
  585. package/dist/core/usecases/fleet.js.map +1 -1
  586. package/dist/core/usecases/operations.js +5 -4
  587. package/dist/core/usecases/operations.js.map +1 -1
  588. package/dist/core/usecases/service/flows.js +9 -6
  589. package/dist/core/usecases/service/flows.js.map +1 -1
  590. package/dist/core/usecases/suites/participants.js +83 -0
  591. package/dist/core/usecases/suites/participants.js.map +1 -0
  592. package/dist/core/usecases/suites/suites.js +45 -0
  593. package/dist/core/usecases/suites/suites.js.map +1 -0
  594. package/dist/core/verify/answers.js +12 -5
  595. package/dist/core/verify/answers.js.map +1 -1
  596. package/dist/core/verify/build.js +102 -82
  597. package/dist/core/verify/build.js.map +1 -1
  598. package/dist/core/verify/checklist.js +3 -0
  599. package/dist/core/verify/checklist.js.map +1 -1
  600. package/dist/core/verify/location.js +67 -0
  601. package/dist/core/verify/location.js.map +1 -0
  602. package/dist/core/verify/pins/lint.js +3 -2
  603. package/dist/core/verify/pins/lint.js.map +1 -1
  604. package/dist/core/verify/pins/pin.js +1 -1
  605. package/dist/core/verify/pins/pin.js.map +1 -1
  606. package/dist/core/verify/read/file.js +224 -0
  607. package/dist/core/verify/read/file.js.map +1 -0
  608. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  609. package/dist/core/verify/read/shape.js.map +1 -0
  610. package/dist/core/verify/read/union.js +105 -0
  611. package/dist/core/verify/read/union.js.map +1 -0
  612. package/dist/core/verify/record.js +35 -12
  613. package/dist/core/verify/record.js.map +1 -1
  614. package/dist/core/verify/store/commit.js +40 -30
  615. package/dist/core/verify/store/commit.js.map +1 -1
  616. package/dist/core/verify/store/render.js +8 -4
  617. package/dist/core/verify/store/render.js.map +1 -1
  618. package/dist/core/vocabulary/issue.js +1 -0
  619. package/dist/core/vocabulary/issue.js.map +1 -1
  620. package/dist/core/vocabulary/maturity.js +12 -2
  621. package/dist/core/vocabulary/maturity.js.map +1 -1
  622. package/examples/README.md +21 -6
  623. package/examples/docs/architecture/landscape.likec4 +8 -0
  624. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  625. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  626. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  627. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  628. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  629. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  630. package/package.json +1 -1
  631. package/dist/commands/validate/checks/vocabulary.js +0 -16
  632. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  633. package/dist/core/agent/agents-md/artifacts.js +0 -221
  634. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  635. package/dist/core/agent/agents-md/cycle.js +0 -180
  636. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  637. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  638. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  639. package/dist/core/agent/agents-md/spine.js +0 -40
  640. package/dist/core/agent/agents-md/spine.js.map +0 -1
  641. package/dist/core/coherence/events/merged.js +0 -40
  642. package/dist/core/coherence/events/merged.js.map +0 -1
  643. package/dist/core/diff/current-state.js +0 -96
  644. package/dist/core/diff/current-state.js.map +0 -1
  645. package/dist/core/diff/victims.js +0 -256
  646. package/dist/core/diff/victims.js.map +0 -1
  647. package/dist/core/staging/snapshot.js.map +0 -1
  648. package/dist/core/verify/file.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"spec.js","sourceRoot":"","sources":["../../../src/core/document/spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAkJzC,MAAM,CAAC,MAAM,OAAO,GAAG,oDAAoD,CAAC;AAC5E,MAAM,CAAC,MAAM,sBAAsB,GAAG,kCAAkC,CAAC;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAE7D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,sCAAsC,CAAC;AAEzE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAE7D;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC;IACrD,IAAI,EAAE,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACjD,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACnC,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IAC7E,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;AACrD,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,MAAM,iBAAiB,GAAG,iCAAiC,CAAC;AAEnE;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAE5C,uFAAuF;AACvF,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,MAAM,CAAC,aAAa,yBAAyB,IAAI,CAAC,CAAC;AAO5F,mFAAmF;AACnF,MAAM,UAAU,yBAAyB,CAAC,WAAwB;IAChE,OAAO,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACvC,MAAM,KAAK,GAAG,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChD,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,qBAAqB,CAAC,IAAmB;IACvD,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,WAAW,IAAI,IAAI,EAAE,CAAC;QAC/B,MAAM,YAAY,GAAG,yBAAyB,CAAC,WAAW,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC;YACpC,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,WAAW,CAAC,EAAE,KAAK,SAAS;gBAC5B,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACvB,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QAC3F,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC;QACD,sEAAsE;QACtE,8DAA8D;QAC9D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;YAAE,SAAS;QACzE,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;QAC3C,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAE,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;IACD,KAAK,MAAM,CAAC,EAAE,EAAE,YAAY,CAAC,IAAI,MAAM,EAAE,CAAC;QACxC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,mBAAmB,CAAC,WAAwB;IAC1D,OAAO,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACvC,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1C,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,WAAwB;IACtD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,WAAW,CAAC;IACnD,OAAO,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AAC5F,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAwB;IAC1D,OAAO;QACL,GAAG,WAAW;QACd,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YAClC,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC;YAChC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE;iBACnB,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;iBACrD,IAAI,CAAC,IAAI,CAAC,CAAC;YACd,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAE,EAAE,IAAI,CAAC,CAAC;QACvC,CAAC,CAAC;KACH,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAAwB;IACxD,OAAO,UAAU,CAAC,QAAQ,CAAC;SACxB,MAAM,CAAC,qBAAqB,CAAC,CAAC,mBAAmB,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;SAC1F,MAAM,CAAC,KAAK,CAAC;SACb,KAAK,CAAC,CAAC,EAAE,yBAAyB,CAAC,CAAC;AACzC,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,qBAAqB,CAAC,IAAmB;IACvD,6EAA6E;IAC7E,8EAA8E;IAC9E,4CAA4C;IAC5C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,KAAK,GAAa,CAAC,oBAAoB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;QACzB,0EAA0E;QAC1E,wEAAwE;QACxE,IAAI,CAAC,CAAC,EAAE,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAClF,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,2EAA2E;QAC3E,qEAAqE;QACrE,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACjF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5G,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC/B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,kBAAkB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YAC3C,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;YACvC,IAAI,IAAI;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG","sourcesContent":["/**\n * What a requirement IS, what makes one the same requirement twice, and the one\n * canonical way it is written back out.\n *\n * Living specs use `## Requirements` with `### Requirement:` + `#### Scenario:`.\n * Feature deltas group requirements under `## ADDED|MODIFIED|REMOVED\n * Requirements`. Scenarios are the acceptance criteria (Given/When/Then) and\n * the source for tests.\n *\n * Identity and serialization cannot be separated here: `requirementDigest` is\n * defined AS the sha256 of the serialized requirement without its baseline, so\n * a second module owning the writer would mean the digest and the bytes it\n * claims to describe could disagree. `./parse.ts` turns markdown into these,\n * `./apply.ts` folds a delta into a living set of them.\n */\nimport { createHash } from \"node:crypto\";\n\nexport type DeltaKind = \"ADDED\" | \"MODIFIED\" | \"REMOVED\" | \"BASE\";\n\nexport interface Scenario {\n name: string;\n lines: string[];\n}\n\nexport interface Requirement {\n /** BASE for a living spec; ADDED/MODIFIED/REMOVED inside a feature delta. */\n kind: DeltaKind;\n /**\n * Stable identity from a `Requirement-ID:` body line. Optional so existing\n * OpenSpec-compatible documents continue to use their heading as identity.\n */\n id?: string;\n /**\n * The living requirement this delta was WRITTEN AGAINST, from a `Based-On:`\n * body line — `requirementDigest` of that requirement at authoring time.\n *\n * Delta-only, and meaningful on MODIFIED/REMOVED alone: those two address a\n * requirement that already exists, and a MODIFIED carries its full new text,\n * so archive does not merge the living wording — it REPLACES it. Two features\n * rewriting one requirement therefore lose the loser's text outright, and the\n * in-flight warning (`delta.modified-conflict`) cannot see the case that\n * actually happens: once the first archives it leaves `changes/`, the second\n * goes green again, and its stale text lands on top without a word. This is\n * the pin that closes that window — the same trick as `sources_digest`\n * (core/provenance/stamp.ts), one requirement wide.\n *\n * Absent on requirements adopted from OpenSpec, which never had the line;\n * those keep the older, weaker protection and are told so once (warn).\n */\n basedOn?: string;\n name: string;\n text: string[];\n /** OpenAPI operationIds this requirement governs, from an `Operations:` line. */\n operations: string[];\n /**\n * What this requirement's scenarios exercise, from a `Covers:` line — the\n * architecture analog of `Operations:`. Entries are C4 element ids, edges\n * (`source -> target`), or health signals (`alert:<id>` / `sli:<id>`);\n * core/c4/arch.ts owns the grammar and the resolution. Parsed everywhere for one\n * grammar's sake, meaningful in arch.spec.md.\n */\n covers: string[];\n /**\n * Authorization this requirement is gated on, from a `Requires:` line —\n * `<subject>/<permission>` entries declared in `architecture/permissions.yaml`.\n *\n * A third join beside `Operations:` and `Covers:`, and it exists because the\n * other two cannot carry it. A permission is not an operation (one operation\n * is gated by several, and the same permission gates several operations) and\n * it is not a C4 element (it is a domain fact, not a structural one). The\n * subject half is what the OpenAPI security model cannot express at all:\n * `security` names what the CALLER must hold, while a permission is as often\n * checked on an entity inside the request — the profile a message is sent\n * from, the org a resource belongs to.\n */\n requires: string[];\n /**\n * Fleet capabilities this requirement realizes part of, from a `Capability:`\n * (also accepted: `Capabilities:`) line — ids declared in\n * `architecture/capabilities.yaml`.\n *\n * A fourth join beside `Operations:`, `Covers:` and `Requires:`, and a LIST\n * because the relation is many-to-many in both directions: one requirement\n * commonly closes part of two capabilities, and a capability is realized by\n * many requirements across several services — a single-valued field would\n * force authors to pick a lie. Neither existing line can carry it: a\n * capability is not an operation (it is behaviour above any one endpoint)\n * and not a C4 element (it is a business fact, not a structural one).\n * The parse is additive exactly as `requires` is — the line rides in\n * `req.text` and therefore inside `requirementDigest`, so no living\n * document's digest moves merely because loam learned to read it.\n */\n capabilities: string[];\n /**\n * Capability REQUIREMENTS this requirement realizes part of, from a\n * `Realizes:` line — entries spelled `<capability-id>#<Requirement-ID>`,\n * resolving against `capabilities/<cap>/spec.md`.\n *\n * A fifth join, and the one that makes the business tree checkable rather\n * than merely present. `Capability:` beside it answers a DIFFERENT question\n * and neither replaces the other: `Capability: checkout` says this\n * requirement is part of that capability, which is a claim about theme;\n * `Realizes: checkout#CHECKOUT-CHARGE-ONCE` says it is part of what makes one\n * named promise true, which is a claim loam can grade in both directions —\n * an entry that resolves to nothing, and a capability requirement nothing\n * realizes.\n *\n * WHY A COMPOSITE ENTRY RATHER THAN TWO LINES. A `Requirement-ID` is only\n * unique inside its own document, so the capability half is not decoration —\n * it is what makes the target addressable at all. Two lines would let a\n * requirement name three capabilities and four ids with nothing saying which\n * belongs to which.\n *\n * WHY THE SEPARATOR IS THE LAST `#` AND NOT THE FIRST. The requirement half\n * has a strict grammar (`REQUIREMENT_ID_RE`) that excludes `#`, while the\n * capability half is a YAML key and a directory name and is not constrained\n * here at all. Splitting at the last `#` is therefore unambiguous for every\n * capability id there is; splitting at the first would mis-parse any id that\n * contained one, and would do it silently.\n *\n * Parsed additively exactly as `capabilities` is — the line rides in\n * `req.text` and therefore inside `requirementDigest`, so no living\n * document's digest moves merely because loam learned to read it.\n */\n realizes: string[];\n /**\n * AsyncAPI message names this requirement governs on the PRODUCING side, from\n * a `Publishes:` line — the event-axis analog of `Operations:`.\n *\n * Two lines rather than one, because the async spine is directional in a way\n * the HTTP one is not: `Publishes:` resolves against this service's OWN\n * contract, while `Consumes:` names a message another service declares, and\n * only a fleet-wide view can say whether anybody does. Grading them alike\n * would either excuse a typo in a producer or point a consumer's error at the\n * wrong repository.\n */\n publishes: string[];\n /** AsyncAPI message names this requirement consumes, from a `Consumes:` line. See `publishes`. */\n consumes: string[];\n scenarios: Scenario[];\n /**\n * The H2 heading of its SOURCE DOCUMENT this requirement was parsed under,\n * verbatim (`## Behavior`), or absent if it preceded every heading. Records where\n * the text came from, so it stays true after a merge re-homes the requirement.\n *\n * `kind` alone cannot explain why a requirement is BASE, and the two BASE cases\n * differ completely: under `## Requirements` a delta is legally quoting the\n * living state, while under `## Behavior` the author wrote a change that archive\n * will silently not merge. `delta.requirement-not-merged` tells them apart.\n */\n section?: string;\n /**\n * 1-based line of this requirement's `### Requirement:` heading in its source\n * document. Body line `text[i]` is therefore at line `line + 1 + i` — the\n * capture is contiguous from the heading to the first scenario — which is what\n * lets `loam rebase` rewrite one `Based-On:` line by surgery instead of\n * reserializing the document and flattening the author's sections and prose.\n */\n line?: number;\n}\n\nexport const KIND_RE = /^##\\s+(ADDED|MODIFIED|REMOVED)\\s+Requirements\\s*$/i;\nexport const REQUIREMENT_ID_LINE_RE = /^\\s*Requirement-ID:\\s*(.*?)\\s*$/i;\nexport const BASED_ON_LINE_RE = /^\\s*Based-On:\\s*(.*?)\\s*$/i;\n\n/**\n * The `Capability:` body line (also accepted: `Capabilities:`), exactly as the\n * parser reads it — `./parse.ts` consumes the capture, migrate-openspec's\n * materializer tests presence with it before appending its own line. One\n * spelling on purpose: two copies of a grammar is how a \"presence\" test and\n * the parse quietly stop agreeing about what counts as the line.\n */\nexport const CAPABILITY_LINE_RE = /^\\s*Capabilit(?:y|ies):\\s*(.+?)\\s*$/i;\n\n/**\n * The `Realizes:` body line. One spelling only — no `Realize:` singular\n * alternative, unlike `Operations?:` and the rest — because the verb already\n * reads correctly for one entry and for six, and the plural forms in the older\n * lines exist to forgive a NOUN that changes shape. A grammar with an\n * alternative nobody needs is one more thing two readers can disagree about.\n */\nexport const REALIZES_LINE_RE = /^\\s*Realizes:\\s*(.+?)\\s*$/i;\n\n/**\n * The separator between a `Realizes:` target and the digest of the capability\n * requirement it was written against — `checkout#CHK-1@9f2c1a4b`.\n *\n * `@` and not a second `#`, because the two halves are read by different rules:\n * `splitRealizesEntry` takes the LAST `#` to find the requirement half, so a\n * second one would move that boundary and re-parse every existing entry. `@`\n * is also excluded from `REQUIREMENT_ID_RE`, which is what makes the split\n * unambiguous rather than a convention — an id can never contain one.\n *\n * NOBODY TYPES THIS. `loam rebase --living` writes it and rewrites it; a human\n * writes `Realizes: checkout#CHK-1` and is never asked for more. An entry\n * without a pin is not a defect and never becomes one — it is a claim that has\n * not been pinned yet, and it grades exactly as it did before pins existed.\n */\nexport const REALIZES_PIN_SEPARATOR = \"@\";\n\n/**\n * One `Realizes:` entry split into the target it names and the digest it was\n * written against, or `pin: null` when it carries none.\n *\n * A suffix that is not a well-formed digest is NOT a pin and NOT a refusal:\n * the whole entry stays the target, so `checkout#CHK-1@nonsense` resolves — or\n * fails to resolve — exactly as it would have before, under\n * `capability.realizes-unknown`, whose message names the entry the author\n * actually typed. Inventing a second refusal for one mistake is the failure\n * `splitRealizesEntry` already refuses two lines up.\n */\nexport function splitRealizesPin(entry: string): { target: string; pin: string | null } {\n const at = entry.lastIndexOf(REALIZES_PIN_SEPARATOR);\n if (at <= 0) return { target: entry, pin: null };\n const suffix = entry.slice(at + 1);\n if (!REQUIREMENT_DIGEST_RE.test(suffix)) return { target: entry, pin: null };\n return { target: entry.slice(0, at), pin: suffix };\n}\n\n/** Portable, review-friendly stable IDs. Case-sensitive by design. */\nexport const REQUIREMENT_ID_RE = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/;\n\n/**\n * How much of the sha256 a `Based-On:` line carries — the length\n * `sources_digest` already uses (core/provenance/stamp.ts), for one reason a reader\n * can hold: every digest loam writes into a document reads the same.\n */\nexport const REQUIREMENT_DIGEST_LENGTH = 16;\n\n/** The value shape of a `Based-On:` line, exactly as `requirementDigest` writes it. */\nexport const REQUIREMENT_DIGEST_RE = new RegExp(`^[0-9a-f]{${REQUIREMENT_DIGEST_LENGTH}}$`);\n\nexport type RequirementIdProblem =\n | { kind: \"invalid\"; requirement: string; value: string }\n | { kind: \"repeated\"; requirement: string; values: string[] }\n | { kind: \"duplicate\"; id: string; requirements: string[] };\n\n/** Every authored Requirement-ID value in a requirement body, including blanks. */\nexport function requirementIdDeclarations(requirement: Requirement): string[] {\n return requirement.text.flatMap((line) => {\n const match = REQUIREMENT_ID_LINE_RE.exec(line);\n return match === null ? [] : [match[1]!.trim()];\n });\n}\n\n/** Structural ID problems within one spec/delta document. */\nexport function requirementIdProblems(reqs: Requirement[]): RequirementIdProblem[] {\n const problems: RequirementIdProblem[] = [];\n const owners = new Map<string, string[]>();\n for (const requirement of reqs) {\n const declarations = requirementIdDeclarations(requirement);\n const values = declarations.length > 0\n ? declarations\n : requirement.id === undefined\n ? []\n : [requirement.id];\n if (declarations.length > 1) {\n problems.push({ kind: \"repeated\", requirement: requirement.name, values: declarations });\n }\n for (const value of values) {\n if (!REQUIREMENT_ID_RE.test(value)) {\n problems.push({ kind: \"invalid\", requirement: requirement.name, value });\n }\n }\n // A repeated declaration is already an error. Do not also pretend its\n // keep-last parse result is an unambiguous document identity.\n if (values.length !== 1 || !REQUIREMENT_ID_RE.test(values[0]!)) continue;\n const names = owners.get(values[0]!) ?? [];\n names.push(requirement.name);\n owners.set(values[0]!, names);\n }\n for (const [id, requirements] of owners) {\n if (requirements.length > 1) problems.push({ kind: \"duplicate\", id, requirements });\n }\n return problems;\n}\n\n/** Every authored `Based-On:` value in a requirement body, including blanks. */\nexport function basedOnDeclarations(requirement: Requirement): string[] {\n return requirement.text.flatMap((line) => {\n const match = BASED_ON_LINE_RE.exec(line);\n return match === null ? [] : [match[1]!.trim()];\n });\n}\n\n/**\n * The requirement without its baseline marker — what a digest is taken over,\n * and what archive merges into the living document.\n *\n * `Based-On:` is a statement ABOUT a delta (\"this was written against that\"),\n * never part of the requirement it describes, and both consequences of\n * forgetting that are bad. Left in the digest input, a requirement's digest\n * would depend on the pin pointing at it, so no baseline could ever be\n * self-consistent. Left in the merged text — `applyRequirementDelta` copies the\n * delta's body into living wholesale — the living document would grow a pin to\n * a version of itself, and the NEXT feature's baseline would be taken over a\n * document containing the previous feature's bookkeeping.\n */\nexport function withoutBaseline(requirement: Requirement): Requirement {\n const { basedOn: _dropped, ...rest } = requirement;\n return { ...rest, text: requirement.text.filter((line) => !BASED_ON_LINE_RE.test(line)) };\n}\n\n/**\n * The requirement with every `Realizes:` pin stripped back to its bare target —\n * the second thing a digest is taken over, for the same reason as the first.\n *\n * `withoutBaseline`'s argument, one axis over: a pin is a statement ABOUT a\n * join (\"this claim was written against that version of the promise\"), never\n * part of the requirement making the claim. Left in the digest input it would\n * make re-pinning a content change, so every requirement realizing a capability\n * would go stale against ITS OWN consumers the moment the capability moved —\n * one edit cascading through the corpus, which is the failure the pin exists to\n * report rather than to cause.\n *\n * The line is kept and only the suffix removed, unlike `withoutBaseline` which\n * drops the whole line: the target ids ARE content, and a requirement that\n * stops naming the promise it keeps is a different requirement.\n */\nexport function withoutRealizesPins(requirement: Requirement): Requirement {\n return {\n ...requirement,\n text: requirement.text.map((line) => {\n const match = REALIZES_LINE_RE.exec(line);\n if (match === null) return line;\n const bare = match[1]!\n .split(\",\")\n .map((entry) => splitRealizesPin(entry.trim()).target)\n .join(\", \");\n return line.replace(match[1]!, bare);\n }),\n };\n}\n\n/**\n * The identity of a requirement's CONTENT: sha256 over its canonical\n * serialization, truncated like every other digest loam stamps.\n *\n * Canonical, not raw bytes, and deliberately: a living spec is rewritten by\n * `serializeRequirements` on every archive, so hashing the file's bytes would\n * make a baseline go stale over reflowed blank lines — a false alarm on the one\n * check whose whole value is that it only fires when something real moved.\n * What the hash therefore covers is exactly what survives a round trip: the\n * heading, the body (`Requirement-ID:` and `Operations:`/`Covers:` lines\n * included), and every scenario with its Given/When/Then lines. `kind` and\n * `section` are not serialized and so are not hashed — a requirement does not\n * change because the document quoting it moved it under another heading.\n *\n * The two normalizations are the two kinds of bookkeeping a requirement can\n * carry about its own joins — the baseline pointing AT it, and the `Realizes:`\n * pins pointing OUT of it. Both are stripped for the same reason: a digest that\n * moved when bookkeeping moved would make every pin self-invalidating.\n */\nexport function requirementDigest(requirement: Requirement): string {\n return createHash(\"sha256\")\n .update(serializeRequirements([withoutRealizesPins(withoutBaseline(requirement))]), \"utf8\")\n .digest(\"hex\")\n .slice(0, REQUIREMENT_DIGEST_LENGTH);\n}\n\n/** Serialize requirements back to OpenSpec markdown (`### Requirement:` + `#### Scenario:`). */\nexport function serializeRequirements(reqs: Requirement[]): string {\n // Framing (blank lines between sections) is normalized here; body content is\n // only edge-trimmed, never collapsed — blank lines inside a scenario (e.g. in\n // fenced code blocks) are verbatim content.\n const chunks: string[] = [];\n for (const r of reqs) {\n const chunk: string[] = [`### Requirement: ${r.name}`];\n const body = [...r.text];\n // Parsed documents already carry the authored line in `text`. This branch\n // also makes programmatically constructed Requirements serialize fully.\n if (r.id !== undefined && !body.some((line) => REQUIREMENT_ID_LINE_RE.test(line))) {\n body.unshift(`Requirement-ID: ${r.id}`);\n }\n // Directly under the identity it pins, which is where `loam rebase` writes\n // it too — the two lines are read together, so they are never apart.\n if (r.basedOn !== undefined && !body.some((line) => BASED_ON_LINE_RE.test(line))) {\n body.splice(body.findIndex((line) => REQUIREMENT_ID_LINE_RE.test(line)) + 1, 0, `Based-On: ${r.basedOn}`);\n }\n const text = body.join(\"\\n\").trim();\n if (text) chunk.push(\"\", text);\n for (const s of r.scenarios) {\n chunk.push(\"\", `#### Scenario: ${s.name}`);\n const body = s.lines.join(\"\\n\").trim();\n if (body) chunk.push(body);\n }\n chunks.push(chunk.join(\"\\n\"));\n }\n return chunks.join(\"\\n\\n\").trim() + \"\\n\";\n}\n\n/**\n * A delta requirement as it lands in the living document, with the bookkeeping\n * that belongs to the DELTA left behind: the `Based-On:` pin (a claim about\n * which living version this was written against — meaningless once it IS the\n * living version, and poison for the next feature's baseline, which would then\n * hash the previous feature's pin) and the line it was parsed from.\n */\n"]}
1
+ {"version":3,"file":"spec.js","sourceRoot":"","sources":["../../../src/core/document/spec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a requirement IS, what makes one the same requirement twice, and the one\n * canonical way it is written back out.\n *\n * Living specs use `## Requirements` with `### Requirement:` + `#### Scenario:`.\n * Feature deltas group requirements under `## ADDED|MODIFIED|REMOVED\n * Requirements`. Scenarios are the acceptance criteria (Given/When/Then) and\n * the source for tests.\n *\n * Identity and serialization cannot be separated here: `requirementDigest` is\n * defined AS the sha256 of the serialized requirement without its baseline, so\n * a second module owning the writer would mean the digest and the bytes it\n * claims to describe could disagree. `./parse.ts` turns markdown into these,\n * `./apply.ts` folds a delta into a living set of them.\n */\nimport { createHash } from \"node:crypto\";\n\nexport type DeltaKind = \"ADDED\" | \"MODIFIED\" | \"REMOVED\" | \"BASE\";\n\nexport interface Scenario {\n name: string;\n lines: string[];\n /**\n * 1-based line of this scenario's `#### Scenario:` heading in its source document\n * — `Requirement.line` one level down, so body line `lines[i]` sits at `line + 1 + i`.\n *\n * NOT part of identity, and that is load-bearing: `serializeRequirements` writes a\n * scenario from `name` and `lines` alone and `requirementDigest` is the hash OF that\n * serialization, so recording a position moves no digest and no living `Based-On:`\n * pin goes stale because the parser learned to count lines.\n */\n line?: number;\n}\n\nexport interface Requirement {\n /** BASE for a living spec; ADDED/MODIFIED/REMOVED inside a feature delta. */\n kind: DeltaKind;\n /**\n * Stable identity from a `Requirement-ID:` body line. Optional so existing\n * OpenSpec-compatible documents continue to use their heading as identity.\n */\n id?: string;\n /**\n * The living requirement this delta was WRITTEN AGAINST, from a `Based-On:`\n * body line — `requirementDigest` of that requirement at authoring time.\n *\n * Delta-only, and meaningful on MODIFIED/REMOVED alone: those two address a\n * requirement that already exists, and a MODIFIED carries its full new text,\n * so archive does not merge the living wording — it REPLACES it. Two features\n * rewriting one requirement therefore lose the loser's text outright, and the\n * in-flight warning (`delta.modified-conflict`) cannot see the case that\n * actually happens: once the first archives it leaves `changes/`, the second\n * goes green again, and its stale text lands on top without a word. This is\n * the pin that closes that window — the same trick as `sources_digest`\n * (core/provenance/stamp.ts), one requirement wide.\n *\n * Absent on requirements adopted from OpenSpec, which never had the line;\n * those keep the older, weaker protection and are told so once (warn).\n */\n basedOn?: string;\n name: string;\n text: string[];\n /** OpenAPI operationIds this requirement governs, from an `Operations:` line. */\n operations: string[];\n /**\n * What this requirement's scenarios exercise, from a `Covers:` line — the\n * architecture analog of `Operations:`. Entries are C4 element ids, edges\n * (`source -> target`), or health signals (`alert:<id>` / `sli:<id>`);\n * core/c4/arch.ts owns the grammar and the resolution. Parsed everywhere for one\n * grammar's sake, meaningful in arch.spec.md.\n */\n covers: string[];\n /**\n * Authorization this requirement is gated on, from a `Requires:` line —\n * `<subject>/<permission>` entries declared in `architecture/permissions.yaml`.\n *\n * A third join beside `Operations:` and `Covers:`, and it exists because the\n * other two cannot carry it. A permission is not an operation (one operation\n * is gated by several, and the same permission gates several operations) and\n * it is not a C4 element (it is a domain fact, not a structural one). The\n * subject half is what the OpenAPI security model cannot express at all:\n * `security` names what the CALLER must hold, while a permission is as often\n * checked on an entity inside the request — the profile a message is sent\n * from, the org a resource belongs to.\n */\n requires: string[];\n /**\n * Fleet capabilities this requirement realizes part of, from a `Capability:`\n * (also accepted: `Capabilities:`) line — ids declared in\n * `architecture/capabilities.yaml`.\n *\n * A fourth join beside `Operations:`, `Covers:` and `Requires:`, and a LIST\n * because the relation is many-to-many in both directions: one requirement\n * commonly closes part of two capabilities, and a capability is realized by\n * many requirements across several services — a single-valued field would\n * force authors to pick a lie. Neither existing line can carry it: a\n * capability is not an operation (it is behaviour above any one endpoint)\n * and not a C4 element (it is a business fact, not a structural one).\n * The parse is additive exactly as `requires` is — the line rides in\n * `req.text` and therefore inside `requirementDigest`, so no living\n * document's digest moves merely because loam learned to read it.\n */\n capabilities: string[];\n /**\n * Capability REQUIREMENTS this requirement realizes part of, from a\n * `Realizes:` line — entries spelled `<capability-id>#<Requirement-ID>`,\n * resolving against `capabilities/<cap>/spec.md`.\n *\n * A fifth join, and the one that makes the business tree checkable rather\n * than merely present. `Capability:` beside it answers a DIFFERENT question\n * and neither replaces the other: `Capability: checkout` says this\n * requirement is part of that capability, which is a claim about theme;\n * `Realizes: checkout#CHECKOUT-CHARGE-ONCE` says it is part of what makes one\n * named promise true, which is a claim loam can grade in both directions —\n * an entry that resolves to nothing, and a capability requirement nothing\n * realizes.\n *\n * WHY A COMPOSITE ENTRY RATHER THAN TWO LINES. A `Requirement-ID` is only\n * unique inside its own document, so the capability half is not decoration —\n * it is what makes the target addressable at all. Two lines would let a\n * requirement name three capabilities and four ids with nothing saying which\n * belongs to which.\n *\n * WHY THE SEPARATOR IS THE LAST `#` AND NOT THE FIRST. The requirement half\n * has a strict grammar (`REQUIREMENT_ID_RE`) that excludes `#`, while the\n * capability half is a YAML key and a directory name and is not constrained\n * here at all. Splitting at the last `#` is therefore unambiguous for every\n * capability id there is; splitting at the first would mis-parse any id that\n * contained one, and would do it silently.\n *\n * Parsed additively exactly as `capabilities` is — the line rides in\n * `req.text` and therefore inside `requirementDigest`, so no living\n * document's digest moves merely because loam learned to read it.\n */\n realizes: string[];\n /**\n * AsyncAPI message names this requirement governs on the PRODUCING side, from\n * a `Publishes:` line — the event-axis analog of `Operations:`.\n *\n * Two lines rather than one, because the async spine is directional in a way\n * the HTTP one is not: `Publishes:` resolves against this service's OWN\n * contract, while `Consumes:` names a message another service declares, and\n * only a fleet-wide view can say whether anybody does. Grading them alike\n * would either excuse a typo in a producer or point a consumer's error at the\n * wrong repository.\n */\n publishes: string[];\n /** AsyncAPI message names this requirement consumes, from a `Consumes:` line. See `publishes`. */\n consumes: string[];\n scenarios: Scenario[];\n /**\n * The H2 heading of its SOURCE DOCUMENT this requirement was parsed under,\n * verbatim (`## Behavior`), or absent if it preceded every heading. Records where\n * the text came from, so it stays true after a merge re-homes the requirement.\n *\n * `kind` alone cannot explain why a requirement is BASE, and the two BASE cases\n * differ completely: under `## Requirements` a delta is legally quoting the\n * living state, while under `## Behavior` the author wrote a change that archive\n * will silently not merge. `delta.requirement-not-merged` tells them apart.\n */\n section?: string;\n /**\n * 1-based line of this requirement's `### Requirement:` heading in its source\n * document. Body line `text[i]` is therefore at line `line + 1 + i` — the\n * capture is contiguous from the heading to the first scenario — which is what\n * lets `loam rebase` rewrite one `Based-On:` line by surgery instead of\n * reserializing the document and flattening the author's sections and prose.\n */\n line?: number;\n}\n\nexport const KIND_RE = /^##\\s+(ADDED|MODIFIED|REMOVED)\\s+Requirements\\s*$/i;\nexport const REQUIREMENT_ID_LINE_RE = /^\\s*Requirement-ID:\\s*(.*?)\\s*$/i;\nexport const BASED_ON_LINE_RE = /^\\s*Based-On:\\s*(.*?)\\s*$/i;\n\n/**\n * The `Capability:` body line (also accepted: `Capabilities:`), exactly as the\n * parser reads it — `./parse.ts` consumes the capture, migrate-openspec's\n * materializer tests presence with it before appending its own line. One\n * spelling on purpose: two copies of a grammar is how a \"presence\" test and\n * the parse quietly stop agreeing about what counts as the line.\n */\nexport const CAPABILITY_LINE_RE = /^\\s*Capabilit(?:y|ies):\\s*(.+?)\\s*$/i;\n\n/**\n * The `Realizes:` body line. One spelling only — no `Realize:` singular\n * alternative, unlike `Operations?:` and the rest — because the verb already\n * reads correctly for one entry and for six, and the plural forms in the older\n * lines exist to forgive a NOUN that changes shape. A grammar with an\n * alternative nobody needs is one more thing two readers can disagree about.\n */\nexport const REALIZES_LINE_RE = /^\\s*Realizes:\\s*(.+?)\\s*$/i;\n\n/**\n * The separator between a `Realizes:` target and the digest of the capability\n * requirement it was written against — `checkout#CHK-1@9f2c1a4b`.\n *\n * `@` and not a second `#`, because the two halves are read by different rules:\n * `splitRealizesEntry` takes the LAST `#` to find the requirement half, so a\n * second one would move that boundary and re-parse every existing entry. `@`\n * is also excluded from `REQUIREMENT_ID_RE`, which is what makes the split\n * unambiguous rather than a convention — an id can never contain one.\n *\n * NOBODY TYPES THIS. `loam rebase --living` writes it and rewrites it; a human\n * writes `Realizes: checkout#CHK-1` and is never asked for more. An entry\n * without a pin is not a defect and never becomes one — it is a claim that has\n * not been pinned yet, and it grades exactly as it did before pins existed.\n */\nexport const REALIZES_PIN_SEPARATOR = \"@\";\n\n/**\n * One `Realizes:` entry split into the target it names and the digest it was\n * written against, or `pin: null` when it carries none.\n *\n * A suffix that is not a well-formed digest is NOT a pin and NOT a refusal:\n * the whole entry stays the target, so `checkout#CHK-1@nonsense` resolves — or\n * fails to resolve — exactly as it would have before, under\n * `capability.realizes-unknown`, whose message names the entry the author\n * actually typed. Inventing a second refusal for one mistake is the failure\n * `splitRealizesEntry` already refuses two lines up.\n */\nexport function splitRealizesPin(entry: string): { target: string; pin: string | null } {\n const at = entry.lastIndexOf(REALIZES_PIN_SEPARATOR);\n if (at <= 0) return { target: entry, pin: null };\n const suffix = entry.slice(at + 1);\n if (!REQUIREMENT_DIGEST_RE.test(suffix)) return { target: entry, pin: null };\n return { target: entry.slice(0, at), pin: suffix };\n}\n\n/** Portable, review-friendly stable IDs. Case-sensitive by design. */\nexport const REQUIREMENT_ID_RE = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/;\n\n/**\n * How much of the sha256 a `Based-On:` line carries — the length\n * `sources_digest` already uses (core/provenance/stamp.ts), for one reason a reader\n * can hold: every digest loam writes into a document reads the same.\n */\nexport const REQUIREMENT_DIGEST_LENGTH = 16;\n\n/** The value shape of a `Based-On:` line, exactly as `requirementDigest` writes it. */\nexport const REQUIREMENT_DIGEST_RE = new RegExp(`^[0-9a-f]{${REQUIREMENT_DIGEST_LENGTH}}$`);\n\nexport type RequirementIdProblem =\n | { kind: \"invalid\"; requirement: string; value: string }\n | { kind: \"repeated\"; requirement: string; values: string[] }\n | { kind: \"duplicate\"; id: string; requirements: string[] };\n\n/** Every authored Requirement-ID value in a requirement body, including blanks. */\nexport function requirementIdDeclarations(requirement: Requirement): string[] {\n return requirement.text.flatMap((line) => {\n const match = REQUIREMENT_ID_LINE_RE.exec(line);\n return match === null ? [] : [match[1]!.trim()];\n });\n}\n\n/** Structural ID problems within one spec/delta document. */\nexport function requirementIdProblems(reqs: Requirement[]): RequirementIdProblem[] {\n const problems: RequirementIdProblem[] = [];\n const owners = new Map<string, string[]>();\n for (const requirement of reqs) {\n const declarations = requirementIdDeclarations(requirement);\n const values = declarations.length > 0\n ? declarations\n : requirement.id === undefined\n ? []\n : [requirement.id];\n if (declarations.length > 1) {\n problems.push({ kind: \"repeated\", requirement: requirement.name, values: declarations });\n }\n for (const value of values) {\n if (!REQUIREMENT_ID_RE.test(value)) {\n problems.push({ kind: \"invalid\", requirement: requirement.name, value });\n }\n }\n // A repeated declaration is already an error. Do not also pretend its\n // keep-last parse result is an unambiguous document identity.\n if (values.length !== 1 || !REQUIREMENT_ID_RE.test(values[0]!)) continue;\n const names = owners.get(values[0]!) ?? [];\n names.push(requirement.name);\n owners.set(values[0]!, names);\n }\n for (const [id, requirements] of owners) {\n if (requirements.length > 1) problems.push({ kind: \"duplicate\", id, requirements });\n }\n return problems;\n}\n\n/** Every authored `Based-On:` value in a requirement body, including blanks. */\nexport function basedOnDeclarations(requirement: Requirement): string[] {\n return requirement.text.flatMap((line) => {\n const match = BASED_ON_LINE_RE.exec(line);\n return match === null ? [] : [match[1]!.trim()];\n });\n}\n\n/**\n * The requirement without its baseline marker — what a digest is taken over,\n * and what archive merges into the living document.\n *\n * `Based-On:` is a statement ABOUT a delta (\"this was written against that\"),\n * never part of the requirement it describes, and both consequences of\n * forgetting that are bad. Left in the digest input, a requirement's digest\n * would depend on the pin pointing at it, so no baseline could ever be\n * self-consistent. Left in the merged text — `applyRequirementDelta` copies the\n * delta's body into living wholesale — the living document would grow a pin to\n * a version of itself, and the NEXT feature's baseline would be taken over a\n * document containing the previous feature's bookkeeping.\n */\nexport function withoutBaseline(requirement: Requirement): Requirement {\n const { basedOn: _dropped, ...rest } = requirement;\n return { ...rest, text: requirement.text.filter((line) => !BASED_ON_LINE_RE.test(line)) };\n}\n\n/**\n * The requirement with every `Realizes:` pin stripped back to its bare target —\n * the second thing a digest is taken over, for the same reason as the first.\n *\n * `withoutBaseline`'s argument, one axis over: a pin is a statement ABOUT a\n * join (\"this claim was written against that version of the promise\"), never\n * part of the requirement making the claim. Left in the digest input it would\n * make re-pinning a content change, so every requirement realizing a capability\n * would go stale against ITS OWN consumers the moment the capability moved —\n * one edit cascading through the corpus, which is the failure the pin exists to\n * report rather than to cause.\n *\n * The line is kept and only the suffix removed, unlike `withoutBaseline` which\n * drops the whole line: the target ids ARE content, and a requirement that\n * stops naming the promise it keeps is a different requirement.\n */\nexport function withoutRealizesPins(requirement: Requirement): Requirement {\n return {\n ...requirement,\n text: requirement.text.map((line) => {\n const match = REALIZES_LINE_RE.exec(line);\n if (match === null) return line;\n const bare = match[1]!\n .split(\",\")\n .map((entry) => splitRealizesPin(entry.trim()).target)\n .join(\", \");\n return line.replace(match[1]!, bare);\n }),\n };\n}\n\n/**\n * The identity of a requirement's CONTENT: sha256 over its canonical\n * serialization, truncated like every other digest loam stamps.\n *\n * Canonical, not raw bytes, and deliberately: archive writes every requirement\n * a delta adds or replaces through `serializeRequirements`, so hashing bytes\n * would make a baseline go stale over reflowed blank lines — a false alarm on\n * the one check whose whole value is that it only fires when something moved.\n * What the hash therefore covers is exactly what survives a round trip: the\n * heading, the body (`Requirement-ID:` and `Operations:`/`Covers:` lines\n * included), and every scenario with its Given/When/Then lines. `kind` and\n * `section` are not serialized and so are not hashed — a requirement does not\n * change because the document quoting it moved it under another heading.\n *\n * The two normalizations are the two kinds of bookkeeping a requirement can\n * carry about its own joins — the baseline pointing AT it, and the `Realizes:`\n * pins pointing OUT of it. Both are stripped for the same reason: a digest that\n * moved when bookkeeping moved would make every pin self-invalidating.\n */\nexport function requirementDigest(requirement: Requirement): string {\n return createHash(\"sha256\")\n .update(serializeRequirements([withoutRealizesPins(withoutBaseline(requirement))]), \"utf8\")\n .digest(\"hex\")\n .slice(0, REQUIREMENT_DIGEST_LENGTH);\n}\n\n/** Serialize requirements back to OpenSpec markdown (`### Requirement:` + `#### Scenario:`). */\nexport function serializeRequirements(reqs: Requirement[]): string {\n // Framing (blank lines between sections) is normalized here; body content is\n // only edge-trimmed, never collapsed — blank lines inside a scenario (e.g. in\n // fenced code blocks) are verbatim content.\n const chunks: string[] = [];\n for (const r of reqs) {\n const chunk: string[] = [`### Requirement: ${r.name}`];\n const body = [...r.text];\n // Parsed documents already carry the authored line in `text`. This branch\n // also makes programmatically constructed Requirements serialize fully.\n if (r.id !== undefined && !body.some((line) => REQUIREMENT_ID_LINE_RE.test(line))) {\n body.unshift(`Requirement-ID: ${r.id}`);\n }\n // Directly under the identity it pins, which is where `loam rebase` writes\n // it too — the two lines are read together, so they are never apart.\n if (r.basedOn !== undefined && !body.some((line) => BASED_ON_LINE_RE.test(line))) {\n body.splice(body.findIndex((line) => REQUIREMENT_ID_LINE_RE.test(line)) + 1, 0, `Based-On: ${r.basedOn}`);\n }\n const text = body.join(\"\\n\").trim();\n if (text) chunk.push(\"\", text);\n for (const s of r.scenarios) {\n chunk.push(\"\", `#### Scenario: ${s.name}`);\n const body = s.lines.join(\"\\n\").trim();\n if (body) chunk.push(body);\n }\n chunks.push(chunk.join(\"\\n\"));\n }\n return chunks.join(\"\\n\\n\").trim() + \"\\n\";\n}\n"]}
@@ -46,6 +46,16 @@ function searchDirs(start) {
46
46
  current = parent;
47
47
  }
48
48
  }
49
+ /**
50
+ * The files loam wrote into the repository this config governs and owns there,
51
+ * relative to its root: loam.json itself and every agent file `agentFiles`
52
+ * records. `sources.unwalked` leaves them out of its denominator — a check
53
+ * that counted loam's own bookkeeping as a part of the service nobody read
54
+ * fired in every service repo the moment `loam init` had run.
55
+ */
56
+ export function loamOwnedFiles(config) {
57
+ return [CONFIG_FILENAME, ...Object.keys(config.agentFiles ?? {})];
58
+ }
49
59
  /** Where a config written HERE goes — never a discovered one. `init` and `saveConfig` use this. */
50
60
  export function localConfigPath(cwd = process.cwd()) {
51
61
  return resolve(cwd, CONFIG_FILENAME);
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/envelope/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join, parse as parsePath, resolve } from \"node:path\";\nimport { resolveInside } from \"../kernel/path-safety.js\";\nimport { docsDirOf, type DocsDir } from \"../kernel/ids/dirs.js\";\nimport { parseServiceId, type ServiceId } from \"../kernel/ids/service.js\";\n\n/** Local config, committed at the root of a service repo (or the docs repo itself). */\nexport const CONFIG_FILENAME = \"loam.json\";\n\nexport interface LoamConfig {\n /**\n * Path to the single shared docs repo (the source of truth). A LOADED config\n * carries it resolved — `parseConfig` resolves the file's spelling against\n * the config file's own directory, which is what earns the brand. The\n * spelling itself is `docsDirAsWritten`, and the shape `saveConfig` persists\n * (`StoredConfig`) keeps it a plain `string`: the stored form is deliberately\n * unresolved (see `storedDocsDir` in commands/init/options.ts), and branding\n * it would state a resolution that never happened.\n */\n docsDir: DocsDir;\n /**\n * Canonical id of the service in the current repo, if this is a service repo.\n *\n * Branded because `parseConfig` below is the one place that checks it, so\n * every command reading `config.service` is reading a name that has already\n * passed the grammar — which is why the config arm of\n * `opts.service ?? config.service` needs no second parse.\n */\n service?: ServiceId;\n /**\n * Where Gherkin lives in this service repo, relative to the repo root\n * (default \"features\" — the cucumber convention). `loam gherkin` writes only\n * inside `<gherkinDir>/loam/`; the rest of the directory stays the team's.\n * Left unresolved on purpose: commands resolve it against the repo they run\n * in, which is where the config file lives.\n */\n gherkinDir?: string;\n /**\n * Where this repo's BUILD writes its contract documents, relative to the\n * repo root — the springdoc/FastAPI/NestJS output, not a second copy anybody\n * maintains.\n *\n * loam reads these and compares a digest; it never writes them, never copies\n * them into the docs repo, and never derives meaning from them. That is what\n * keeps the field on the right side of the no-extractor line: it parses a\n * standard document somebody else generated, at a path a human named, in\n * exactly the way `verify --results` already ingests a cucumber report.\n *\n * The committed `services/<id>/openapi.yaml` stays the contract, and the copy\n * stays a human `cp` reviewed in a pull request. Automating the copy would\n * make the docs-repo document a cache of the build; leaving it manual and\n * merely REPORTING the divergence keeps it a document somebody agreed to.\n */\n contracts?: { openapi?: string };\n /**\n * The agent tools `loam init` has written command and skill files for in this\n * repo, by AGENT_TOOLS id.\n *\n * Recorded because the two ways a generated file can be absent look identical\n * on disk: the binary grew a command nobody has re-run `init` for, or nobody\n * ever selected that tool here. Only this list tells them apart, and `loam\n * doctor` needs the difference to know which absence is worth reporting.\n *\n * ACCUMULATED, never replaced — a later `init --tools cursor` does not\n * un-write the claude files an earlier run left on disk, so the record of\n * what this repo holds has to keep them too.\n *\n * Optional, and unvalidated against the registry on purpose: a config written\n * by an older binary has no list at all, and one written by a NEWER binary\n * may name a tool this one has never heard of. Neither may refuse to load.\n */\n agentTools?: string[];\n /**\n * Digests of generated agent files loam may safely refresh. Keys are\n * repo-relative portable paths; a file whose bytes differ from its recorded\n * digest is human-owned and is never overwritten.\n */\n agentFiles?: Record<string, string>;\n /** Which workflow subset `loam init` manages; absent means the compatible full set. */\n agentProfile?: \"full\" | \"service\" | \"docs\";\n /**\n * The directory the config was actually FOUND in — the repo root, which is\n * not necessarily the cwd now that discovery walks upward. Everything a\n * command resolves against \"this repo\" (gherkinDir, relative `sources:`)\n * belongs here, not against process.cwd(): running `loam gherkin` from\n * `src/deep/` must write the same files as running it from the root.\n *\n * Derived, never stored: `saveConfig` strips it, so it can never be spelled\n * in loam.json and drift from where the file really is.\n */\n root?: string;\n /**\n * `docsDir` exactly as the file spells it, before resolution. Kept because\n * \"relative\" and \"absolute\" are not recoverable from the resolved path, and\n * the difference is the whole point: an absolute docsDir in a COMMITTED\n * config is a path that only exists on the machine that ran `loam init`.\n *\n * Derived, never stored — same contract as `root`.\n */\n docsDirAsWritten?: string;\n}\n\n/**\n * A loam.json that exists but cannot be believed. Carries the field at fault\n * (null when the whole document is), because \"which field\" is the difference\n * between a fix and a guess — and because both `loadConfig` and `loam doctor`\n * report the same sentence from it.\n */\nexport class ConfigError extends Error {\n constructor(\n readonly field: string | null,\n message: string,\n ) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\n/**\n * The directories a config search may look in: `start`, then each ancestor, up\n * to AND INCLUDING the git root — or the filesystem root when there is no git\n * repo above.\n *\n * The git root is the stop because it is the boundary the user already thinks\n * in: a loam.json outside it belongs to some other checkout (or, worse, to a\n * home directory), and silently adopting it would point a service repo's\n * commands at a docs repo nobody in this repo ever chose.\n */\nfunction searchDirs(start: string): string[] {\n const out: string[] = [];\n let current = resolve(start);\n const root = parsePath(current).root;\n for (;;) {\n out.push(current);\n // `.git` is a directory in a normal clone and a FILE in a worktree or\n // submodule — existsSync answers both without caring which.\n if (existsSync(join(current, \".git\"))) return out;\n const parent = dirname(current);\n if (parent === current || current === root) return out;\n current = parent;\n }\n}\n\n/** Where a config written HERE goes — never a discovered one. `init` and `saveConfig` use this. */\nexport function localConfigPath(cwd: string = process.cwd()): string {\n return resolve(cwd, CONFIG_FILENAME);\n}\n\n/**\n * The nearest loam.json at or above `cwd`, or null. Exported because `init` has\n * to be able to ask a different question from \"is there one here\": a config in\n * an ancestor already governs this directory, and a second one written below it\n * would quietly shadow the first for every command run from the subtree.\n */\nexport function findConfigPath(cwd: string = process.cwd()): string | null {\n for (const dir of searchDirs(cwd)) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * The config that governs `cwd`: the discovered one, or — when there is none —\n * where it would have to be written. The fallback keeps this total, which is\n * what lets a caller distinguish \"absent\" from \"present but unloadable\" with a\n * single existsSync (see `reportNoConfig`).\n */\nexport function configPath(cwd: string = process.cwd()): string {\n return findConfigPath(cwd) ?? localConfigPath(cwd);\n}\n\n/**\n * THE config validator. One implementation, because two of them are two\n * opinions: `loam doctor` used to carry its own copy, and a config doctor\n * called healthy could still be one every other command refused to load.\n *\n * `configDir` is the directory the file lives in — every relative path in the\n * document is resolved against it, so the doc comments above stay true no\n * matter where the caller happens to be standing.\n */\nexport function parseConfig(raw: string, configDir: string): LoamConfig {\n const file = join(configDir, CONFIG_FILENAME);\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch (err) {\n throw new ConfigError(null, `${file} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);\n }\n // Checked before any field access: `JSON.parse(\"null\")` and `JSON.parse(\"5\")`\n // are valid JSON, and reaching for `.docsDir` on them is how a config typo\n // used to surface as a raw Node TypeError instead of a named field.\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new ConfigError(null, `${file} must contain a JSON object.`);\n }\n const record = value as Record<string, unknown>;\n\n if (typeof record.docsDir !== \"string\" || record.docsDir === \"\") {\n throw new ConfigError(\"docsDir\", `${file}: \"docsDir\" must be a non-empty string.`);\n }\n // Same discipline as docsDir: a malformed fact refuses the whole config\n // rather than being silently defaulted over — `5` or `\"\"` here is a typo,\n // and defaulting would send generated files somewhere nobody chose.\n // `parseServiceId` rather than a bare check: the same call both refuses the\n // malformed value and produces the branded one below, so there is no window\n // in which a checked name is still typed as an unchecked string. It takes a\n // `string`, hence the shape guard first — the id grammar's own message is\n // about spelling, and `5` is not a spelling.\n let service: ServiceId | undefined;\n if (record.service !== undefined) {\n if (typeof record.service !== \"string\") {\n throw new ConfigError(\"service\", `${file}: \"service\" must be a string.`);\n }\n const parsed = parseServiceId(record.service, '\"service\"');\n if (!parsed.ok) throw new ConfigError(\"service\", `${file}: ${parsed.problem}`);\n service = parsed.id;\n }\n if (record.gherkinDir !== undefined\n && (typeof record.gherkinDir !== \"string\" || record.gherkinDir === \"\")) {\n throw new ConfigError(\"gherkinDir\", `${file}: \"gherkinDir\" must be a non-empty string when present.`);\n }\n const contracts = readContracts(record.contracts, file);\n // Shape only. Which ids are legal is the registry's question, and asking it\n // here would make a config written by a newer binary — one that knows a tool\n // this one does not — unloadable for every command, not just for `init`.\n if (record.agentTools !== undefined\n && (!Array.isArray(record.agentTools)\n || record.agentTools.some((t) => typeof t !== \"string\" || t === \"\"))) {\n throw new ConfigError(\n \"agentTools\",\n `${file}: \"agentTools\" must be an array of non-empty strings when present.`,\n );\n }\n if (record.agentFiles !== undefined) {\n if (record.agentFiles === null || typeof record.agentFiles !== \"object\" || Array.isArray(record.agentFiles)) {\n throw new ConfigError(\"agentFiles\", `${file}: \"agentFiles\" must be a path-to-digest mapping when present.`);\n }\n const entries = Object.entries(record.agentFiles as Record<string, unknown>);\n if (entries.some(([path, digest]) => path === \"\" || typeof digest !== \"string\" || !/^[a-f0-9]{64}$/.test(digest))) {\n throw new ConfigError(\n \"agentFiles\",\n `${file}: \"agentFiles\" must map non-empty paths to lowercase sha256 digests.`,\n );\n }\n }\n if (record.agentProfile !== undefined\n && record.agentProfile !== \"full\"\n && record.agentProfile !== \"service\"\n && record.agentProfile !== \"docs\") {\n throw new ConfigError(\n \"agentProfile\",\n `${file}: \"agentProfile\" must be one of: full, service, docs.`,\n );\n }\n if (typeof record.gherkinDir === \"string\") {\n // Validate the owned output directory, not merely its spelling. This also\n // catches an otherwise-contained path whose existing parent is a symlink\n // out of the service repo.\n try {\n resolveInside(configDir, join(record.gherkinDir, \"loam\"), `\"gherkinDir\"`);\n } catch (err) {\n throw new ConfigError(\"gherkinDir\", `${file}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return {\n // Resolved here, against the file's own directory, so the doc comment on\n // `docsDir` is true no matter where a caller later resolves the path from —\n // which is exactly the provenance `docsDirOf` demands.\n docsDir: docsDirOf(resolve(configDir, record.docsDir)),\n ...(service === undefined ? {} : { service }),\n ...(record.gherkinDir === undefined ? {} : { gherkinDir: record.gherkinDir as string }),\n ...(contracts === undefined ? {} : { contracts }),\n ...(record.agentTools === undefined ? {} : { agentTools: record.agentTools as string[] }),\n ...(record.agentFiles === undefined\n ? {}\n : { agentFiles: record.agentFiles as Record<string, string> }),\n ...(record.agentProfile === undefined\n ? {}\n : { agentProfile: record.agentProfile as \"full\" | \"service\" | \"docs\" }),\n root: resolve(configDir),\n docsDirAsWritten: record.docsDir,\n };\n}\n\n/**\n * Every way asking for the config can come out, as data. It used to be\n * `LoamConfig | null` with a `console.error` on the invalid arm — core's one\n * print outside the envelope adapter, and a null that collapsed \"no config\"\n * and \"a config nobody can read\" into one answer `reportNoConfig` then had to\n * re-derive from the filesystem (a TOCTOU: the file could change between the\n * load and the re-check). The union says which case it is, carries the parse\n * problem for the command layer to render, and prints nothing.\n */\nexport type ConfigLoad =\n | { kind: \"loaded\"; config: LoamConfig }\n | { kind: \"absent\" }\n | { kind: \"invalid\"; path: string; problem: string };\n\nexport async function loadConfig(cwd: string = process.cwd()): Promise<ConfigLoad> {\n const p = findConfigPath(cwd);\n if (p === null) return { kind: \"absent\" };\n try {\n // The read belongs INSIDE the try: `existsSync` only says the name is\n // taken, not that it names a file this process can read. A loam.json that\n // is a directory (EISDIR), one whose permissions refuse us (EACCES), or one\n // deleted between the search and the read (ENOENT) all fail here rather\n // than in `parseConfig` — and read outside, each of them escaped every\n // command as an `internal` crash carrying a bare errno. Caught here they\n // join the same \"exists but cannot be believed\" arm the parse failures take.\n return { kind: \"loaded\", config: parseConfig(await readFile(p, \"utf8\"), dirname(p)) };\n } catch (err) {\n return { kind: \"invalid\", path: p, problem: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/**\n * What `saveConfig` persists: `LoamConfig` with `docsDir` back to a plain\n * `string`, because the stored form is the SPELLING (deliberately relative —\n * `storedDocsDir`), not the resolved `DocsDir` a loaded config carries. The\n * two derived fields ride along as optional so a caller may spread a loaded\n * config forward — `saveConfig` strips them on the way out either way.\n */\nexport type StoredConfig = Omit<LoamConfig, \"docsDir\"> & { docsDir: string };\n\n/**\n * Write the config for the repo rooted at `cwd`. The derived fields are dropped\n * on the way out: they are facts about where the file was found and how it was\n * spelled, and a stored copy could only ever disagree with the file itself.\n */\nexport async function saveConfig(config: StoredConfig, cwd: string = process.cwd()): Promise<string> {\n const p = localConfigPath(cwd);\n const { root: _root, docsDirAsWritten: _asWritten, ...stored } = config;\n await writeFile(p, JSON.stringify(stored, null, 2) + \"\\n\", \"utf8\");\n return p;\n}\n\n/**\n * The optional `contracts` block, validated to shape only.\n *\n * SHAPE ONLY, exactly as `agentTools` is, and for a sharper version of the same\n * reason: containment is not checked here because a build output legitimately\n * sits outside the repo root's own subtree in some layouts, and the reader that\n * resolves it is where an escaping path must be refused — with the service in\n * hand, so the refusal can name it. A config that throws takes down every\n * command, including the ones with nothing to do with contracts.\n */\nfunction readContracts(value: unknown, file: string): { openapi?: string } | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new ConfigError(\"contracts\", `${file}: \"contracts\" must be a mapping with an optional \"openapi\" path.`);\n }\n const record = value as Record<string, unknown>;\n // Only `openapi` today. The event axis gets a key when it gets a check —\n // a config key nothing reads is worse than a missing one, because a fleet\n // sets it and believes something is being graded.\n const out: { openapi?: string } = {};\n for (const key of [\"openapi\"] as const) {\n const spelled = record[key];\n if (spelled === undefined) continue;\n if (typeof spelled !== \"string\" || spelled === \"\") {\n throw new ConfigError(\"contracts\", `${file}: \"contracts.${key}\" must be a non-empty string when present.`);\n }\n out[key] = spelled;\n }\n return out.openapi === undefined ? undefined : out;\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/envelope/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join, parse as parsePath, resolve } from \"node:path\";\nimport { resolveInside } from \"../kernel/path-safety.js\";\nimport { docsDirOf, type DocsDir } from \"../kernel/ids/dirs.js\";\nimport { parseServiceId, type ServiceId } from \"../kernel/ids/service.js\";\n\n/** Local config, committed at the root of a service repo (or the docs repo itself). */\nexport const CONFIG_FILENAME = \"loam.json\";\n\nexport interface LoamConfig {\n /**\n * Path to the single shared docs repo (the source of truth). A LOADED config\n * carries it resolved — `parseConfig` resolves the file's spelling against\n * the config file's own directory, which is what earns the brand. The\n * spelling itself is `docsDirAsWritten`, and the shape `saveConfig` persists\n * (`StoredConfig`) keeps it a plain `string`: the stored form is deliberately\n * unresolved (see `storedDocsDir` in commands/init/options.ts), and branding\n * it would state a resolution that never happened.\n */\n docsDir: DocsDir;\n /**\n * Canonical id of the service in the current repo, if this is a service repo.\n *\n * Branded because `parseConfig` below is the one place that checks it, so\n * every command reading `config.service` is reading a name that has already\n * passed the grammar — which is why the config arm of\n * `opts.service ?? config.service` needs no second parse.\n */\n service?: ServiceId;\n /**\n * Where Gherkin lives in this service repo, relative to the repo root\n * (default \"features\" — the cucumber convention). `loam gherkin` writes only\n * inside `<gherkinDir>/loam/`; the rest of the directory stays the team's.\n * Left unresolved on purpose: commands resolve it against the repo they run\n * in, which is where the config file lives.\n */\n gherkinDir?: string;\n /**\n * Where this repo's BUILD writes its contract documents, relative to the\n * repo root — the springdoc/FastAPI/NestJS output, not a second copy anybody\n * maintains.\n *\n * loam reads these and compares a digest; it never writes them, never copies\n * them into the docs repo, and never derives meaning from them. That is what\n * keeps the field on the right side of the no-extractor line: it parses a\n * standard document somebody else generated, at a path a human named, in\n * exactly the way `verify --results` already ingests a cucumber report.\n *\n * The committed `services/<id>/openapi.yaml` stays the contract, and the copy\n * stays a human `cp` reviewed in a pull request. Automating the copy would\n * make the docs-repo document a cache of the build; leaving it manual and\n * merely REPORTING the divergence keeps it a document somebody agreed to.\n */\n contracts?: { openapi?: string };\n /**\n * The agent tools `loam init` has written command and skill files for in this\n * repo, by AGENT_TOOLS id.\n *\n * Recorded because the two ways a generated file can be absent look identical\n * on disk: the binary grew a command nobody has re-run `init` for, or nobody\n * ever selected that tool here. Only this list tells them apart, and `loam\n * doctor` needs the difference to know which absence is worth reporting.\n *\n * ACCUMULATED, never replaced — a later `init --tools cursor` does not\n * un-write the claude files an earlier run left on disk, so the record of\n * what this repo holds has to keep them too.\n *\n * Optional, and unvalidated against the registry on purpose: a config written\n * by an older binary has no list at all, and one written by a NEWER binary\n * may name a tool this one has never heard of. Neither may refuse to load.\n */\n agentTools?: string[];\n /**\n * Digests of generated agent files loam may safely refresh. Keys are\n * repo-relative portable paths; a file whose bytes differ from its recorded\n * digest is human-owned and is never overwritten.\n */\n agentFiles?: Record<string, string>;\n /** Which workflow subset `loam init` manages; absent means the compatible full set. */\n agentProfile?: \"full\" | \"service\" | \"docs\";\n /**\n * The directory the config was actually FOUND in — the repo root, which is\n * not necessarily the cwd now that discovery walks upward. Everything a\n * command resolves against \"this repo\" (gherkinDir, relative `sources:`)\n * belongs here, not against process.cwd(): running `loam gherkin` from\n * `src/deep/` must write the same files as running it from the root.\n *\n * Derived, never stored: `saveConfig` strips it, so it can never be spelled\n * in loam.json and drift from where the file really is.\n */\n root?: string;\n /**\n * `docsDir` exactly as the file spells it, before resolution. Kept because\n * \"relative\" and \"absolute\" are not recoverable from the resolved path, and\n * the difference is the whole point: an absolute docsDir in a COMMITTED\n * config is a path that only exists on the machine that ran `loam init`.\n *\n * Derived, never stored — same contract as `root`.\n */\n docsDirAsWritten?: string;\n}\n\n/**\n * A loam.json that exists but cannot be believed. Carries the field at fault\n * (null when the whole document is), because \"which field\" is the difference\n * between a fix and a guess — and because both `loadConfig` and `loam doctor`\n * report the same sentence from it.\n */\nexport class ConfigError extends Error {\n constructor(\n readonly field: string | null,\n message: string,\n ) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\n/**\n * The directories a config search may look in: `start`, then each ancestor, up\n * to AND INCLUDING the git root — or the filesystem root when there is no git\n * repo above.\n *\n * The git root is the stop because it is the boundary the user already thinks\n * in: a loam.json outside it belongs to some other checkout (or, worse, to a\n * home directory), and silently adopting it would point a service repo's\n * commands at a docs repo nobody in this repo ever chose.\n */\nfunction searchDirs(start: string): string[] {\n const out: string[] = [];\n let current = resolve(start);\n const root = parsePath(current).root;\n for (;;) {\n out.push(current);\n // `.git` is a directory in a normal clone and a FILE in a worktree or\n // submodule — existsSync answers both without caring which.\n if (existsSync(join(current, \".git\"))) return out;\n const parent = dirname(current);\n if (parent === current || current === root) return out;\n current = parent;\n }\n}\n\n/**\n * The files loam wrote into the repository this config governs and owns there,\n * relative to its root: loam.json itself and every agent file `agentFiles`\n * records. `sources.unwalked` leaves them out of its denominator — a check\n * that counted loam's own bookkeeping as a part of the service nobody read\n * fired in every service repo the moment `loam init` had run.\n */\nexport function loamOwnedFiles(config: LoamConfig): string[] {\n return [CONFIG_FILENAME, ...Object.keys(config.agentFiles ?? {})];\n}\n\n/** Where a config written HERE goes — never a discovered one. `init` and `saveConfig` use this. */\nexport function localConfigPath(cwd: string = process.cwd()): string {\n return resolve(cwd, CONFIG_FILENAME);\n}\n\n/**\n * The nearest loam.json at or above `cwd`, or null. Exported because `init` has\n * to be able to ask a different question from \"is there one here\": a config in\n * an ancestor already governs this directory, and a second one written below it\n * would quietly shadow the first for every command run from the subtree.\n */\nexport function findConfigPath(cwd: string = process.cwd()): string | null {\n for (const dir of searchDirs(cwd)) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * The config that governs `cwd`: the discovered one, or — when there is none —\n * where it would have to be written. The fallback keeps this total, which is\n * what lets a caller distinguish \"absent\" from \"present but unloadable\" with a\n * single existsSync (see `reportNoConfig`).\n */\nexport function configPath(cwd: string = process.cwd()): string {\n return findConfigPath(cwd) ?? localConfigPath(cwd);\n}\n\n/**\n * THE config validator. One implementation, because two of them are two\n * opinions: `loam doctor` used to carry its own copy, and a config doctor\n * called healthy could still be one every other command refused to load.\n *\n * `configDir` is the directory the file lives in — every relative path in the\n * document is resolved against it, so the doc comments above stay true no\n * matter where the caller happens to be standing.\n */\nexport function parseConfig(raw: string, configDir: string): LoamConfig {\n const file = join(configDir, CONFIG_FILENAME);\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch (err) {\n throw new ConfigError(null, `${file} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);\n }\n // Checked before any field access: `JSON.parse(\"null\")` and `JSON.parse(\"5\")`\n // are valid JSON, and reaching for `.docsDir` on them is how a config typo\n // used to surface as a raw Node TypeError instead of a named field.\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new ConfigError(null, `${file} must contain a JSON object.`);\n }\n const record = value as Record<string, unknown>;\n\n if (typeof record.docsDir !== \"string\" || record.docsDir === \"\") {\n throw new ConfigError(\"docsDir\", `${file}: \"docsDir\" must be a non-empty string.`);\n }\n // Same discipline as docsDir: a malformed fact refuses the whole config\n // rather than being silently defaulted over — `5` or `\"\"` here is a typo,\n // and defaulting would send generated files somewhere nobody chose.\n // `parseServiceId` rather than a bare check: the same call both refuses the\n // malformed value and produces the branded one below, so there is no window\n // in which a checked name is still typed as an unchecked string. It takes a\n // `string`, hence the shape guard first — the id grammar's own message is\n // about spelling, and `5` is not a spelling.\n let service: ServiceId | undefined;\n if (record.service !== undefined) {\n if (typeof record.service !== \"string\") {\n throw new ConfigError(\"service\", `${file}: \"service\" must be a string.`);\n }\n const parsed = parseServiceId(record.service, '\"service\"');\n if (!parsed.ok) throw new ConfigError(\"service\", `${file}: ${parsed.problem}`);\n service = parsed.id;\n }\n if (record.gherkinDir !== undefined\n && (typeof record.gherkinDir !== \"string\" || record.gherkinDir === \"\")) {\n throw new ConfigError(\"gherkinDir\", `${file}: \"gherkinDir\" must be a non-empty string when present.`);\n }\n const contracts = readContracts(record.contracts, file);\n // Shape only. Which ids are legal is the registry's question, and asking it\n // here would make a config written by a newer binary — one that knows a tool\n // this one does not — unloadable for every command, not just for `init`.\n if (record.agentTools !== undefined\n && (!Array.isArray(record.agentTools)\n || record.agentTools.some((t) => typeof t !== \"string\" || t === \"\"))) {\n throw new ConfigError(\n \"agentTools\",\n `${file}: \"agentTools\" must be an array of non-empty strings when present.`,\n );\n }\n if (record.agentFiles !== undefined) {\n if (record.agentFiles === null || typeof record.agentFiles !== \"object\" || Array.isArray(record.agentFiles)) {\n throw new ConfigError(\"agentFiles\", `${file}: \"agentFiles\" must be a path-to-digest mapping when present.`);\n }\n const entries = Object.entries(record.agentFiles as Record<string, unknown>);\n if (entries.some(([path, digest]) => path === \"\" || typeof digest !== \"string\" || !/^[a-f0-9]{64}$/.test(digest))) {\n throw new ConfigError(\n \"agentFiles\",\n `${file}: \"agentFiles\" must map non-empty paths to lowercase sha256 digests.`,\n );\n }\n }\n if (record.agentProfile !== undefined\n && record.agentProfile !== \"full\"\n && record.agentProfile !== \"service\"\n && record.agentProfile !== \"docs\") {\n throw new ConfigError(\n \"agentProfile\",\n `${file}: \"agentProfile\" must be one of: full, service, docs.`,\n );\n }\n if (typeof record.gherkinDir === \"string\") {\n // Validate the owned output directory, not merely its spelling. This also\n // catches an otherwise-contained path whose existing parent is a symlink\n // out of the service repo.\n try {\n resolveInside(configDir, join(record.gherkinDir, \"loam\"), `\"gherkinDir\"`);\n } catch (err) {\n throw new ConfigError(\"gherkinDir\", `${file}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return {\n // Resolved here, against the file's own directory, so the doc comment on\n // `docsDir` is true no matter where a caller later resolves the path from —\n // which is exactly the provenance `docsDirOf` demands.\n docsDir: docsDirOf(resolve(configDir, record.docsDir)),\n ...(service === undefined ? {} : { service }),\n ...(record.gherkinDir === undefined ? {} : { gherkinDir: record.gherkinDir as string }),\n ...(contracts === undefined ? {} : { contracts }),\n ...(record.agentTools === undefined ? {} : { agentTools: record.agentTools as string[] }),\n ...(record.agentFiles === undefined\n ? {}\n : { agentFiles: record.agentFiles as Record<string, string> }),\n ...(record.agentProfile === undefined\n ? {}\n : { agentProfile: record.agentProfile as \"full\" | \"service\" | \"docs\" }),\n root: resolve(configDir),\n docsDirAsWritten: record.docsDir,\n };\n}\n\n/**\n * Every way asking for the config can come out, as data. It used to be\n * `LoamConfig | null` with a `console.error` on the invalid arm — core's one\n * print outside the envelope adapter, and a null that collapsed \"no config\"\n * and \"a config nobody can read\" into one answer `reportNoConfig` then had to\n * re-derive from the filesystem (a TOCTOU: the file could change between the\n * load and the re-check). The union says which case it is, carries the parse\n * problem for the command layer to render, and prints nothing.\n */\nexport type ConfigLoad =\n | { kind: \"loaded\"; config: LoamConfig }\n | { kind: \"absent\" }\n | { kind: \"invalid\"; path: string; problem: string };\n\nexport async function loadConfig(cwd: string = process.cwd()): Promise<ConfigLoad> {\n const p = findConfigPath(cwd);\n if (p === null) return { kind: \"absent\" };\n try {\n // The read belongs INSIDE the try: `existsSync` only says the name is\n // taken, not that it names a file this process can read. A loam.json that\n // is a directory (EISDIR), one whose permissions refuse us (EACCES), or one\n // deleted between the search and the read (ENOENT) all fail here rather\n // than in `parseConfig` — and read outside, each of them escaped every\n // command as an `internal` crash carrying a bare errno. Caught here they\n // join the same \"exists but cannot be believed\" arm the parse failures take.\n return { kind: \"loaded\", config: parseConfig(await readFile(p, \"utf8\"), dirname(p)) };\n } catch (err) {\n return { kind: \"invalid\", path: p, problem: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/**\n * What `saveConfig` persists: `LoamConfig` with `docsDir` back to a plain\n * `string`, because the stored form is the SPELLING (deliberately relative —\n * `storedDocsDir`), not the resolved `DocsDir` a loaded config carries. The\n * two derived fields ride along as optional so a caller may spread a loaded\n * config forward — `saveConfig` strips them on the way out either way.\n */\nexport type StoredConfig = Omit<LoamConfig, \"docsDir\"> & { docsDir: string };\n\n/**\n * Write the config for the repo rooted at `cwd`. The derived fields are dropped\n * on the way out: they are facts about where the file was found and how it was\n * spelled, and a stored copy could only ever disagree with the file itself.\n */\nexport async function saveConfig(config: StoredConfig, cwd: string = process.cwd()): Promise<string> {\n const p = localConfigPath(cwd);\n const { root: _root, docsDirAsWritten: _asWritten, ...stored } = config;\n await writeFile(p, JSON.stringify(stored, null, 2) + \"\\n\", \"utf8\");\n return p;\n}\n\n/**\n * The optional `contracts` block, validated to shape only.\n *\n * SHAPE ONLY, exactly as `agentTools` is, and for a sharper version of the same\n * reason: containment is not checked here because a build output legitimately\n * sits outside the repo root's own subtree in some layouts, and the reader that\n * resolves it is where an escaping path must be refused — with the service in\n * hand, so the refusal can name it. A config that throws takes down every\n * command, including the ones with nothing to do with contracts.\n */\nfunction readContracts(value: unknown, file: string): { openapi?: string } | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new ConfigError(\"contracts\", `${file}: \"contracts\" must be a mapping with an optional \"openapi\" path.`);\n }\n const record = value as Record<string, unknown>;\n // Only `openapi` today. The event axis gets a key when it gets a check —\n // a config key nothing reads is worse than a missing one, because a fleet\n // sets it and believes something is being graded.\n const out: { openapi?: string } = {};\n for (const key of [\"openapi\"] as const) {\n const spelled = record[key];\n if (spelled === undefined) continue;\n if (typeof spelled !== \"string\" || spelled === \"\") {\n throw new ConfigError(\"contracts\", `${file}: \"contracts.${key}\" must be a non-empty string when present.`);\n }\n out[key] = spelled;\n }\n return out.openapi === undefined ? undefined : out;\n}\n"]}
@@ -21,6 +21,7 @@
21
21
  * where a key mirrors a frontmatter field verbatim; camelCase everywhere else.
22
22
  */
23
23
  import { relative } from "node:path";
24
+ import { loadConfig } from "./config.js";
24
25
  import { LOAM_VERSION } from "./version.js";
25
26
  /**
26
27
  * Version of the top-level JSON envelope, independent of the CLI/package
@@ -147,6 +148,22 @@ export function fail(json, code, message) {
147
148
  sayExplain(code);
148
149
  process.exitCode = 1;
149
150
  }
151
+ /**
152
+ * A refusal or failure with its stable `--json` code attached, for the commands
153
+ * whose refusals are THROWN — from a plan phase or a rollback path several
154
+ * calls below the one handler that prints — rather than returned. The handler
155
+ * `fail`s with `code`, so a merge that could not be computed reports as a merge
156
+ * failure rather than as `internal`. What anything ELSE that escapes means is
157
+ * each handler's to say. One class, because archive, unarchive and
158
+ * migrate-openspec had each written this same one.
159
+ */
160
+ export class CodedFailure extends Error {
161
+ code;
162
+ constructor(code, message) {
163
+ super(message);
164
+ this.code = code;
165
+ }
166
+ }
150
167
  /** The message every command prints when there is no loam.json. */
151
168
  const NO_CONFIG_MESSAGE = "No loam.json found. Run `loam init --docs <dir>` first.";
152
169
  /**
@@ -172,4 +189,21 @@ export function reportNoConfig(json, load) {
172
189
  }
173
190
  fail(json, "no-config", NO_CONFIG_MESSAGE);
174
191
  }
192
+ /**
193
+ * The config a command action stands on, or null once `reportNoConfig` has
194
+ * refused — the caller's whole answer to null is `return`. Every command that
195
+ * needs a wired repo opens with this question, and it was spelled as the same
196
+ * six lines in each of them; one spelling is what keeps the absent/invalid
197
+ * split above from being forgotten in a twenty-fourth. `init` and `doctor` do
198
+ * not come through here: a missing or broken config is their subject, not a
199
+ * reason to stop.
200
+ */
201
+ export async function configOrRefuse(json) {
202
+ const loaded = await loadConfig();
203
+ if (loaded.kind !== "loaded") {
204
+ reportNoConfig(json, loaded);
205
+ return null;
206
+ }
207
+ return loaded.config;
208
+ }
175
209
  //# sourceMappingURL=json.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"json.js","sourceRoot":"","sources":["../../../src/core/envelope/json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AA0H3C;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAgC;IACvD,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,SAAS,CAAC,EAAE,eAAe,EAAE,qBAAqB,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CACjH,CAAC;AACJ,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,GAAW;IACnD,OAAO,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAe,EACf,OAAe,EACf,OAAO,GAA4B,EAAE;IAErC,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,SAAS,CACZ,EAAE,eAAe,EAAE,qBAAqB,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,EAClH,IAAI,EACJ,CAAC,CACF,CACF,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACrB,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA2B,IAAI,GAAG,CAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC;AAEjG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,UAAU,CAAC,IAAe;IACxC,IAAI,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO;IACzC,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,qBAAqB,IAAI,EAAE,CAAC,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,IAAI,CAAC,IAAa,EAAE,IAAe,EAAE,OAAe;IAClE,IAAI,IAAI,EAAE,CAAC;QACT,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC7B,OAAO;IACT,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvB,UAAU,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC;AAED,mEAAmE;AACnE,MAAM,iBAAiB,GAAG,yDAAyD,CAAC;AAEpF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,yDAAyD,CAAC;AAE5F;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa,EAAE,IAA6E;IACzH,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CACF,IAAI,EACJ,gBAAgB,EAChB,sBAAsB,IAAI,CAAC,OAAO,mDAAmD,CACtF,CAAC;QACF,OAAO;IACT,CAAC;IACD,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;AAC7C,CAAC","sourcesContent":["/**\n * The `--json` envelope — loam's machine contract.\n *\n * Every JSON response carries `ok`. Success spreads the payload alongside it;\n * failure carries a stable `error.code` a caller can branch on without parsing\n * prose. Errors go to stdout too: a consumer reads one stream and always gets\n * valid JSON, whether the command succeeded or not. The exit code still tells\n * the shell what happened.\n *\n * Three keys identify the envelope before its payload is read, and they answer\n * three different questions: `contractVersion` is the SHAPE's version,\n * `version` is the BINARY's, and `command` (on every success payload) says\n * which verb produced it. The first two are stamped by the emitters below so\n * no command can forget them; the third is the payload's own first key,\n * because only the command knows its name.\n *\n * Key casing: camelCase, with one deliberate exception — a key that mirrors a\n * frontmatter field verbatim keeps that field's snake_case spelling\n * (`last_verified`, `sources_digest` in vouch's payload), so the envelope and\n * the document it describes spell the same fact the same way. snake_case only\n * where a key mirrors a frontmatter field verbatim; camelCase everywhere else.\n */\nimport { relative } from \"node:path\";\nimport { LOAM_VERSION } from \"./version.js\";\n\n/**\n * Version of the top-level JSON envelope, independent of the CLI/package\n * version. Additive payload fields do not require a bump; incompatible changes\n * to `ok`, `error`, or their semantics do.\n */\nexport const JSON_CONTRACT_VERSION = \"1.0\";\n\n/**\n * Stable failure codes. Prose may change; these may not.\n *\n * The `sources-*` pair mirrors the `sources.*` finding codes on purpose: the\n * same breach is recognisable whether it arrives as a refusal from `vouch` or\n * as a finding from `validate`.\n *\n * The `feature-active` / `snapshot-*` group is `unarchive` refusing to guess:\n * each names a different reason the undo is not one, and a caller has to tell\n * them apart to know whether re-running could ever work. Its commit failures\n * split the same way archive's do: `restore-failed` means nothing was restored\n * or everything was rolled back — the living docs are unchanged and re-running\n * can work; `rollback-incomplete` (shared with archive, same meaning) means\n * some files could not be put back, and the message lists them.\n *\n * The `answers-*` group is `loam verify --record` refusing an answer set that\n * does not answer the current checklist. They are separated for the same reason:\n * an unreadable file is a bug in whatever wrote it, a mismatch means the feature\n * moved and the claims have to be answered again, and an unevidenced\n * confirmation is the one an agent can fix on the spot.\n *\n * The archive group is `loam archive` refusing or failing, and each code is a\n * different answer to \"what do I fix, and can I trust the repo?\": `not-coherent`\n * is the gate — issues in the FEATURE, carried in `issues[]`. Most yield to\n * `--approve`; the ones that do not name an illegal service id — a\n * `specs/<svc>/` directory name (`delta.service-id-invalid`) or a\n * `metadata { service }` binding (`c4.service-binding-invalid`) — because the\n * name IS the path the merge would write, and the fix is a rename, never a\n * flag. A consumer branches on data, not prose: coherence issues carry the\n * additive `overridable` key resolved per issue, and a refusal `--approve`\n * cannot move says \"--approve does not override this\" in `error.message`;\n * `living-outside-requirements` is the refusal whose fault is not the\n * feature's — the LIVING spec holds requirements outside `## Requirements`,\n * and the merge would duplicate them, so the fix is in the living docs, and\n * `--approve` does not move it either; `archive-exists` is a\n * destination collision under `features/archive/`; `merge-failed` is a merge\n * that could not be computed or was rolled back — either way the living docs\n * are unchanged; `rollback-incomplete` is the one that demands a human: the\n * merge failed AND some files could not be restored.\n *\n * `config-invalid` is distinct from `no-config` because the fixes point in\n * opposite directions: a missing config wants `loam init`, a corrupt one wants\n * repair — and an agent that ran `init` on a corrupt file would silently\n * rewrite it.\n *\n * `internal` is the one code with no stable meaning: an unexpected throw. It\n * exists so a `--json` consumer still receives an envelope instead of a stack\n * trace on stdout's sibling stream.\n */\nexport type ErrorCode =\n | \"no-config\"\n | \"config-invalid\"\n | \"unknown-target\"\n | \"invalid-option\"\n | \"already-exists\"\n | \"sources-absent\"\n | \"sources-path-missing\"\n | \"not-coherent\"\n | \"living-outside-requirements\"\n | \"archive-exists\"\n | \"merge-failed\"\n | \"rollback-incomplete\"\n | \"feature-active\"\n | \"snapshot-missing\"\n | \"snapshot-stale\"\n /** A snapshot pre-image whose bytes no longer match the digest archive recorded for them: `unarchive` will not restore text nobody wrote. */\n | \"snapshot-corrupt\"\n /** A `loam archive`/`unarchive` was killed mid-commit: the living docs are half-written, and this run refuses rather than writing over the evidence. */\n | \"commit-interrupted\"\n | \"restore-failed\"\n | \"answers-unreadable\"\n | \"answers-mismatch\"\n | \"answers-unevidenced\"\n | \"service-mismatch\"\n | \"unknown-service\"\n | \"repository-unavailable\"\n /** `loam verify --record` without `--service` over a federated (schema 2) record: it would erase other repositories' attestations. */\n | \"record-federated\"\n /** A `verification.yaml` that exists but cannot be read as a record — never overwritten, never reported as absent. */\n | \"record-unreadable\"\n /** `loam verify --record` found the record changed between its locked read and the swap — an editor or a lock-ignoring writer landed first. Nothing was written; re-running merges over the record as it now stands. */\n | \"record-raced\"\n /** `loam gherkin <FEAT>` refusing to overwrite a `.feature` file owned by another feature still in flight. */\n | \"gherkin-conflict\"\n /** `loam vouch` found the spec changed under it between reading and stamping — another vouch or an edit landed first, and nothing was written. */\n | \"vouch-raced\"\n /** `loam vouch` with nothing on the other end of stdin and no `--yes`: the stamp is a person's claim, and nobody was asked. */\n | \"vouch-unattended\"\n /** `loam vouch` could not learn who is vouching — git names no `user.email` here, so the stamp would record a claim with nobody behind it. */\n | \"vouch-unattributable\"\n /** `loam vouch` was asked to confirm and the answer was no. Nothing was stamped; this is a successful refusal, not a failure. */\n | \"vouch-declined\"\n /** `docsDir` in loam.json points at nothing: the docs repo was never cloned, or the path is wrong. A read command refuses rather than reporting an empty fleet. */\n | \"docs-missing\"\n /** `docsDir` is a directory but has no `services/`: it is some other directory, most often the service repo itself after a typo. */\n | \"services-missing\"\n /** Another loam writer holds the docs repo's advisory lock: nothing was read or written, and re-running once it finishes works. `verify --record` waits out a short holder before giving this answer. */\n | \"docs-busy\"\n /** `loam subsystem rm` refusing a subsystem that still holds members — services or child subsystems, named in the message. A destructive command never picks targets the caller did not name: move them out (or remove the children) first, then re-run. */\n | \"subsystem-not-empty\"\n /** `loam subsystem move`/`rename` refusing because git reports uncommitted or untracked paths under a directory being moved — the ONLY move-specific refusal: the rename would sweep them into a move nobody reviewed. Commit them, `git stash -u` (plain `stash` leaves untracked files behind), or remove them, then re-run. When git cannot answer at all the move proceeds — refusal needs positive evidence. */\n | \"move-uncommitted\"\n /** A subsystem move/rename failed and was rolled back cleanly: every rename undone, the generated views file restored, the docs unchanged — re-running can work. Distinct from `merge-failed` because no merge was computed; a failure that could NOT be fully undone is `rollback-incomplete`, exactly as for archive. */\n | \"move-failed\"\n /** `loam open` found no service checkout bound to this docs repo under any scanned root — the workspace would hold only the docs repo. Re-running can succeed once a bound checkout exists beside it, or with `--root` pointing where the checkouts live. */\n | \"no-members\"\n /** Two discovered repositories' committed loam.json files declare the same `service` for this docs repo — two checkouts of one service, or a copied config. loam will not guess which checkout speaks for the service; narrow the scan with `--root` or fix the stray binding, then re-run. */\n | \"binding-duplicate\"\n /** `loam list --owners` could not use the user-named CODEOWNERS file: the path cannot be read, or a line in it cannot be parsed as `pattern owner…` (the message names the line). Fail-closed like `answers-unreadable` for the other user-named file — a half-read ownership file must never file a service under the wrong team. Re-running succeeds once the path or the line is fixed. */\n | \"owners-unreadable\"\n /** `loam seed`'s fleet file is missing, unreadable, not YAML, the wrong shape, carries an illegal id or name, or no longer names every existing `services/<id>/` (that arm carries the additive `missingServices` payload key). The message names the file and, where one exists, the line; editing the file and re-running succeeds. */\n | \"seed-file-invalid\"\n /** fleet.yaml declares one name twice — as two services, or as both a service and an external/subsystem. Service ids, subsystem names and externals share one flat namespace (a call endpoint must name exactly one thing); the message names both declaration lines. Rename one and re-run. */\n | \"seed-duplicate-service\"\n /** A service's `subsystem:` in fleet.yaml names nothing `subsystems:` declares; the message carries a did-you-mean hint over the names the file really declares. Fix the spelling (or add the subsystem) and re-run. */\n | \"seed-unknown-subsystem\"\n /** architecture/landscape.likec4 carries hand edits (the line-1 stamp's digest no longer matches) or was authored some other way (no stamp, and not the scaffold's untouched stub). Seed never overwrites human work and nothing was written; re-running cannot succeed until the file is deleted or the edits are folded into fleet.yaml. */\n | \"seed-landscape-edited\"\n | \"internal\";\n\n/**\n * `version` is stamped HERE rather than by each command for the reason the\n * `command` discriminator was not, and had to be retrofitted one call site at a\n * time: a key every envelope owes is a key no command can be trusted to\n * remember. Two lines in the two emitters give it to every command and every\n * refusal at once, and a command added tomorrow cannot ship without it.\n *\n * Why a consumer needs it at all: `docs.binary-behind` exists to say that a\n * green run from an old binary is worth less than it looks, and a caller\n * holding only a `--json` payload could not tell which loam produced it — so it\n * could not apply that caution, and could not see a mixed-version fleet in the\n * contract. It sits beside `contractVersion`, never inside it: the envelope\n * SHAPE and the binary that filled it version independently, and collapsing\n * them would make every release look like a contract break.\n */\nexport function emitJson(payload: Record<string, unknown>): void {\n console.log(\n JSON.stringify({ contractVersion: JSON_CONTRACT_VERSION, version: LOAM_VERSION, ok: true, ...payload }, null, 2),\n );\n}\n\n/** Paths in the contract are repo-relative, with forward slashes: diffable across machines. */\nexport function repoPath(docsDir: string, abs: string): string {\n return relative(docsDir, abs).split(/[\\\\/]/).join(\"/\");\n}\n\n/**\n * Emit a failure envelope and set the exit code. Returns false, to `return`\n * from a caller.\n *\n * `version` is on this path too, and deliberately: a refusal is the envelope a\n * consumer is most likely to be holding at the moment it needs to know which\n * binary answered — \"this loam does not have that flag / that code\" is a\n * question about the build, and an unversioned refusal makes it unanswerable.\n *\n * There is no `command` here, and adding one is not a small change. The error\n * emitter has no command in scope: `fail()` is called from deep inside core-ish\n * helpers that never learn which verb the user typed, so threading it means\n * either module-level mutable state — a hazard this codebase names outright,\n * because a value cached at import time leaks across the forked test processes\n * and across invocations in a long-running host — or one more argument threaded\n * through `fail()`, `reportNoConfig()` and every deep helper that refuses, each\n * of which would then carry a value it has no other use for. A consumer that\n * needs to know which command refused already knows: it made the call.\n */\nexport function emitJsonError(\n code: ErrorCode,\n message: string,\n details: Record<string, unknown> = {},\n): false {\n console.log(\n JSON.stringify(\n { contractVersion: JSON_CONTRACT_VERSION, version: LOAM_VERSION, ok: false, error: { code, message }, ...details },\n null,\n 2,\n ),\n );\n process.exitCode = 1;\n return false;\n}\n\n/**\n * The refusals that print no `loam explain` pointer, each with the reason it is\n * here. ONE named set rather than a condition per call site: \"is this refusal\n * worth looking up?\" is a single question, and answering it in seven places is\n * how two of them come to disagree.\n *\n * - `vouch-declined` — loam asked whether to stamp, and a person said no.\n * Nothing failed and there is nothing to fix, so a pointer would invite\n * somebody to go read the meaning of their own answer.\n *\n * `internal` was weighed and deliberately LEFT OUT. It is the one code with no\n * stable meaning, which reads like a reason to stay silent — but its\n * explanation carries the only instruction available to a person staring at an\n * unexpected throw (a repeatable `internal` is a loam defect worth reporting),\n * and that is worth more than the silence the old behaviour gave them.\n *\n * Exported so test/explain.test.ts can pin the membership: an entry added here\n * silently removes a pointer nobody would notice was gone.\n */\nexport const NO_EXPLAIN_POINTER: ReadonlySet<ErrorCode> = new Set<ErrorCode>([\"vouch-declined\"]);\n\n/**\n * The one-line pointer from a refusal to the command that explains it.\n *\n * Measured over eight wrong invocations a person makes in their first hour —\n * no loam.json, an unknown service, an unknown feature, an unbound `gherkin`,\n * a malformed feature id — every single one printed prose and no code, and\n * only `loam validate` mentioned `loam explain` at all. `explain` is the best\n * usability asset loam has and the message that most needs it could not reach\n * it: to look a refusal up you first had to re-run the whole command with\n * `--json` and read `error.code` out of the envelope, which is precisely the\n * knowledge a first-hour user does not have. The code is printed as well as\n * the invocation because the two are different needs: the bare code is what\n * goes into a bug report, a grep or a CI branch, and the invocation is what\n * answers it here.\n *\n * STDERR, never stdout, and that is the load-bearing half: a refusal's\n * diagnostic must not pollute a piped payload. `loam show svc > out.json`\n * already keeps its message off stdout, and a pointer that broke that rule\n * would corrupt exactly the pipelines text mode was careful never to touch.\n * (Findings do the opposite — their codes go to stdout with the report, in\n * document order, for validate/report.ts's own reason. Same fact, two streams,\n * because a finding is the answer and a refusal is the absence of one.)\n *\n * Nothing runs here under `--json`: the envelope already carries `error.code`\n * as data, and a machine reader has no use for being told a second command\n * exists — the same doctrine `EXPLAIN_FOOTER` follows in\n * commands/policy/format.ts.\n *\n * No ANSI escape, though the line is meant to READ as dim. Nothing in this\n * codebase emits escape sequences, the output is graded by tests that compare\n * strings, and a colour that survives into a redirected file or a CI log is\n * noise nobody asked for. The typography does the dimming: the code, a spaced\n * middot, the command — an aside, not a sentence.\n */\nexport function sayExplain(code: ErrorCode): void {\n if (NO_EXPLAIN_POINTER.has(code)) return;\n console.error(`${code} · loam explain ${code}`);\n}\n\n/**\n * Report a failure in whichever mode the caller is in. Text mode goes to\n * stderr as it always has — now with the code and its lookup on a second line;\n * JSON mode goes into the envelope, whose stdout bytes are unchanged.\n */\nexport function fail(json: boolean, code: ErrorCode, message: string): void {\n if (json) {\n emitJsonError(code, message);\n return;\n }\n console.error(message);\n sayExplain(code);\n process.exitCode = 1;\n}\n\n/** The message every command prints when there is no loam.json. */\nconst NO_CONFIG_MESSAGE = \"No loam.json found. Run `loam init --docs <dir>` first.\";\n\n/**\n * The message the service-scoped commands print when neither `--service` nor\n * loam.json names one. Four commands ask the same question and owe the same two\n * answers, and a fifth would have been written by hand. `loam delta` keeps its\n * own longer sentence on purpose — it can say which feature was being read.\n */\nexport const NO_SERVICE_MESSAGE = \"No service. Pass --service <id> or set it in loam.json.\";\n\n/**\n * Report \"no config\" in whichever mode the caller is in — distinguishing a\n * config that is absent from one that exists but would not load, because the\n * two point at opposite fixes: a missing config wants `loam init`, a corrupt\n * one wants repair, and `init` would silently rewrite the corrupt file. The\n * distinction arrives as DATA on the load outcome rather than being re-derived\n * from the filesystem here — the file could change between the load and a\n * re-check, and the parse problem itself is worth showing.\n */\nexport function reportNoConfig(json: boolean, load: { kind: \"absent\" } | { kind: \"invalid\"; path: string; problem: string }): void {\n if (load.kind === \"invalid\") {\n fail(\n json,\n \"config-invalid\",\n `Invalid loam.json: ${load.problem}. Fix it (or delete it and re-run \\`loam init\\`).`,\n );\n return;\n }\n fail(json, \"no-config\", NO_CONFIG_MESSAGE);\n}\n"]}
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../../../src/core/envelope/json.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `--json` envelope — loam's machine contract.\n *\n * Every JSON response carries `ok`. Success spreads the payload alongside it;\n * failure carries a stable `error.code` a caller can branch on without parsing\n * prose. Errors go to stdout too: a consumer reads one stream and always gets\n * valid JSON, whether the command succeeded or not. The exit code still tells\n * the shell what happened.\n *\n * Three keys identify the envelope before its payload is read, and they answer\n * three different questions: `contractVersion` is the SHAPE's version,\n * `version` is the BINARY's, and `command` (on every success payload) says\n * which verb produced it. The first two are stamped by the emitters below so\n * no command can forget them; the third is the payload's own first key,\n * because only the command knows its name.\n *\n * Key casing: camelCase, with one deliberate exception — a key that mirrors a\n * frontmatter field verbatim keeps that field's snake_case spelling\n * (`last_verified`, `sources_digest` in vouch's payload), so the envelope and\n * the document it describes spell the same fact the same way. snake_case only\n * where a key mirrors a frontmatter field verbatim; camelCase everywhere else.\n */\nimport { relative } from \"node:path\";\nimport { loadConfig, type LoamConfig } from \"./config.js\";\nimport { LOAM_VERSION } from \"./version.js\";\n\n/**\n * Version of the top-level JSON envelope, independent of the CLI/package\n * version. Additive payload fields do not require a bump; incompatible changes\n * to `ok`, `error`, or their semantics do.\n */\nexport const JSON_CONTRACT_VERSION = \"1.0\";\n\n/**\n * Stable failure codes. Prose may change; these may not.\n *\n * The `sources-*` pair mirrors the `sources.*` finding codes on purpose: the\n * same breach is recognisable whether it arrives as a refusal from `vouch` or\n * as a finding from `validate`.\n *\n * The `feature-active` / `snapshot-*` group is `unarchive` refusing to guess:\n * each names a different reason the undo is not one, and a caller has to tell\n * them apart to know whether re-running could ever work. Its commit failures\n * split the same way archive's do: `restore-failed` means nothing was restored\n * or everything was rolled back — the living docs are unchanged and re-running\n * can work; `rollback-incomplete` (shared with archive, same meaning) means\n * some files could not be put back, and the message lists them.\n *\n * The `answers-*` group is `loam verify --record` refusing an answer set that\n * does not answer the current checklist. They are separated for the same reason:\n * an unreadable file is a bug in whatever wrote it, a mismatch means the feature\n * moved and the claims have to be answered again, and an unevidenced\n * confirmation is the one an agent can fix on the spot.\n *\n * The archive group is `loam archive` refusing or failing, and each code is a\n * different answer to \"what do I fix, and can I trust the repo?\": `not-coherent`\n * is the gate — issues in the FEATURE, carried in `issues[]`. Most yield to\n * `--approve`; the ones that do not name an illegal service id — a\n * `specs/<svc>/` directory name (`delta.service-id-invalid`) or a\n * `metadata { service }` binding (`c4.service-binding-invalid`) — because the\n * name IS the path the merge would write, and the fix is a rename, never a\n * flag. A consumer branches on data, not prose: coherence issues carry the\n * additive `overridable` key resolved per issue, and a refusal `--approve`\n * cannot move says \"--approve does not override this\" in `error.message`;\n * `living-outside-requirements` is the refusal whose fault is not the\n * feature's — the LIVING spec holds requirements outside `## Requirements`,\n * and the merge would duplicate them, so the fix is in the living docs, and\n * `--approve` does not move it either; `archive-exists` is a\n * destination collision under `features/archive/`; `merge-failed` is a merge\n * that could not be computed or was rolled back — either way the living docs\n * are unchanged; `rollback-incomplete` is the one that demands a human: the\n * merge failed AND some files could not be restored.\n *\n * `config-invalid` is distinct from `no-config` because the fixes point in\n * opposite directions: a missing config wants `loam init`, a corrupt one wants\n * repair — and an agent that ran `init` on a corrupt file would silently\n * rewrite it.\n *\n * `internal` is the one code with no stable meaning: an unexpected throw. It\n * exists so a `--json` consumer still receives an envelope instead of a stack\n * trace on stdout's sibling stream.\n */\nexport type ErrorCode =\n | \"no-config\"\n | \"config-invalid\"\n | \"unknown-target\"\n | \"invalid-option\"\n | \"already-exists\"\n | \"sources-absent\"\n | \"sources-path-missing\"\n | \"not-coherent\"\n | \"living-outside-requirements\"\n | \"archive-exists\"\n | \"merge-failed\"\n | \"rollback-incomplete\"\n | \"feature-active\"\n | \"snapshot-missing\"\n | \"snapshot-stale\"\n /** A snapshot pre-image whose bytes no longer match the digest archive recorded for them: `unarchive` will not restore text nobody wrote. */\n | \"snapshot-corrupt\"\n /** A `loam archive`/`unarchive` was killed mid-commit: the living docs are half-written, and this run refuses rather than writing over the evidence. */\n | \"commit-interrupted\"\n | \"restore-failed\"\n | \"answers-unreadable\"\n | \"answers-mismatch\"\n | \"answers-unevidenced\"\n | \"service-mismatch\"\n | \"unknown-service\"\n | \"repository-unavailable\"\n /** `loam verify --record` without `--service` over a federated (schema 2) record: it would erase other repositories' attestations. */\n | \"record-federated\"\n /** A file of the verification record (`verification.yaml` or a `verification/<svc>.yaml`) that exists but cannot be read — never overwritten, never reported as absent. */\n | \"record-unreadable\"\n /** `loam verify --record` found the record changed between its locked read and the swap — an editor or a lock-ignoring writer landed first. Nothing was written; re-running merges over the record as it now stands. */\n | \"record-raced\"\n /** `loam gherkin <FEAT>` refusing to overwrite a `.feature` file owned by another feature still in flight. */\n | \"gherkin-conflict\"\n /** `loam vouch` found the spec changed under it between reading and stamping — another vouch or an edit landed first, and nothing was written. */\n | \"vouch-raced\"\n /** `loam vouch` with nothing on the other end of stdin and no `--yes`: the stamp is a person's claim, and nobody was asked. */\n | \"vouch-unattended\"\n /** `loam vouch` could not learn who is vouching — git names no `user.email` here, so the stamp would record a claim with nobody behind it. */\n | \"vouch-unattributable\"\n /** `loam vouch` was asked to confirm and the answer was no. Nothing was stamped; this is a successful refusal, not a failure. */\n | \"vouch-declined\"\n /** `docsDir` in loam.json points at nothing: the docs repo was never cloned, or the path is wrong. A read command refuses rather than reporting an empty fleet. */\n | \"docs-missing\"\n /** `docsDir` is a directory but has no `services/`: it is some other directory, most often the service repo itself after a typo. */\n | \"services-missing\"\n /** Another loam writer holds the docs repo's advisory lock: nothing was read or written, and re-running once it finishes works. `verify --record` waits out a short holder before giving this answer. */\n | \"docs-busy\"\n /** `loam subsystem rm` refusing a subsystem that still holds members — services or child subsystems, named in the message. A destructive command never picks targets the caller did not name: move them out (or remove the children) first, then re-run. */\n | \"subsystem-not-empty\"\n /** `loam subsystem move`/`rename` refusing because git reports uncommitted or untracked paths under a directory being moved — the ONLY move-specific refusal: the rename would sweep them into a move nobody reviewed. Commit them, `git stash -u` (plain `stash` leaves untracked files behind), or remove them, then re-run. When git cannot answer at all the move proceeds — refusal needs positive evidence. */\n | \"move-uncommitted\"\n /** A subsystem move/rename failed and was rolled back cleanly: every rename undone, the generated views file restored, the docs unchanged — re-running can work. Distinct from `merge-failed` because no merge was computed; a failure that could NOT be fully undone is `rollback-incomplete`, exactly as for archive. */\n | \"move-failed\"\n /** `loam open` found no service checkout bound to this docs repo under any scanned root — the workspace would hold only the docs repo. Re-running can succeed once a bound checkout exists beside it, or with `--root` pointing where the checkouts live. */\n | \"no-members\"\n /** Two discovered repositories' committed loam.json files declare the same `service` for this docs repo — two checkouts of one service, or a copied config. loam will not guess which checkout speaks for the service; narrow the scan with `--root` or fix the stray binding, then re-run. */\n | \"binding-duplicate\"\n /** `loam list --owners` could not use the user-named CODEOWNERS file: the path cannot be read, or a line in it cannot be parsed as `pattern owner…` (the message names the line). Fail-closed like `answers-unreadable` for the other user-named file — a half-read ownership file must never file a service under the wrong team. Re-running succeeds once the path or the line is fixed. */\n | \"owners-unreadable\"\n /** `loam seed`'s fleet file is missing, unreadable, not YAML, the wrong shape, carries an illegal id or name, or no longer names every existing `services/<id>/` (that arm carries the additive `missingServices` payload key). The message names the file and, where one exists, the line; editing the file and re-running succeeds. */\n | \"seed-file-invalid\"\n /** fleet.yaml declares one name twice — as two services, or as both a service and an external/subsystem. Service ids, subsystem names and externals share one flat namespace (a call endpoint must name exactly one thing); the message names both declaration lines. Rename one and re-run. */\n | \"seed-duplicate-service\"\n /** A service's `subsystem:` in fleet.yaml names nothing `subsystems:` declares; the message carries a did-you-mean hint over the names the file really declares. Fix the spelling (or add the subsystem) and re-run. */\n | \"seed-unknown-subsystem\"\n /** architecture/landscape.likec4 carries hand edits (the line-1 stamp's digest no longer matches) or was authored some other way (no stamp, and not the scaffold's untouched stub). Seed never overwrites human work and nothing was written; re-running cannot succeed until the file is deleted, which loses nothing only while every edit is something fleet.yaml can say. Once an edge carries an op (which fleet.yaml cannot), the map grows through `loam adopt` and feature C4 deltas instead, never through seed again. */\n | \"seed-landscape-edited\"\n | \"internal\";\n\n/**\n * `version` is stamped HERE rather than by each command for the reason the\n * `command` discriminator was not, and had to be retrofitted one call site at a\n * time: a key every envelope owes is a key no command can be trusted to\n * remember. Two lines in the two emitters give it to every command and every\n * refusal at once, and a command added tomorrow cannot ship without it.\n *\n * Why a consumer needs it at all: `docs.binary-behind` exists to say that a\n * green run from an old binary is worth less than it looks, and a caller\n * holding only a `--json` payload could not tell which loam produced it — so it\n * could not apply that caution, and could not see a mixed-version fleet in the\n * contract. It sits beside `contractVersion`, never inside it: the envelope\n * SHAPE and the binary that filled it version independently, and collapsing\n * them would make every release look like a contract break.\n */\nexport function emitJson(payload: Record<string, unknown>): void {\n console.log(\n JSON.stringify({ contractVersion: JSON_CONTRACT_VERSION, version: LOAM_VERSION, ok: true, ...payload }, null, 2),\n );\n}\n\n/** Paths in the contract are repo-relative, with forward slashes: diffable across machines. */\nexport function repoPath(docsDir: string, abs: string): string {\n return relative(docsDir, abs).split(/[\\\\/]/).join(\"/\");\n}\n\n/**\n * Emit a failure envelope and set the exit code. Returns false, to `return`\n * from a caller.\n *\n * `version` is on this path too, and deliberately: a refusal is the envelope a\n * consumer is most likely to be holding at the moment it needs to know which\n * binary answered — \"this loam does not have that flag / that code\" is a\n * question about the build, and an unversioned refusal makes it unanswerable.\n *\n * There is no `command` here, and adding one is not a small change. The error\n * emitter has no command in scope: `fail()` is called from deep inside core-ish\n * helpers that never learn which verb the user typed, so threading it means\n * either module-level mutable state — a hazard this codebase names outright,\n * because a value cached at import time leaks across the forked test processes\n * and across invocations in a long-running host — or one more argument threaded\n * through `fail()`, `reportNoConfig()` and every deep helper that refuses, each\n * of which would then carry a value it has no other use for. A consumer that\n * needs to know which command refused already knows: it made the call.\n */\nexport function emitJsonError(\n code: ErrorCode,\n message: string,\n details: Record<string, unknown> = {},\n): false {\n console.log(\n JSON.stringify(\n { contractVersion: JSON_CONTRACT_VERSION, version: LOAM_VERSION, ok: false, error: { code, message }, ...details },\n null,\n 2,\n ),\n );\n process.exitCode = 1;\n return false;\n}\n\n/**\n * The refusals that print no `loam explain` pointer, each with the reason it is\n * here. ONE named set rather than a condition per call site: \"is this refusal\n * worth looking up?\" is a single question, and answering it in seven places is\n * how two of them come to disagree.\n *\n * - `vouch-declined` — loam asked whether to stamp, and a person said no.\n * Nothing failed and there is nothing to fix, so a pointer would invite\n * somebody to go read the meaning of their own answer.\n *\n * `internal` was weighed and deliberately LEFT OUT. It is the one code with no\n * stable meaning, which reads like a reason to stay silent — but its\n * explanation carries the only instruction available to a person staring at an\n * unexpected throw (a repeatable `internal` is a loam defect worth reporting),\n * and that is worth more than the silence the old behaviour gave them.\n *\n * Exported so test/explain.test.ts can pin the membership: an entry added here\n * silently removes a pointer nobody would notice was gone.\n */\nexport const NO_EXPLAIN_POINTER: ReadonlySet<ErrorCode> = new Set<ErrorCode>([\"vouch-declined\"]);\n\n/**\n * The one-line pointer from a refusal to the command that explains it.\n *\n * Measured over eight wrong invocations a person makes in their first hour —\n * no loam.json, an unknown service, an unknown feature, an unbound `gherkin`,\n * a malformed feature id — every single one printed prose and no code, and\n * only `loam validate` mentioned `loam explain` at all. `explain` is the best\n * usability asset loam has and the message that most needs it could not reach\n * it: to look a refusal up you first had to re-run the whole command with\n * `--json` and read `error.code` out of the envelope, which is precisely the\n * knowledge a first-hour user does not have. The code is printed as well as\n * the invocation because the two are different needs: the bare code is what\n * goes into a bug report, a grep or a CI branch, and the invocation is what\n * answers it here.\n *\n * STDERR, never stdout, and that is the load-bearing half: a refusal's\n * diagnostic must not pollute a piped payload. `loam show svc > out.json`\n * already keeps its message off stdout, and a pointer that broke that rule\n * would corrupt exactly the pipelines text mode was careful never to touch.\n * (Findings do the opposite — their codes go to stdout with the report, in\n * document order, for validate/report.ts's own reason. Same fact, two streams,\n * because a finding is the answer and a refusal is the absence of one.)\n *\n * Nothing runs here under `--json`: the envelope already carries `error.code`\n * as data, and a machine reader has no use for being told a second command\n * exists — the same doctrine `EXPLAIN_FOOTER` follows in\n * commands/policy/format.ts.\n *\n * No ANSI escape, though the line is meant to READ as dim. Nothing in this\n * codebase emits escape sequences, the output is graded by tests that compare\n * strings, and a colour that survives into a redirected file or a CI log is\n * noise nobody asked for. The typography does the dimming: the code, a spaced\n * middot, the command — an aside, not a sentence.\n */\nexport function sayExplain(code: ErrorCode): void {\n if (NO_EXPLAIN_POINTER.has(code)) return;\n console.error(`${code} · loam explain ${code}`);\n}\n\n/**\n * Report a failure in whichever mode the caller is in. Text mode goes to\n * stderr as it always has — now with the code and its lookup on a second line;\n * JSON mode goes into the envelope, whose stdout bytes are unchanged.\n */\nexport function fail(json: boolean, code: ErrorCode, message: string): void {\n if (json) {\n emitJsonError(code, message);\n return;\n }\n console.error(message);\n sayExplain(code);\n process.exitCode = 1;\n}\n\n/**\n * A refusal or failure with its stable `--json` code attached, for the commands\n * whose refusals are THROWN — from a plan phase or a rollback path several\n * calls below the one handler that prints — rather than returned. The handler\n * `fail`s with `code`, so a merge that could not be computed reports as a merge\n * failure rather than as `internal`. What anything ELSE that escapes means is\n * each handler's to say. One class, because archive, unarchive and\n * migrate-openspec had each written this same one.\n */\nexport class CodedFailure extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n ) {\n super(message);\n }\n}\n\n/** The message every command prints when there is no loam.json. */\nconst NO_CONFIG_MESSAGE = \"No loam.json found. Run `loam init --docs <dir>` first.\";\n\n/**\n * The message the service-scoped commands print when neither `--service` nor\n * loam.json names one. Four commands ask the same question and owe the same two\n * answers, and a fifth would have been written by hand. `loam delta` keeps its\n * own longer sentence on purpose — it can say which feature was being read.\n */\nexport const NO_SERVICE_MESSAGE = \"No service. Pass --service <id> or set it in loam.json.\";\n\n/**\n * Report \"no config\" in whichever mode the caller is in — distinguishing a\n * config that is absent from one that exists but would not load, because the\n * two point at opposite fixes: a missing config wants `loam init`, a corrupt\n * one wants repair, and `init` would silently rewrite the corrupt file. The\n * distinction arrives as DATA on the load outcome rather than being re-derived\n * from the filesystem here — the file could change between the load and a\n * re-check, and the parse problem itself is worth showing.\n */\nexport function reportNoConfig(json: boolean, load: { kind: \"absent\" } | { kind: \"invalid\"; path: string; problem: string }): void {\n if (load.kind === \"invalid\") {\n fail(\n json,\n \"config-invalid\",\n `Invalid loam.json: ${load.problem}. Fix it (or delete it and re-run \\`loam init\\`).`,\n );\n return;\n }\n fail(json, \"no-config\", NO_CONFIG_MESSAGE);\n}\n\n/**\n * The config a command action stands on, or null once `reportNoConfig` has\n * refused — the caller's whole answer to null is `return`. Every command that\n * needs a wired repo opens with this question, and it was spelled as the same\n * six lines in each of them; one spelling is what keeps the absent/invalid\n * split above from being forgotten in a twenty-fourth. `init` and `doctor` do\n * not come through here: a missing or broken config is their subject, not a\n * reason to stop.\n */\nexport async function configOrRefuse(json: boolean): Promise<LoamConfig | null> {\n const loaded = await loadConfig();\n if (loaded.kind !== \"loaded\") {\n reportNoConfig(json, loaded);\n return null;\n }\n return loaded.config;\n}\n"]}
@@ -7,7 +7,7 @@
7
7
  * the argument is right: the tables ship in the binary, test/codes-drift.test.ts
8
8
  * holds every emitted finding code against them, and a registry restating them
9
9
  * would be free to trail the bytes `loam instructions loam-check` prints. None
10
- * of that applies here. These 65 codes have no fix-table row in any workflow
10
+ * of that applies here. These 67 codes have no fix-table row in any workflow
11
11
  * body — /loam-check grades DOCUMENTS, and these four families grade a
12
12
  * repository's wiring (`doctor`), a recommendation (`next`), a change between
13
13
  * two refs (`diff`) and a deploy question (`gate`). There is nothing to trail:
@@ -71,7 +71,7 @@ const DOCTOR = {
71
71
  "doctor.service-unbound": { severityNote: "warning", meaning: "loam.json declares no `service`, so nothing binds this checkout to one governed boundary and the service-repo checks have nothing to run against — `sources.stale`, a recording `loam verify`, `loam gherkin`. Deliberately not emitted inside the docs repo itself, where having no binding is the correct state rather than a gap.", fix: "Run `loam init --docs <dir> --service <id>` here." },
72
72
  "doctor.service-unknown": { severityNote: "warning", meaning: "loam.json names a `service` the fleet has no services/<id>/ for — and the fleet WAS read, so this is a claim about a real inventory rather than one nobody could take.", fix: "Run `loam adopt --service <id>` to onboard it, or fix `service` in loam.json." },
73
73
  "doctor.agent-files-missing": { severityNote: "warning", meaning: "Some of the command and skill files this loam lays down for the repo's recorded tools and profile are not here: the repo was initialized by an older loam, or they were deleted. An out-of-date command set costs entry points, never a refusal, which is why it is a warning.", fix: "Re-run `loam init --docs <dir> --tools <ids>` here. It creates absent files, refreshes only pointers whose current bytes still match their recorded digest, preserves every customized file, and keeps this repo's service binding." },
74
- "doctor.agent-files-stale": { severityNote: "warning", meaning: "Command or skill files that ARE here carry no loam stamp, or one older than this binary, so the protocol and code tables they instruct an agent with may describe a loam that no longer exists. Staleness reads only the stamp; safe refresh is a separate digest check performed by init.", fix: "Re-run `loam init` to refresh a pointer only when its recorded digest still matches. Review a customized file against the current body yourself and update its stamp, or delete it so init can recreate it; your edits always outrank the template." },
74
+ "doctor.agent-files-stale": { severityNote: "warning", meaning: "Command or skill files that ARE here differ from the body this binary writes and carry no loam stamp, or one older than this binary, so what they instruct an agent with may describe a loam that no longer exists. A file whose only difference is an older stamp is current and not reported; a customized body under a current stamp is not reported either, because the stamp records that somebody reviewed it. Safe refresh is a separate digest check performed by init.", fix: "Re-run `loam init` to refresh a pointer only when its recorded digest still matches. Review a customized file against the current body yourself and update its stamp, or delete it so init can recreate it; your edits always outrank the template." },
75
75
  },
76
76
  };
77
77
  /**
@@ -86,10 +86,11 @@ const NEXT_STEPS = {
86
86
  codes: {
87
87
  "next.adopt-bound": { severityNote: "step", meaning: "The fleet form's first entry: this repository's loam.json says it is a service the fleet has no services/<id>/ for, so nothing about the boundary you are standing in is written down yet. Ahead of every other service's adoption work, because it is the only step that is about HERE.", fix: "The step IS the action: run its `command`, `loam adopt --service <id> --json`." },
88
88
  "next.author-landscape": { severityNote: "step", meaning: "The first rung of the first-hour ladder, over a fleet with no services and no features: architecture/landscape.likec4 does not exist, or is still the scaffold's untouched map with no service drawn on it.", fix: "The step IS the action: run its `command` — `loam init --create` when the file is absent, `loam seed --from fleet.yaml` when it is the untouched scaffold. loam never guesses who calls whom." },
89
- "next.bind-service": { severityNote: "step", meaning: "First-hour ladder: no service repository is bound to this fleet yet, so no command can ever run from a service's own checkout.", fix: "The step IS the action: run its `command`, `loam init --docs <path-to-this-docs-repo> --service <service-id>`, from a service's own repo, and commit the loam.json it writes." },
89
+ "next.bind-service": { severityNote: "step", meaning: "First-hour ladder: no service repository is bound to this fleet yet, so no command can ever run from a service's own checkout.", fix: "The step IS the action: run its `command`, `loam init --docs <path-to-this-docs-repo> --service <service-id> --agent-profile service`, from a service's own repo, and commit the loam.json it writes. The service profile leaves loam-feature and loam-ship, which this docs repo keeps, out of the service checkout." },
90
90
  "next.adopt-first": { severityNote: "step", meaning: "First-hour ladder: services/ is empty, so nothing about any service is written down and nothing can be graded against anything.", fix: "The step IS the action: run its `command`, `loam adopt --service <service-id> --json`; the brief it prints walks an agent through writing the baseline docs as draft." },
91
91
  "next.adopt": { severityNote: "step", meaning: "One service in the fleet has no spec.md — nothing about it is written down, so no feature can be graded against it. One entry per undocumented service.", fix: "The step IS the action: run its `command`, `loam adopt --service <id> --json`." },
92
92
  "next.complete-service": { severityNote: "step", meaning: "Adoption does not end at spec.md: a service has one and no model.likec4, the state archive warns about and the fleet gate reports incomplete until something models it.", fix: "The step IS the action: run its `command`, `loam validate --service <id> --json`." },
93
+ "next.revouch": { severityNote: "step", meaning: "A vouched service's spec.md or arch.spec.md changed after the person read it — `content.stale`, computable from the docs repo because it needs nothing but the document. `status: verified` still stands, so every fleet readout says vouched (stale) and this step keeps `next.fleet-clean` from saying nothing is owed. One entry per service, naming the documents that moved.", fix: "Not an agent's step: its `execution` is a human review. Prepare the reading list with `loam vouch --pack --service <id>`, then a person re-reads what moved and runs the step's `command`, `loam vouch --service <id>`, from that service's own repository. Never revert the document or touch `content_digest` to silence it." },
93
94
  "next.feature": { severityNote: "step", meaning: "One feature in flight that is not ready to ship, named with the stage it sits at and what it is missing — or, when it is blocked by another feature, the features it is waiting on.", fix: "The step IS the action: run its `command`, `loam status <FEAT-id> --json`, for that feature's own next list." },
94
95
  "next.archive": { severityNote: "step", meaning: "In the fleet form, a feature that is authored and verified: ship it, and everything queued behind it is released. In a feature's own list it is always the last entry, saying the feature ships once everything above it is done.", fix: "The step IS the action: run its `command`, `loam archive <FEAT-id> --dry-run --json`; the dry run writes nothing and lists every file the merge would touch." },
95
96
  "next.elided": { severityNote: "step", meaning: "The fleet list is capped, and this says how many steps of the same kinds it left out. The list is ordered most-unblocking first, so what was elided is the least urgent — a list nobody reads is not navigation.", fix: "The step IS the action: run its `command`, `loam list --json`, or work down the printed list and run status again." },
@@ -106,17 +107,19 @@ const NEXT_STEPS = {
106
107
  "next.archive-first": { severityNote: "step", meaning: "The feature builds on other features still in flight, which have to archive before it can.", fix: "The step IS the action: run its `command`, `loam dependencies <FEAT-id> --json`." },
107
108
  "next.fix-coherence": { severityNote: "step", meaning: "The feature has findings that stop it, named starting with the first. Everything EITHER gate refuses on is counted, so a warning marked as gating archive is in the tally even though `validate` grades the feature valid — which is why its exit code alone will not show you why archive refuses.", fix: "The step IS the action: run its `command`, `loam validate <FEAT-id> --json`." },
108
109
  "next.generate-tests": { severityNote: "step", meaning: "The feature's delta for a service carries scenarios no test run has answered yet. `loam gherkin` writes the .feature files whose green run is the only thing that may answer a `scenario.tested` claim, and it writes into the service's own repository — so the step names which repo to run it in when that is not this one.", fix: "The step IS the action: run its `command`, `loam gherkin <FEAT-id> --service <id>`, from that service's own checkout." },
109
- "next.verify": { severityNote: "step", meaning: "The done-check has not been started, or its record cannot be used. Five situations share this code because they share the work — nothing recorded, a verification.yaml that does not read as a record, a record the feature has moved out from under, a record that answers no claims at all, and a feature with nothing to derive a checklist from yet — and the step's own statement says which.", fix: "The step IS the action: run its `command`, `loam verify <FEAT-id> --json`, which derives the checklist and prints what it asks. A record nothing can read is repaired or deleted by hand: nothing overwrites it while it is unreadable." },
110
+ "next.verify": { severityNote: "step", meaning: "The done-check has not been started, or its record cannot be used. Five situations share this code because they share the work — nothing recorded, a record file that does not read as a record, a record the feature has moved out from under, a record that answers no claims at all, and a feature with nothing to derive a checklist from yet — and the step's own statement says which.", fix: "The step IS the action: run its `command`, `loam verify <FEAT-id> --json`, which derives the checklist and prints what it asks. A record file nothing can read is repaired by hand or, when it is one service's own `verification/<svc>.yaml`, deleted and recorded again by that service; the one-file `verification.yaml` may hold other services' attestations and is restored from version control, never deleted. Nothing overwrites it while it is unreadable." },
110
111
  "next.verify-unconfirmed": { severityNote: "step", meaning: "Claims on the feature are open — unconfirmed or unanswered — and this repository is not one of the services that owes an answer, so the recording form is not a step anyone can take from here.", fix: "The step IS the action: run its `command`, `loam verify <FEAT-id> --json`. The open claims are closed from each affected service's own repository." },
111
112
  "next.attest-service": { severityNote: "step", meaning: "Claims are open AND this repository is one of the services that owes an answer, so the recording form is nameable here: `--record --service` binds the answers to this repo's HEAD and refuses anywhere else.", fix: "The step IS the action: run its `command`, `loam verify <FEAT-id> --record answers.json --service <id>`, answering the claims filed under this service; the rest belong to their own repositories." },
112
- "next.verify-attested": { severityNote: "step", meaning: "Every claim is answered and some scenario claims are confirmed on an agent's word rather than on a test run (`verify.scenario-attested`) — complete work resting on an assertion, which is what `ready` has always meant here.", fix: "The step IS the action: run its `command`, `loam verify <FEAT-id> --results <cucumber-report.json>`, and the record stops resting on an assertion." },
113
+ "next.verify-attested": { severityNote: "step", meaning: "Every claim is answered and some scenario claims are confirmed on an agent's word rather than on a test run (`verify.scenario-attested`) — complete work resting on an assertion, which is what `ready` has always meant here.", fix: "The step IS the action: run its `command`, `loam verify <FEAT-id> --results <report.json>` — a cucumber JSON report, or loam's runner-neutral `loamScenarioReport` shape for any other runner — and the record stops resting on an assertion." },
113
114
  },
114
115
  };
115
116
  /**
116
117
  * `loam diff` — what CHANGED between a base git ref of the docs repo and the
117
118
  * working tree, not what is WRONG with either. That is why most rows below have
118
119
  * no repair: the meanings are the `DiffCode` union's own doc comments
119
- * (core/diff/semantic.ts), and the two error rows are the only ones naming work.
120
+ * (core/diff/semantic.ts), and the two error rows are the only ones naming a
121
+ * repair. The two `-modified` rows name work too, but not loam's: whether a
122
+ * changed schema breaks its readers is the specialist diff's answer.
120
123
  */
121
124
  const DIFF = {
122
125
  scope: "loam diff --base <ref>",
@@ -127,13 +130,15 @@ const DIFF = {
127
130
  "diff.requirement-removed": { severityNote: "warn", meaning: "A requirement present at the base ref is no longer in the living spec under the same identity.", fix: "A review question. A requirement that CHANGED identity — a new `Requirement-ID:`, a renamed heading — reads here as a removal plus an addition, which is the join being honest about what it can see rather than guessing a rename." },
128
131
  "diff.requirement-modified": { severityNote: "ok", meaning: "The same requirement identity is on both sides and its digest moved. A rebase pin never moves that digest, so this is a real edit to the requirement's text.", fix: "Nothing to fix: this is the class of the change, for a reviewer to read." },
129
132
  "diff.op-added": { severityNote: "ok", meaning: "An operationId in the living contract that was absent at the base ref; the message names its method and path.", fix: "Nothing to fix: this is the class of the change, for a reviewer to read." },
130
- "diff.op-removed": { severityNote: "warn", meaning: "A base operationId is gone from the living contract and no current landscape edge or living requirement names it — or, in the second wording this code also carries, whether one does could NOT be fully answered because some consumer documents were not scannable, and the details name them.", fix: "Read which of the two sentences it is. Nobody could be scanned is never nobody names it: the unreadable documents carry the run to exit 1 on their own, and the removal stays unreviewed until they can be read." },
131
- "diff.op-removed-consumed": { severityNote: "error", meaning: "An operation was removed since the base ref and the current fleet still names it — a landscape edge, or a foreign service's living requirement. The details list the victims, and the finding sets `breaking`.", fix: "These joins break. Either restore the operation, or land the consumers' own deltas in the same change so nothing in the fleet is left calling something that is gone." },
133
+ "diff.op-removed": { severityNote: "warn", meaning: "A base operationId is gone from the living contract and no current edge of the `architecture/` project, requirement or use-case hop names it — or, in the second wording this code also carries, whether one does could NOT be fully answered, and the details name the possible consumers nobody could check: a consumer document that was not scannable, or a landscape edge into this service drawn with no `metadata { op }`.", fix: "Read which of the two sentences it is. Nobody could be checked is never nobody names it: an unreadable document carries the run to exit 1 on its own, and an edge with no op is answered by putting the operation it calls on the edge (`spine.op-link-missing` names the same edge)." },
134
+ "diff.op-removed-consumed": { severityNote: "error", meaning: "An operation was removed since the base ref and the current fleet still names it — an edge of the `architecture/` project, another service's requirement in spec.md or arch.spec.md (not one naming its own service's operation of the same id), or a use-case hop attributed to it: the consumers the archive gate refuses on too. The details list the victims, and the finding sets `breaking`.", fix: "These joins break. Either restore the operation, or land the consumers' own deltas in the same change so nothing in the fleet is left calling something that is gone." },
132
135
  "diff.op-deprecated": { severityNote: "warn", meaning: "`deprecated: true` was introduced on an operation since the base ref. The details name the current consumers, under one of three deliberately honest phrasings: some still name it, none does, or whether any does could not be fully answered.", fix: "Deprecation is a promise to the consumers the details name, and nothing is broken yet. Read which phrasing it is before treating a silent list as an empty one." },
136
+ "diff.op-modified": { severityNote: "warn", meaning: "The same operationId is on both sides and the operation changed: its own definition, its method or path, or anything its internal `$ref`s reach — a field renamed in a shared `components` schema counts. The details name the authored consumers, the same scan `diff.op-deprecated` runs. loam does not read what a schema says, so it never grades whether the change is breaking.", fix: "Run `oasdiff breaking` on the two states of this contract to learn whether it breaks anyone; the details are who to tell. A warning on purpose: a new optional field and a renamed required one look identical from here." },
133
137
  "diff.message-added": { severityNote: "ok", meaning: "An AsyncAPI message name absent at the base ref.", fix: "Nothing to fix: this is the class of the change, for a reviewer to read." },
134
- "diff.message-removed": { severityNote: "warn", meaning: "A base message is gone. The finding says which of three cases it is: another current service still declares the name, nothing currently consumes it, or whether anything does could not be fully answered because some consumer documents were not scannable.", fix: "Read which case it is. A name another service still declares is not orphaned, and an unanswerable scan is not an empty one — only positive evidence vouches here." },
135
- "diff.message-removed-consumed": { severityNote: "error", meaning: "A message was removed since the base ref and the current fleet still consumes it — a `Consumes:` line, or a consumes edge. The details list the victims, and the finding sets `breaking`.", fix: "These joins break. Either restore the message, or land the consumers' own deltas in the same change." },
136
- "diff.consumer-added": { severityNote: "ok", meaning: "A cross-service join appeared since base: another service's living requirements now name this service's operation or message. Filed on the PROVIDER — the service whose surface the join lands on.", fix: "Nothing to fix, and the change class most worth reading: a new consumer is a new obligation on this service's contract, and removing that operation later is what `diff.op-removed-consumed` will be about." },
138
+ "diff.message-removed": { severityNote: "warn", meaning: "A base message is gone. The finding says which of three cases it is: another current service still sends it (an `action: send` operation — a service that only receives it is a consumer) or an `#external` element still publishes it (a `publishes` edge from it in the `architecture/` project, the outside producer `spine.message-unproduced` accepts), nothing currently consumes it, or whether anything does could not be fully answered because some consumer documents were not scannable.", fix: "Read which case it is. A name another service still sends, or an `#external` element still publishes, is not orphaned, and an unanswerable scan is not an empty one — only positive evidence vouches here." },
139
+ "diff.message-removed-consumed": { severityNote: "error", meaning: "A message was removed since the base ref and the current fleet still consumes it — a `Consumes:` line in either spec file, a consumes edge of the `architecture/` project, or a use-case hop it backs. The details list the victims, and the finding sets `breaking`.", fix: "These joins break. Either restore the message, or land the consumers' own deltas in the same change." },
140
+ "diff.message-modified": { severityNote: "warn", meaning: "The same AsyncAPI message name is on both sides and its declaration, or anything its internal `$ref`s reach, changed — a payload field renamed counts. The details name the authored consumers: consumes-edges, `Consumes:` lines and the use-case hops they back. Content equality only: nothing inside the payload is ever joined on, and whether the change breaks a consumer is not graded.", fix: "Run `asyncapi diff` on the two states of this contract to learn whether it breaks anyone; the details are who to tell." },
141
+ "diff.consumer-added": { severityNote: "ok", meaning: "A cross-service join appeared since base: another service's living requirements — spec.md or arch.spec.md — now name this service's operation or message. A line naming an operation the consumer's own contract defines names its own, and is no join. Filed on the PROVIDER — the service whose surface the join lands on.", fix: "Nothing to fix, and the change class most worth reading: a new consumer is a new obligation on this service's contract, and removing that operation later is what `diff.op-removed-consumed` will be about." },
137
142
  "diff.consumer-removed": { severityNote: "ok", meaning: "A cross-service join present at the base ref is gone: a consumer's living requirements no longer name this service's operation or message.", fix: "Nothing to fix: this is the class of the change, for a reviewer to read." },
138
143
  },
139
144
  };
@@ -148,7 +153,7 @@ const GATE = {
148
153
  "gate.partners-unknown": { severityNote: "warn when the map is absent; error when it exists and cannot be used", meaning: "The partner set for the gated service could not be derived from architecture/landscape.likec4 — it is absent, or it exists and does not parse. That means nobody could look, never that nothing joins this service. Absent is a warning because a repo before its first adopt legitimately has no map yet; a broken map is an error because `loam validate --all` fails that repo for the same file, and a deploy gate passing where the fleet gate fails would be the quieter of two contradictory verdicts.", fix: "Fix the landscape and re-run. `loam validate --all` diagnoses the file itself; this check only refuses the false silence." },
149
154
  "gate.service-undocumented": { severityNote: "error", meaning: "The service being deployed sits below `documented` on the adoption ladder, so the docs cannot say what its joins even are and no recorded evidence can answer the deploy question. The details list what the next rung wants.", fix: "Author the baseline — `loam adopt --service <id>` briefs exactly what is missing — and this check passes once the required artifact set exists." },
150
155
  "gate.partner-undocumented": { severityNote: "warn", meaning: "A service that joins the gated one has no services/<id>/ directory at all, or has one sitting below `documented`: its side of the join is not recorded, so the gate cannot see what that partner promises. An `#external` partner is exempt on purpose — somebody else's system is unrecorded by design.", fix: "Adopt the partner (`loam adopt --service <id>` briefs it), bind its landscape element to the right service with `metadata { service '<id>' }`, or tag it `#external` if it is not ours." },
151
- "gate.feature-unverified": { severityNote: "warn", meaning: "An active feature touching the gated service is not verified: no record at all, a verification.yaml that does not read as a record, a record the feature moved out from under, a record answering no claims, or open claims counted in the message. A record that is merely `attested` is NOT reported under this code — it carries `verify.scenario-attested` instead, because complete work resting on an assertion is a different fact from work nobody checked.", fix: "Derive the checklist and answer every claim with evidence: `loam verify <FEAT-id> --json`. A record nothing can read is repaired or deleted by hand; nothing overwrites it while it is unreadable." },
156
+ "gate.feature-unverified": { severityNote: "warn", meaning: "An active feature touching the gated service — or, under `--since <ref>`, one archived after that docs-repo ref (its `features/archive/` directory absent at the ref, or changed since: a feature unarchived and archived again ships again) — is not verified: no record at all, a record file that does not read as a record, a record the feature moved out from under (for an archived feature, one that no longer answered the checklist `loam archive` captured as it merged), a record answering no claims, or open claims counted in the message. A record that is merely `attested` is NOT reported under this code — it carries `verify.scenario-attested` instead, because complete work resting on an assertion is a different fact from work nobody checked. Without `--since` an archived feature is never read, so archiving one takes it out of this check.", fix: "Derive the checklist and answer every claim with evidence: `loam verify <FEAT-id> --json`. A record file nothing can read is repaired by hand or, when it is one service's own `verification/<svc>.yaml`, deleted and recorded again by that service; the one-file `verification.yaml` may hold other services' attestations and is restored from version control, never deleted. Nothing overwrites it while it is unreadable. An archived feature's record is frozen: `loam unarchive <FEAT-id>` re-opens it if its claims must be answered before the deploy." },
152
157
  },
153
158
  };
154
159
  /** In listing order, though every consumer sorts by code — the order is documentation, not contract. */