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

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 (1026) hide show
  1. package/CHANGELOG.md +2548 -86
  2. package/COMPARISON.md +80 -63
  3. package/CONTRIBUTING.md +84 -0
  4. package/MIGRATING-from-OpenSpec.md +213 -54
  5. package/QUICKSTART.md +125 -0
  6. package/README.md +450 -150
  7. package/ROADMAP.md +494 -0
  8. package/SCHEMA.md +2355 -256
  9. package/SECURITY.md +21 -3
  10. package/WORKFLOW.md +701 -0
  11. package/dist/cli.js +113 -19
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/adopt/adopt.js +209 -0
  14. package/dist/commands/adopt/adopt.js.map +1 -0
  15. package/dist/commands/adopt/render.js +268 -0
  16. package/dist/commands/adopt/render.js.map +1 -0
  17. package/dist/commands/archive/archive.js +77 -0
  18. package/dist/commands/archive/archive.js.map +1 -0
  19. package/dist/commands/archive/plan/contracts/asyncapi.js +178 -0
  20. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -0
  21. package/dist/commands/archive/plan/contracts/openapi.js +159 -0
  22. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -0
  23. package/dist/commands/archive/plan/gate.js +317 -0
  24. package/dist/commands/archive/plan/gate.js.map +1 -0
  25. package/dist/commands/archive/plan/landscape.js +161 -0
  26. package/dist/commands/archive/plan/landscape.js.map +1 -0
  27. package/dist/commands/archive/plan/refusal.js +111 -0
  28. package/dist/commands/archive/plan/refusal.js.map +1 -0
  29. package/dist/commands/archive/plan/specs.js +237 -0
  30. package/dist/commands/archive/plan/specs.js.map +1 -0
  31. package/dist/commands/archive/plan/state.js +27 -0
  32. package/dist/commands/archive/plan/state.js.map +1 -0
  33. package/dist/commands/archive/run.js +251 -0
  34. package/dist/commands/archive/run.js.map +1 -0
  35. package/dist/commands/context/context.js +99 -0
  36. package/dist/commands/context/context.js.map +1 -0
  37. package/dist/commands/context/print.js +283 -0
  38. package/dist/commands/context/print.js.map +1 -0
  39. package/dist/commands/delta/delta.js +279 -0
  40. package/dist/commands/delta/delta.js.map +1 -0
  41. package/dist/commands/delta/print.js +205 -0
  42. package/dist/commands/delta/print.js.map +1 -0
  43. package/dist/commands/dependencies.js +9 -7
  44. package/dist/commands/dependencies.js.map +1 -1
  45. package/dist/commands/diff/diff.js +93 -0
  46. package/dist/commands/diff/diff.js.map +1 -0
  47. package/dist/commands/diff/print.js +38 -0
  48. package/dist/commands/diff/print.js.map +1 -0
  49. package/dist/commands/doctor.js +5 -4
  50. package/dist/commands/doctor.js.map +1 -1
  51. package/dist/commands/explain/explain.js +151 -0
  52. package/dist/commands/explain/explain.js.map +1 -0
  53. package/dist/commands/explore.js +29 -24
  54. package/dist/commands/explore.js.map +1 -1
  55. package/dist/commands/gate/gate.js +115 -0
  56. package/dist/commands/gate/gate.js.map +1 -0
  57. package/dist/commands/gate/print.js +54 -0
  58. package/dist/commands/gate/print.js.map +1 -0
  59. package/dist/commands/gherkin/commit.js +146 -0
  60. package/dist/commands/gherkin/commit.js.map +1 -0
  61. package/dist/commands/gherkin/gherkin.js +246 -0
  62. package/dist/commands/gherkin/gherkin.js.map +1 -0
  63. package/dist/commands/gherkin/reconcile.js +149 -0
  64. package/dist/commands/gherkin/reconcile.js.map +1 -0
  65. package/dist/commands/gherkin/render.js +77 -0
  66. package/dist/commands/gherkin/render.js.map +1 -0
  67. package/dist/commands/init/example.js +204 -0
  68. package/dist/commands/init/example.js.map +1 -0
  69. package/dist/commands/init/first-hour.js +28 -0
  70. package/dist/commands/init/first-hour.js.map +1 -0
  71. package/dist/commands/{init.js → init/init.js} +146 -71
  72. package/dist/commands/init/init.js.map +1 -0
  73. package/dist/commands/init/mcp.js +131 -0
  74. package/dist/commands/init/mcp.js.map +1 -0
  75. package/dist/commands/init/options.js +78 -0
  76. package/dist/commands/init/options.js.map +1 -0
  77. package/dist/commands/instructions.js +72 -14
  78. package/dist/commands/instructions.js.map +1 -1
  79. package/dist/commands/list/campaign/campaign.js +65 -0
  80. package/dist/commands/list/campaign/campaign.js.map +1 -0
  81. package/dist/commands/list/campaign/owners.js +65 -0
  82. package/dist/commands/list/campaign/owners.js.map +1 -0
  83. package/dist/commands/list/json.js +157 -0
  84. package/dist/commands/list/json.js.map +1 -0
  85. package/dist/commands/list/list.js +343 -0
  86. package/dist/commands/list/list.js.map +1 -0
  87. package/dist/commands/list/print.js +301 -0
  88. package/dist/commands/list/print.js.map +1 -0
  89. package/dist/commands/list/review.js +120 -0
  90. package/dist/commands/list/review.js.map +1 -0
  91. package/dist/commands/list/views.js +105 -0
  92. package/dist/commands/list/views.js.map +1 -0
  93. package/dist/commands/mcp/dispatch.js +183 -0
  94. package/dist/commands/mcp/dispatch.js.map +1 -0
  95. package/dist/commands/mcp/mcp.js +34 -0
  96. package/dist/commands/mcp/mcp.js.map +1 -0
  97. package/dist/commands/mcp/serve.js +55 -0
  98. package/dist/commands/mcp/serve.js.map +1 -0
  99. package/dist/commands/migrate-openspec/materialize/active.js +170 -0
  100. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -0
  101. package/dist/commands/migrate-openspec/materialize/feature.js +41 -0
  102. package/dist/commands/migrate-openspec/materialize/feature.js.map +1 -0
  103. package/dist/commands/migrate-openspec/materialize/living.js +62 -0
  104. package/dist/commands/migrate-openspec/materialize/living.js.map +1 -0
  105. package/dist/commands/migrate-openspec/materialize/stage.js +142 -0
  106. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -0
  107. package/dist/commands/migrate-openspec/materialize/target.js +219 -0
  108. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -0
  109. package/dist/commands/migrate-openspec/migrate-openspec.js +142 -0
  110. package/dist/commands/migrate-openspec/migrate-openspec.js.map +1 -0
  111. package/dist/commands/migrate-openspec/openspec/decisions.js +91 -0
  112. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -0
  113. package/dist/commands/migrate-openspec/openspec/error.js +19 -0
  114. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -0
  115. package/dist/commands/migrate-openspec/openspec/mapping.js +170 -0
  116. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -0
  117. package/dist/commands/migrate-openspec/openspec/paths.js +43 -0
  118. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -0
  119. package/dist/commands/migrate-openspec/openspec/read.js +49 -0
  120. package/dist/commands/migrate-openspec/openspec/read.js.map +1 -0
  121. package/dist/commands/migrate-openspec/print.js +89 -0
  122. package/dist/commands/migrate-openspec/print.js.map +1 -0
  123. package/dist/commands/new/commit.js +76 -0
  124. package/dist/commands/new/commit.js.map +1 -0
  125. package/dist/commands/new/new.js +283 -0
  126. package/dist/commands/new/new.js.map +1 -0
  127. package/dist/commands/new/templates.js +355 -0
  128. package/dist/commands/new/templates.js.map +1 -0
  129. package/dist/commands/open.js +203 -0
  130. package/dist/commands/open.js.map +1 -0
  131. package/dist/commands/policy/format.js +152 -0
  132. package/dist/commands/policy/format.js.map +1 -0
  133. package/dist/commands/{docs-repo-gate.js → policy/gate.js} +11 -11
  134. package/dist/commands/policy/gate.js.map +1 -0
  135. package/dist/commands/rebase/edit.js +106 -0
  136. package/dist/commands/rebase/edit.js.map +1 -0
  137. package/dist/commands/rebase/living.js +185 -0
  138. package/dist/commands/rebase/living.js.map +1 -0
  139. package/dist/commands/rebase/plan.js +245 -0
  140. package/dist/commands/rebase/plan.js.map +1 -0
  141. package/dist/commands/rebase/rebase.js +275 -0
  142. package/dist/commands/rebase/rebase.js.map +1 -0
  143. package/dist/commands/seed/commit.js +192 -0
  144. package/dist/commands/seed/commit.js.map +1 -0
  145. package/dist/commands/seed/plan.js +133 -0
  146. package/dist/commands/seed/plan.js.map +1 -0
  147. package/dist/commands/seed/seed.js +172 -0
  148. package/dist/commands/seed/seed.js.map +1 -0
  149. package/dist/commands/show/feature.js +178 -0
  150. package/dist/commands/show/feature.js.map +1 -0
  151. package/dist/commands/show/marks.js +24 -0
  152. package/dist/commands/show/marks.js.map +1 -0
  153. package/dist/commands/{show.js → show/service.js} +63 -152
  154. package/dist/commands/show/service.js.map +1 -0
  155. package/dist/commands/show/show.js +77 -0
  156. package/dist/commands/show/show.js.map +1 -0
  157. package/dist/commands/status/json.js +45 -0
  158. package/dist/commands/status/json.js.map +1 -0
  159. package/dist/commands/status/print.js +157 -0
  160. package/dist/commands/status/print.js.map +1 -0
  161. package/dist/commands/status/status.js +119 -0
  162. package/dist/commands/status/status.js.map +1 -0
  163. package/dist/commands/steps/steps.js +189 -0
  164. package/dist/commands/steps/steps.js.map +1 -0
  165. package/dist/commands/subsystem/edit.js +192 -0
  166. package/dist/commands/subsystem/edit.js.map +1 -0
  167. package/dist/commands/subsystem/move.js +265 -0
  168. package/dist/commands/subsystem/move.js.map +1 -0
  169. package/dist/commands/subsystem/query.js +115 -0
  170. package/dist/commands/subsystem/query.js.map +1 -0
  171. package/dist/commands/subsystem/subsystem.js +101 -0
  172. package/dist/commands/subsystem/subsystem.js.map +1 -0
  173. package/dist/commands/subsystem/sync.js +103 -0
  174. package/dist/commands/subsystem/sync.js.map +1 -0
  175. package/dist/commands/subsystem/txn/txn.js +94 -0
  176. package/dist/commands/subsystem/txn/txn.js.map +1 -0
  177. package/dist/commands/subsystem/txn/views.js +70 -0
  178. package/dist/commands/subsystem/txn/views.js.map +1 -0
  179. package/dist/commands/unarchive/manifest.js +160 -0
  180. package/dist/commands/unarchive/manifest.js.map +1 -0
  181. package/dist/commands/{unarchive.js → unarchive/restore.js} +47 -160
  182. package/dist/commands/unarchive/restore.js.map +1 -0
  183. package/dist/commands/unarchive/unarchive.js +67 -0
  184. package/dist/commands/unarchive/unarchive.js.map +1 -0
  185. package/dist/commands/validate/arch-coverage.js +194 -0
  186. package/dist/commands/validate/arch-coverage.js.map +1 -0
  187. package/dist/commands/validate/checks/fleet-shape.js +296 -0
  188. package/dist/commands/validate/checks/fleet-shape.js.map +1 -0
  189. package/dist/commands/validate/checks/health-deps.js +55 -0
  190. package/dist/commands/validate/checks/health-deps.js.map +1 -0
  191. package/dist/commands/validate/checks/requirements.js +254 -0
  192. package/dist/commands/validate/checks/requirements.js.map +1 -0
  193. package/dist/commands/validate/checks/sources.js +76 -0
  194. package/dist/commands/validate/checks/sources.js.map +1 -0
  195. package/dist/commands/validate/checks/vocabulary.js +31 -0
  196. package/dist/commands/validate/checks/vocabulary.js.map +1 -0
  197. package/dist/commands/validate/feature.js +216 -0
  198. package/dist/commands/validate/feature.js.map +1 -0
  199. package/dist/commands/validate/fleet/census.js +69 -0
  200. package/dist/commands/validate/fleet/census.js.map +1 -0
  201. package/dist/commands/validate/fleet/kind-tags.js +39 -0
  202. package/dist/commands/validate/fleet/kind-tags.js.map +1 -0
  203. package/dist/commands/validate/fleet/landscape.js +353 -0
  204. package/dist/commands/validate/fleet/landscape.js.map +1 -0
  205. package/dist/commands/validate/fleet/load.js +52 -0
  206. package/dist/commands/validate/fleet/load.js.map +1 -0
  207. package/dist/commands/validate/fleet/obligations.js +236 -0
  208. package/dist/commands/validate/fleet/obligations.js.map +1 -0
  209. package/dist/commands/validate/fleet/scorecard/adoption.js +102 -0
  210. package/dist/commands/validate/fleet/scorecard/adoption.js.map +1 -0
  211. package/dist/commands/validate/fleet/scorecard/contracts.js +98 -0
  212. package/dist/commands/validate/fleet/scorecard/contracts.js.map +1 -0
  213. package/dist/commands/validate/fleet/scorecard/print.js +63 -0
  214. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -0
  215. package/dist/commands/validate/fleet/scorecard/scorecard.js +156 -0
  216. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -0
  217. package/dist/commands/validate/fleet/usecases/capability-tag.js +94 -0
  218. package/dist/commands/validate/fleet/usecases/capability-tag.js.map +1 -0
  219. package/dist/commands/validate/fleet/usecases/place.js +13 -0
  220. package/dist/commands/validate/fleet/usecases/place.js.map +1 -0
  221. package/dist/commands/validate/fleet/usecases/requirement-tag.js +95 -0
  222. package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +1 -0
  223. package/dist/commands/validate/fleet/usecases/steps.js +197 -0
  224. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -0
  225. package/dist/commands/validate/fleet/usecases/usecases.js +99 -0
  226. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -0
  227. package/dist/commands/validate/fleet/views/ids.js +40 -0
  228. package/dist/commands/validate/fleet/views/ids.js.map +1 -0
  229. package/dist/commands/validate/fleet/views/stale.js +59 -0
  230. package/dist/commands/validate/fleet/views/stale.js.map +1 -0
  231. package/dist/commands/validate/links/corpus.js +92 -0
  232. package/dist/commands/validate/links/corpus.js.map +1 -0
  233. package/dist/commands/validate/links/glossary.js +75 -0
  234. package/dist/commands/validate/links/glossary.js.map +1 -0
  235. package/dist/commands/validate/report.js +269 -0
  236. package/dist/commands/validate/report.js.map +1 -0
  237. package/dist/commands/validate/service/api.js +338 -0
  238. package/dist/commands/validate/service/api.js.map +1 -0
  239. package/dist/commands/validate/service/events/events.js +219 -0
  240. package/dist/commands/validate/service/events/events.js.map +1 -0
  241. package/dist/commands/validate/service/events/message-producers.js +77 -0
  242. package/dist/commands/validate/service/events/message-producers.js.map +1 -0
  243. package/dist/commands/validate/service/evidence-pins.js +131 -0
  244. package/dist/commands/validate/service/evidence-pins.js.map +1 -0
  245. package/dist/commands/validate/service/service.js +250 -0
  246. package/dist/commands/validate/service/service.js.map +1 -0
  247. package/dist/commands/validate/service/specs.js +219 -0
  248. package/dist/commands/validate/service/specs.js.map +1 -0
  249. package/dist/commands/validate/service/spine.js +113 -0
  250. package/dist/commands/validate/service/spine.js.map +1 -0
  251. package/dist/commands/validate/validate.js +338 -0
  252. package/dist/commands/validate/validate.js.map +1 -0
  253. package/dist/commands/verify/cross/diff.js +182 -0
  254. package/dist/commands/verify/cross/diff.js.map +1 -0
  255. package/dist/commands/verify/evidence/contract.js +32 -0
  256. package/dist/commands/verify/evidence/contract.js.map +1 -0
  257. package/dist/commands/verify/evidence/pins.js +52 -0
  258. package/dist/commands/verify/evidence/pins.js.map +1 -0
  259. package/dist/commands/verify/evidence/read.js +72 -0
  260. package/dist/commands/verify/evidence/read.js.map +1 -0
  261. package/dist/commands/verify/frozen.js +241 -0
  262. package/dist/commands/verify/frozen.js.map +1 -0
  263. package/dist/commands/verify/record.js +226 -0
  264. package/dist/commands/verify/record.js.map +1 -0
  265. package/dist/commands/verify/report.js +223 -0
  266. package/dist/commands/verify/report.js.map +1 -0
  267. package/dist/commands/verify/results.js +246 -0
  268. package/dist/commands/verify/results.js.map +1 -0
  269. package/dist/commands/verify/verify.js +217 -0
  270. package/dist/commands/verify/verify.js.map +1 -0
  271. package/dist/commands/vouch/contract.js +11 -0
  272. package/dist/commands/vouch/contract.js.map +1 -0
  273. package/dist/commands/vouch/pack/contract.js +2 -0
  274. package/dist/commands/vouch/pack/contract.js.map +1 -0
  275. package/dist/commands/vouch/pack/pack.js +266 -0
  276. package/dist/commands/vouch/pack/pack.js.map +1 -0
  277. package/dist/commands/vouch/pack/print.js +271 -0
  278. package/dist/commands/vouch/pack/print.js.map +1 -0
  279. package/dist/commands/vouch/pack/sections.js +142 -0
  280. package/dist/commands/vouch/pack/sections.js.map +1 -0
  281. package/dist/commands/vouch/prompt/confirm.js +46 -0
  282. package/dist/commands/vouch/prompt/confirm.js.map +1 -0
  283. package/dist/commands/vouch/report.js +101 -0
  284. package/dist/commands/vouch/report.js.map +1 -0
  285. package/dist/commands/vouch/run.js +285 -0
  286. package/dist/commands/vouch/run.js.map +1 -0
  287. package/dist/commands/vouch/sample/plan.js +125 -0
  288. package/dist/commands/vouch/sample/plan.js.map +1 -0
  289. package/dist/commands/vouch/sample/print.js +109 -0
  290. package/dist/commands/vouch/sample/print.js.map +1 -0
  291. package/dist/commands/vouch/vet/verify.js +123 -0
  292. package/dist/commands/vouch/vet/verify.js.map +1 -0
  293. package/dist/commands/vouch/vouch.js +176 -0
  294. package/dist/commands/vouch/vouch.js.map +1 -0
  295. package/dist/core/agent/agents-md/artifacts.js +220 -0
  296. package/dist/core/agent/agents-md/artifacts.js.map +1 -0
  297. package/dist/core/agent/agents-md/command-map.js +324 -0
  298. package/dist/core/agent/agents-md/command-map.js.map +1 -0
  299. package/dist/core/agent/agents-md/cycle.js +165 -0
  300. package/dist/core/agent/agents-md/cycle.js.map +1 -0
  301. package/dist/core/agent/agents-md/map/explain.js +34 -0
  302. package/dist/core/agent/agents-md/map/explain.js.map +1 -0
  303. package/dist/core/agent/agents-md/map/lenses/context.js +41 -0
  304. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -0
  305. package/dist/core/agent/agents-md/map/lenses/diff.js +46 -0
  306. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -0
  307. package/dist/core/agent/agents-md/map/lenses/gate.js +57 -0
  308. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -0
  309. package/dist/core/agent/agents-md/map/mcp.js +41 -0
  310. package/dist/core/agent/agents-md/map/mcp.js.map +1 -0
  311. package/dist/core/agent/agents-md/map/status.js +62 -0
  312. package/dist/core/agent/agents-md/map/status.js.map +1 -0
  313. package/dist/core/agent/agents-md/map/subsystem.js +32 -0
  314. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -0
  315. package/dist/core/agent/agents-md/map/usecases.js +60 -0
  316. package/dist/core/agent/agents-md/map/usecases.js.map +1 -0
  317. package/dist/core/agent/agents-md/refusals.js +246 -0
  318. package/dist/core/agent/agents-md/refusals.js.map +1 -0
  319. package/dist/core/agent/agents-md/shipped/archive-gate.js +98 -0
  320. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +1 -0
  321. package/dist/core/agent/agents-md/spine.js +40 -0
  322. package/dist/core/agent/agents-md/spine.js.map +1 -0
  323. package/dist/core/agent/agents-md.js +170 -0
  324. package/dist/core/agent/agents-md.js.map +1 -0
  325. package/dist/core/{agents-stamp.js → agent/agents-stamp.js} +50 -0
  326. package/dist/core/agent/agents-stamp.js.map +1 -0
  327. package/dist/core/agent/contract.js +2 -0
  328. package/dist/core/agent/contract.js.map +1 -0
  329. package/dist/core/agent/protocol.js +193 -0
  330. package/dist/core/agent/protocol.js.map +1 -0
  331. package/dist/core/agent/scaffold.js +259 -0
  332. package/dist/core/agent/scaffold.js.map +1 -0
  333. package/dist/core/agent/support/report.js +103 -0
  334. package/dist/core/agent/support/report.js.map +1 -0
  335. package/dist/core/agent/tools/dialects.js +150 -0
  336. package/dist/core/agent/tools/dialects.js.map +1 -0
  337. package/dist/core/agent/tools/registry.js +228 -0
  338. package/dist/core/agent/tools/registry.js.map +1 -0
  339. package/dist/core/agent/workflows/adopt.js +133 -0
  340. package/dist/core/agent/workflows/adopt.js.map +1 -0
  341. package/dist/core/agent/workflows/check.js +334 -0
  342. package/dist/core/agent/workflows/check.js.map +1 -0
  343. package/dist/core/agent/workflows/closing.js +262 -0
  344. package/dist/core/agent/workflows/closing.js.map +1 -0
  345. package/dist/core/agent/workflows/feature.js +105 -0
  346. package/dist/core/agent/workflows/feature.js.map +1 -0
  347. package/dist/core/agent/workflows/implement.js +105 -0
  348. package/dist/core/agent/workflows/implement.js.map +1 -0
  349. package/dist/core/agent/workflows/reference/authoring.js +209 -0
  350. package/dist/core/agent/workflows/reference/authoring.js.map +1 -0
  351. package/dist/core/agent/workflows/reference/codes.js +65 -0
  352. package/dist/core/agent/workflows/reference/codes.js.map +1 -0
  353. package/dist/core/agent/workflows/reference/done-check.js +188 -0
  354. package/dist/core/agent/workflows/reference/done-check.js.map +1 -0
  355. package/dist/core/agent/workflows/reference/reference.js +16 -0
  356. package/dist/core/agent/workflows/reference/reference.js.map +1 -0
  357. package/dist/core/agent/workflows/reference/spine.js +297 -0
  358. package/dist/core/agent/workflows/reference/spine.js.map +1 -0
  359. package/dist/core/asyncapi/baseline/plan.js +110 -0
  360. package/dist/core/asyncapi/baseline/plan.js.map +1 -0
  361. package/dist/core/asyncapi/baseline/surfaces.js +54 -0
  362. package/dist/core/asyncapi/baseline/surfaces.js.map +1 -0
  363. package/dist/core/asyncapi/depth.js +164 -0
  364. package/dist/core/asyncapi/depth.js.map +1 -0
  365. package/dist/core/asyncapi/digest.js +141 -0
  366. package/dist/core/asyncapi/digest.js.map +1 -0
  367. package/dist/core/asyncapi/merge/components.js +91 -0
  368. package/dist/core/asyncapi/merge/components.js.map +1 -0
  369. package/dist/core/asyncapi/merge/error.js +33 -0
  370. package/dist/core/asyncapi/merge/error.js.map +1 -0
  371. package/dist/core/asyncapi/merge/markers.js +308 -0
  372. package/dist/core/asyncapi/merge/markers.js.map +1 -0
  373. package/dist/core/asyncapi/merge/merge.js +215 -0
  374. package/dist/core/asyncapi/merge/merge.js.map +1 -0
  375. package/dist/core/asyncapi/merge/pin.js +107 -0
  376. package/dist/core/asyncapi/merge/pin.js.map +1 -0
  377. package/dist/core/asyncapi/model.js +11 -0
  378. package/dist/core/asyncapi/model.js.map +1 -0
  379. package/dist/core/asyncapi/producers.js +50 -0
  380. package/dist/core/asyncapi/producers.js.map +1 -0
  381. package/dist/core/{asyncapi.js → asyncapi/read.js} +95 -109
  382. package/dist/core/asyncapi/read.js.map +1 -0
  383. package/dist/core/brief/brief.js +225 -0
  384. package/dist/core/brief/brief.js.map +1 -0
  385. package/dist/core/brief/checks.js +250 -0
  386. package/dist/core/brief/checks.js.map +1 -0
  387. package/dist/core/brief/landscape.js +194 -0
  388. package/dist/core/brief/landscape.js.map +1 -0
  389. package/dist/core/brief/targets.js +226 -0
  390. package/dist/core/brief/targets.js.map +1 -0
  391. package/dist/core/brief/unchecked.js +47 -0
  392. package/dist/core/brief/unchecked.js.map +1 -0
  393. package/dist/core/c4/arch.js +282 -0
  394. package/dist/core/c4/arch.js.map +1 -0
  395. package/dist/core/c4/likec4.js +88 -0
  396. package/dist/core/c4/likec4.js.map +1 -0
  397. package/dist/core/c4/parsed/dynamic-views.js +132 -0
  398. package/dist/core/c4/parsed/dynamic-views.js.map +1 -0
  399. package/dist/core/c4/parsed/specification.js +78 -0
  400. package/dist/core/c4/parsed/specification.js.map +1 -0
  401. package/dist/core/c4/parsed/values.js +57 -0
  402. package/dist/core/c4/parsed/values.js.map +1 -0
  403. package/dist/core/c4/parsed/view-ids.js +75 -0
  404. package/dist/core/c4/parsed/view-ids.js.map +1 -0
  405. package/dist/core/c4/project/architecture.js +45 -0
  406. package/dist/core/c4/project/architecture.js.map +1 -0
  407. package/dist/core/c4/project/documents.js +58 -0
  408. package/dist/core/c4/project/documents.js.map +1 -0
  409. package/dist/core/c4/project/load.js +183 -0
  410. package/dist/core/c4/project/load.js.map +1 -0
  411. package/dist/core/c4/resolve/service.js +113 -0
  412. package/dist/core/c4/resolve/service.js.map +1 -0
  413. package/dist/core/c4/seed/fleet-file.js +201 -0
  414. package/dist/core/c4/seed/fleet-file.js.map +1 -0
  415. package/dist/core/c4/seed/items.js +91 -0
  416. package/dist/core/c4/seed/items.js.map +1 -0
  417. package/dist/core/c4/seed/stamp.js +48 -0
  418. package/dist/core/c4/seed/stamp.js.map +1 -0
  419. package/dist/core/c4/seed/template.js +109 -0
  420. package/dist/core/c4/seed/template.js.map +1 -0
  421. package/dist/core/c4/source-mask.js +82 -0
  422. package/dist/core/c4/source-mask.js.map +1 -0
  423. package/dist/core/c4/source-scan.js +227 -0
  424. package/dist/core/c4/source-scan.js.map +1 -0
  425. package/dist/core/c4/splice/authored-source.js +101 -0
  426. package/dist/core/c4/splice/authored-source.js.map +1 -0
  427. package/dist/core/c4/splice/contract.js +10 -0
  428. package/dist/core/c4/splice/contract.js.map +1 -0
  429. package/dist/core/c4/splice/delta-blocks.js +126 -0
  430. package/dist/core/c4/splice/delta-blocks.js.map +1 -0
  431. package/dist/core/c4/splice/identity/edges.js +36 -0
  432. package/dist/core/c4/splice/identity/edges.js.map +1 -0
  433. package/dist/core/c4/splice/landscape-merge.js +269 -0
  434. package/dist/core/c4/splice/landscape-merge.js.map +1 -0
  435. package/dist/core/c4/splice/placement.js +194 -0
  436. package/dist/core/c4/splice/placement.js.map +1 -0
  437. package/dist/core/c4/workspace.js +172 -0
  438. package/dist/core/c4/workspace.js.map +1 -0
  439. package/dist/core/capabilities/authoring/notes.js +141 -0
  440. package/dist/core/capabilities/authoring/notes.js.map +1 -0
  441. package/dist/core/capabilities/capabilities.js +124 -0
  442. package/dist/core/capabilities/capabilities.js.map +1 -0
  443. package/dist/core/capabilities/delta/doc.js +50 -0
  444. package/dist/core/capabilities/delta/doc.js.map +1 -0
  445. package/dist/core/capabilities/delta/overlay.js +129 -0
  446. package/dist/core/capabilities/delta/overlay.js.map +1 -0
  447. package/dist/core/capabilities/delta/summary.js +57 -0
  448. package/dist/core/capabilities/delta/summary.js.map +1 -0
  449. package/dist/core/capabilities/delta/tree.js +43 -0
  450. package/dist/core/capabilities/delta/tree.js.map +1 -0
  451. package/dist/core/capabilities/delta/uncovered.js +222 -0
  452. package/dist/core/capabilities/delta/uncovered.js.map +1 -0
  453. package/dist/core/capabilities/findings.js +342 -0
  454. package/dist/core/capabilities/findings.js.map +1 -0
  455. package/dist/core/capabilities/realizes/findings.js +181 -0
  456. package/dist/core/capabilities/realizes/findings.js.map +1 -0
  457. package/dist/core/capabilities/realizes/join.js +143 -0
  458. package/dist/core/capabilities/realizes/join.js.map +1 -0
  459. package/dist/core/capabilities/rollup.js +199 -0
  460. package/dist/core/capabilities/rollup.js.map +1 -0
  461. package/dist/core/capabilities/tree.js +126 -0
  462. package/dist/core/capabilities/tree.js.map +1 -0
  463. package/dist/core/capabilities/usecase-join.js +277 -0
  464. package/dist/core/capabilities/usecase-join.js.map +1 -0
  465. package/dist/core/coherence/authoring/scaffold.js +177 -0
  466. package/dist/core/coherence/authoring/scaffold.js.map +1 -0
  467. package/dist/core/coherence/authoring/sentinels.js +74 -0
  468. package/dist/core/coherence/authoring/sentinels.js.map +1 -0
  469. package/dist/core/coherence/coherence.js +251 -0
  470. package/dist/core/coherence/coherence.js.map +1 -0
  471. package/dist/core/coherence/declared.js +269 -0
  472. package/dist/core/coherence/declared.js.map +1 -0
  473. package/dist/core/coherence/events/declared.js +206 -0
  474. package/dist/core/coherence/events/declared.js.map +1 -0
  475. package/dist/core/coherence/events/events.js +204 -0
  476. package/dist/core/coherence/events/events.js.map +1 -0
  477. package/dist/core/coherence/events/grades.js +166 -0
  478. package/dist/core/coherence/events/grades.js.map +1 -0
  479. package/dist/core/coherence/events/lookups.js +122 -0
  480. package/dist/core/coherence/events/lookups.js.map +1 -0
  481. package/dist/core/coherence/events/merged.js +40 -0
  482. package/dist/core/coherence/events/merged.js.map +1 -0
  483. package/dist/core/coherence/living.js +198 -0
  484. package/dist/core/coherence/living.js.map +1 -0
  485. package/dist/core/coherence/lookups.js +155 -0
  486. package/dist/core/coherence/lookups.js.map +1 -0
  487. package/dist/core/coherence/pending.js +39 -0
  488. package/dist/core/coherence/pending.js.map +1 -0
  489. package/dist/core/conflict-markers.js +60 -0
  490. package/dist/core/conflict-markers.js.map +1 -0
  491. package/dist/core/delta/claims.js +73 -0
  492. package/dist/core/delta/claims.js.map +1 -0
  493. package/dist/core/delta/delta.js +296 -0
  494. package/dist/core/delta/delta.js.map +1 -0
  495. package/dist/core/delta/document.js +177 -0
  496. package/dist/core/delta/document.js.map +1 -0
  497. package/dist/core/delta/scope.js +48 -0
  498. package/dist/core/delta/scope.js.map +1 -0
  499. package/dist/core/delta/select.js +237 -0
  500. package/dist/core/delta/select.js.map +1 -0
  501. package/dist/core/dependencies/dependencies.js +141 -0
  502. package/dist/core/dependencies/dependencies.js.map +1 -0
  503. package/dist/core/dependencies/facts.js +165 -0
  504. package/dist/core/dependencies/facts.js.map +1 -0
  505. package/dist/core/dependencies/fanin.js +88 -0
  506. package/dist/core/dependencies/fanin.js.map +1 -0
  507. package/dist/core/dependencies/graph.js +106 -0
  508. package/dist/core/dependencies/graph.js.map +1 -0
  509. package/dist/core/diff/base-git.js +148 -0
  510. package/dist/core/diff/base-git.js.map +1 -0
  511. package/dist/core/diff/base-state.js +209 -0
  512. package/dist/core/diff/base-state.js.map +1 -0
  513. package/dist/core/diff/current-state.js +96 -0
  514. package/dist/core/diff/current-state.js.map +1 -0
  515. package/dist/core/diff/scope/changed-paths.js +188 -0
  516. package/dist/core/diff/scope/changed-paths.js.map +1 -0
  517. package/dist/core/diff/semantic.js +243 -0
  518. package/dist/core/diff/semantic.js.map +1 -0
  519. package/dist/core/diff/victims.js +247 -0
  520. package/dist/core/diff/victims.js.map +1 -0
  521. package/dist/core/docs.js +7 -64
  522. package/dist/core/docs.js.map +1 -1
  523. package/dist/core/doctor/agents.js +222 -0
  524. package/dist/core/doctor/agents.js.map +1 -0
  525. package/dist/core/doctor/config.js +135 -0
  526. package/dist/core/doctor/config.js.map +1 -0
  527. package/dist/core/doctor/doctor.js +275 -0
  528. package/dist/core/doctor/doctor.js.map +1 -0
  529. package/dist/core/doctor/report.js +13 -0
  530. package/dist/core/doctor/report.js.map +1 -0
  531. package/dist/core/doctor/residue.js +110 -0
  532. package/dist/core/doctor/residue.js.map +1 -0
  533. package/dist/core/document/apply.js +77 -0
  534. package/dist/core/document/apply.js.map +1 -0
  535. package/dist/core/{frontmatter.js → document/frontmatter.js} +48 -2
  536. package/dist/core/document/frontmatter.js.map +1 -0
  537. package/dist/core/document/grammar/directives.js +143 -0
  538. package/dist/core/document/grammar/directives.js.map +1 -0
  539. package/dist/core/document/parse.js +263 -0
  540. package/dist/core/document/parse.js.map +1 -0
  541. package/dist/core/document/scenarios.js +221 -0
  542. package/dist/core/document/scenarios.js.map +1 -0
  543. package/dist/core/document/spec.js +241 -0
  544. package/dist/core/document/spec.js.map +1 -0
  545. package/dist/core/{config.js → envelope/config.js} +79 -18
  546. package/dist/core/envelope/config.js.map +1 -0
  547. package/dist/core/envelope/json.js +175 -0
  548. package/dist/core/envelope/json.js.map +1 -0
  549. package/dist/core/{version.js → envelope/version.js} +3 -3
  550. package/dist/core/envelope/version.js.map +1 -0
  551. package/dist/core/explain/families.js +175 -0
  552. package/dist/core/explain/families.js.map +1 -0
  553. package/dist/core/explain/fix-tables.js +178 -0
  554. package/dist/core/explain/fix-tables.js.map +1 -0
  555. package/dist/core/explain/lookup.js +181 -0
  556. package/dist/core/explain/lookup.js.map +1 -0
  557. package/dist/core/explain/refusals.js +49 -0
  558. package/dist/core/explain/refusals.js.map +1 -0
  559. package/dist/core/explain/terms.js +144 -0
  560. package/dist/core/explain/terms.js.map +1 -0
  561. package/dist/core/explore/describe.js +105 -0
  562. package/dist/core/explore/describe.js.map +1 -0
  563. package/dist/core/{explore.js → explore/explore.js} +50 -101
  564. package/dist/core/explore/explore.js.map +1 -0
  565. package/dist/core/fleet-context.js +120 -78
  566. package/dist/core/fleet-context.js.map +1 -1
  567. package/dist/core/gate/checks.js +201 -0
  568. package/dist/core/gate/checks.js.map +1 -0
  569. package/dist/core/gate/partners.js +205 -0
  570. package/dist/core/gate/partners.js.map +1 -0
  571. package/dist/core/gate/report.js +47 -0
  572. package/dist/core/gate/report.js.map +1 -0
  573. package/dist/core/gate/verification.js +113 -0
  574. package/dist/core/gate/verification.js.map +1 -0
  575. package/dist/core/gherkin/digest.js +60 -0
  576. package/dist/core/gherkin/digest.js.map +1 -0
  577. package/dist/core/gherkin/emit.js +177 -0
  578. package/dist/core/gherkin/emit.js.map +1 -0
  579. package/dist/core/gherkin/read.js +76 -0
  580. package/dist/core/gherkin/read.js.map +1 -0
  581. package/dist/core/gherkin/stale.js +234 -0
  582. package/dist/core/gherkin/stale.js.map +1 -0
  583. package/dist/core/gherkin/stamp.js +77 -0
  584. package/dist/core/gherkin/stamp.js.map +1 -0
  585. package/dist/core/gherkin/steps/catalogue.js +154 -0
  586. package/dist/core/gherkin/steps/catalogue.js.map +1 -0
  587. package/dist/core/gherkin/steps/inventory.js +98 -0
  588. package/dist/core/gherkin/steps/inventory.js.map +1 -0
  589. package/dist/core/gherkin/steps/phrase.js +72 -0
  590. package/dist/core/gherkin/steps/phrase.js.map +1 -0
  591. package/dist/core/glossary/delta.js +106 -0
  592. package/dist/core/glossary/delta.js.map +1 -0
  593. package/dist/core/glossary/tree.js +86 -0
  594. package/dist/core/glossary/tree.js.map +1 -0
  595. package/dist/core/kernel/concurrency.js.map +1 -0
  596. package/dist/core/{document-bytes.js → kernel/document-bytes.js} +1 -1
  597. package/dist/core/kernel/document-bytes.js.map +1 -0
  598. package/dist/core/kernel/fences.js +42 -0
  599. package/dist/core/kernel/fences.js.map +1 -0
  600. package/dist/core/kernel/ids/capability.js +72 -0
  601. package/dist/core/kernel/ids/capability.js.map +1 -0
  602. package/dist/core/kernel/ids/dirs.js +33 -0
  603. package/dist/core/kernel/ids/dirs.js.map +1 -0
  604. package/dist/core/kernel/ids/feature.js +75 -0
  605. package/dist/core/kernel/ids/feature.js.map +1 -0
  606. package/dist/core/kernel/ids/service.js +156 -0
  607. package/dist/core/kernel/ids/service.js.map +1 -0
  608. package/dist/core/kernel/ids/subsystem.js +63 -0
  609. package/dist/core/kernel/ids/subsystem.js.map +1 -0
  610. package/dist/core/{path-safety.js → kernel/path-safety.js} +18 -4
  611. package/dist/core/kernel/path-safety.js.map +1 -0
  612. package/dist/core/kernel/records.js.map +1 -0
  613. package/dist/core/links/backlinks.js +68 -0
  614. package/dist/core/links/backlinks.js.map +1 -0
  615. package/dist/core/links/case.js +74 -0
  616. package/dist/core/links/case.js.map +1 -0
  617. package/dist/core/links/corpus.js +104 -0
  618. package/dist/core/links/corpus.js.map +1 -0
  619. package/dist/core/links/findings.js +110 -0
  620. package/dist/core/links/findings.js.map +1 -0
  621. package/dist/core/links/parse.js +236 -0
  622. package/dist/core/links/parse.js.map +1 -0
  623. package/dist/core/mcp/argv.js +111 -0
  624. package/dist/core/mcp/argv.js.map +1 -0
  625. package/dist/core/mcp/author-tools.js +72 -0
  626. package/dist/core/mcp/author-tools.js.map +1 -0
  627. package/dist/core/mcp/framing.js +46 -0
  628. package/dist/core/mcp/framing.js.map +1 -0
  629. package/dist/core/mcp/protocol.js +280 -0
  630. package/dist/core/mcp/protocol.js.map +1 -0
  631. package/dist/core/mcp/tools.js +314 -0
  632. package/dist/core/mcp/tools.js.map +1 -0
  633. package/dist/core/obligations/obligations.js +106 -0
  634. package/dist/core/obligations/obligations.js.map +1 -0
  635. package/dist/core/openapi/baseline/gate.js +166 -0
  636. package/dist/core/openapi/baseline/gate.js.map +1 -0
  637. package/dist/core/openapi/baseline/plan.js +112 -0
  638. package/dist/core/openapi/baseline/plan.js.map +1 -0
  639. package/dist/core/openapi/baseline/record.js +175 -0
  640. package/dist/core/openapi/baseline/record.js.map +1 -0
  641. package/dist/core/openapi/depth.js +122 -0
  642. package/dist/core/openapi/depth.js.map +1 -0
  643. package/dist/core/openapi/digest.js +189 -0
  644. package/dist/core/openapi/digest.js.map +1 -0
  645. package/dist/core/{openapi.js → openapi/doc.js} +44 -106
  646. package/dist/core/openapi/doc.js.map +1 -0
  647. package/dist/core/openapi/merge/components.js +234 -0
  648. package/dist/core/openapi/merge/components.js.map +1 -0
  649. package/dist/core/openapi/merge/error.js +26 -0
  650. package/dist/core/openapi/merge/error.js.map +1 -0
  651. package/dist/core/openapi/merge/markers.js +208 -0
  652. package/dist/core/openapi/merge/markers.js.map +1 -0
  653. package/dist/core/openapi/merge/merge.js +256 -0
  654. package/dist/core/openapi/merge/merge.js.map +1 -0
  655. package/dist/core/openapi/merge/pin.js +128 -0
  656. package/dist/core/openapi/merge/pin.js.map +1 -0
  657. package/dist/core/openspec/decide/capabilities.js +244 -0
  658. package/dist/core/openspec/decide/capabilities.js.map +1 -0
  659. package/dist/core/openspec/decide/dispositions.js +63 -0
  660. package/dist/core/openspec/decide/dispositions.js.map +1 -0
  661. package/dist/core/openspec/decide/renames.js +179 -0
  662. package/dist/core/openspec/decide/renames.js.map +1 -0
  663. package/dist/core/openspec/decide/verdict.js +84 -0
  664. package/dist/core/openspec/decide/verdict.js.map +1 -0
  665. package/dist/core/openspec/inventory.js +221 -0
  666. package/dist/core/openspec/inventory.js.map +1 -0
  667. package/dist/core/openspec/model/mapping.js +76 -0
  668. package/dist/core/openspec/model/mapping.js.map +1 -0
  669. package/dist/core/openspec/model/model.js +65 -0
  670. package/dist/core/openspec/model/model.js.map +1 -0
  671. package/dist/core/openspec/scan/changes.js +235 -0
  672. package/dist/core/openspec/scan/changes.js.map +1 -0
  673. package/dist/core/openspec/scan/config.js +200 -0
  674. package/dist/core/openspec/scan/config.js.map +1 -0
  675. package/dist/core/openspec/scan/shape.js +260 -0
  676. package/dist/core/openspec/scan/shape.js.map +1 -0
  677. package/dist/core/openspec/scan/walk.js +133 -0
  678. package/dist/core/openspec/scan/walk.js.map +1 -0
  679. package/dist/core/openspec/scan/workspace.js +71 -0
  680. package/dist/core/openspec/scan/workspace.js.map +1 -0
  681. package/dist/core/owners/codeowners.js +141 -0
  682. package/dist/core/owners/codeowners.js.map +1 -0
  683. package/dist/core/pack/features.js +120 -0
  684. package/dist/core/pack/features.js.map +1 -0
  685. package/dist/core/pack/joins.js +112 -0
  686. package/dist/core/pack/joins.js.map +1 -0
  687. package/dist/core/pack/living.js +118 -0
  688. package/dist/core/pack/living.js.map +1 -0
  689. package/dist/core/pack/pack.js +76 -0
  690. package/dist/core/pack/pack.js.map +1 -0
  691. package/dist/core/permissions/permissions.js +87 -0
  692. package/dist/core/permissions/permissions.js.map +1 -0
  693. package/dist/core/projection/api.js +84 -0
  694. package/dist/core/projection/api.js.map +1 -0
  695. package/dist/core/projection/arch-slice.js +63 -0
  696. package/dist/core/projection/arch-slice.js.map +1 -0
  697. package/dist/core/projection/events.js +34 -0
  698. package/dist/core/projection/events.js.map +1 -0
  699. package/dist/core/provenance/findings.js +208 -0
  700. package/dist/core/provenance/findings.js.map +1 -0
  701. package/dist/core/provenance/git.js +225 -0
  702. package/dist/core/provenance/git.js.map +1 -0
  703. package/dist/core/provenance/gitq/moves.js +182 -0
  704. package/dist/core/provenance/gitq/moves.js.map +1 -0
  705. package/dist/core/provenance/gitq/vouched-ancestor.js +179 -0
  706. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -0
  707. package/dist/core/provenance/sample/scope.js +238 -0
  708. package/dist/core/provenance/sample/scope.js.map +1 -0
  709. package/dist/core/provenance/sample/sections.js +61 -0
  710. package/dist/core/provenance/sample/sections.js.map +1 -0
  711. package/dist/core/provenance/sources.js +263 -0
  712. package/dist/core/provenance/sources.js.map +1 -0
  713. package/dist/core/provenance/stamp.js +232 -0
  714. package/dist/core/provenance/stamp.js.map +1 -0
  715. package/dist/core/provenance/walk.js +157 -0
  716. package/dist/core/provenance/walk.js.map +1 -0
  717. package/dist/core/repo/authored/paths.js +192 -0
  718. package/dist/core/repo/authored/paths.js.map +1 -0
  719. package/dist/core/repo/entries.js +126 -0
  720. package/dist/core/repo/entries.js.map +1 -0
  721. package/dist/core/repo/paths.js +236 -0
  722. package/dist/core/repo/paths.js.map +1 -0
  723. package/dist/core/repo/repo.js +248 -0
  724. package/dist/core/repo/repo.js.map +1 -0
  725. package/dist/core/repo/service-target.js +153 -0
  726. package/dist/core/repo/service-target.js.map +1 -0
  727. package/dist/core/repo/state.js +58 -0
  728. package/dist/core/repo/state.js.map +1 -0
  729. package/dist/core/repo/tree/find.js +52 -0
  730. package/dist/core/repo/tree/find.js.map +1 -0
  731. package/dist/core/repo/tree/fs.js +64 -0
  732. package/dist/core/repo/tree/fs.js.map +1 -0
  733. package/dist/core/repo/tree/marker.js +61 -0
  734. package/dist/core/repo/tree/marker.js.map +1 -0
  735. package/dist/core/repo/tree/views.js +172 -0
  736. package/dist/core/repo/tree/views.js.map +1 -0
  737. package/dist/core/repo/tree/walk.js +234 -0
  738. package/dist/core/repo/tree/walk.js.map +1 -0
  739. package/dist/core/results.js +99 -2
  740. package/dist/core/results.js.map +1 -1
  741. package/dist/core/scaffold/landscape.js +131 -0
  742. package/dist/core/scaffold/landscape.js.map +1 -0
  743. package/dist/core/scaffold/readme.js +84 -0
  744. package/dist/core/scaffold/readme.js.map +1 -0
  745. package/dist/core/staging/commit.js +241 -0
  746. package/dist/core/staging/commit.js.map +1 -0
  747. package/dist/core/staging/interrupted.js +96 -0
  748. package/dist/core/staging/interrupted.js.map +1 -0
  749. package/dist/core/staging/lock.js +206 -0
  750. package/dist/core/staging/lock.js.map +1 -0
  751. package/dist/core/staging/recovery/finding.js +35 -0
  752. package/dist/core/staging/recovery/finding.js.map +1 -0
  753. package/dist/core/staging/recovery/intent.js +98 -0
  754. package/dist/core/staging/recovery/intent.js.map +1 -0
  755. package/dist/core/staging/recovery/recover.js +126 -0
  756. package/dist/core/staging/recovery/recover.js.map +1 -0
  757. package/dist/core/staging/recovery/residue.js +62 -0
  758. package/dist/core/staging/recovery/residue.js.map +1 -0
  759. package/dist/core/staging/snapshot.js +202 -0
  760. package/dist/core/staging/snapshot.js.map +1 -0
  761. package/dist/core/staging/txn/forward.js +171 -0
  762. package/dist/core/staging/txn/forward.js.map +1 -0
  763. package/dist/core/staging/txn/journal.js +139 -0
  764. package/dist/core/staging/txn/journal.js.map +1 -0
  765. package/dist/core/staging/txn/transaction.js +75 -0
  766. package/dist/core/staging/txn/transaction.js.map +1 -0
  767. package/dist/core/staging/writes.js +113 -0
  768. package/dist/core/staging/writes.js.map +1 -0
  769. package/dist/core/status/actions/execution.js +49 -0
  770. package/dist/core/status/actions/execution.js.map +1 -0
  771. package/dist/core/status/contracts.js +102 -0
  772. package/dist/core/status/contracts.js.map +1 -0
  773. package/dist/core/status/feature/artifacts.js +171 -0
  774. package/dist/core/status/feature/artifacts.js.map +1 -0
  775. package/dist/core/status/feature/feature.js +207 -0
  776. package/dist/core/status/feature/feature.js.map +1 -0
  777. package/dist/core/status/feature/next.js +150 -0
  778. package/dist/core/status/feature/next.js.map +1 -0
  779. package/dist/core/status/feature/state.js +21 -0
  780. package/dist/core/status/feature/state.js.map +1 -0
  781. package/dist/core/status/fleet/fleet.js +187 -0
  782. package/dist/core/status/fleet/fleet.js.map +1 -0
  783. package/dist/core/status/fleet/next.js +207 -0
  784. package/dist/core/status/fleet/next.js.map +1 -0
  785. package/dist/core/status/interrupted.js +97 -0
  786. package/dist/core/status/interrupted.js.map +1 -0
  787. package/dist/core/status/report.js +31 -0
  788. package/dist/core/status/report.js.map +1 -0
  789. package/dist/core/status/scan.js +41 -0
  790. package/dist/core/status/scan.js.map +1 -0
  791. package/dist/core/status/verification.js +201 -0
  792. package/dist/core/status/verification.js.map +1 -0
  793. package/dist/core/usecases/capability.js +169 -0
  794. package/dist/core/usecases/capability.js.map +1 -0
  795. package/dist/core/usecases/delta/claims.js +116 -0
  796. package/dist/core/usecases/delta/claims.js.map +1 -0
  797. package/dist/core/usecases/delta/flows.js +103 -0
  798. package/dist/core/usecases/delta/flows.js.map +1 -0
  799. package/dist/core/usecases/delta/overlay.js +235 -0
  800. package/dist/core/usecases/delta/overlay.js.map +1 -0
  801. package/dist/core/usecases/fleet.js +137 -0
  802. package/dist/core/usecases/fleet.js.map +1 -0
  803. package/dist/core/usecases/operations.js +111 -0
  804. package/dist/core/usecases/operations.js.map +1 -0
  805. package/dist/core/usecases/place.js +44 -0
  806. package/dist/core/usecases/place.js.map +1 -0
  807. package/dist/core/usecases/touch.js +101 -0
  808. package/dist/core/usecases/touch.js.map +1 -0
  809. package/dist/core/verify/answers.js +173 -0
  810. package/dist/core/verify/answers.js.map +1 -0
  811. package/dist/core/verify/build.js +150 -0
  812. package/dist/core/verify/build.js.map +1 -0
  813. package/dist/core/verify/checklist.js +201 -0
  814. package/dist/core/verify/checklist.js.map +1 -0
  815. package/dist/core/verify/claims/identity.js +61 -0
  816. package/dist/core/verify/claims/identity.js.map +1 -0
  817. package/dist/core/verify/cross/diff.js +97 -0
  818. package/dist/core/verify/cross/diff.js.map +1 -0
  819. package/dist/core/verify/evidence/contract.js +169 -0
  820. package/dist/core/verify/evidence/contract.js.map +1 -0
  821. package/dist/core/verify/file.js +257 -0
  822. package/dist/core/verify/file.js.map +1 -0
  823. package/dist/core/verify/pins/lint.js +157 -0
  824. package/dist/core/verify/pins/lint.js.map +1 -0
  825. package/dist/core/verify/pins/pin.js +95 -0
  826. package/dist/core/verify/pins/pin.js.map +1 -0
  827. package/dist/core/verify/record.js +142 -0
  828. package/dist/core/verify/record.js.map +1 -0
  829. package/dist/core/verify/store/commit.js +89 -0
  830. package/dist/core/verify/store/commit.js.map +1 -0
  831. package/dist/core/verify/store/render.js +57 -0
  832. package/dist/core/verify/store/render.js.map +1 -0
  833. package/dist/core/vocabulary/health.js +91 -0
  834. package/dist/core/vocabulary/health.js.map +1 -0
  835. package/dist/core/vocabulary/issue.js +55 -0
  836. package/dist/core/vocabulary/issue.js.map +1 -0
  837. package/dist/core/vocabulary/maturity.js +145 -0
  838. package/dist/core/vocabulary/maturity.js.map +1 -0
  839. package/dist/core/{report.js → vocabulary/report.js} +30 -2
  840. package/dist/core/vocabulary/report.js.map +1 -0
  841. package/dist/core/vocabulary/steps.js +266 -0
  842. package/dist/core/vocabulary/steps.js.map +1 -0
  843. package/dist/core/workspace/discover.js +239 -0
  844. package/dist/core/workspace/discover.js.map +1 -0
  845. package/dist/core/workspace/render.js +52 -0
  846. package/dist/core/workspace/render.js.map +1 -0
  847. package/examples/README.md +189 -0
  848. package/examples/docs/architecture/adrs/0001-transactional-outbox.md +33 -0
  849. package/examples/docs/architecture/capabilities.yaml +31 -0
  850. package/examples/docs/architecture/landscape.likec4 +280 -0
  851. package/examples/docs/architecture/obligations.yaml +23 -0
  852. package/examples/docs/architecture/permissions.yaml +50 -0
  853. package/examples/docs/architecture/subsystems.likec4 +11 -0
  854. package/examples/docs/architecture/usecases/order-notification.likec4 +65 -0
  855. package/examples/docs/capabilities/checkout/spec.md +55 -0
  856. package/examples/docs/capabilities/identity/tokens/spec.md +42 -0
  857. package/examples/docs/capabilities/order-notifications/spec.md +43 -0
  858. package/examples/docs/features/FEAT-101-payment-splitting/adrs/0001-dedicated-split-service.md +18 -0
  859. package/examples/docs/features/FEAT-101-payment-splitting/delta.likec4 +94 -0
  860. package/examples/docs/features/FEAT-101-payment-splitting/intent.md +44 -0
  861. package/examples/docs/features/FEAT-101-payment-splitting/specs/checkout-web/spec.md +21 -0
  862. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +129 -0
  863. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/spec.md +36 -0
  864. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/arch.spec.md +40 -0
  865. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +116 -0
  866. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/spec.md +55 -0
  867. package/examples/docs/features/FEAT-112-retire-order-v1/intent.md +35 -0
  868. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +28 -0
  869. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/spec.md +17 -0
  870. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/architecture/landscape.likec4 +253 -0
  871. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/order-service/spec.md +121 -0
  872. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/openapi.yaml +111 -0
  873. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/spec.md +58 -0
  874. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/manifest.json +32 -0
  875. package/examples/docs/features/archive/FEAT-088-refunds/adrs/0001-refunds-stay-in-payment-service.md +26 -0
  876. package/examples/docs/features/archive/FEAT-088-refunds/delta.likec4 +28 -0
  877. package/examples/docs/features/archive/FEAT-088-refunds/intent.md +26 -0
  878. package/examples/docs/features/archive/FEAT-088-refunds/specs/order-service/spec.md +37 -0
  879. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +70 -0
  880. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/spec.md +23 -0
  881. package/examples/docs/features/archive/FEAT-088-refunds/verification.yaml +76 -0
  882. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/architecture/landscape.likec4 +275 -0
  883. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/asyncapi.yaml +96 -0
  884. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/spec.md +82 -0
  885. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/asyncapi.yaml +164 -0
  886. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/spec.md +91 -0
  887. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/manifest.json +46 -0
  888. package/examples/docs/features/archive/FEAT-120-refund-notification/delta.likec4 +36 -0
  889. package/examples/docs/features/archive/FEAT-120-refund-notification/intent.md +22 -0
  890. package/examples/docs/features/archive/FEAT-120-refund-notification/scenario-report.json +10 -0
  891. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/asyncapi.yaml +208 -0
  892. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/spec.md +24 -0
  893. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/asyncapi.yaml +137 -0
  894. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/spec.md +31 -0
  895. package/examples/docs/features/archive/FEAT-120-refund-notification/verification.yaml +84 -0
  896. package/examples/docs/glossary/order.md +15 -0
  897. package/examples/docs/glossary/payments/authorization.md +14 -0
  898. package/examples/docs/likec4.config.json +9 -0
  899. package/examples/docs/loam.json +3 -0
  900. package/examples/docs/services/checkout-web/model.likec4 +109 -0
  901. package/examples/docs/services/checkout-web/spec.md +91 -0
  902. package/examples/docs/services/checkout-web/ui/pages/checkout.page.yaml +40 -0
  903. package/examples/docs/services/checkout-web/ui/pages/order-status.page.yaml +24 -0
  904. package/examples/docs/services/checkout-web/ui/pages/split-payment.page.yaml +26 -0
  905. package/examples/docs/services/order-service/adrs/0001-orchestrated-order-lifecycle.md +52 -0
  906. package/examples/docs/services/order-service/arch.spec.md +127 -0
  907. package/examples/docs/services/order-service/asyncapi.yaml +108 -0
  908. package/examples/docs/services/order-service/health.yaml +57 -0
  909. package/examples/docs/services/order-service/model.likec4 +138 -0
  910. package/examples/docs/services/order-service/openapi.yaml +257 -0
  911. package/examples/docs/services/order-service/runbook.md +62 -0
  912. package/examples/docs/services/order-service/spec.md +134 -0
  913. package/examples/docs/services/payment-service/adrs/0001-payments-outbox.md +18 -0
  914. package/examples/docs/services/payment-service/adrs/0002-idempotency-keys-on-authorization.md +27 -0
  915. package/examples/docs/services/payment-service/arch.spec.md +73 -0
  916. package/examples/docs/services/payment-service/asyncapi.yaml +130 -0
  917. package/examples/docs/services/payment-service/health.yaml +39 -0
  918. package/examples/docs/services/payment-service/model.likec4 +67 -0
  919. package/examples/docs/services/payment-service/openapi.yaml +151 -0
  920. package/examples/docs/services/payment-service/runbook.md +37 -0
  921. package/examples/docs/services/payment-service/spec.md +88 -0
  922. package/examples/docs/services/platform/identity-service/adrs/0001-token-introspection-over-local-validation.md +60 -0
  923. package/examples/docs/services/platform/identity-service/arch.spec.md +117 -0
  924. package/examples/docs/services/platform/identity-service/health.yaml +70 -0
  925. package/examples/docs/services/platform/identity-service/model.likec4 +83 -0
  926. package/examples/docs/services/platform/identity-service/openapi.yaml +197 -0
  927. package/examples/docs/services/platform/identity-service/runbook.md +95 -0
  928. package/examples/docs/services/platform/identity-service/spec.md +119 -0
  929. package/examples/docs/services/platform/notification-service/asyncapi.yaml +199 -0
  930. package/examples/docs/services/platform/notification-service/health.yaml +35 -0
  931. package/examples/docs/services/platform/notification-service/model.likec4 +118 -0
  932. package/examples/docs/services/platform/notification-service/runbook.md +48 -0
  933. package/examples/docs/services/platform/notification-service/spec.md +112 -0
  934. package/examples/docs/services/platform/subsystem.yaml +2 -0
  935. package/examples/fleet.yaml +57 -0
  936. package/package.json +19 -6
  937. package/dist/commands/adopt.js +0 -240
  938. package/dist/commands/adopt.js.map +0 -1
  939. package/dist/commands/archive.js +0 -1313
  940. package/dist/commands/archive.js.map +0 -1
  941. package/dist/commands/delta.js +0 -388
  942. package/dist/commands/delta.js.map +0 -1
  943. package/dist/commands/docs-repo-gate.js.map +0 -1
  944. package/dist/commands/format.js +0 -17
  945. package/dist/commands/format.js.map +0 -1
  946. package/dist/commands/gherkin.js +0 -309
  947. package/dist/commands/gherkin.js.map +0 -1
  948. package/dist/commands/init.js.map +0 -1
  949. package/dist/commands/list.js +0 -295
  950. package/dist/commands/list.js.map +0 -1
  951. package/dist/commands/migrate-openspec.js +0 -950
  952. package/dist/commands/migrate-openspec.js.map +0 -1
  953. package/dist/commands/new.js +0 -413
  954. package/dist/commands/new.js.map +0 -1
  955. package/dist/commands/rebase.js +0 -356
  956. package/dist/commands/rebase.js.map +0 -1
  957. package/dist/commands/show.js.map +0 -1
  958. package/dist/commands/status.js +0 -262
  959. package/dist/commands/status.js.map +0 -1
  960. package/dist/commands/unarchive.js.map +0 -1
  961. package/dist/commands/validate.js +0 -1773
  962. package/dist/commands/validate.js.map +0 -1
  963. package/dist/commands/verify.js +0 -775
  964. package/dist/commands/verify.js.map +0 -1
  965. package/dist/commands/vouch.js +0 -421
  966. package/dist/commands/vouch.js.map +0 -1
  967. package/dist/core/agent.js +0 -2173
  968. package/dist/core/agent.js.map +0 -1
  969. package/dist/core/agents-stamp.js.map +0 -1
  970. package/dist/core/arch.js +0 -130
  971. package/dist/core/arch.js.map +0 -1
  972. package/dist/core/asyncapi.js.map +0 -1
  973. package/dist/core/brief.js +0 -755
  974. package/dist/core/brief.js.map +0 -1
  975. package/dist/core/coherence.js +0 -577
  976. package/dist/core/coherence.js.map +0 -1
  977. package/dist/core/concurrency.js.map +0 -1
  978. package/dist/core/config.js.map +0 -1
  979. package/dist/core/delta.js +0 -476
  980. package/dist/core/delta.js.map +0 -1
  981. package/dist/core/dependencies.js +0 -356
  982. package/dist/core/dependencies.js.map +0 -1
  983. package/dist/core/doctor.js +0 -596
  984. package/dist/core/doctor.js.map +0 -1
  985. package/dist/core/document-bytes.js.map +0 -1
  986. package/dist/core/explore.js.map +0 -1
  987. package/dist/core/frontmatter.js.map +0 -1
  988. package/dist/core/gherkin.js +0 -463
  989. package/dist/core/gherkin.js.map +0 -1
  990. package/dist/core/health.js +0 -67
  991. package/dist/core/health.js.map +0 -1
  992. package/dist/core/ids.js +0 -122
  993. package/dist/core/ids.js.map +0 -1
  994. package/dist/core/issue.js +0 -9
  995. package/dist/core/issue.js.map +0 -1
  996. package/dist/core/json.js +0 -74
  997. package/dist/core/json.js.map +0 -1
  998. package/dist/core/likec4.js +0 -489
  999. package/dist/core/likec4.js.map +0 -1
  1000. package/dist/core/maturity.js +0 -66
  1001. package/dist/core/maturity.js.map +0 -1
  1002. package/dist/core/openapi-merge.js +0 -552
  1003. package/dist/core/openapi-merge.js.map +0 -1
  1004. package/dist/core/openapi.js.map +0 -1
  1005. package/dist/core/openspec-inventory.js +0 -1440
  1006. package/dist/core/openspec-inventory.js.map +0 -1
  1007. package/dist/core/path-safety.js.map +0 -1
  1008. package/dist/core/provenance.js +0 -787
  1009. package/dist/core/provenance.js.map +0 -1
  1010. package/dist/core/records.js.map +0 -1
  1011. package/dist/core/repo.js +0 -438
  1012. package/dist/core/repo.js.map +0 -1
  1013. package/dist/core/report.js.map +0 -1
  1014. package/dist/core/spec.js +0 -505
  1015. package/dist/core/spec.js.map +0 -1
  1016. package/dist/core/staging.js +0 -890
  1017. package/dist/core/staging.js.map +0 -1
  1018. package/dist/core/status.js +0 -1132
  1019. package/dist/core/status.js.map +0 -1
  1020. package/dist/core/steps.js +0 -72
  1021. package/dist/core/steps.js.map +0 -1
  1022. package/dist/core/verify.js +0 -807
  1023. package/dist/core/verify.js.map +0 -1
  1024. package/dist/core/version.js.map +0 -1
  1025. /package/dist/core/{concurrency.js → kernel/concurrency.js} +0 -0
  1026. /package/dist/core/{records.js → kernel/records.js} +0 -0
package/SCHEMA.md CHANGED
@@ -1,25 +1,56 @@
1
1
  # loam docs-repo schema
2
2
 
3
- This documents the layout of the **docs repo** — the single shared source of truth that `loam` reads and writes. A runnable example lives under [`examples/docs/`](examples/docs).
3
+ This documents the layout of the **docs repo** — the single shared source of truth for the governed
4
+ system that `loam` reads and writes. A runnable distributed example lives in the repository under
5
+ [`examples/docs/`](https://github.com/ybotok/loam/tree/main/examples/docs).
4
6
 
5
- Everything here is plain files. `loam` derives views and indexes from them; delete `loam` and the docs remain.
7
+ Everything here is plain files. `loam` derives **projections** and indexes from them; delete `loam`
8
+ and the docs remain.
6
9
 
7
10
  ## Layer stack
8
11
 
9
- C4 is the center. Each artifact is a **source** (authored), a **hybrid** (agent-written at bootstrap, authored forward), or a **derived** view (generated from the model + spine).
12
+ C4 is the center. Each artifact is a **source** (authored), a **hybrid** (agent-written at
13
+ bootstrap, authored forward), or a **derived** view (generated from the model + spine).
10
14
 
11
15
  ```
12
16
  presentation │ UI page-prototypes ─(consume)─► endpoints spec = source · proto = derived [later]
13
17
  behavior │ gherkin scenarios ──(govern)──► pages source
14
18
  arch behavior│ arch.spec.md ──(covers)──► C4 · health source
15
19
  contract │ OpenAPI ◄──(detail of C4 "exposes/calls") hybrid
16
- async │ AsyncAPI ◄──(detail of C4 "publishes/consumes") hybrid [no merge yet]
17
- structure │ C4 model — services, relationships source (adopt-seeded)
20
+ async │ AsyncAPI ◄──(detail of C4 "publishes/consumes") hybrid
21
+ authorization│ permissions.yaml ◄──(required by)── requirements source [system vocabulary]
22
+ capability │ capabilities/<cap>/spec.md + capabilities.yaml source [business tree + vocabulary, opt-in]
23
+ │ ◄──(realized by)── requirements
24
+ structure │ C4 model — governed boundaries and relationships source (adopt-seeded)
18
25
  ops / why │ ADR · runbook · health source
19
26
  truth │ code ground truth
20
27
  ```
21
28
 
22
- All artifacts are wired by one **ID spine**: `FEAT-<id>`, service id, and C4 element ids, carried in frontmatter and LikeC4 tags.
29
+ The artifacts are wired by a family of explicit joins rather than filenames that merely look
30
+ related. The canonical join table below is the contract; `loam validate` grades each mechanically.
31
+
32
+ ## Topology and the frozen `service` term
33
+
34
+ `service` is loam's frozen CLI, path and finding vocabulary for **one governed implementation
35
+ boundary**. It does not assert that the boundary is a network service or a microservice. A boundary
36
+ is the unit whose code, living requirements, contracts, provenance and verification evidence are
37
+ reviewed together and filed under one `services/<id>/` directory.
38
+
39
+ - A **modular monolith** normally has one service id. Its bound C4 root contains nested modules or
40
+ components; architecture requirements can name those elements and their edges with `Covers:`, and
41
+ feature deltas can change them without turning them into fictional services.
42
+ - A **distributed system** normally has several service ids. Relationships between their bound
43
+ roots or descendants can join to OpenAPI operations, AsyncAPI messages and cross-boundary use
44
+ cases.
45
+ - A **hybrid** uses both forms in one landscape. The nearest bound ancestor decides which service
46
+ owns a nested element, so C4 depth never decides topology on its own.
47
+
48
+ The boundary is also the aggregation level of the existing fleet reports: maturity, dependency
49
+ order and the scorecard do not mint independent deployment states for nested modules. A module that
50
+ needs its own repository binding, contracts, evidence and deploy status is a separate governed
51
+ boundary; one that does not stays an element inside its application. The schema therefore broadens
52
+ no command or path and weakens no service-level check — it states what the resolver and loam's own
53
+ single-binary self-model already do.
23
54
 
24
55
  ## Layout
25
56
 
@@ -29,10 +60,18 @@ docs/
29
60
  loam.json {"docsDir": "."} — makes the repo self-describing [init --create]
30
61
  likec4.config.json one LikeC4 project, scoped to architecture/ [init --create]
31
62
  architecture/
32
- landscape.likec4 global C4 (fleet landscape)
63
+ landscape.likec4 global C4 (system landscape)
64
+ subsystems.likec4 one view per subsystem, views only [GENERATED: loam subsystem sync]
65
+ permissions.yaml system-wide authorization vocabulary [authored, opt-in]
66
+ capabilities.yaml system-wide capability vocabulary [authored, opt-in]
67
+ obligations.yaml system-wide architectural obligations [authored, opt-in]
68
+ adrs/NNNN-*.md SYSTEM-level MADR decisions [authored, opt-in]
33
69
  landscape.health.yaml composed health model [derived, later]
34
- services/<svc>/
35
- model.likec4 service C4 (containers/components) [adopt]
70
+ capabilities/<cap>/ the AUTHORED business tree; nesting spelled by the tree [authored, opt-in]
71
+ spec.md narrative, then `## Requirements` — `Requirement-ID:` REQUIRED
72
+ glossary/<term>.md the system's domain vocabulary, one file per term; nesting allowed [authored, opt-in]
73
+ services/<svc>/ at the root (unfiled), or inside subsystem directories at any depth
74
+ model.likec4 boundary C4 (containers/components) [adopt]
36
75
  spec.md living requirements (Requirement/Scenario) [adopt]
37
76
  arch.spec.md living ARCHITECTURE requirements (outbox, retries, alerts; Covers:) [authored]
38
77
  openapi.yaml API contract [adopt / authored]
@@ -40,14 +79,18 @@ docs/
40
79
  adrs/NNNN-*.md MADR decisions [adopt-seed / authored]
41
80
  runbook.md operational runbook [adopt-draft / authored]
42
81
  health.yaml SLI/SLO, checks, critical deps [authored]
82
+ steps.yaml the step phrases this suite defines [authored, opt-in]
43
83
  ui/pages/*.page.yaml page-specs (UI services) [later]
44
- flows/ interaction flows -> sequence views [later]
45
84
  features/<FEAT>/
46
85
  intent.md business intent / proposal (why) [authored]
47
86
  delta.likec4 C4 delta (architecture) [authored]
48
87
  specs/<svc>/spec.md requirement delta (ADDED/MODIFIED/REMOVED + scenarios) [authored]
49
88
  specs/<svc>/arch.spec.md architectural requirement delta, same algebra [authored]
50
89
  specs/<svc>/openapi.yaml the endpoints this feature adds [authored]
90
+ specs/<svc>/asyncapi.yaml the event-contract delta, slot-pinned [authored]
91
+ capabilities/<cap>/spec.md capability delta, same requirement grammar [authored]
92
+ glossary/<term>.md a word this feature brings with it; create-only [authored]
93
+ usecases/<name>.likec4 a FLOW this feature brings; views-only, create-only [authored]
51
94
  adrs/NNNN-*.md feature-level decisions [authored]
52
95
  verification.yaml the done-check: claims + verdicts + evidence, one attestation per service [loam verify --service <id> --record]
53
96
  features/archive/<FEAT>/ a shipped change — the same files, plus:
@@ -56,294 +99,2038 @@ docs/
56
99
  .loam-commit transient: the commit intent journal [archive/unarchive only]
57
100
  ```
58
101
 
59
- The last two exist only inside an `archive`/`unarchive` commit window and are removed after it. Both belong in the docs repo's `.gitignore`. Finding one at rest means a write did not finish: `loam doctor` grades it, and the next `archive`/`unarchive` repairs or refuses.
102
+ There is no `flows/` tree, and there will not be one. A cross-service **interaction flow** is a
103
+ `dynamic view` under `architecture/` — the one place that already holds every cross-service edge —
104
+ tagged with the capability it realizes. A flow that spans services does not belong under one
105
+ service's directory, and the view's steps join to the landscape's own relationships, which is what
106
+ makes the flow checkable rather than merely drawn.
107
+
108
+ A feature **brings** one the way it brings every other axis: `features/<FEAT>/usecases/<name>.likec4`
109
+ is a views-only document that `loam archive` copies into `architecture/usecases/` and
110
+ `loam unarchive` takes back. Create-only, and graded before the copy against the map the same merge
111
+ would leave behind — so a flow may draw a hop into a service the feature's own `delta.likec4` adds,
112
+ and a hop into an element the merge does NOT land is refused at plan time rather than archived into
113
+ a map the next reader's `loam validate --all` fails on.
114
+
115
+ ## Subsystems — a navigable tree under `services/`
116
+
117
+ `services/` may be one flat level (the compatibility case, and forever legal) or an unbounded tree
118
+ of **subsystem** directories grouping the service directories. The whole design turns on one
119
+ invariant: **a service id is the leaf directory name, and placement is never part of any identity.**
120
+ `loam.json`'s `service`, `metadata { service }`, spec frontmatter, `features/<FEAT>/specs/<svc>/`
121
+ and every digest are byte-identical before and after a service moves; `--service` never takes a
122
+ path, and no `subsystem` field exists in any frontmatter. The tree exists so a human can find a
123
+ service and so views can be scoped — it carries **no policy**, and nothing branches on it.
124
+
125
+ A directory under `services/` is classified **three ways, never two**:
126
+
127
+ 1. holding `subsystem.yaml` it is a **subsystem**, and is walked;
128
+ 2. holding any service artifact (`model.likec4`, `spec.md`, `arch.spec.md`, `openapi.yaml`,
129
+ `asyncapi.yaml`, `runbook.md`, `health.yaml`, `adrs/`) it is a **service**, and is not walked
130
+ deeper;
131
+ 3. holding neither while containing subdirectories it is an **error** (`subsystem.unmarked`) naming
132
+ every service found beneath it — which stays enumerated. The third branch exists because two-way
133
+ classification loses services silently on an ordinary clean merge: one branch deletes an emptied
134
+ subsystem's marker while another moves a service in, and the group directory would otherwise be
135
+ read as one empty service. The fleet is never reported smaller than it is.
136
+
137
+ `subsystem.yaml` is a **marker, not a manifest**:
138
+
139
+ ```yaml
140
+ title: Payments # optional
141
+ description: Money movement # optional
142
+ owner: payments-team # optional
143
+ ```
144
+
145
+ An empty file is a valid marker — presence classifies; content is metadata. It **never** lists
146
+ members: the directory itself is the membership record, kept exactly once. A `members` key is
147
+ `subsystem.invalid`; a marker beside service artifacts is `subsystem.marker-misplaced` (the
148
+ directory stays a service); an unreadable marker still classifies its directory as a subsystem and
149
+ reports exactly one `subsystem.invalid`.
150
+
151
+ Names share **one flat namespace**: every subsystem name and service id, unique across the whole
152
+ tree at any depth, never colliding with each other (`subsystem.name-collision` names every
153
+ claimant). Subsystem names take the service-id grammar (`subsystem.name-invalid` otherwise); depth
154
+ is unbounded and is the author's problem, not the tool's. A service directly under `services/` is
155
+ **unfiled** — permanent, normal, and never a finding; `loam list` carries the count
156
+ (`unfiledServices`, plus `services[].subsystem` and `subsystems[]`), and an empty subsystem is
157
+ legal, since `loam subsystem new` must be usable before anything moves in.
158
+
159
+ The tree is mirrored into **one generated file, never into the authored landscape**:
160
+ `architecture/subsystems.likec4` holds LikeC4 **views only** — no model, no tags, no `specification`
161
+ — one view per subsystem, transitively enumerating the services beneath it as `include` lines naming
162
+ landscape *element* ids (resolved through the same `metadata { service }`-then-title join every
163
+ check uses; a member nothing models is omitted, which `landscape.service-unmodelled` already
164
+ reports). The output is deterministic and line-oriented — subsystems sorted by path, members by id,
165
+ one `include` per line, no timestamps, no absolute paths — so it is byte-reproducible on any machine
166
+ and two concurrent moves into different groups merge in git without intervention.
167
+ `loam subsystem sync` is the one regenerator (it also *removes* the file when the tree has no
168
+ subsystems), every `loam subsystem` write verb regenerates it inside the same transaction, and
169
+ `validate --all` grades staleness by **byte comparison** — exactly one error,
170
+ `subsystem.views-stale`, on exactly that file; nothing in loam ever parses it: a views-only document
171
+ does not parse standalone (its `include` lines reference elements the landscape defines), and it
172
+ holds **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
173
+ which merges the whole `architecture/` project, resolves its includes.
174
+
175
+ A move is **one journaled transaction over N directory renames plus the regenerated views file**
176
+ (`loam subsystem move <name>... --into <sub|.>`; `rename` is a one-rename move; whole subtrees move
177
+ as one rename). It refuses only when git reports uncommitted changes under a directory being moved
178
+ (`move-uncommitted` — where git will not say, it proceeds), and after landing it stages the renames
179
+ and the views file in git without committing. Archive snapshots survive any of this: a version-3
180
+ snapshot re-keys `services/` entries by `(service, artifact)` and restores wherever the service
181
+ lives *today*.
182
+
183
+ ## Canonical joins
184
+
185
+ | Meaning | Producer | Consumer | Join key |
186
+ |---|---|---|---|
187
+ | Change identity | `features/<FEAT>/`, intent frontmatter | tagged delta elements/edges, generated Gherkin | feature id / `#<FEAT>` |
188
+ | Service identity | `services/<svc>/`, `loam.json.service` | LikeC4 element metadata and feature deltas | service id / `metadata { service }` |
189
+ | Synchronous operation | LikeC4 edge `metadata { op }` | requirement `Operations:` and provider OpenAPI | exact `operationId` |
190
+ | Asynchronous message | LikeC4 edge `publishes` / `consumes` | requirement `Publishes:` / `Consumes:` and AsyncAPI 3 operation/message | exact message id |
191
+ | Architecture coverage | arch requirement `Covers:` | C4 element/edge or `health.yaml` signal | element/edge id, `alert:<id>`, `sli:<id>` |
192
+ | Authorization | requirement `Requires:` | `architecture/permissions.yaml` | `<subject>/<permission>` |
193
+ | Capability | requirement `Capability:` (list; also `Capabilities:`) | `architecture/capabilities.yaml` OR `capabilities/<cap>/spec.md` | declared capability id, `/` allowed for nesting |
194
+ | Capability requirement | requirement `Realizes:` (list) | a requirement in `capabilities/<cap>/spec.md` | `<capability-id>#<Requirement-ID>`, split at the LAST `#`, with an optional `@<digest>` pin |
195
+ | Capability requirement (flow) | a `dynamic view` tag `#req-<slug>` | a requirement of the capability the view's `#cap-` tag names | the `Requirement-ID` with every character outside `[A-Za-z0-9_-]` flattened to `-` |
196
+
197
+ These keys are independent: an operation id is not a permission and a service id is not inferred
198
+ from a feature directory. A file can therefore be structurally valid while a join is broken; the
199
+ broken join is the finding loam reports.
200
+
201
+ The last two exist only inside an `archive`/`unarchive` commit window and are removed after it. Both
202
+ belong in the docs repo's `.gitignore`. Finding one at rest means a write did not finish:
203
+ `loam doctor` grades it, and the next `archive`/`unarchive` repairs or refuses.
60
204
 
61
205
  ## `loam.json` — the wiring, one file per repository
62
206
 
63
- The docs repo is one repository; each service is another. Every one of them carries a **committed** `loam.json` at its root, and that file is the only thing joining them: there is no environment variable, no global config, no registry. A command that finds no `loam.json` at or above its working directory refuses with `no-config` rather than guessing which fleet it is in. (Discovery walks upward from the cwd to the repository root, so running loam from a subdirectory works; `loam init` writes the file where it is standing.)
207
+ The docs repo is one repository; each governed implementation boundary is represented by a code
208
+ repository binding. Every one of them carries a **committed** `loam.json` at its root, and that file
209
+ is the only thing joining them: there is no environment variable, no global config, no registry. A
210
+ command that finds no `loam.json` at or above its working directory refuses with `no-config` rather
211
+ than guessing which system it is in. (Discovery walks upward from the cwd to the repository root,
212
+ so running loam from a subdirectory works; `loam init` writes the file where it is standing.)
64
213
 
65
214
  ```json
66
215
  { "docsDir": "../docs-repo", "service": "payment-service", "gherkinDir": "features" }
67
216
  ```
68
217
 
69
- - **`docsDir`** (required) — where the docs repo is. It is stored **exactly as it was passed** and resolved against the directory holding the `loam.json`, so `../docs-repo` keeps resolving on every machine that checks the two repos out side by side. That is the whole reason the file is committed, and why an absolute path is a warning (`doctor.docs-absolute`): it resolves only on the machine that ran `loam init`. The docs repo's own file says `"docsDir": "."` — see the bootstrap step under "Operating at fleet scale". A `docsDir` pointing at nothing refuses with `docs-missing`; one pointing at a directory with no `services/` refuses with `services-missing` (most often the service repo itself, after a typo), because an unreadable fleet and an empty fleet are different facts and only one of them is fine.
70
- - **`service`** (required in a service repo, absent in the docs repo) — the canonical id of the service this repository contains: the `services/<id>/` directory it is allowed to speak for. `loam vouch`, `loam gherkin` and `loam verify --service` all bind to it and refuse (`repository-unavailable`) without it — a stamp, a generated suite or an attestation has to be tied to the code it is about. It must be a legal directory name under the same grammar `--service` is checked against (`[A-Za-z0-9][A-Za-z0-9._-]*`); anything else is `config-invalid`.
71
- - **`gherkinDir`** (optional, default `features`) — the directory `loam gherkin` writes its own `loam/` subtree into.
72
-
73
- `loam init --docs <dir>` **joins** an existing docs repo (one with `services/` and `AGENTS.md`); creating a new one requires `--create`, so a mistyped path cannot quietly scaffold a second source of truth beside the real one. `--docs` wins only when it is actually passed: a re-run in a repo whose committed `loam.json` already names a `docsDir` keeps that pointer, `--create` included, and spreads the rest of the config forward. Otherwise following `doctor`'s own advice to re-run `init` would repoint a wired repo at an empty decoy, over which `validate --all` goes green. `loam init --json` says which source won, as `docsDirSource: "flag" | "config" | "default"`. `loam doctor` is the read-only preflight over all of the above: it reads the file defensively (corrupt JSON included), resolves `docsDir`, probes access, checks `services/` and the fleet map, and reports the binding — every finding carrying a concrete `fix`.
218
+ - **`docsDir`** (required) — where the docs repo is. It is stored **exactly as it was passed** and
219
+ resolved against the directory holding the `loam.json`, so `../docs-repo` keeps resolving on every
220
+ machine that checks the two repos out side by side. That is the whole reason the file is
221
+ committed, and why an absolute path is a warning (`doctor.docs-absolute`): it resolves only on the
222
+ machine that ran `loam init`. The docs repo's own file says `"docsDir": "."` — see the bootstrap
223
+ step under "Operating at fleet scale". A `docsDir` pointing at nothing refuses with
224
+ `docs-missing`; one pointing at a directory with no `services/` refuses with `services-missing`
225
+ (most often the service repo itself, after a typo), because an unreadable fleet and an empty fleet
226
+ are different facts and only one of them is fine.
227
+ - **`service`** (required in a governed code repo, absent in the docs repo) — the canonical id of
228
+ the implementation boundary this repository speaks for: the `services/<id>/` directory it is
229
+ allowed to write evidence about. The field name is a frozen compatibility term, not a topology
230
+ claim: `commerce-app` may be a modular monolith with dozens of nested C4 elements and still have
231
+ exactly one service id.
232
+ `loam vouch`, `loam gherkin`, and `loam verify --service` **when it writes with `--record` or
233
+ `--results`** bind to it and refuse (`repository-unavailable`) without it — a stamp, generated
234
+ suite or attestation has to be tied to the code it is about. The read-only
235
+ `loam verify <FEAT> --service <id>` checklist needs no binding and works from the docs repo. The
236
+ id must satisfy the same grammar as `--service` (`[A-Za-z0-9][A-Za-z0-9._-]*`); anything else is
237
+ `config-invalid`.
238
+ - **`gherkinDir`** (optional, default `features`) — the directory `loam gherkin` writes its own
239
+ `loam/` subtree into.
240
+ - **`contracts`** (optional) — where this repository's **build** writes its OpenAPI document,
241
+ e.g. `{ "contracts": { "openapi": "build/openapi.yaml" } }`. `loam validate --service`, run in
242
+ this repo, digests that document and compares it with the committed
243
+ `services/<id>/openapi.yaml`: `openapi.generated-stale` (warn) when they differ,
244
+ `contracts.source-missing` (error) when nothing is there, `contracts.source-invalid` (error) when
245
+ the path escapes the repository or the file does not parse.
246
+
247
+ **loam reads it and never writes it.** The copy into the docs repo stays a human `cp` reviewed in
248
+ a pull request — that is what keeps the committed contract a document somebody agreed to rather
249
+ than a cache of whatever the build last produced. It also stays on the right side of the
250
+ no-extractor line: a standard document the team's own build emitted, at a path a human named,
251
+ parsed and digested, with no meaning derived and no line of service code read. Exactly the
252
+ category `verify --results` already occupies with a cucumber report.
253
+
254
+ The digest is taken over **canonical JSON**, never raw bytes, so two generator versions that order
255
+ keys differently — or a dumper that re-wraps a description — are silent. A permanent warning
256
+ nobody can clear is the first thing a fleet turns off. The whole family is silent from the docs
257
+ repo (there is no build output to read) and for any repo with no `contracts` block.
258
+
259
+ The premise this exists to check is the one this document otherwise assumes without saying so:
260
+ that `services/<id>/openapi.yaml` is the contract the service actually serves. Most fleets
261
+ generate that file and copy it across by hand, so the assumption decays quietly — the spine checks
262
+ go on grading last quarter's endpoints, and every one of them stays green.
263
+ - **`agentTools` / `agentProfile` / `agentFiles`** (optional, written by `loam init`) — the selected
264
+ tool adapters, the `full|service|docs` lifecycle-workflow subset (the support-only `loam-report`
265
+ is present in all three), and a repo-relative path → lowercase
266
+ sha256 manifest for generated command/skill pointers. A digest grants narrowly scoped refresh
267
+ authority: init may update that file only while its current bytes still match. A human edit breaks
268
+ the match and is preserved. These fields never affect validation or lifecycle semantics.
269
+
270
+ `loam init --docs <dir>` **joins** an existing docs repo (one with `services/` and `AGENTS.md`);
271
+ creating a new one requires `--create`, so a mistyped path cannot quietly scaffold a second source
272
+ of truth beside the real one. `--docs` wins only when it is actually passed: a re-run in a repo
273
+ whose committed `loam.json` already names a `docsDir` keeps that pointer, `--create` included, and
274
+ spreads the rest of the config forward. Otherwise following `doctor`'s own advice to re-run `init`
275
+ would repoint a wired repo at an empty decoy, over which `validate --all` goes green.
276
+ `loam init --json` says which source won, as `docsDirSource: "flag" | "config" | "default"`.
277
+ `loam doctor` is the read-only preflight over all of the above: it reads the file defensively
278
+ (corrupt JSON included), resolves `docsDir`, probes access, checks `services/` and the fleet map,
279
+ and reports the binding — every finding carrying a concrete `fix`.
280
+
281
+ The committed bindings are also what `loam open` derives an editor workspace from: each service
282
+ repo's `docsDir` is the join, so a `.code-workspace` naming the docs repo and every checkout bound
283
+ to it can be generated — discovered by a shallow scan beside the docs repo and the repo the command
284
+ runs in (`--root` replaces those roots) — rather than maintained by hand.
74
285
 
75
286
  ## Conventions
76
287
 
77
- **Frontmatter**: `status`, `owner`, `service` or `feature`, `last_verified`, `vouched_by`, `sources` (literal files and directories), `sources_digest`, `content_digest`, `sources_files`. `loam validate` reads it in exactly three files — a service's living `spec.md`, its living `arch.spec.md` when present (same conventions, same checks, only the label differs), and a feature's `intent.md`: the documents whose identity and status everything else joins on. `adrs/` and `runbook.md` are presence-tracked only (`loam list` says whether they exist); their frontmatter is read by nothing today, so a field written there is a note to a future reader, not a claim any check will catch. `health.yaml`'s frontmatter is equally unread, but the file itself is not inert: `loam validate` reads its top-level `slis:`/`alerts:` ids for `Covers:` resolution and the `health.uncovered` warning (see the architecture spec axis below). For the two checked files:
78
-
79
- - `status` — services: `draft` -> `verified`; features: `proposed` -> `in_progress` -> `built` -> `done`. An undocumented value is an **error**: a typo (`verifed`) would otherwise read as unverified forever.
80
- - a frontmatter block that does not parse as YAML is an **error** of its own (`frontmatter.malformed`), and the only finding that file's header produces: the fields are unreadable, not absent, and the cascade the silent parse used to trigger — "owner missing", "status missing", "names no sources" — sent authors adding lines to a block YAML refuses. The field checks and the sources chain resume once the header parses.
81
- - `service` / `feature` — must match the directory the file lives under. A mismatch is an **error**; absence is a warning.
82
- - `sources` — paths in the *service's own repo* the artifact was written from: literal files and directories only, a directory meaning everything beneath it. Resolved when `loam` runs inside that repo (`service` in `loam.json`); a path that no longer exists is an **error**, and so is a glob-looking entry (`*`, `?` or `[`) — patterns are not supported, and are refused loudly rather than matched, because a pattern dialect nobody can be sure of digests a different file set than its author intended, corrupting the trust signal silently. This is the only mechanical tie between the docs and the code: everything else loam checks is internal consistency, which a fluent fiction satisfies too.
83
- - `sources_digest` / `last_verified` / `vouched_by` — written by `loam vouch`, never by hand. `vouched_by` is git's identity for the repo the vouch ran in (`Name <email>`): a name, not a signature, and it proves nothing cryptographically — what it buys is that a reviewer can ask a *specific person* what they read, where `status: verified` alone recorded only that some process wrote the word. The digest is a content hash of the files `sources` names, taken when a human last read them; `loam validate`, run in that repo, recomputes it and reports `sources.current`, `sources.stale` (the code moved since anyone looked), or `sources.unvouched` (`sources` with no digest — nobody has ever stamped it). All warnings: staleness is a signal, and only a person can say whether the doc is still right.
84
- - `content_digest` — written by `loam vouch`, never by hand, like its siblings: a hash of the document's OWN body, so the stamp covers the words as well as the code. Editing a spec after it was vouched used to leave `status: verified` standing over prose nobody read — the exact forgery an agent-written corpus invites. `loam validate` recomputes it wherever the doc is readable; this check needs **no service repo**, so unlike the `sources.*` chain it runs for `--service` and `--all` alike, from the docs repo included. A `verified` spec whose current body hash mismatches gets `content.stale` (**warning**, the `sources.stale` grading: the doc changed since it was vouched, and only a person can say whether verified still holds). A verified doc with no `content_digest` predates the stamp and stays quiet — the fix is re-vouching.
288
+ **Documents link to each other with standard markdown links**, and the target is a relative path:
289
+ `[0001 — transactional outbox](../../architecture/adrs/0001-transactional-outbox.md)`. Two reasons,
290
+ and the second is the one that decides it. A markdown link renders as a link in pull-request review,
291
+ which is where these documents are actually read, while `[[0001 transactional outbox]]` renders
292
+ there as the literal brackets somebody typed. And its target is a real relative path, so "does this
293
+ link resolve" is a filesystem question with a yes-or-no answer — where resolving a wikilink means
294
+ reimplementing Obsidian's shortest-unique-path search across the whole repo, which is guessing, and
295
+ loam refuses and names rather than guessing. Nothing is lost by the choice: a docs repo is a folder
296
+ of Markdown files, which is exactly what Obsidian reads, so graph view, backlinks and search work
297
+ with no integration at all — and Obsidian's per-vault `Use [[Wikilinks]]` setting turns off, after
298
+ which its own autocomplete and rename-tracking produce markdown links too. The authoring comfort is
299
+ a toggle in an editor, not a decision the fleet has to carry, which is why there is no `loam.json`
300
+ field for it. **`loam validate` resolves these links** — `link.unresolved` (error), one finding per
301
+ document listing every target that names nothing, with its line. A link here is a **join** and not
302
+ decoration: a requirement that names a term wants that term's document linked and that document
303
+ updated; an ADR that supersedes another wants to say which. Those are relationships of the same kind
304
+ as `Operations:` and `Covers:`, and only the ones written in a form that resolves can be resolved —
305
+ which is what makes the convention a contract rather than a preference. Four things are deliberately
306
+ not graded, and each is a question loam has no answer to rather than a leniency: a target outside
307
+ the docs repo (that tree may not be checked out beside these documents), a link to a heading
308
+ (`#section`), and any link inside a fenced block or an inline code span (which is how a document
309
+ shows the convention). CASE is graded, and is the one place loam looks past the filesystem's own
310
+ answer: `existsSync` is case-insensitive on Windows and macOS while GitHub's renderer and every
311
+ Linux CI runner are not, so a link is resolved only when the target exists AND each path segment is
312
+ spelled the way the directory stores it. When only the case is wrong the finding names the stored
313
+ spelling — on every platform alike, so the diagnosis does not depend on which one ran the check. A
314
+ document in the corpus that cannot be decoded is `link.unreadable` (error) and names itself, so an
315
+ ADR saved as UTF-16 cannot blank its service's whole report.
316
+
317
+ **Fleet-level decisions live in `architecture/adrs/`** — "event publishers use a transactional
318
+ outbox", "cross-service calls carry a circuit breaker" — beside the landscape and the two
319
+ vocabularies, because filing a decision about the fleet under whichever service happened to be
320
+ adopted first is how the other fifty never find it. Same `NNNN-<slug>.md` MADR convention as
321
+ `services/<svc>/adrs/`, and exactly the same treatment: `loam list --json` counts the files as
322
+ `fleetAdrs`, the text view prints the count when there is one, and **nothing grades their content**
323
+ — only the links they write, like every other document above. A fleet with no fleet-level ADRs owes
324
+ none, produces no finding and is not behind — which is also why `loam init` does not scaffold the
325
+ directory: git does not keep an empty one, and an empty `adrs/` in a fresh repo would read as an
326
+ obligation nobody has met.
327
+
328
+ **Architectural obligations live in three places on purpose.** An architect has always had one
329
+ checked channel to a team here — a landscape edge carrying `metadata { op 'authorizePayment' }`
330
+ obliges the provider to define that operationId, or `spine.op-undefined` fails the gate — and this
331
+ is the same handoff for the rules that VARY: an outbox on this publisher and not that one, a circuit
332
+ breaker on two edges out of five. A fleet-wide policy document is the wrong shape for those, which
333
+ is why the decision and its scope are separate things. An **ADR** in `architecture/adrs/` says WHAT
334
+ was decided. A **`#obl-<name>` tag** on an element or an edge in `architecture/landscape.likec4`
335
+ says WHERE it applies, so one ADR governs three edges and not the fourth without the document
336
+ forking. **`architecture/obligations.yaml`** declares the names —
337
+ `obligations: {<id>: {description, adr}}`, with `adr` a path relative to the docs repo root — so a
338
+ mistyped tag is an error rather than a word nobody notices. And the owning team's **`Covers:`** line
339
+ says it is met, with a scenario proving it.
340
+
341
+ The file is the axis's opt-in and a fleet without one hears nothing. With one, four grades:
342
+ `obligation.unknown` (error) for a tag no declaration answers, `obligation.unapplied` (warn) for a
343
+ declaration no tag applies — the same asymmetry `permissions.unknown`/`permissions.unenforced`
344
+ carry, because a typo reads like a rule while an unadopted word is an honest state —
345
+ `obligation.uncovered` (warn) for a tagged object no living requirement covers, and
346
+ `obligation.adr-missing` (error) for an `adr:` naming no file. `obligation.invalid` suspends the
347
+ family, as the other two vocabularies' invalid states do. An obligation id **is** its tag suffix, so
348
+ it may hold letters, digits, `_` and `-` only: the capability axis flattens `identity/tokens` into
349
+ `#cap-identity-tokens` and pays for it with a collision arm at every join, while an obligation id
350
+ has no life outside its own tag and is better refused at the source.
351
+
352
+ **`obligation.uncovered` is `c4.uncovered`'s question asked of the map the fleet actually runs on.**
353
+ `c4.uncovered` grades a NEW tagged element or edge in a feature's `delta.likec4` and has never been
354
+ able to say anything about the living landscape; this asks the same thing against the union of every
355
+ service's living `Covers:` lines — both requirement documents, because `Covers:` is one grammar and
356
+ a fleet that wrote its outbox requirement in `spec.md` has said the thing loam is asking about. One
357
+ finding per tagged object, `subject` = the service that owns it, so a fleet-wide report reads as a
358
+ worklist per team. Only a tag that RESOLVES is asked about coverage: an undeclared one has earned
359
+ its error already, and "no requirement covers `#obl-typo`" would be the same breach restated as a
360
+ request to write a requirement for a rule that does not exist.
361
+
362
+ **The fleet's domain vocabulary lives in `glossary/<term>.md`** — one FILE per term, nesting allowed
363
+ (`glossary/payments/authorization.md` is the term `payments/authorization`), and no `glossary.yaml`
364
+ anywhere. A definition is prose, and the rule this settles is general: an entry with prose gets a
365
+ file, an entry without prose stays a line in YAML. So the DIRECTORY is the list, exactly as it is
366
+ for `capabilities/`, and a second list is the drift `loam init`'s removed service manifest was
367
+ removed for. The directory's existence is the axis's opt-in — a fleet without one produces no
368
+ finding and pays one `existsSync`. `README.md` is not a term: a glossary big enough to want an index
369
+ page should not have that page reported as a word nobody uses.
370
+
371
+ **A term is checkable because a link is a join.** The document that uses the word cites the
372
+ definition with an ordinary relative link —
373
+ `An [Order](../../glossary/order.md) is what a customer confirmed.` — which `link.unresolved`
374
+ already resolves, so "which documents use this term" is a query rather than a grep, and
375
+ `glossary.unlinked` (warn) names a term no document outside `glossary/` cites: a word the fleet
376
+ never adopted, or a definition left behind by a rename. The alternative that was rejected is worth
377
+ naming, because it is the obvious one: matching a term's WORDS against prose would convict a
378
+ payments capability for the word "payments", and loam does not run that class of check anywhere. **A
379
+ citation from another term does not count as adoption** — a glossary is a network of definitions, so
380
+ `order-line` linking to `order` says the glossary is consistent, not that the fleet speaks it; the
381
+ citing terms are listed in the finding's `details` rather than hidden. Terms cited only from
382
+ `features/archive/` do not count either: a shipped feature's documents describe the tree as it was,
383
+ and a word retired two releases ago must not cite itself alive forever. `loam list glossary --json`
384
+ carries every term with its `linkedBy` documents, and a sibling `links` object whose `unreadable`
385
+ list names what the citation walk could not decode — an empty `linkedBy` must never be confusable
386
+ with "nobody looked".
387
+
388
+ **A feature can bring a new word with it**: `features/<FEAT>/glossary/<term>.md` is copied into
389
+ `glossary/` by `loam archive` and removed again by `loam unarchive`, so the vocabulary a change
390
+ introduces ships and unships with it and is reviewed beside the requirements that use it. The route
391
+ is **create-only**, and that is a decision rather than an unfinished half: a capability delta is a
392
+ delta because it merges into a living document part by part, while a term document has one part, so
393
+ the merge is a whole-file copy with nothing to merge partially and no pin to collide on. A term the
394
+ living glossary already defines is `glossary.term-exists` (error), and **`--approve` does not
395
+ override it** — approving it would be approving a deletion nobody described, which is the same class
396
+ of refusal `c4.service-binding-invalid` is. Change a definition by editing `glossary/<term>.md`
397
+ directly in the same pull request, where git produces an ordinary conflict.
398
+
399
+ **A citation inside a requirement is re-expressed by the merge, and it has to be.** A feature's
400
+ `specs/<svc>/spec.md` sits four directories below the docs root and the living
401
+ `services/<svc>/spec.md` it merges into sits two, so a relative link copied verbatim lands pointing
402
+ above the repository. `loam archive` therefore rewrites every relative link in the text it moves —
403
+ the target is unchanged, only the route to it — which is what lets an author write the link that
404
+ resolves where they wrote it, in the pull request that reviews it, and keeps it resolving after it
405
+ ships. This applies to the service and capability spec merges alike, and predates the glossary: a
406
+ delta requirement citing an ADR always had it. A link inside a fenced block or a code span, an
407
+ absolute URL, a site-root path and a bare `#section` are left exactly as written.
408
+
409
+ **Frontmatter**: `status`, `owner`, `service` or `feature`, `last_verified`, `vouched_by`, `sources`
410
+ (literal files and directories), `sources_digest`, `content_digest`, `sources_files`, `vouch_scope`.
411
+ `loam validate` reads it in exactly three files — a service's living `spec.md`, its living
412
+ `arch.spec.md` when present (same conventions, same checks, only the label differs), and a feature's
413
+ `intent.md`: the documents whose identity and status everything else joins on. `adrs/` and
414
+ `runbook.md` are presence-tracked (`loam list` says whether they exist), as is the fleet's own
415
+ `architecture/adrs/` beside them; their frontmatter is read by nothing today, so a field written
416
+ there is a note to a future reader, not a claim any check will catch. Their markdown links are the
417
+ one thing in them loam does read — `link.unresolved` above. `health.yaml`'s frontmatter is equally
418
+ unread, but the file itself is not inert: `loam validate` reads its top-level `slis:`/`alerts:` ids
419
+ for `Covers:` resolution and the `health.uncovered` warning, and its top-level `dependencies:` ids,
420
+ which weigh in as model-shape evidence (`c4.no-relationships`) and are reconciled against the
421
+ service's own model (`health.dependency-unmodelled`) — see the architecture spec axis below. For the
422
+ two checked files:
423
+
424
+ - `status` — services: `draft` -> `verified`; features: `proposed` -> `in_progress` -> `built` ->
425
+ `done`. An undocumented value is an **error**: a typo (`verifed`) would otherwise read as
426
+ unverified forever.
427
+ - a frontmatter block that does not parse as YAML is an **error** of its own
428
+ (`frontmatter.malformed`), and the only finding that file's header produces: the fields are
429
+ unreadable, not absent, and the cascade the silent parse used to trigger — "owner missing",
430
+ "status missing", "names no sources" — sent authors adding lines to a block YAML refuses. The
431
+ field checks and the sources chain resume once the header parses.
432
+ - `service` / `feature` — must match the directory the file lives under. A mismatch is an **error**;
433
+ absence is a warning.
434
+ - `sources` — paths in the *service's own repo* the artifact was written from: literal files and
435
+ directories only, a directory meaning everything beneath it. Resolved when `loam` runs inside that
436
+ repo (`service` in `loam.json`); a path that no longer exists is an **error**, and so is a
437
+ glob-looking entry (`*`, `?` or `[`) — patterns are not supported, and are refused loudly rather
438
+ than matched, because a pattern dialect nobody can be sure of digests a different file set than
439
+ its author intended, corrupting the trust signal silently. This is the only mechanical tie between
440
+ the docs and the code: everything else loam checks is internal consistency, which a fluent fiction
441
+ satisfies too.
442
+ - `sources_digest` / `last_verified` / `vouched_by` — written by `loam vouch`, never by hand.
443
+ `vouched_by` is git's identity for the repo the vouch ran in (`Name <email>`): a name, not a
444
+ signature, and it proves nothing cryptographically — what it buys is that a reviewer can ask a
445
+ *specific person* what they read, where `status: verified` alone recorded only that some process
446
+ wrote the word. The digest is a content hash of the files `sources` names, taken when a human last
447
+ read them; `loam validate`, run in that repo, recomputes it and reports `sources.current`,
448
+ `sources.stale` (the code moved since anyone looked), or `sources.unvouched` (`sources` with no
449
+ digest — nobody has ever stamped it). All warnings: staleness is a signal, and only a person can
450
+ say whether the doc is still right.
451
+ - `content_digest` — written by `loam vouch`, never by hand, like its siblings: a hash of the
452
+ document's OWN body, so the stamp covers the words as well as the code. Editing a spec after it
453
+ was vouched used to leave `status: verified` standing over prose nobody read — the exact forgery
454
+ an agent-written corpus invites. `loam validate` recomputes it wherever the doc is readable; this
455
+ check needs **no service repo**, so unlike the `sources.*` chain it runs for `--service` and
456
+ `--all` alike, from the docs repo included. A `verified` spec whose current body hash mismatches
457
+ gets `content.stale` (**warning**, the `sources.stale` grading: the doc changed since it was
458
+ vouched, and only a person can say whether verified still holds). A verified doc with no
459
+ `content_digest` predates the stamp and stays quiet — the fix is re-vouching.
460
+ - `vouch_scope` — written by `loam vouch --sample <n>`, never by hand, and present only when the
461
+ person who vouched read a recorded SAMPLE of the document rather than all of it. One flat string,
462
+ `sampled <k>/<n> seed=<16 hex>`: k sections read of n, and the seed that chose them. `status`
463
+ stays `verified` and the maturity rung stays `vouched` — the scope *qualifies* that claim, it is
464
+ not a fourth status — while `loam validate` reports `sources.sampled-vouch` (**warning**, the
465
+ `content.stale` grading: doc-side, so it fires from the docs repo too), `loam list` shows
466
+ `vouched (sampled)` and adds `vouchScope: "sampled"` to the row, `loam show` renders
467
+ `verified (sampled)`, and `validate --all`'s scorecard counts it under
468
+ `provenance.sampledVouched`. A value that does not decode is graded **sampled anyway** — the test
469
+ is whether the KEY is there, not whether its value is legible, so a scope rewritten as a YAML
470
+ mapping or sequence still reads as a partial vouch everywhere; mangling one field must never
471
+ promote a partial read to a full one. Either spec axis carrying one makes the SERVICE sampled on
472
+ `loam list`, `loam show` and `loam status`, since the sample is per file. A later FULL
473
+ `loam vouch` deletes the field, which is the only thing that clears the warning; deleting it by
474
+ hand forges a full vouch out of a partial one, and the generated `AGENTS.md` says so.
85
475
  - Missing fields are warnings, and `loam list` reports the fleet's draft/verified/unmarked split.
86
476
 
87
- **Vouching** (`loam vouch --service <id>`, run in the service's own repo) is the human promotion `draft` -> `verified`: it stamps `status`, `last_verified`, `vouched_by`, `sources_digest`, `content_digest` and `sources_files` together — into the living `spec.md`, and into `arch.spec.md` beside it when the service has one — rewriting only the frontmatter. It refuses what it cannot verify — a frontmatter block that will not parse as YAML (its fields are unreadable, and a stamp would have to rewrite the header wholesale, losing the author's lines), no `sources`, a glob-pattern entry (not supported), a source path that is gone, sources expanding to no files at all, or a repo that is not that service's — checked per file, the refusal naming which, and all-or-nothing per service: one unverifiable file refuses the whole run, so a `verified` status always has a digest behind it and never means half the pair was stamped. The write keeps the same promise: the pair is committed through staged temp-file swaps (archive's machinery), so a failure between the two files rolls the first back (`merge-failed` — nothing stamped, re-run) instead of leaving half a stamp; only a rollback that itself fails leaves the repo to a human (`rollback-incomplete`, the message naming the files). It also refuses to be run BY the thing it is meant to check: without `--yes` it requires a terminal and asks (`vouch-unattended` on a pipe or in `--json`, `vouch-declined` on anything but an explicit yes), and it refuses `vouch-unattributable` when git can name nobody at all. The command and skill files `loam init` writes pre-approve loam's other verbs one by one and deliberately not this one, so an agent that has finished a draft hands back and says a vouch is owed. And because the stamp is computed from bytes read a moment earlier, the swap re-checks them: a document that changed under the vouch — a concurrent vouch, a hand edit — refuses with `vouch-raced` and writes nothing, rather than stamping a digest of text that is no longer there.
88
-
89
- The digest recipe (stable, and part of the contract): expand `sources` to repo-relative file paths — a file is itself, a directory means everything beneath it, dot-entries are skipped while walking (though a path naming one outright is honoured) — sort them, hash each file's bytes with sha256, feed `<path>\0<sha256-hex>\n` per file into an outer sha256, and keep the first 16 hex characters.
90
-
91
- Two exclusions are part of that expansion, because a digest is a *staleness signal* and both of these turned it into noise:
92
-
93
- - **What git ignores is not hashed.** `node_modules` is skipped by name unconditionally, and the rest is asked of git itself — one `git check-ignore` over the expanded list, run in the service repo. A build output tree under a listed source used to flip `sources.stale` on every compile, which is the fastest way to teach a team to ignore the signal. The fallback direction is deliberate: if git cannot answer (not a repository, git absent, the call times out) **everything is hashed**, because a missed exclusion costs noise while an invented one costs a file the stamp silently stops watching. A `sources` entry that expands to nothing after this filter is `sources.empty` (warn) — a digest over no files reads as current forever — and `loam vouch` refuses to stamp it with the same sentence.
94
- - **Symlinks are followed only inside the repository.** A link to a file inside the repo is hashed under the link's own spelling; a link to a directory inside the repo is walked, with a stack of realpaths as the cycle guard (a global visited-set would make the file set depend on `readdir` order when one directory is reachable by two spellings — two machines with identical bytes, two different digests). A link whose target is outside the repository, dangling, or neither a file nor a directory is **not** hashed and is reported instead: `sources.skipped` (warn), naming each path and why. It is not closable by editing the document — either the link should not be under a listed source, or `sources` should name the real path.
95
-
96
- Alongside the digest, `loam vouch` stamps **`sources_files`**: the per-file index (`<sha256> <path>` lines, sha256sum layout, in a block scalar) the digest was computed over. It is what lets a later `sources.stale` say *which* paths were added, changed or removed instead of only that something moved. Above `SOURCE_INDEX_LIMIT` (100) files the field records the count alone — a readability budget — and an absent or unparseable field reads as "the stamp said nothing about individual files", explicitly not "every file was deleted". Like its siblings it is written by `loam vouch` and never by hand. Glob patterns are not part of the recipe: an entry containing `*`, `?` or `[` never expands — `loam vouch` refuses it and `loam validate` errors on it — because a pattern dialect nobody can be sure of silently digests a different file set than intended, corrupting the trust signal in both directions. (The literal-path expansion is unchanged from when patterns existed, so documents vouched with literal paths keep their digests.) Content, not mtime: git does not preserve modification times, so an mtime check would call every file stale after a fresh clone.
97
-
98
- The body digest recipe (`content_digest`, same discipline): sha256 of the artifact's body bytes AFTER the frontmatter block — the closing `---` line and its newline belong to the header, every byte below them is body — first 16 hex characters kept. Byte-exact, no normalization, and body-only on purpose: vouch itself rewrites the frontmatter as it stamps, so a header-only edit (a later vouch, a corrected `owner`) must not read as the document moving.
99
-
100
- **Tags (LikeC4)**: element kinds and tags are declared in a `specification` block; a delta's new/changed elements carry the feature id as a tag (`#FEAT-101`) so `loam` can project the delta by tag and validate it. The tag is loam's only way of seeing the delta, so a `delta.likec4` that declares elements or relationships with none carrying the feature's tag is an **error** (`delta.nothing-tagged`): everything in it is invisible to the checks, and the archive would merge nothing.
101
-
102
- **Service binding (element ↔ `services/<svc>/`)**: an element says which service it *is* with `metadata { service 'payment-service' }`. Without one, its **title** is used — which is what every existing repo relies on, and also the trap: rename a box in a diagram and every check that joined it to its directory silently stops matching. The binding is what makes a rename safe.
103
-
104
- There is no manifest of services: the directories under `services/` **are** the list. `loam validate --all` cross-checks that list against the landscape in both directions:
105
-
106
- - a `services/<svc>/` no element resolves to → `landscape.service-unmodelled` (**error**): the fleet map is incomplete, and every view derived from it is wrong;
107
- - an element that looks like a service, with no directory → `landscape.service-undocumented` (**warning**): it may be someone else's system. Tag it `#external` to say so, and the warning stops. **Depth is not a fact about a service.** The check used to keep only top-level elements, which ordinary grouped C4 breaks the moment services sit under an `enterprise`/`group`/`boundary` parent: every nested element was discarded, so a perfectly bound service read as unmodelled. The tree is walked instead — an element is graded at service level when no *ancestor* already stands for a service (its binding first, then a title naming a real `services/<id>/`), and what sits inside one of those is that service's container. Two consequences worth stating: an element that CONTAINS a service is a grouping, has nothing to bind and nothing to tag, and never raises this warning; and a `person`-kind element is exempt as before;
108
- - an element whose explicit binding names a directory that does not exist → `landscape.binding-unknown` (**error**), graded **at any depth**, not only at top level — a typo one level down used to bind an edge to a service that does not exist with nothing saying so. A binding is a claim about this repo; a title that fails to match is only a guess at one, which is why the two are graded differently;
109
- - two elements resolving to the **same** `services/<svc>/` → `landscape.binding-duplicate` (**warning**): every element→service join picks one of them arbitrarily, so the other's edges are quietly filed under a service that does not own them.
110
-
111
- **`architecture/landscape.likec4` is required, not optional.** Its absence is a finding, never a skipped check: an **error** as soon as `services/` holds at least one service directory, and a **warning** in a docs repo whose `services/` is still empty (`landscape.missing`, naming the file and what to write in it — a `specification` block declaring the kinds, a `model` block with one bound element per service, one edge per cross-service call carrying `metadata { op }`). The reason it cannot be silence: with no map, the whole cross-service layer — the C4↔API spine, `landscape.service-unmodelled`, every derived view — computes nothing and *reports* nothing, which reads exactly like a clean fleet. `loam init` scaffolds the file (empty, commented, parsing clean) so a fresh docs repo starts from a real one rather than from that ambiguity, and `loam adopt` briefs the block each service still owes it.
112
-
113
- Absences inside a service directory are graded too. A directory with no `spec.md` is a **warning** (`service.no-spec`) — partial adoption is a supported state, but the checks that file feeds are silently vacuous without it, and that has to be said.
114
-
115
- A missing `openapi.yaml` is graded on what already points into it, under one code, `service.no-openapi`, at three grades:
116
-
117
- - **silence**, on positive evidence that nobody expects an API there: the landscape parses and no op-carrying edge targets the service;
118
- - **warning**, when nothing joins into the file and the landscape cannot prove nobody calls the service (absent or unparseable) — "I could not look" and "nobody calls me" are different facts and only one of them is fine;
119
- - **error**, when something already joins into it: a living non-`REMOVED` requirement carrying an `Operations:` line, or an op-linked landscape edge whose target resolves here. Those operationIds ride in the finding's `details`. A requirement governing an operation that resolves to nothing is the same breach `spec-api.op-undefined` grades when the file exists and the op does not; the file being absent is not a reason to grade it lower.
120
-
121
- And `loam validate --service <id>` for a directory that does not exist at all is an **error** (`service.unknown`) that suggests close ids — never `loam adopt`, which would steer an agent into adopting the typo. A `features/<FEAT>/specs/<svc>/` directory addressing a service that neither exists as `services/<svc>/` nor is introduced by that feature's own tagged `delta.likec4` is the same class of mistake and gets its own **error**, `delta.service-unknown`, naming close ids: archiving would otherwise materialise a phantom service directory out of a typo in `--touches`. A delta that did not parse suspends the question rather than guessing (`delta.invalid` is that finding).
122
-
123
- **API linkage (operationId)**: OpenAPI's `operationId` joins architecture and behavior — it is the token both C4 and specs reference.
124
- - a C4 relationship names the operation it calls: `... -> ... 'Calls createSplit' { metadata { op 'createSplit' } }`;
477
+ **Vouching** (`loam vouch --service <id>`, run in the service's own repo) is the human promotion
478
+ `draft` -> `verified`: it stamps `status`, `last_verified`, `vouched_by`, `sources_digest`,
479
+ `content_digest` and `sources_files` together — into the living `spec.md`, and into `arch.spec.md`
480
+ beside it when the service has one — rewriting only the frontmatter. It refuses what it cannot
481
+ verify — a frontmatter block that will not parse as YAML (its fields are unreadable, and a stamp
482
+ would have to rewrite the header wholesale, losing the author's lines), no `sources`, a glob-pattern
483
+ entry (not supported), a source path that is gone, sources expanding to no files at all, or a repo
484
+ that is not that service's — checked per file, the refusal naming which, and all-or-nothing per
485
+ service: one unverifiable file refuses the whole run, so a `verified` status always has a digest
486
+ behind it and never means half the pair was stamped. The write keeps the same promise: the pair is
487
+ committed through staged temp-file swaps (archive's machinery), so a failure between the two files
488
+ rolls the first back (`merge-failed` — nothing stamped, re-run) instead of leaving half a stamp;
489
+ only a rollback that itself fails leaves the repo to a human (`rollback-incomplete`, the message
490
+ naming the files). It also refuses to be run BY the thing it is meant to check: without `--yes` it
491
+ requires a terminal and asks (`vouch-unattended` on a pipe or in `--json`, `vouch-declined` on
492
+ anything but an explicit yes), and it refuses `vouch-unattributable` when git can name nobody at
493
+ all. The command and skill files `loam init` writes pre-approve loam's other verbs one by one and
494
+ deliberately not this one, so an agent that has finished a draft hands back and says a vouch is
495
+ owed. And because the stamp is computed from bytes read a moment earlier, the swap re-checks them: a
496
+ document that changed under the vouch — a concurrent vouch, a hand edit — refuses with `vouch-raced`
497
+ and writes nothing, rather than stamping a digest of text that is no longer there. `--sample <n>`
498
+ narrows what the person is asked to read (see **Sampling** below) and nothing else: the same
499
+ refusals fire, before the reading list is printed rather than after it, the same attendance gates
500
+ hold, and a document that moved while somebody was reading the sample refuses `vouch-raced` too —
501
+ the sections they read are not the sections the stamp would cover.
502
+
503
+ The digest recipe (stable, and part of the contract): expand `sources` to repo-relative file paths —
504
+ a file is itself, a directory means everything beneath it, dot-entries are skipped while walking
505
+ (though a path naming one outright is honoured) — sort them, hash each file's bytes with sha256,
506
+ feed `<path>\0<sha256-hex>\n` per file into an outer sha256, and keep the first 16 hex characters.
507
+
508
+ Two exclusions are part of that expansion, because a digest is a *staleness signal* and both of
509
+ these turned it into noise:
510
+
511
+ - **What git ignores is not hashed.** `node_modules` is skipped by name unconditionally, and the
512
+ rest is asked of git itself — one `git check-ignore` over the expanded list, run in the service
513
+ repo. A build output tree under a listed source used to flip `sources.stale` on every compile,
514
+ which is the fastest way to teach a team to ignore the signal. The fallback direction is
515
+ deliberate: if git cannot answer (not a repository, git absent, the call times out) **everything
516
+ is hashed**, because a missed exclusion costs noise while an invented one costs a file the stamp
517
+ silently stops watching. A `sources` entry that expands to nothing after this filter is
518
+ `sources.empty` (warn) — a digest over no files reads as current forever — and `loam vouch`
519
+ refuses to stamp it with the same sentence.
520
+ - **Symlinks are followed only inside the repository.** A link to a file inside the repo is hashed
521
+ under the link's own spelling; a link to a directory inside the repo is walked, with a stack of
522
+ realpaths as the cycle guard (a global visited-set would make the file set depend on `readdir`
523
+ order when one directory is reachable by two spellings — two machines with identical bytes, two
524
+ different digests). A link whose target is outside the repository, dangling, or neither a file nor
525
+ a directory is **not** hashed and is reported instead: `sources.skipped` (warn), naming each path
526
+ and why. It is not closable by editing the document — either the link should not be under a listed
527
+ source, or `sources` should name the real path.
528
+
529
+ Alongside the digest, `loam vouch` stamps **`sources_files`**: the per-file index (`<sha256> <path>`
530
+ lines, sha256sum layout, in a block scalar) the digest was computed over. It is what lets a later
531
+ `sources.stale` say *which* paths were added, changed or removed instead of only that something
532
+ moved. Above `SOURCE_INDEX_LIMIT` (100) files the field records the count alone — a readability
533
+ budget — and an absent or unparseable field reads as "the stamp said nothing about individual
534
+ files", explicitly not "every file was deleted". Like its siblings it is written by `loam vouch` and
535
+ never by hand. Glob patterns are not part of the recipe: an entry containing `*`, `?` or `[` never
536
+ expands — `loam vouch` refuses it and `loam validate` errors on it — because a pattern dialect
537
+ nobody can be sure of silently digests a different file set than intended, corrupting the trust
538
+ signal in both directions. (The literal-path expansion is unchanged from when patterns existed, so
539
+ documents vouched with literal paths keep their digests.) Content, not mtime: git does not preserve
540
+ modification times, so an mtime check would call every file stale after a fresh clone.
541
+
542
+ The body digest recipe (`content_digest`, same discipline): sha256 of the artifact's body bytes
543
+ AFTER the frontmatter block — the closing `---` line and its newline belong to the header, every
544
+ byte below them is body — first 16 hex characters kept. Byte-exact, no normalization, and body-only
545
+ on purpose: vouch itself rewrites the frontmatter as it stamps, so a header-only edit (a later
546
+ vouch, a corrected `owner`) must not read as the document moving.
547
+
548
+ **Sampling** (`loam vouch --sample <n>`) is the same act over less of the document, recorded
549
+ honestly. Its recipe is part of the contract too, because the value is written into documents and
550
+ the whole point is that anybody can recompute what a person was shown:
551
+
552
+ 1. the sampling UNITS of a file are every H2 and H3 heading of its body outside a code fence, each
553
+ standing for the text under it up to the next heading of either level, in document order. H3
554
+ counts because `## Requirements` holds every `### Requirement:` in a living spec, and a sample
555
+ whose unit is most of the file is not a sample. The text above the first heading is never a unit:
556
+ the reading list names it separately ("read the preamble") outside the k/n count;
557
+ 2. the SEED is `sha256(<service> \0 <content_digest> \0 <sources_digest>)`, first 16 hex characters
558
+ — per FILE, since both inputs are per-file stamps, and derived from the document's own content so
559
+ that the pick cannot be re-rolled in place: a random seed would let an agent run the vouch (or
560
+ `--pack --sample`) until the section it wrote carelessly fell outside the sample, leaving no
561
+ trace at all, where content derivation makes every re-roll an EDIT the docs repo's history
562
+ records. It does **not** make the sample unpredictable, and the difference is worth being plain
563
+ about: this recipe is published, so anybody holding the document can compute the pick before
564
+ anybody vouches — that is the audit property, and the two are in direct tension. Steering costs
565
+ one cosmetic edit per attempt in a version-controlled file, immediately before asking a person to
566
+ vouch; auditability was chosen over unpredictability because hiding the recipe needs a secret
567
+ loam has nowhere to keep, and a sample nobody can check is not evidence of anything;
568
+ 3. the PICK ranks each unit by `sha256(<seed> \0 <0-based index> \0 <heading line, trimmed>)`, sorts
569
+ by that hex ascending (index breaking a tie), takes the first `<n>`, and lists them in document
570
+ order. No PRNG, no dependency, one section at a time recomputable with a shell;
571
+ 4. a file with `<n>` or fewer units is read in FULL and carries no `vouch_scope`; every `sources`
572
+ entry is read whole in either case, because nothing in loam maps a source file to a doc section
573
+ and inventing that mapping would be exactly the meaning-derivation loam refuses. The sample is of
574
+ sections; the digest still covers all of the code.
575
+
576
+ The pick is UNIFORM by design: it is deliberately not weighted by fan-in (`loam list
577
+ --review-order`), which counts dependants per SERVICE with no section-level join, would zero-weight
578
+ the narrative sections most likely to be quietly wrong, and would take its weights from documents an
579
+ agent can edit — reopening the steering that a content-derived seed closes.
580
+ `loam vouch --pack --sample <n>` prints the same pick before the vouch; a pack read after a sampled
581
+ vouch recomputes what that vouch read from the stamped seed, so its "already covered" list can never
582
+ count sections nobody opened.
583
+
584
+ **Tags (LikeC4)**: element kinds and tags are declared in a `specification` block; a delta's
585
+ new/changed elements carry the feature id as a tag (`#FEAT-101`) so `loam` can project the delta by
586
+ tag and validate it. The tag is loam's only way of seeing the delta, so a `delta.likec4` that
587
+ declares elements or relationships with none carrying the feature's tag is an **error**
588
+ (`delta.nothing-tagged`): everything in it is invisible to the checks, and the archive would merge
589
+ nothing.
590
+
591
+ **Service binding (element ↔ `services/<svc>/`)**: an element says which service it *is* with
592
+ `metadata { service 'payment-service' }`. Without one, its **title** is used — which is what every
593
+ existing repo relies on, and also the trap: rename a box in a diagram and every check that joined it
594
+ to its directory silently stops matching. The binding is what makes a rename safe.
595
+
596
+ There is no manifest of services: the directories under `services/` **are** the list.
597
+ `loam validate --all` cross-checks that list against the landscape in both directions:
598
+
599
+ - a `services/<svc>/` no element resolves to → `landscape.service-unmodelled` (**error**): the fleet
600
+ map is incomplete, and every view derived from it is wrong;
601
+ - an element that looks like a service, with no directory → `landscape.service-undocumented`
602
+ (**warning**): it may be someone else's system. Tag it `#external` to say so, and the warning
603
+ stops. **Depth is not a fact about a service.** The check used to keep only top-level elements,
604
+ which ordinary grouped C4 breaks the moment services sit under an `enterprise`/`group`/`boundary`
605
+ parent: every nested element was discarded, so a perfectly bound service read as unmodelled. The
606
+ tree is walked instead — an element is graded at service level when no *ancestor* already stands
607
+ for a service (its binding first, then a title naming a real `services/<id>/`), and what sits
608
+ inside one of those is that service's container. Two consequences worth stating: an element that
609
+ CONTAINS a service is a grouping, has nothing to bind and nothing to tag, and never raises this
610
+ warning; and a `person`-kind element is exempt as before;
611
+ - an element whose explicit binding names a directory that does not exist →
612
+ `landscape.binding-unknown` (**error**), graded **at any depth**, not only at top level — a typo
613
+ one level down used to bind an edge to a service that does not exist with nothing saying so. A
614
+ binding is a claim about this repo; a title that fails to match is only a guess at one, which is
615
+ why the two are graded differently;
616
+ - two elements resolving to the **same** `services/<svc>/` → `landscape.binding-duplicate`
617
+ (**warning**): every element→service join picks one of them arbitrarily, so the other's edges are
618
+ quietly filed under a service that does not own them.
619
+
620
+ **`architecture/landscape.likec4` is required, not optional.** Its absence is a finding, never a
621
+ skipped check: an **error** as soon as `services/` holds at least one service directory, and a
622
+ **warning** in a docs repo whose `services/` is still empty (`landscape.missing`, naming the file
623
+ and what to write in it — a `specification` block declaring the kinds, a `model` block with one
624
+ bound element per service, one edge per cross-service call carrying `metadata { op }`). The reason
625
+ it cannot be silence: with no map, the whole cross-service layer — the C4↔API spine,
626
+ `landscape.service-unmodelled`, every derived view — computes nothing and *reports* nothing, which
627
+ reads exactly like a clean fleet. `loam init` scaffolds the file (empty, commented, parsing clean)
628
+ so a fresh docs repo starts from a real one rather than from that ambiguity, and `loam adopt` briefs
629
+ the block each service still owes it.
630
+
631
+ Absences inside a service directory are graded too. A directory with no `spec.md` is a **warning**
632
+ (`service.no-spec`) — partial adoption is a supported state, but the checks that file feeds are
633
+ silently vacuous without it, and that has to be said.
634
+
635
+ A missing `openapi.yaml` is graded on what already points into it, under one code,
636
+ `service.no-openapi`, at three grades:
637
+
638
+ - **silence**, on positive evidence that nobody expects an API there: the landscape parses and no
639
+ op-carrying edge targets the service;
640
+ - **warning**, when nothing joins into the file and the landscape cannot prove nobody calls the
641
+ service (absent or unparseable) — "I could not look" and "nobody calls me" are different facts and
642
+ only one of them is fine;
643
+ - **error**, when something already joins into it: a living non-`REMOVED` requirement carrying an
644
+ `Operations:` line, or an op-linked landscape edge whose target resolves here. Those operationIds
645
+ ride in the finding's `details`. A requirement governing an operation that resolves to nothing is
646
+ the same breach `spec-api.op-undefined` grades when the file exists and the op does not; the file
647
+ being absent is not a reason to grade it lower.
648
+
649
+ And `loam validate --service <id>` for a directory that does not exist at all is an **error**
650
+ (`service.unknown`) that suggests close ids — never `loam adopt`, which would steer an agent into
651
+ adopting the typo. Only a legal id earns that grading: a name that is neither an existing directory
652
+ nor a legal service id is refused as `invalid-option` before any path is built from it. A
653
+ `features/<FEAT>/specs/<svc>/` directory addressing a service that neither exists as
654
+ `services/<svc>/` nor is introduced by that feature's own tagged `delta.likec4` is the same class of
655
+ mistake and gets its own **error**, `delta.service-unknown`, naming close ids: archiving would
656
+ otherwise materialise a phantom service directory out of a typo in `--touches`. A delta that did not
657
+ parse suspends the question rather than guessing (`delta.invalid` is that finding).
658
+
659
+ **API linkage (operationId)**: OpenAPI's `operationId` joins architecture and behavior — it is the
660
+ token both C4 and specs reference.
661
+ - a C4 relationship names the operation it calls:
662
+ `... -> ... 'Calls createSplit' { metadata { op 'createSplit' } }`;
125
663
  - a requirement declares the operations it governs: an `Operations: createSplit` line;
126
664
  - the operation is defined in the provider service's `openapi.yaml`.
127
665
 
128
- `loam validate` then checks the **contract** (every edge's `op` exists in the target's OpenAPI — consumer-driven, gates) and **coverage** (every operation is governed by a requirement). Only the eight HTTP methods of a path item are read for operationIds — an id tucked inside a vendor extension (`x-legacy:`) does not exist, because a phantom id would mask a contract break. A service whose openapi defines operations while not one requirement carries an `Operations:` line gets a single **warning** (`api.ops-unlinked`): the API axis is unchecked there until someone links it — the exact state an adopted OpenSpec repo starts in. An `openapi.yaml` that exists but does not parse is an **error** (`openapi.invalid`) — including one whose bytes are not valid UTF-8, which is graded as unreadable rather than handed to the YAML parser with U+FFFD substituted in. The checks that read the contract — the `api.*` family and the spine's op resolution — are suspended for that service: an unreadable contract used to read as an EMPTY one, grading every inbound edge `spine.op-undefined` when the truth was the file. **A missing file suspends them the same way**, and for the same reason: a contract that is not there proves nothing about an edge, neither broken nor resolved. So one absent contract is exactly one finding (`service.no-openapi`, carrying the stranded operationIds) rather than one error per inbound consumer, and no `spine.resolved` is claimed either.
129
-
130
- Two `operationId`s that are the same in one document is a **warning** (`openapi.duplicate-operationid`), naming both `METHOD /path` slots — in a living contract and in a feature delta alike, one breach with one name wherever it is found. The document is legal OpenAPI; the ambiguity is in everything that joins on the id, each of which picks one slot arbitrarily.
131
-
132
- **Deprecation is visible on the spine.** Real fleets migrate — legacy operations coexist with their replacements — and OpenAPI already carries the standard per-operation `deprecated: true` flag, so loam reads it (nothing else changes in what the reader touches: still the eight HTTP methods, still the operationId) and turns it into lifecycle warnings, letting the spine say "migrating off" instead of only alive-or-broken. A landscape edge calling a deprecated operation warns per inbound edge (`spine.op-deprecated`: the contract holds, but the consumer should be migrating — two op-linked edges from one consumer are two warnings, and a deprecated op nobody calls raises no spine warning); a requirement whose `Operations:` list resolves only to deprecated operations warns once (`api.requirement-deprecated`: the behaviour it governs is on its way out — migrate it to the replacement operation, or retire it); and a feature delta whose NEW tagged edge consumes an op the living provider contract deprecates warns in feature scope (`c4-api.op-deprecated`, advisory — it never gates `archive`: building new consumption on a dying op deserves an eye, not a block; quiet when the feature's own openapi delta restates the op without the flag — that feature IS the un-deprecation, and the warning family stops once it archives). All three are warnings, and `--strict` is the escalation.
133
-
134
- **Removal is an explicit feature delta.** To retire an operation after deprecation, put the exact living path and HTTP method in the feature's `specs/<svc>/openapi.yaml`, keep its `operationId`, and add `x-loam-remove: true` **inside that operation object**, beside its `operationId`. A marker one level up, at path level beside the methods, names no operation: it retires nothing, and it is not a contract key either. `loam archive` gates it (`openapi.remove-marker-path-level`, error, overridable with `--approve` like the other judgement gates) and the marker never reaches the living document under any flag — `x-loam-remove` and `x-loam-based-on` are stripped at path level and operation level alike, on every merge branch. A path item consisting of nothing but a marker publishes no phantom path. The same feature must carry a `REMOVED` requirement whose `Operations:` line names that id. Coherence gates an absent path+method, an id mismatch, an unjustified marker, a removed requirement with no marker, and a new tagged C4 edge consuming the operation being removed. `serviceOperationIds` evaluates the feature as living upserts minus removals, so other same-feature checks see the post-archive contract. Archive deletes only the matching method, reports removals in text and `openapiRemovals` JSON, never copies the marker into living OpenAPI, and deliberately does not garbage-collect components. A removal marker in a living `openapi.yaml` is `openapi.remove-marker-living` (error). The usual staged-write rollback and `unarchive` snapshot restore apply byte-for-byte.
666
+ `loam validate` then checks the **contract** (every edge's `op` exists in the target's OpenAPI —
667
+ consumer-driven, gates) and **coverage** (every operation is governed by a requirement). Only the
668
+ eight HTTP methods of a path item are read for operationIds — an id tucked inside a vendor extension
669
+ (`x-legacy:`) does not exist, because a phantom id would mask a contract break. A service whose
670
+ openapi defines operations while not one requirement carries an `Operations:` line gets a single
671
+ **warning** (`api.ops-unlinked`): the API axis is unchecked there until someone links it — the exact
672
+ state an adopted OpenSpec repo starts in. An `openapi.yaml` that exists but does not parse is an
673
+ **error** (`openapi.invalid`) — including one whose bytes are not valid UTF-8, which is graded as
674
+ unreadable rather than handed to the YAML parser with U+FFFD substituted in. The checks that read
675
+ the contract — the `api.*` family and the spine's op resolution — are suspended for that service: an
676
+ unreadable contract used to read as an EMPTY one, grading every inbound edge `spine.op-undefined`
677
+ when the truth was the file. **A missing file suspends them the same way**, and for the same reason:
678
+ a contract that is not there proves nothing about an edge, neither broken nor resolved. So one
679
+ absent contract is exactly one finding (`service.no-openapi`, carrying the stranded operationIds)
680
+ rather than one error per inbound consumer, and no `spine.resolved` is claimed either.
681
+
682
+ Two `operationId`s that are the same in one document is a **warning**
683
+ (`openapi.duplicate-operationid`), naming both `METHOD /path` slots — in a living contract and in a
684
+ feature delta alike, one breach with one name wherever it is found. The document is legal OpenAPI;
685
+ the ambiguity is in everything that joins on the id, each of which picks one slot arbitrarily.
686
+
687
+ **Refusal paths are governed too.** For every exact three-digit `4xx` or `5xx` response declared by
688
+ an operation, at least one scenario of a requirement naming that operation should mention the status
689
+ code in its body; Scenario Outline example rows count. Missing coverage is `api.response-ungoverned`
690
+ (warning). `default`, `4XX` and `5XX` wildcard responses are excluded because no single scenario can
691
+ name the concrete branch they represent. This is a textual reachability check, not proof that the
692
+ implementation returns the response.
693
+
694
+ **Deprecation is visible on the spine.** Long-lived APIs migrate — legacy operations coexist with
695
+ their replacements — and OpenAPI already carries the standard per-operation `deprecated: true` flag,
696
+ so loam reads it (nothing else changes in what the reader touches: still the eight HTTP methods,
697
+ still the operationId) and turns it into lifecycle warnings, letting the spine say "migrating off"
698
+ instead of only alive-or-broken. A landscape edge calling a deprecated operation warns per inbound
699
+ edge (`spine.op-deprecated`: the contract holds, but the consumer should be migrating — two
700
+ op-linked edges from one consumer are two warnings, and a deprecated op nobody calls raises no spine
701
+ warning); a requirement whose `Operations:` list resolves only to deprecated operations warns once
702
+ (`api.requirement-deprecated`: the behaviour it governs is on its way out — migrate it to the
703
+ replacement operation, or retire it); and a feature delta whose NEW tagged edge consumes an op the
704
+ living provider contract deprecates warns in feature scope (`c4-api.op-deprecated`, advisory — it
705
+ never gates `archive`: building new consumption on a dying op deserves an eye, not a block; quiet
706
+ when the feature's own openapi delta restates the op without the flag — that feature IS the
707
+ un-deprecation, and the warning family stops once it archives). All three are warnings, and
708
+ `--strict` is the escalation.
709
+
710
+ **Removal is an explicit feature delta.** To retire an operation after deprecation, put the exact
711
+ living path and HTTP method in the feature's `specs/<svc>/openapi.yaml`, keep its `operationId`, and
712
+ add `x-loam-remove: true` **inside that operation object**, beside its `operationId`. A marker one
713
+ level up, at path level beside the methods, names no operation: it retires nothing, and it is not a
714
+ contract key either. `loam archive` gates it (`openapi.remove-marker-path-level`, error, overridable
715
+ with `--approve` like the other judgement gates) and the marker never reaches the living document
716
+ under any flag — `x-loam-remove` and `x-loam-based-on` are stripped at path level and operation
717
+ level alike, on every merge branch. A path item consisting of nothing but a marker publishes no
718
+ phantom path. The same feature must carry a `REMOVED` requirement whose `Operations:` line names
719
+ that id. Coherence gates an absent path+method, an id mismatch, an unjustified marker, a removed
720
+ requirement with no marker, and a new tagged C4 edge consuming the operation being removed.
721
+ `serviceOperationIds` evaluates the feature as living upserts minus removals, so other same-feature
722
+ checks see the post-archive contract. Archive deletes only the matching method, reports removals in
723
+ text and `openapiRemovals` JSON, never copies the marker into living OpenAPI, and deliberately does
724
+ not garbage-collect components. A removal marker in a living `openapi.yaml` is
725
+ `openapi.remove-marker-living` (error). The usual staged-write rollback and `unarchive` snapshot
726
+ restore apply byte-for-byte.
135
727
 
136
728
  ### The async contract axis (`asyncapi.yaml`)
137
729
 
138
- A `paymentService -> kafka 'Publishes PaymentAuthorized'` edge used to read as fully documented while naming no contract anybody could check. The event axis is the API axis's mirror, in **AsyncAPI 3.0** — only 3.0, because its operations are named top-level objects carrying `action: send|receive`, the exact analog of an `operationId`, where 2.x spells the same thing as `publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A 2.x document declares no `operations` and reads as a contract with no messages rather than as a mis-parsed one. Like OpenAPI it is read with a shallow structural walk over the `yaml` parse, never a real AsyncAPI parser: the slice loam joins on is small enough that the parser's model would be dead weight, and adding it would be a fourth runtime dependency.
730
+ A `paymentService -> kafka 'Publishes PaymentAuthorized'` edge used to read as fully documented
731
+ while naming no contract anybody could check. The event axis is the API axis's mirror, in **AsyncAPI
732
+ 3.0** — only 3.0, because its operations are named top-level objects carrying
733
+ `action: send|receive`, the exact analog of an `operationId`, where 2.x spells the same thing as
734
+ `publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A 2.x
735
+ document declares no `operations` and reads as a contract with no messages rather than as a
736
+ mis-parsed one. Like OpenAPI it is read with a shallow structural walk over the `yaml` parse, never
737
+ a real AsyncAPI parser: the slice loam joins on is small enough that the parser's model would be
738
+ dead weight, and adding it would be a fourth runtime dependency.
739
+
740
+ **The join token is a message name**, spelled three times and required to be identical: a landscape
741
+ edge's `metadata { publishes '...' }` / `metadata { consumes '...' }`, a requirement's `Publishes:`
742
+ / `Consumes:` line (both requirement files — an outbox requirement in `arch.spec.md` is the
743
+ canonical home of a `Publishes:` line, and the same keep-last quirk applies as to `Operations:`),
744
+ and the message's `name` in `asyncapi.yaml`, or its declaration key when it has none.
745
+
746
+ **The payload is opaque, by decision.** loam reads message names and the `action` of the operation
747
+ carrying them, and nothing inside `payload`. That is the whole property that keeps the payload
748
+ format a documentation choice: a fleet writes JSON Schema today, and one that later adopts Avro
749
+ changes a `schemaFormat` line and nothing in loam. Keep payloads in `components/schemas` in the same
750
+ document rather than `$ref`-ing an external `.avsc` — external references are out of scope here
751
+ exactly as on the OpenAPI axis, so a schema in another file will not travel with the message that
752
+ needs it.
753
+
754
+ **The direction is not decoration, and the check inverts.** On the API axis the PROVIDER owns the
755
+ contract, so "does this operation exist" is answered inside one service's directory. An event's
756
+ schema lives in the PRODUCER's repository, so "does anybody publish what I consume" is a fleet
757
+ question with no local answer — which is why `spine.message-unproduced` has no OpenAPI analog, and
758
+ why a consumer joined to nothing was previously indistinguishable from a healthy one. A producer
759
+ OUTSIDE the fleet is expressible: an `#external` element whose edge carries
760
+ `metadata { publishes '<msg>' }` is a recognized producer, `spine.message-unproduced` stays quiet,
761
+ and the contract question shifts to the consumer's own `asyncapi.yaml` — `spine.message-external`
762
+ (warn) fires while that file defines no shape for the message and stops once it does. Its other half
763
+ is `asyncapi.message-contested`: two services declaring they send one name means every consumer's
764
+ join picks one arbitrarily. That one fleet question is also the axis's only cost — a per-service run
765
+ reads every service's `asyncapi.yaml`, which is N small YAML parses rather than N LikeC4 workspace
766
+ spins, and it is paid only where something actually consumes.
767
+
768
+ **Absence has two grades, not three.** `service.no-openapi` warns when the landscape cannot prove
769
+ nobody calls a service, because most services expose HTTP. An event contract is genuinely optional
770
+ and most services touch no topic, so an absent `asyncapi.yaml` is an **error** when something
771
+ already joins into it (the stranded message names ride in `details`) and **silence** otherwise. The
772
+ file is presence-tracked in `loam list` and deliberately kept out of the maturity ladder: a fourth
773
+ required artifact would demote every already-`documented` service in the fleet without one byte of
774
+ their files changing.
775
+
776
+ **Draw the topic, not the broker.** An edge may point wherever it likes and every check joins the
777
+ same way — an endpoint inside a nested element resolves to the element that owns it — so this is a
778
+ modelling convention rather than a rule loam enforces, and it is on the
779
+ [unchecked list](https://github.com/ybotok/loam/blob/main/src/core/brief/unchecked.ts) with the
780
+ reason spelled out. It matters anyway because it is the one modelling mistake whose cost is
781
+ invisible until it is expensive: a broker drawn as ONE element is the node every service in the
782
+ fleet points at, so a fleet map that read fine at five services is a star with sixty spokes at
783
+ sixty, and nothing warns on the way there because loam parses the model and renders no view. Nesting
784
+ a topic per channel inside the broker's element and pointing the edges at `kafka.<topic>` splits one
785
+ hub into a dozen small ones, and it is the truer model besides: what a producer and a consumer share
786
+ is the channel, never the server. Declare the kind with the tag on it —
787
+ `element topic { #external }` — because LikeC4 does not inherit tags, and an untagged topic under an
788
+ `#external` broker is a `landscape.service-undocumented` asking for a `services/` directory nobody
789
+ owes. The
790
+ [worked landscape](https://github.com/ybotok/loam/blob/main/examples/docs/architecture/landscape.likec4)
791
+ includes three views (`exclude element.tag = #external` for the synchronous map, one for the event
792
+ spine, one over `#platform`) that keep the picture readable once the model is. It also puts
793
+ `#platform` on the topic KIND rather than on `kafka` alone, which is what the split makes necessary:
794
+ `landscape.platform-candidate` grades each drawn element by how many services consume it, and once
795
+ the edges point at channels it is the channels that collect three consumers each — tagging only the
796
+ broker leaves a row of untagged hubs behind it.
797
+
798
+ **The feature lifecycle, mirrored from the OpenAPI axis.** A feature changes this contract through
799
+ `features/<FEAT>/specs/<svc>/asyncapi.yaml` — a **complete AsyncAPI 3.0 document**, exactly as an
800
+ openapi delta is a complete document: the author restates the living contract around the part they
801
+ are changing, and the machinery tells restatement from edit. Identity is the **slot** — the pair
802
+ (section, key) over the three named sections `channels.<key>`, `operations.<key>` and
803
+ `components.messages.<key>` — and the join token stays the message name. Three feature-only keys,
804
+ spelled and shaped exactly as on the OpenAPI axis: `x-loam-based-on` inside a slot's value (written
805
+ by `loam rebase`; the same 16-hex sha256, taken over canonical JSON of the slot's value with the pin
806
+ itself excluded) records which living version of the slot the delta was written against, so the
807
+ merge can tell a **quote** (equal to its own pin — never a merge input) from an **edit** (differs —
808
+ written); `x-loam-remove: true` inside a slot's value retires that slot at archive; and the root
809
+ record **`x-loam-baselines`** (`{ components: {"<kind>/<name>": <16-hex>} }`) pins what an in-value
810
+ key cannot reach — every `components.<kind>.<name>` **outside `messages`**, which is a **surface**
811
+ rather than a slot. A `components.schemas` value is a JSON Schema, where an in-value loam key would
812
+ be a schema keyword and where JSON Schema's own `true` is a legal component no in-value key survives
813
+ at all; `components.messages` is deliberately absent from the record, because a message already
814
+ carries its own pin and two identities for one value would be two verdicts that need not agree.
815
+ Surfaces are merged, graded and reported exactly as slots are — a quote is skipped, an edit is
816
+ written and warned as `asyncapi.component-modified`, an unpinned restatement counts into the same
817
+ per-service `asyncapi.baseline-missing`, a stale entry is `asyncapi.baseline-stale`, and a malformed
818
+ record or an entry naming nothing the delta declares is `asyncapi.baseline-invalid`. Nothing
819
+ under `payload` ever contributes a join token: a slot digest hashes payload bytes as **content
820
+ identity only** — equality, never a join on payload fields — so Avro stays a `schemaFormat` line.
821
+ One honest consequence: a `schemaFormat` migration changes payload bytes, so even quoted slots
822
+ surface `asyncapi.baseline-stale` — that is the pin reporting a real content change under a delta
823
+ that restates it, not a false positive; re-read the living slot and `loam rebase`. The same is true
824
+ of a surface: a migration that rewrites a `components.schemas` entry moves its digest, so a delta
825
+ that merely restates the schema reports the change rather than hiding it, and the fix is the same
826
+ one command.
827
+
828
+ **Inline channel messages are channel interior, by decision.** A message declared inline at
829
+ `channels.<ck>.messages.<mk>` still joins the spine (the reader declares it as a first-class
830
+ message), but its identity under the feature lifecycle is **part of the `channels.<ck>` slot's
831
+ content**: it is not a slot of its own, it cannot carry its own pin, and the slot walker never
832
+ visits it. A feature delta that declares or retires an **individual** message must do so under
833
+ `components.messages` (the channel entry aliasing it with a `$ref`); retiring a whole channel
834
+ retires its inline interior with it — the channel's own `x-loam-remove: true` **is** the removal
835
+ marker for every message the living channel declares inline, so it satisfies a justifying `REMOVED`
836
+ requirement's marker debt and the retirement is gated on consumers exactly as a message-slot marker
837
+ is. An `x-loam-remove` nested on an inline message is graded as part of the channel slot's content —
838
+ it makes the channel an edit, never the message a removal — and since the removal it asks for would
839
+ silently not happen, `loam archive` refuses it by name (`asyncapi.remove-marker-inline`, error,
840
+ overridable with `--approve`); the merge strips loam's two keys at that nested depth exactly as at
841
+ slot depth either way, so a nested marker can never reach the living contract inside a channel that
842
+ merges.
843
+
844
+ **This supersedes the recorded decision against a removal family.** An earlier revision of this
845
+ section declared deprecation markers and removals absent rather than pending, on the consumer-lag
846
+ argument: a consumer reading a topic with lag breaks on a removed field regardless of what the
847
+ document says about it. The roadmap's exit criterion for completing this axis — retiring a message
848
+ identity through a feature delta, gated on nothing living still consuming it — is the compatibility
849
+ decision that reverses that stance: the removal family exists, a message-slot removal must be
850
+ justified by a `REMOVED` requirement's `Publishes:`/`Consumes:` line (channel and operation slots
851
+ need exactness only — requirements join on message names and never on channel or operation keys, so
852
+ demanding a justifying requirement there would invent a join the corpus cannot express), and
853
+ `asyncapi.remove-message-consumed` gates a removal the living fleet still consumes. The lag argument
854
+ survives as an **operational warning**, not a veto: passing the gate proves the *documents* agree
855
+ nobody consumes the message — it does not prove no consumer is still reading the topic with lag, so
856
+ sequence the runtime retirement accordingly.
857
+
858
+ **Payload-schema correctness stays out of scope, stated so the boundary reads as a decision rather
859
+ than a gap.** loam never validates that a payload schema is correct JSON Schema, correct Avro, or
860
+ correct anything: digests are content identity and every check joins on names. A fleet that wants
861
+ payload validation runs an external validator (for example `@asyncapi/parser` in CI) over the same
862
+ files, outside loam — no runtime dependency, and nothing in the lifecycle above depends on its
863
+ verdict.
864
+
865
+ ### The authorization vocabulary (`architecture/permissions.yaml`)
866
+
867
+ Authorization is a fleet-owned vocabulary joined to requirements by `Requires:`. It is opt-in: a
868
+ fleet with no `Requires:` line owes no file, but the first such line makes an absent declaration an
869
+ error rather than inventing a permission locally.
139
870
 
140
- **The join token is a message name**, spelled three times and required to be identical: a landscape edge's `metadata { publishes '...' }` / `metadata { consumes '...' }`, a requirement's `Publishes:` / `Consumes:` line (both requirement files — an outbox requirement in `arch.spec.md` is the canonical home of a `Publishes:` line, and the same keep-last quirk applies as to `Operations:`), and the message's `name` in `asyncapi.yaml`, or its declaration key when it has none.
141
-
142
- **The payload is opaque, by decision.** loam reads message names and the `action` of the operation carrying them, and nothing inside `payload`. That is the whole property that keeps the payload format a documentation choice: a fleet writes JSON Schema today, and one that later adopts Avro changes a `schemaFormat` line and nothing in loam. Keep payloads in `components/schemas` in the same document rather than `$ref`-ing an external `.avsc` — external references are out of scope here exactly as on the OpenAPI axis, so a schema in another file will not travel with the message that needs it.
871
+ ```yaml
872
+ subjects:
873
+ user:
874
+ description: a human account
875
+ permissions:
876
+ user:
877
+ payments:refund:
878
+ description: refund a captured payment
879
+ owned_by: identity-service
880
+ enforced_by: [payment-service]
881
+ ```
143
882
 
144
- **The direction is not decoration, and the check inverts.** On the API axis the PROVIDER owns the contract, so "does this operation exist" is answered inside one service's directory. An event's schema lives in the PRODUCER's repository, so "does anybody publish what I consume" is a fleet question with no local answer — which is why `spine.message-unproduced` has no OpenAPI analog, and why a consumer joined to nothing was previously indistinguishable from a healthy one. Its other half is `asyncapi.message-contested`: two services declaring they send one name means every consumer's join picks one arbitrarily. That one fleet question is also the axis's only cost — a per-service run reads every service's `asyncapi.yaml`, which is N small YAML parses rather than N LikeC4 workspace spins, and it is paid only where something actually consumes.
883
+ The unit is the pair `<subject>/<permission>`: `Requires: user/payments:refund`. A requirement may
884
+ list several comma-separated pairs. The subject is the entity the check is performed on (`user`,
885
+ `profile`, `service`), not necessarily the caller.
145
886
 
146
- **Absence has two grades, not three.** `service.no-openapi` warns when the landscape cannot prove nobody calls a service, because most services expose HTTP. An event contract is genuinely optional and most services touch no topic, so an absent `asyncapi.yaml` is an **error** when something already joins into it (the stranded message names ride in `details`) and **silence** otherwise. The file is presence-tracked in `loam list` and deliberately kept out of the maturity ladder: a fourth required artifact would demote every already-`documented` service in the fleet without one byte of their files changing.
887
+ - `permissions.unknown` (**error**) — a requirement names a pair the vocabulary does not declare;
888
+ when the file is absent, the `Requires:` line is the opt-in and the message asks for the first
889
+ declaration.
890
+ - `permissions.invalid` (**error**) — the file is unreadable or a permission sits under an
891
+ undeclared subject. It is reported alone because grading every `Requires:` against an unreadable
892
+ vocabulary would be a cascade.
893
+ - `permissions.unenforced` (**warning**, fleet scope) — a declared permission no requirement names,
894
+ the authorization mirror of `api.ungoverned`.
147
895
 
148
- **What this axis does not do yet, stated so the absence is not read as a defect:** there is no `features/<FEAT>/specs/<svc>/asyncapi.yaml`, no merge, no baseline pin and no `verify` claim — a feature cannot add a message, and one is retired by editing the living contract in a reviewed PR. Deprecation markers and a removal family are absent rather than pending: an async contract that only ever grows has nothing to mark, because a consumer reading a topic with lag breaks on a removed field regardless of what the document says about it.
896
+ This is a spelling and coverage join, not an identity-provider audit. `owned_by` and `enforced_by`
897
+ are explanatory fields and are not resolved against service directories; loam does not prove that
898
+ the running system implements a declared permission.
149
899
 
150
- **Page-specs** (`ui/pages/*.page.yaml`) are `[later]`: an example ships under `examples/docs/`, but no command reads pages yet, so the file's format is illustrative rather than contract. What is settled is only the job — a page names the operations it consumes and the scenarios that govern it, joining the same spine as everything else.
900
+ **Page-specs** (`ui/pages/*.page.yaml`) are `[later]`: an example ships under `examples/docs/`, but
901
+ no command reads pages yet, so the file's format is illustrative rather than contract. What is
902
+ settled is only the job — a page names the operations it consumes and the scenarios that govern it,
903
+ joining the same spine as everything else.
151
904
 
152
905
  ## Living spec vs delta (OpenSpec model)
153
906
 
154
- Behaviour follows OpenSpec conventions: a **requirement** (`### Requirement:`, RFC-2119 SHALL/MUST) with **scenarios** (`#### Scenario:`, Given/When/Then). Scenarios are the acceptance criteria and the source for tests.
155
-
156
- - **Living spec** — `services/<svc>/spec.md` (+ `landscape.likec4`): the complete current state — the "final spec of the whole product".
157
- - **Delta** — `features/<FEAT>/specs/<svc>/spec.md` (+ `delta.likec4`): a change, reviewed as a diff, tagged to the feature.
158
- - **`loam archive <FEAT>`** merges the delta into the living state on three axes — **requirements** (`spec.md`, and `arch.spec.md` beside it: the two files ride one merge code path, parameterized by filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4`) — then archives the feature, so the living state stays complete. Archived deltas are the evolution history (like `git log`). `--dry-run` prints the whole plan and writes nothing.
159
- - **Coherence gate:** `loam validate --feature` checks the three axes agree (C4 edge `op` ↔ OpenAPI `operationId` ↔ requirement `Operations:`). `loam archive` **blocks on the gating issues**. Severity and gating answer two different questions — severity says whether the *document* is valid (`validate` fails on errors), gating says whether the *merge* is safe — and they usually agree: errors gate, warnings do not. Where they diverge, the finding says so (`gates` in `--json`); today that is exactly `delta.requirement-not-merged`, a warning (the shape is legal OpenSpec, so adopted repos keep a green `validate`) that gates (the merge would silently drop the requirement). Advisory warnings are printed with the plan and never block. `--approve` overrides the gating issues only, and names each one it overrode. An operation that is missing from the provider's OpenAPI but defined by another feature still in flight is graded down to a warning (`spec-api.op-pending` / `c4-api.op-pending`) naming that feature — archive it first — because the fix is ordering, not authoring.
160
- - **`loam unarchive <FEAT>`** takes an archive back: it restores the living docs and re-opens the feature.
161
- - **The requirement baseline.** A MODIFIED requirement carries its full new text rather than a diff, so the merge REPLACES the living requirement instead of folding into it — and two features rewriting one requirement destroy the loser's text entirely. `loam validate` names the other feature while both are in flight (`delta.modified-conflict`, warn), but that check is blind to the case a fleet actually produces: the first feature archives and stops being active, the second revalidates green, and its archive lands text written against a document that no longer exists — `+0 ~1 -0`, exit 0, undetectable afterwards (`unarchive` of the first then refuses `snapshot-stale`, and `--force` takes the second out with it). So a MODIFIED/REMOVED requirement pins what it was written against with a **`Based-On:`** body line under its `Requirement-ID:` — `requirementDigest` of the living requirement, the same 16-hex-character truncation `sources_digest` uses. It is written by **`loam rebase <FEAT> [--service <id>] [--dry-run]`**, never by hand: rebase takes the docs lock, computes each pin from the living text as it stands, and rewrites only that one line per requirement (line surgery, not reserialization — a delta document's sections, order and prose are authored content). The digest is taken over the requirement's *canonical serialization*, not its bytes, so archive's own reflowing of a living spec never makes a pin go stale falsely. `delta.baseline-stale` is an error and gates archive: there is no reading of a stale delta under which the merge is what its author meant. `delta.baseline-missing` is only a warning — deltas adopted from OpenSpec never carried the line, and refusing to archive a migrated corpus is not a safety property. `Based-On:` is stripped on the way into the living document: it is a claim about a delta, meaningless once it *is* the living text, and leaving it in would make the next feature's baseline hash the previous feature's bookkeeping. Re-pinning is the last step of resolving a collision and never the resolution itself — a pin says "I read this version", so `loam rebase` reports every requirement that moved and says plainly that restamping without re-reading merges your text over theirs. There is deliberately no three-way merge: requirement prose does not merge.
162
- - **The same baseline on the contract axis, where it matters more.** A feature's `openapi.yaml` is a **complete document, not a patch** — authors restate the living contract around the slot they change — and the merge upserts every operation the document spells. The collision therefore needs no overlap between the two features at all: two deltas over one service, editing *different* operations, destroy each other, because whichever archives second pushes its authoring-time copy of the operation it never meant to touch back over what landed in between. On a service with thirty operations a delta quotes twenty-nine of them, and each is a candidate. So every operation in a feature contract carries **`x-loam-based-on`** — the same 16-hex digest, in the vendor-extension shape this axis already uses for `x-loam-remove`, written by `loam rebase` and stripped on merge exactly like `Based-On:`. The digest is taken over the operation's *canonical* form (object keys sorted), which is the same comparison `isDeepStrictEqual` makes when the merge decides an operation "differs" — so a pin goes stale exactly when the merge would say something moved, never over a reordered key. The pin is always the digest of the **living** operation, and that one rule yields both merge verdicts by itself: an operation the author only quoted is byte-equal to living, so its pin equals its own content and **the merge skips it entirely** (reported as `· quotes …` in the plan, so the plan never silently writes less than the delta spells); one the author edited differs from its pin, so the merge writes it — unless the living operation has also moved, which is `openapi.baseline-stale` (error, gates). Skipping a quote is mechanical rather than advisory, so `--approve` does not turn it back on: overriding a gate is a decision, reverting an operation nobody edited is a bug. `openapi.baseline-missing` is a warning and is reported **once per service** rather than per operation — the fix is one command, and twenty-nine identical findings teach people to filter the code out. **Not covered, by decision:** path-level keys (`parameters`, `servers`) and `components/schemas` are still upserted wholesale, so a restated schema can still revert another feature's change to it — `openapi.path-item-modified` and `openapi.component-modified` remain the only signal there; and removal markers carry no pin, since `openapi.remove-target-mismatch` already checks that exact slot.
163
-
164
- A requirement may carry a stable identity in its body: `Requirement-ID: <id>`, where the ID matches `[A-Za-z][A-Za-z0-9._-]{0,127}`. It is optional for backward compatibility. When present in a delta, archive matches the living requirement by ID; a `MODIFIED` block may therefore change the `### Requirement:` heading without losing history or verification evidence. When absent, the exact heading remains the legacy identity. A legacy `MODIFIED` that matches an ID-bearing living requirement by name inherits that ID, which supports incremental adoption. `Requirement-ID` may appear only once per requirement and must be unique within each `spec.md` / `arch.spec.md`; invalid, repeated, duplicate, or ID-versus-heading collisions are errors, and the merge refuses to guess. This is loam's explicit rename mechanism: stable ID plus `MODIFIED`, not OpenSpec's `RENAMED` section.
165
-
166
- Two `### Requirement:` blocks with one name in one LIVING document are still an **error** (`spec.duplicate-requirement`, per file — `spec.md` and `arch.spec.md` are separate namespaces, so one name in both is legal): a legacy name-based merge cannot choose safely. A repeated `Operations:` or `Covers:` line inside one requirement body is a **warning** (`spec.repeated-operations` / `spec.repeated-covers`, living specs and feature deltas alike): the last line REPLACES the earlier ones — assignment, not append, the documented keep-last quirk, whose semantics deliberately do not change — so the author's "long list in two lines" pattern silently loses its first line; merge them into one comma-separated line.
907
+ Behaviour follows OpenSpec conventions: a **requirement** (`### Requirement:`, RFC-2119 SHALL/MUST)
908
+ with **scenarios** (`#### Scenario:`, Given/When/Then). Scenarios are the acceptance criteria and
909
+ the source for tests.
910
+
911
+ - **Living spec** — `services/<svc>/spec.md` (+ `landscape.likec4`): the complete current state —
912
+ the "final spec of the whole product".
913
+ - **Delta** — `features/<FEAT>/specs/<svc>/spec.md` (+ `delta.likec4`): a change, reviewed as a
914
+ diff, tagged to the feature.
915
+ - **`loam archive <FEAT>`** merges the delta into the living state on three axes — **requirements**
916
+ (`spec.md`, and `arch.spec.md` beside it: the two files ride one merge code path, parameterized by
917
+ filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4`) — then archives the
918
+ feature, so the living state stays complete. Archived deltas are the evolution history (like
919
+ `git log`). `--dry-run` prints the whole plan and writes nothing.
920
+ - **Coherence gate:** `loam validate --feature` checks the three axes agree (C4 edge `op` ↔ OpenAPI
921
+ `operationId` ↔ requirement `Operations:`). `loam archive` **blocks on the gating issues**.
922
+ Severity and gating answer two different questions — severity says whether the *document* is valid
923
+ (`validate` fails on errors), gating says whether the *merge* is safe — and they usually agree:
924
+ errors gate, warnings do not. Where they diverge, the finding says so (`gates` in `--json`); the
925
+ warn-that-gates set holds five codes today — `delta.requirement-not-merged` (the shape is legal
926
+ OpenSpec, but the merge would silently drop the requirement), `delta.baseline-missing` and
927
+ `openapi.baseline-missing` (an unpinned delta is the silent-rollback shape — the baseline bullets
928
+ below), and `scaffold.placeholder` and `intent.empty` (an unedited scaffold must not become living
929
+ truth) — each a legal document whose merge would publish or destroy something nobody chose.
930
+ Advisory warnings are printed with the plan and never block. `--approve` overrides the gating
931
+ issues only, and names each one it overrode. An operation that is missing from the provider's
932
+ OpenAPI but defined by another feature still in flight is graded down to a warning
933
+ (`spec-api.op-pending` / `c4-api.op-pending`) naming that feature — archive it first — because the
934
+ fix is ordering, not authoring.
935
+ - **`loam unarchive <FEAT>`** takes an archive back: it restores the living docs and re-opens the
936
+ feature.
937
+ - **The requirement baseline.** A MODIFIED requirement carries its full new text rather than a diff,
938
+ so the merge REPLACES the living requirement instead of folding into it — and two features
939
+ rewriting one requirement destroy the loser's text entirely. `loam validate` names the other
940
+ feature while both are in flight (`delta.modified-conflict`, warn), but that check is blind to the
941
+ case a fleet actually produces: the first feature archives and stops being active, the second
942
+ revalidates green, and its archive lands text written against a document that no longer exists —
943
+ `+0 ~1 -0`, exit 0, undetectable afterwards (`unarchive` of the first then refuses
944
+ `snapshot-stale`, and `--force` takes the second out with it). So a MODIFIED/REMOVED requirement
945
+ pins what it was written against with a **`Based-On:`** body line under its `Requirement-ID:` —
946
+ `requirementDigest` of the living requirement, the same 16-hex-character truncation
947
+ `sources_digest` uses. It is written by **`loam rebase <FEAT> [--service <id>] [--dry-run]`**,
948
+ never by hand: rebase takes the docs lock, computes each pin from the living text as it stands,
949
+ and rewrites only that one line per requirement (line surgery, not reserialization — a delta
950
+ document's sections, order and prose are authored content). The digest is taken over the
951
+ requirement's *canonical serialization*, not its bytes, so archive's own reflowing of a living
952
+ spec never makes a pin go stale falsely. `delta.baseline-stale` is an error and gates archive:
953
+ there is no reading of a stale delta under which the merge is what its author meant.
954
+ `delta.baseline-missing` is a warning that also gates: the document is legal — deltas adopted from
955
+ OpenSpec never carried the line, so `loam validate` stays green — but an unpinned MODIFIED/REMOVED
956
+ is exactly the shape that replaces whatever landed in between, so the archive refuses it. A
957
+ migrated corpus is not stranded: one `loam rebase <FEAT>` per staged feature pins everything
958
+ (already the documented migration follow-up), and `--approve` archives unpinned when the loss is
959
+ meant. `Based-On:` is stripped on the way into the living document: it is a claim about a delta,
960
+ meaningless once it *is* the living text, and leaving it in would make the next feature's baseline
961
+ hash the previous feature's bookkeeping. Re-pinning is the last step of resolving a collision and
962
+ never the resolution itself — a pin says "I read this version", so `loam rebase` reports every
963
+ requirement that moved and says plainly that restamping without re-reading merges your text over
964
+ theirs. There is deliberately no three-way merge: requirement prose does not merge.
965
+ - **The same baseline on the contract axis, where it matters more.** A feature's `openapi.yaml` is a
966
+ **complete document, not a patch** — authors restate the living contract around the slot they
967
+ change — and the merge upserts every operation the document spells. The collision therefore needs
968
+ no overlap between the two features at all: two deltas over one service, editing *different*
969
+ operations, destroy each other, because whichever archives second pushes its authoring-time copy
970
+ of the operation it never meant to touch back over what landed in between. On a service with
971
+ thirty operations a delta quotes twenty-nine of them, and each is a candidate. So every operation
972
+ in a feature contract carries **`x-loam-based-on`** — the same 16-hex digest, in the
973
+ vendor-extension shape this axis already uses for `x-loam-remove`, written by `loam rebase` and
974
+ stripped on merge exactly like `Based-On:`. The digest is taken over the operation's *canonical*
975
+ form (object keys sorted), which is the same comparison `isDeepStrictEqual` makes when the merge
976
+ decides an operation "differs" — so a pin goes stale exactly when the merge would say something
977
+ moved, never over a reordered key. The pin is always the digest of the **living** operation, and
978
+ that one rule yields both merge verdicts by itself: an operation the author only quoted is
979
+ byte-equal to living, so its pin equals its own content and **the merge skips it entirely**
980
+ (reported as `· quotes …` in the plan, so the plan never silently writes less than the delta
981
+ spells); one the author edited differs from its pin, so the merge writes it — unless the living
982
+ operation has also moved, which is `openapi.baseline-stale` (error, gates). Skipping a quote is
983
+ mechanical rather than advisory, so `--approve` does not turn it back on: overriding a gate is a
984
+ decision, reverting an operation nobody edited is a bug. `openapi.baseline-missing` is a warning
985
+ that gates the archive, exactly like its requirement-axis sibling — every unpinned restatement is
986
+ a candidate rollback — and it is reported **once per service** rather than per operation: the fix
987
+ is one command, and twenty-nine identical findings teach people to filter the code out. **Covered
988
+ since the path-item/component baselines landed:** the same discipline now reaches the two surfaces
989
+ the operation pin cannot — path-level keys (`parameters`, `servers`, `summary`, `x-*`) and every
990
+ `components/<kind>/<name>` — through a single feature-only root record **`x-loam-baselines`** (`{
991
+ pathItems: {<path>: {<key>: <16-hex>}}, components: {"<kind>/<name>": <16-hex>} }`), written by
992
+ `loam rebase`, stripped from every living document, and graded through the same three codes: an
993
+ unpinned restatement counts into the per-service `openapi.baseline-missing` warn, a stale one is
994
+ `openapi.baseline-stale`, a malformed record or an entry naming nothing the delta restates is
995
+ `openapi.baseline-invalid`. The identity of a path-item field is `(path, key)` plus the
996
+ canonical-form digest of its value; a component's is `(kind, name)` likewise — recursion is
997
+ deliberately NOT part of identity: each component carries its own pin, and reference closure only
998
+ ever decides copying. A quoted surface is skipped by the merge exactly as a quoted operation is;
999
+ component copying is verdict-driven, with genuinely new components copied only when reachable from
1000
+ content actually written. Removal markers still carry no pin, since
1001
+ `openapi.remove-target-mismatch` already checks that exact slot.
1002
+
1003
+ A requirement may carry a stable identity in its body: `Requirement-ID: <id>`, where the ID matches
1004
+ `[A-Za-z][A-Za-z0-9._-]{0,127}`. It is optional for backward compatibility. When present in a delta,
1005
+ archive matches the living requirement by ID; a `MODIFIED` block may therefore change the
1006
+ `### Requirement:` heading without losing history or verification evidence. When absent, the exact
1007
+ heading remains the legacy identity. A legacy `MODIFIED` that matches an ID-bearing living
1008
+ requirement by name inherits that ID, which supports incremental adoption. `Requirement-ID` may
1009
+ appear only once per requirement and must be unique within each `spec.md` / `arch.spec.md`; invalid,
1010
+ repeated, duplicate, or ID-versus-heading collisions are errors, and the merge refuses to guess.
1011
+ This is loam's explicit rename mechanism: stable ID plus `MODIFIED`, not OpenSpec's `RENAMED`
1012
+ section.
1013
+
1014
+ Two `### Requirement:` blocks with one name in one LIVING document are still an **error**
1015
+ (`spec.duplicate-requirement`, per file — `spec.md` and `arch.spec.md` are separate namespaces, so
1016
+ one name in both is legal): a legacy name-based merge cannot choose safely. A repeated `Operations:`
1017
+ or `Covers:` line inside one requirement body is a **warning** (`spec.repeated-operations` /
1018
+ `spec.repeated-covers`, living specs and feature deltas alike): the last line REPLACES the earlier
1019
+ ones — assignment, not append, the documented keep-last quirk, whose semantics deliberately do not
1020
+ change — so the author's "long list in two lines" pattern silently loses its first line; merge them
1021
+ into one comma-separated line.
167
1022
 
168
1023
  ### The architecture spec axis (`arch.spec.md`)
169
1024
 
170
- The business spec will never mention the transactional outbox — that is architecture. Retries, idempotency, metrics, alerts: real obligations, invisible to every business scenario, and exactly where agent-generated code cuts corners unless the obligations are derived mechanically. So they get their own spec file, in the same grammar:
171
-
172
- - **Living** — `services/<svc>/arch.spec.md`: the architectural requirements as they stand, under `## Requirements`.
173
- - **Delta** — `features/<FEAT>/specs/<svc>/arch.spec.md`: the change, under the same `## ADDED|MODIFIED|REMOVED Requirements` algebra.
174
-
175
- Frontmatter follows `spec.md`'s conventions exactly (`service`, `status`, `sources` — the same provenance pass reads both, only the label differs), and `loam vouch` stamps the pair in one all-or-nothing run when this file is present. **Absence is not a finding**: partial adoption is supported, and the obligations below fire only when there is something to cover.
176
-
177
- **The `Covers:` line** is the architecture analog of `Operations:` — where a business requirement declares the operations it governs, an architecture requirement declares the model objects its scenarios exercise. Comma-separated, same keep-last-line quirk (and the same repeated-line warning, `spec.repeated-covers`), three entry forms:
178
-
179
- - a **C4 element** — its id (`paymentService.db`), or the service a bound/titled element stands for; resolved against the service's own model plus the landscape (for a feature delta: the feature's `delta.likec4`, the landscape, and the service's model);
180
- - an **edge** — `paymentService -> kafka`, each side resolved the same way against the declared relationships;
1025
+ The business spec will never mention the transactional outbox — that is architecture. Retries,
1026
+ idempotency, metrics, alerts: real obligations, invisible to every business scenario, and exactly
1027
+ where agent-generated code cuts corners unless the obligations are derived mechanically. So they get
1028
+ their own spec file, in the same grammar:
1029
+
1030
+ - **Living** — `services/<svc>/arch.spec.md`: the architectural requirements as they stand, under
1031
+ `## Requirements`.
1032
+ - **Delta** — `features/<FEAT>/specs/<svc>/arch.spec.md`: the change, under the same
1033
+ `## ADDED|MODIFIED|REMOVED Requirements` algebra.
1034
+
1035
+ Frontmatter follows `spec.md`'s conventions exactly (`service`, `status`, `sources` — the same
1036
+ provenance pass reads both, only the label differs), and `loam vouch` stamps the pair in one
1037
+ all-or-nothing run when this file is present. **Absence is not a finding**: partial adoption is
1038
+ supported, and the obligations below fire only when there is something to cover.
1039
+
1040
+ **The `Covers:` line** is the architecture analog of `Operations:` — where a business requirement
1041
+ declares the operations it governs, an architecture requirement declares the model objects its
1042
+ scenarios exercise. Comma-separated, same keep-last-line quirk (and the same repeated-line warning,
1043
+ `spec.repeated-covers`), three entry forms:
1044
+
1045
+ - a **C4 element** — its id (`paymentService.db`), or the service a bound/titled element stands for;
1046
+ resolved against the service's own model plus the landscape (for a feature delta: the feature's
1047
+ `delta.likec4`, the landscape, and the service's model);
1048
+ - an **edge** — `paymentService -> kafka`, each side resolved the same way against the declared
1049
+ relationships;
181
1050
  - a **health signal** — `alert:<id>` / `sli:<id>`, ids the service's `health.yaml` declares.
182
1051
 
183
- An entry that resolves to nothing is `covers.unknown` (**warning**, with close ids offered where they exist): the typo guard, because a mistyped entry silently costs exactly the coverage it was written for.
184
-
185
- **Coverage obligations** — both warnings, deliberately: they never gate `archive`, and `--strict` is the CI escalation.
186
-
187
- - `c4.uncovered` (feature scope): a NEW tagged element or tagged edge in the feature's `delta.likec4` that no requirement across the feature's `arch.spec.md` deltas covers. Grouping-only elements follow the landscape checks' exemptions (`person`-kind elements, `#external` tags).
188
- - `health.uncovered` (service scope): an alert or SLI declared in `services/<svc>/health.yaml` that no requirement in the LIVING `arch.spec.md` covers. This is the moment `health.yaml` stops being inert — and all loam reads out of it is ids: the recognized keys are top-level `slis:` and `alerts:`, each a sequence whose entries contribute their `name` (or `id` when there is no name; a plain string entry is its own id). A `health.yaml` that is missing, or declares nothing recognizable, yields no findings — an absent file must not manufacture obligations. One that EXISTS but does not parse is different evidence and gets its own **warning** (`health.invalid`): its ids are unknown, not empty, so `health.uncovered` stays quiet AND the `Covers: alert:/sli:` entries of that service go ungraded — the old silence turned every such entry into a false `covers.unknown` "typo" pointing at the spec, when the truth was the file.
189
-
190
- The rest of the machinery treats the axis as what it is — requirements. `loam archive` merges an `arch.spec.md` delta into the living one through the same code path as `spec.md` (same delta algebra, same prose-preserving rewrite, same guards and delta-shape checks — the two files are separate requirement namespaces, so an arch requirement never collides with a business one of the same name); the snapshot covers it and `unarchive` restores it. `loam verify` derives `scenario.tested` claims from ADDED/MODIFIED arch requirements exactly as from business ones — the claim id, the claim text AND the scenario digest carry `arch.spec.md` (the arch axis salts the body hash), so an identically-worded scenario in both files stays two questions with two digests, and the answering agent knows an integration/ops test is being asked for. `loam delta --json` projects arch requirement deltas as `archRequirements`, the same item shape as `requirements` (each item also carries `covers`).
191
-
192
- **Test levels, mapped once:** a business scenario is an acceptance test — emitted as a generated `.feature` by `loam gherkin`; an arch scenario is an integration or operational test (the outbox relay, the retry, the alert rule) — the same emission, tagged `@architecture`; `api.exposes` is a contract test; unit tests sit below spec granularity and stay the coding agent's TDD concern.
1052
+ An entry that resolves to nothing is `covers.unknown` (**warning**, with close ids offered where
1053
+ they exist): the typo guard, because a mistyped entry silently costs exactly the coverage it was
1054
+ written for.
1055
+
1056
+ **Coverage obligations** — both warnings, deliberately: they never gate `archive`, and `--strict` is
1057
+ the CI escalation.
1058
+
1059
+ - `c4.uncovered` (feature scope): a NEW tagged element or tagged edge in the feature's
1060
+ `delta.likec4` that no requirement across the feature's `arch.spec.md` deltas covers.
1061
+ Grouping-only elements follow the landscape checks' exemptions (`person`-kind elements,
1062
+ `#external` tags).
1063
+ - `health.uncovered` (service scope): an alert or SLI declared in `services/<svc>/health.yaml` that
1064
+ no requirement in the LIVING `arch.spec.md` covers. This is the moment `health.yaml` stops being
1065
+ inert — and all loam reads out of it is ids: the recognized keys are top-level `slis:` and
1066
+ `alerts:`, each a sequence whose entries contribute their `name` (or `id` when there is no name; a
1067
+ plain string entry is its own id), and top-level `dependencies:`, whose entries contribute their
1068
+ `id` (or `service`, or `name` — both spellings exist in the wild; a plain string entry is its own
1069
+ id). Queries, thresholds, checks, criticality stay authored prose no check reads. A `health.yaml`
1070
+ that is missing, or declares nothing recognizable, yields no findings — an absent file must not
1071
+ manufacture obligations. One that EXISTS but does not parse is different evidence and gets its own
1072
+ **warning** (`health.invalid`): its ids are unknown, not empty, so `health.uncovered` stays quiet
1073
+ AND the `Covers: alert:/sli:` entries of that service go ungraded — the old silence turned every
1074
+ such entry into a false `covers.unknown` "typo" pointing at the spec, when the truth was the file.
1075
+ - `health.dependency-unmodelled` (service scope): a `dependencies:` id that nothing in the service's
1076
+ OWN `model.likec4` answers to — by element id, `metadata { service }` binding, or title; exact
1077
+ match, with a did-you-mean hint. Deliberately the service model and never the landscape: the
1078
+ landscape carries only what crosses the service boundary, the model carries everything the service
1079
+ touches (a private store as a nested container — `landscape.datastore-private` is the check
1080
+ pushing it there), and that is the set the on-call file must agree with.
1081
+
1082
+ The rest of the machinery treats the axis as what it is — requirements. `loam archive` merges an
1083
+ `arch.spec.md` delta into the living one through the same code path as `spec.md` (same delta
1084
+ algebra, same prose-preserving rewrite, same guards and delta-shape checks — the two files are
1085
+ separate requirement namespaces, so an arch requirement never collides with a business one of the
1086
+ same name); the snapshot covers it and `unarchive` restores it. `loam verify` derives
1087
+ `scenario.tested` claims from ADDED/MODIFIED arch requirements exactly as from business ones — the
1088
+ claim id, the claim text AND the scenario digest carry `arch.spec.md` (the arch axis salts the body
1089
+ hash), so an identically-worded scenario in both files stays two questions with two digests, and the
1090
+ answering agent knows an integration/ops test is being asked for. `loam delta --json` projects arch
1091
+ requirement deltas as `archRequirements`, the same item shape as `requirements` (each item also
1092
+ carries `covers`).
1093
+
1094
+ **Test levels, mapped once:** a business scenario is an acceptance test — emitted as a generated
1095
+ `.feature` by `loam gherkin`; an arch scenario is an integration or operational test (the outbox
1096
+ relay, the retry, the alert rule) — the same emission, tagged `@architecture`; `api.exposes` is a
1097
+ contract test; unit tests sit below spec granularity and stay the coding agent's TDD concern.
193
1098
 
194
1099
  ### The generated Gherkin suite (`loam gherkin`)
195
1100
 
196
- Scenarios are the source for tests, and `loam gherkin` is where that stops being prose: it emits them as real `.feature` files into the SERVICE'S repo — the only loam command that writes there, because tests live with the code they gate. The output root is `<gherkinDir>/loam/`, where `gherkinDir` is an optional `loam.json` fact (default `features`, the cucumber convention; a non-string refuses the config like a malformed `docsDir`). The `loam/` subdirectory is loam's own derived space, and the ownership rule is absolute both ways: regenerating a scope rewrites that scope's files and deletes its orphans (reported), and loam never writes or deletes a byte outside `loam/` — step definitions and hand-written features live outside it, untouched. An emission with nothing to emit creates nothing: an empty `loam/` would read as a whole suite gone missing.
197
-
198
- Two scopes, one emitter, run inside the service's repo (anywhere else refuses — vouch's discipline, because the files land where loam is standing):
199
-
200
- - **Feature mode** — `loam gherkin <FEAT> [--service <id>]`: the feature's ADDED and MODIFIED requirements for that service, both spec axes, each file tagged with the feature id.
201
- - **Living mode** — `loam gherkin --service <id>`: the full acceptance suite from the living `spec.md` + `arch.spec.md` — the regression skeleton a legacy service gets at adoption.
202
-
203
- **The mapping is deterministic** — same specs, same bytes, which is what lets staleness be judged by digest:
204
-
205
- - one `.feature` per requirement: `Feature:` is the requirement name, the requirement's body text is the feature description, its scenarios are `Scenario:` blocks;
206
- - file naming: `<slug>.feature` (business axis) / `arch--<slug>.feature` (arch axis) — flat and diffable. Slugs lowercase the name and collapse every non-alphanumeric run to one hyphen, so no business slug can ever begin `arch--`; requirements slugging identically are numbered in document order (`retry.feature`, `retry-2.feature`);
207
- - steps: a scenario body line that is a list bullet (`-`, `*`, `+`) whose text — after stripping the marker and a `**bold**` wrapper around the keyword, trailing colon tolerated — opens with Given/When/Then/And/But (case-insensitive, so the OpenSpec `- **WHEN** ...` convention counts) becomes that keyword's step. Every other non-blank body line is kept, edge-trimmed, as the scenario's description, rendered before the steps (Gherkin ends a description at the first step keyword) — prose is never dropped. A scenario that yields ZERO steps (numbered-step or prose-only legacy bodies) still emits, but the emission says so per scenario (text, and `stepless` per file under `--json`): cucumber runs a step-less scenario vacuously green while `verify --results` requires at least one passed step, so it is permanently unconfirmable until the spec's bullets are reworded;
208
- - tags, on the `Feature:` line and inherited by its scenarios: `@<FEAT>` in feature mode (living emissions carry no feature tag), `@architecture` on the arch axis — the test-level marker;
209
- - stamps: line 1 is `# generated by loam v<version> — …` (the AGENTS.md stamp pattern — the file is never to be hand-edited), and each scenario carries the tag `@loam-digest-<16hex>` on the line above its `Scenario:` keyword: the first 16 hex characters of `loam verify`'s scenario body hash — sha256 of the body lines joined and edge-trimmed, salted with the file name on the arch axis, the exact recipe `scenario.tested` claim ids fold in — so the stamp, the claim and the spec can never disagree about what a scenario says, and identically-worded scenarios across `spec.md` and `arch.spec.md` can never share a digest (a business-axis test run cannot answer for an arch scenario). A tag rather than a comment because cucumber's JSON report carries tags per scenario (`elements[].tags`): the stamp rides through the runner untouched, which is what `loam verify --results` matches on.
210
-
211
- **Staleness** (`loam validate --service <id>`, service-repo-scoped like the `sources.*` chain): fires only where the repo is known AND `<gherkinDir>/loam/` exists — a service that never generated has not opted in, and stays quiet. The living specs are the reference; digests decide content, requirement names decide identity, and every comparison is axis-scoped (the two spec files are two namespaces: an arch scenario's integration test is not answered by a business `.feature` spelling the same words). Three warnings — never gates, `--strict` is the CI escalation — plus `gherkin.current` (ok) when a suite exists and agrees:
212
-
213
- - `gherkin.missing` — a living scenario whose digest no stamped scenario carries: the suite has no test for these words;
214
- - `gherkin.stale` — a stamped scenario whose digest matches no living scenario while its file's requirement still exists: the spec moved under the file. A reworded scenario reports stale + missing together, and one regeneration clears both;
215
- - `gherkin.orphaned` — a file whose requirement no longer exists in its axis's living spec, reported once per file: every scenario in it is moot together.
216
-
217
- A digest is content identity within an axis, deliberately: two identically-worded scenarios of one axis share one, and one stamped copy covers both. A file tagged with a feature still **in flight** is exempt from stale/orphan grading — it answers to its feature's delta (`loam gherkin <FEAT>` is its regeneration) until the feature archives, at which point its requirements ARE living requirements, digests unchanged, and the same bytes grade current with no rewrite. The exemption guards the writer too: a living-mode `loam gherkin` neither deletes NOR overwrites an in-flight file, even when a MODIFIED requirement's living emission collides with its filename — the file is reported as kept (`action: "kept"` under `--json`), because replacing it would silently revert the delta's wording mid-flight. An abandoned feature's tag names nothing active, so its files fall to `gherkin.orphaned` and regeneration removes them. Mid-flight, a MODIFIED requirement's reworded living scenarios do report `gherkin.missing` — the suite tests the delta's words while the living spec still promises the old ones; that is true, and archiving the feature clears it.
218
-
219
- The loop is closed end to end: generate → write step definitions (outside `loam/`) → run the suite with a JSON report — `cucumber-js --format json:report.json`, the CI recipe; cucumber-jvm, behave and SpecFlow emit the same format — → implement until green → `loam verify <FEAT> --service <id> --results report.json [--record rest.json]`, run in that service's own repo. The digest tags ride through the runner into the report, so the done-check's `scenario.tested` claims are answered by the green run itself; see "The verification record".
1101
+ Scenarios are the source for tests, and `loam gherkin` is where that stops being prose: it emits
1102
+ them as real `.feature` files into the SERVICE'S repo — the only loam command that writes there,
1103
+ because tests live with the code they gate. The output root is `<gherkinDir>/loam/`, where
1104
+ `gherkinDir` is an optional `loam.json` fact (default `features`, the cucumber convention; a
1105
+ non-string refuses the config like a malformed `docsDir`). The `loam/` subdirectory is loam's own
1106
+ derived space, and the ownership rule is absolute both ways: regenerating a scope rewrites that
1107
+ scope's files and deletes its orphans (reported), and loam never writes or deletes a byte outside
1108
+ `loam/` — step definitions and hand-written features live outside it, untouched. An emission with
1109
+ nothing to emit creates nothing: an empty `loam/` would read as a whole suite gone missing.
1110
+
1111
+ Two scopes, one emitter, run inside the service's repo (anywhere else refuses — vouch's discipline,
1112
+ because the files land where loam is standing):
1113
+
1114
+ - **Feature mode** — `loam gherkin <FEAT> [--service <id>]`: the feature's ADDED and MODIFIED
1115
+ requirements for that service, both spec axes, each file tagged with the feature id.
1116
+ - **Living mode** — `loam gherkin --service <id>`: the full acceptance suite from the living
1117
+ `spec.md` + `arch.spec.md` — the regression skeleton a legacy service gets at adoption.
1118
+
1119
+ **The mapping is deterministic** — same specs, same bytes, which is what lets staleness be judged by
1120
+ digest:
1121
+
1122
+ - one `.feature` per requirement: `Feature:` is the requirement name, the requirement's body text is
1123
+ the feature description, its scenarios are `Scenario:` blocks;
1124
+ - file naming: `<slug>.feature` (business axis) / `arch--<slug>.feature` (arch axis) — flat and
1125
+ diffable. Slugs lowercase the name and collapse every non-alphanumeric run to one hyphen, so no
1126
+ business slug can ever begin `arch--`; requirements slugging identically are numbered in document
1127
+ order (`retry.feature`, `retry-2.feature`);
1128
+ - steps: a scenario body line that is a list bullet (`-`, `*`, `+`) whose text — after stripping the
1129
+ marker and a `**bold**` wrapper around the keyword, trailing colon tolerated — opens with
1130
+ Given/When/Then/And/But (case-insensitive, so the OpenSpec `- **WHEN** ...` convention counts)
1131
+ becomes that keyword's step. Every other non-blank body line is kept, edge-trimmed, as the
1132
+ scenario's description, rendered before the steps (Gherkin ends a description at the first step
1133
+ keyword) — prose is never dropped. A scenario that yields ZERO steps (numbered-step or prose-only
1134
+ legacy bodies) still emits, but the emission says so per scenario (text, and `stepless` per file
1135
+ under `--json`): cucumber runs a step-less scenario vacuously green while `verify --results`
1136
+ requires at least one passed step, so it is permanently unconfirmable until the spec's bullets are
1137
+ reworded;
1138
+ - examples: a Markdown table **at the body’s own left margin** becomes `Examples:` and changes the
1139
+ emission to `Scenario Outline`; its header names the `<placeholders>` used by steps and each row is
1140
+ one case. The header needs at least one data row and every row needs the same column count, and a
1141
+ cell may hold a literal pipe by writing `|`. A malformed table is preserved as description instead
1142
+ of being dropped, and the command reports that scenario in `malformedExamples` because it will run
1143
+ once rather than once per row. Only the FIRST margin table is `Examples`;
1144
+ - step arguments — the two forms a dense component test needs, both written UNDER the step they
1145
+ belong to and both riding into the scenario digest like every other body byte:
1146
+ - an **indented** Markdown table becomes that step’s Gherkin **data table** (the markdown separator
1147
+ row is dropped; Gherkin has no such row). Indentation is the whole disambiguation: indented under
1148
+ a step means “this step’s rows”, at the margin means “the scenario’s cases”. This is how an
1149
+ expected-state assertion — what a row, a topic or an outbox must hold after one data pass — is
1150
+ written at all;
1151
+ - a **fenced block** (```` ``` ```` or `~~~`, with its info string carried through as the
1152
+ docstring’s content type) becomes that step’s **docstring**. Its lines keep their indentation
1153
+ rather than being trimmed like description — a payload whose leading whitespace has been eaten is
1154
+ one no step definition can compare against. Fences are recognised before tables, so a pipe table
1155
+ inside a payload is part of the payload and is never hoisted out as `Examples`;
1156
+ - a block that no step precedes, or an indented table loam cannot read, stays in the description and
1157
+ is reported per scenario as `strandedBlocks`: the document still reads correctly and the TEST has
1158
+ lost its argument, which is a loss nothing downstream can otherwise notice;
1159
+ - tags, on the `Feature:` line and inherited by its scenarios: `@<FEAT>` in feature mode (living
1160
+ emissions carry no feature tag), `@architecture` on the arch axis — the test-level marker;
1161
+ - stamps: line 1 is `# generated by loam v<version> — …` (the AGENTS.md stamp pattern — the file is
1162
+ never to be hand-edited), and each scenario carries the tag `@loam-digest-<16hex>` on the line
1163
+ above its `Scenario:` keyword: the first 16 hex characters of `loam verify`'s scenario body hash —
1164
+ sha256 of the body lines joined and edge-trimmed, salted with the file name on the arch axis, the
1165
+ exact recipe `scenario.tested` claim ids fold in — so the stamp, the claim and the spec can never
1166
+ disagree about what a scenario says, and identically-worded scenarios across `spec.md` and
1167
+ `arch.spec.md` can never share a digest (a business-axis test run cannot answer for an arch
1168
+ scenario). A tag rather than a comment because cucumber's JSON report carries tags per scenario
1169
+ (`elements[].tags`): the stamp rides through the runner untouched, which is what
1170
+ `loam verify --results` matches on.
1171
+
1172
+ **Staleness** (`loam validate --service <id>`, service-repo-scoped like the `sources.*` chain):
1173
+ fires only where the repo is known AND `<gherkinDir>/loam/` exists — a service that never generated
1174
+ has not opted in, and stays quiet. The living specs are the reference; digests decide content,
1175
+ requirement names decide identity, and every comparison is axis-scoped (the two spec files are two
1176
+ namespaces: an arch scenario's integration test is not answered by a business `.feature` spelling
1177
+ the same words). Three warnings — never gates, `--strict` is the CI escalation — plus
1178
+ `gherkin.current` (ok) when a suite exists and agrees:
1179
+
1180
+ - `gherkin.missing` — a living scenario whose digest no stamped scenario carries: the suite has no
1181
+ test for these words;
1182
+ - `gherkin.stale` — a stamped scenario whose digest matches no living scenario while its file's
1183
+ requirement still exists: the spec moved under the file. A reworded scenario reports stale +
1184
+ missing together, and one regeneration clears both;
1185
+ - `gherkin.orphaned` — a file whose requirement no longer exists in its axis's living spec, reported
1186
+ once per file: every scenario in it is moot together.
1187
+
1188
+ A digest is content identity within an axis, deliberately: two identically-worded scenarios of one
1189
+ axis share one, and one stamped copy covers both. A file tagged with a feature still **in flight**
1190
+ is exempt from stale/orphan grading — it answers to its feature's delta (`loam gherkin <FEAT>` is
1191
+ its regeneration) until the feature archives, at which point its requirements ARE living
1192
+ requirements, digests unchanged, and the same bytes grade current with no rewrite. The exemption
1193
+ guards the writer too: a living-mode `loam gherkin` neither deletes NOR overwrites an in-flight
1194
+ file, even when a MODIFIED requirement's living emission collides with its filename — the file is
1195
+ reported as kept (`action: "kept"` under `--json`), because replacing it would silently revert the
1196
+ delta's wording mid-flight. An abandoned feature's tag names nothing active, so its files fall to
1197
+ `gherkin.orphaned` and regeneration removes them. Mid-flight, a MODIFIED requirement's reworded
1198
+ living scenarios do report `gherkin.missing` — the suite tests the delta's words while the living
1199
+ spec still promises the old ones; that is true, and archiving the feature clears it.
1200
+
1201
+ The loop is closed end to end: generate → write step definitions (outside `loam/`) → run the suite
1202
+ with a JSON report — `cucumber-js --format json:report.json`, the CI recipe; cucumber-jvm, behave
1203
+ and SpecFlow emit the same format — → implement until green →
1204
+ `loam verify <FEAT> --service <id> --results report.json [--record rest.json]`, run in that
1205
+ service's own repo. The digest tags ride through the runner into the report, so the done-check's
1206
+ `scenario.tested` claims are answered by the green run itself; see "The verification record".
1207
+
1208
+ **How much glue that second step is, and how much of it is already written**, is `loam steps`. It
1209
+ counts the distinct step PHRASES a service's living scenarios collapse onto — `{n}`, `{s}` and `{p}`
1210
+ placeholders folded, keyword dropped, because a runner resolves `Given` and `When` through one
1211
+ registry — reports how few phrases cover 80% of every written step, and groups the near-duplicates
1212
+ that differ only by an article or a trailing clause. It reads the living specs, so it answers in a
1213
+ repo that has never run `loam gherkin`, and it emits no file and no finding.
1214
+
1215
+ `services/<svc>/steps.yaml` is the optional other half: **the step catalogue**, a `steps:` list of
1216
+ step texts recording which phrases the team has decided its suite defines. `loam steps` reports the
1217
+ written phrases against it — written-but-not-catalogued is the work-list, catalogued-but-not-written
1218
+ is a definition with nothing left to match — so the report becomes a diff against a decision instead
1219
+ of the same recount every time.
1220
+
1221
+ It is **authored, not emitted**, and that is the axis's one real design decision. loam does not read
1222
+ code and must not present a derived thing as truth, so what a team can honestly write down is which
1223
+ phrases they have AGREED to define; nothing here claims a step definition exists. It stays a line in
1224
+ YAML rather than a file per phrase for the rule the glossary and capability trees settled — an entry
1225
+ with prose gets a file, an entry without prose stays a line — and it lives in the docs repo rather
1226
+ than beside the code because `loam steps` needs no service repo to stand in, and because
1227
+ `<gherkinDir>/loam/` is written and deleted wholesale by `loam gherkin`. Entries are step TEXTS, not
1228
+ keys: the key is loam's normalisation, and asking an author to type it would make an internal
1229
+ spelling a hand-written contract. **The axis carries no finding code and `loam validate` never reads
1230
+ it** — a phrase written before its glue is the normal order of work, and a phrase-similarity warning
1231
+ is the kind of check a fleet turns off, taking the real findings beside it with it.
220
1232
 
221
1233
  ### The smallest legal feature
222
1234
 
223
- A one-service bugfix needs exactly one file: `features/<FEAT>/specs/<svc>/spec.md` holding one `MODIFIED` requirement with its scenarios. That floor is verified against the checks, not asserted: with no `intent.md`, no `delta.likec4` and no openapi delta, `loam validate --feature` passes with zero errors and zero warnings, and `loam archive` plans exactly two writes — the living spec update and the move into `features/archive/`.
224
-
225
- Each axis may be legitimately empty, and the checks grade absence differently from emptiness-by-accident:
226
-
227
- - **`delta.likec4`** — absent, or present with an empty `model {}` (both verified: `delta.valid`, no landscape write in the plan): a behaviour fix moves no boxes and no edges, and the architecture merge simply has nothing to do. The line is drawn at content: a file that *declares* elements or relationships with none carrying the feature tag is an error (`delta.nothing-tagged`), because declared-but-untagged is almost always a forgotten tag, while declaring nothing is a legible statement that the architecture is unchanged.
228
- - **openapi delta** — absent whenever the feature adds no operations; a requirement change that stays inside the existing contract has no API axis to speak on.
229
- - **requirement delta** — the axis that is nearly always present, because a feature that changes no requirement changes no promised behaviour. The exception is architecture-only work: a new service in the C4 delta with no `specs/<svc>/spec.md` gets `service.no-requirement-delta` as a warning, not an error.
230
-
231
- `intent.md` is required by no check today — an absent one produces no finding at all — but it is where the "why" lives, and `loam new` scaffolds it for a reason: the delta says what changed, and nothing else says what for.
232
-
233
- ### Where a capability lives (and why there is no capability layer)
234
-
235
- A business capability — "payment splitting" — is spread across the living specs of every service that carries part of it. The obvious fix is a `capabilities/` layer holding the whole story in one place. **There is none, and there should not be one.**
236
-
237
- The story is already whole and already derivable. Every requirement in a living spec arrived through exactly one feature, and that feature is still on disk under `features/archive/` with its intent, its C4 delta and its requirement deltas intact. "Which feature introduced this requirement" is a search, not a record — so a capabilities layer would be a second copy of text that already exists, which is a second thing that can disagree with the first. That is the same reason there is no service manifest.
1235
+ A one-service bugfix needs two authored files: a non-empty `features/<FEAT>/intent.md`, and
1236
+ `features/<FEAT>/specs/<svc>/spec.md` holding one `MODIFIED` requirement with its scenarios. The
1237
+ requirement also needs a current `Based-On:` pin before archive; `loam rebase <FEAT>` writes it.
1238
+ `delta.likec4` and an OpenAPI delta may both be absent when architecture and contract are unchanged.
1239
+ Omitting or leaving the intent as scaffold text raises the gating `intent.empty` /
1240
+ `scaffold.placeholder` warning, so the old one-file shape is not archive-ready.
1241
+
1242
+ Each axis may be legitimately empty, and the checks grade absence differently from
1243
+ emptiness-by-accident:
1244
+
1245
+ - **`delta.likec4`** — absent, or present with an empty `model {}` (both verified: `delta.valid`, no
1246
+ landscape write in the plan): a behaviour fix moves no boxes and no edges, and the architecture
1247
+ merge simply has nothing to do. The line is drawn at content: a file that *declares* elements or
1248
+ relationships with none carrying the feature tag is an error (`delta.nothing-tagged`), because
1249
+ declared-but-untagged is almost always a forgotten tag, while declaring nothing is a legible
1250
+ statement that the architecture is unchanged.
1251
+ - **openapi delta** — absent whenever the feature adds no operations; a requirement change that
1252
+ stays inside the existing contract has no API axis to speak on.
1253
+ - **requirement delta** — the axis that is nearly always present, because a feature that changes no
1254
+ requirement changes no promised behaviour. The exception is architecture-only work: a new service
1255
+ in the C4 delta with no `specs/<svc>/spec.md` gets `service.no-requirement-delta` as a warning,
1256
+ not an error.
1257
+
1258
+ `intent.md` is where the "why" lives — the delta says what changed, and nothing else says what for —
1259
+ and the archive gate holds it to that: `intent.empty` (a warning that gates, `--approve`
1260
+ overridable) fires when the file is missing or says nothing outside the scaffold's own comments, so
1261
+ a feature cannot fold into the living docs with its reason unwritten. Its sibling
1262
+ `scaffold.placeholder` refuses the rest of an unedited `loam new` scaffold the same way — the exact
1263
+ template strings, a requirement or description nobody authored, must not become living truth.
1264
+
1265
+ ### Where a capability lives (a declared name, and the document behind it)
1266
+
1267
+ A business capability — "payment splitting" — is spread across the living specs of every service
1268
+ that carries part of it. What was rejected here, and stays rejected, is an UNCHECKED free-text
1269
+ label: a hand-written `capability:` field has no ground truth, so it would end up on some features
1270
+ and not others, in three spellings of the same theme, never revisited — while creating the
1271
+ impression that an index exists.
1272
+
1273
+ A DECLARED name is different in exactly the way that mattered: it is checkable. A capability may be
1274
+ declared on either of two sides, and the vocabulary is their UNION.
1275
+
1276
+ **A name alone is a line in `architecture/capabilities.yaml`** — ids with an optional `description`
1277
+ and `owner`, nested ids such as `payments/refunds` kept as one flat key — and a requirement joins it
1278
+ with a `Capability:` list line in either spec file.
1279
+
1280
+ **A name with prose behind it gets a document: `capabilities/<cap>/spec.md`,** the authored business
1281
+ tree. The rule that separates the two is general and settles the question for every future
1282
+ vocabulary: **an entry with prose gets a file; an entry without prose stays a line in YAML.**
1283
+ (`architecture/permissions.yaml` is the principled exception — a `user/profile:read` pair has no
1284
+ prose, and a document per pair is ceremony.) A single YAML file grown to hold paragraphs is
1285
+ unworkable at fleet scale and is a second list nothing keeps current, which is the drift
1286
+ `loam init`'s removed `loam.docs.json` was removed for. So the DIRECTORY is the list: a directory is
1287
+ a capability if and only if it holds `spec.md`, nesting is spelled by the tree
1288
+ (`capabilities/payments/refunds/spec.md`), and a directory may be both a capability and the parent
1289
+ of others. `loam init` does not scaffold `capabilities/` — its existence is the opt-in, exactly as
1290
+ `architecture/adrs/`'s is.
1291
+
1292
+ The document carries narrative and then `## Requirements`, in the same grammar and read by the same
1293
+ parser as every other `spec.md` — this is where OpenSpec's `## Purpose` prose finally has a loam
1294
+ equivalent — with two rules of its own:
1295
+
1296
+ - **Every requirement needs a `Requirement-ID:`.** Optional in a service spec for OpenSpec
1297
+ compatibility, required here: these documents outlive every service that realizes them, so
1298
+ identity is the line and not the heading — otherwise rewording a heading is a removal and an
1299
+ addition, and every join made to it breaks silently. Missing is
1300
+ `capability.requirement-unidentified` (**error**).
1301
+ - **A capability requirement must be observable OUTSIDE the fleet.** `Operations:`, `Covers:`,
1302
+ `Publishes:` and `Consumes:` each resolve against one service's own contract or model, so a
1303
+ requirement carrying any of them is a service requirement filed at the wrong altitude:
1304
+ `capability.requirement-service-scoped` (**error**). `Requires:` is deliberately exempt — a
1305
+ permission is a domain fact, observable outside the fleet. The companion rule that a capability
1306
+ requirement **names no service** is an authoring rule that PR review holds and loam does not
1307
+ check: the only mechanical reading of it is scanning prose for declared service ids, which is a
1308
+ heuristic, and loam refuses-and-names rather than guessing.
1309
+
1310
+ A directory under `capabilities/` that holds neither the document nor a capability beneath it is
1311
+ `capability.doc-missing` (**warning**) — what `mkdir` leaves behind halfway through creating one. It
1312
+ declares nothing, so a `Capability:` line naming it still reports `capability.unknown`. And the
1313
+ axis's own two joins — `Capability:` and `Realizes:` — are refused inside a capability document
1314
+ (`capability.requirement-inert-join`, **error**): they point INTO the tree, so nothing reads them
1315
+ written in it, while both parse and read exactly like the working joins one directory over.
1316
+
1317
+ #### `Realizes:` — the join a service requirement makes into the tree
1318
+
1319
+ A service requirement says which promise it serves with a `Realizes:` line, entries spelled
1320
+ `<capability-id>#<Requirement-ID>`:
1321
+
1322
+ ```markdown
1323
+ ### Requirement: Authorize a payment
1324
+ Requirement-ID: PAY-AUTHORIZE
1325
+ The service SHALL reserve funds for an order before capture…
1326
+
1327
+ Operations: authorizePayment
1328
+ Realizes: checkout#CHECKOUT-CHARGE-ONCE
1329
+ ```
238
1330
 
239
- A hand-written `capability:` label on a feature was considered and rejected for a sharper reason: **nothing could check it.** `sources` is hand-written too, but it is checkable — the paths exist or they do not, the digest matches or it does not. There is no ground truth for what counts as a capability, so the field would end up on some features and not others, in three spellings of the same theme, never revisited — while creating the impression that an index exists.
1331
+ This is **not** the same claim as `Capability:` beside it, and a requirement commonly carries both:
1332
+ `Capability: checkout` names a theme, `Realizes: checkout#CHECKOUT-CHARGE-ONCE` names one promise.
1333
+ Only the second can be graded in both directions, which is what makes the business tree checkable
1334
+ rather than merely present. It is written by whoever implements the requirement, never by the
1335
+ analyst — a business document must not be edited every time the fleet rearranges which service
1336
+ carries which part — and the API hop needs no line of its own, because a capability requirement
1337
+ reaches its operations by composing this join with the `Operations:` lines that already exist.
1338
+
1339
+ Two details of the grammar are load-bearing. The capability half is what makes the target
1340
+ addressable at all, since a `Requirement-ID` is unique only inside its own document. And the
1341
+ separator is the **last** `#`, not the first: the requirement half's grammar excludes `#` while a
1342
+ capability id is a YAML key and a directory name and is constrained nowhere, so splitting at the
1343
+ last one is unambiguous for every id there is.
1344
+
1345
+ - `capability.realizes-unknown` (**error**) — the entry names no capability requirement. Five
1346
+ distinct failures with five different fixes, and the message says which: the entry is not spelled
1347
+ `<capability-id>#<Requirement-ID>`; the capability is undeclared (close names offered); it is
1348
+ declared but has no document; the document declares no requirements yet; or it declares
1349
+ requirements but not this id (close names again). In a feature delta it gates `loam archive`
1350
+ (`--approve` overrides), for `capability.unknown`'s reason.
1351
+ - `capability.requirement-unrealized` (**warning**, fleet scope) — a capability requirement no
1352
+ living service requirement's `Realizes:` line names, one warning per requirement, subject
1353
+ `<capability>#<id>`. The sharper sibling of `capability.unrealized` and the one that survives a
1354
+ healthy-looking row: a capability with four requirements and three realized reports nothing at all
1355
+ through the other code. It never gates, because writing the business document ahead of the fleet
1356
+ is the intended use.
1357
+
1358
+ ##### The pin: `@<digest>`, and the one thing it is for
1359
+
1360
+ An entry may carry the digest of the capability requirement it was written against:
1361
+
1362
+ ```markdown
1363
+ Realizes: checkout#CHECKOUT-CHARGE-ONCE@9f2c1a4b6d0e5713
1364
+ ```
240
1365
 
241
- For a theme that genuinely crosses services and matches no structural unit, the mechanism already exists and is checkable: **a LikeC4 tag**. Tags are declared in a `specification` block, so a misspelling is a parse error rather than quiet drift; `validate` already reads them, `archive` already handles them, and they show up in the diagram.
1366
+ **Nobody types this.** `loam rebase --living` writes it and rewrites it; an author writes the bare
1367
+ entry and is never asked for more. An entry without a pin is not a defect and never becomes one, so
1368
+ a corpus that has never been pinned grades exactly as it did before pins existed.
1369
+
1370
+ It exists because every other check on this axis is an EXISTENCE constraint.
1371
+ `capability.realizes-unknown` fires when the target is **gone** and is silent when the target has
1372
+ merely **changed** — so an analyst can narrow a promise and every requirement claiming to keep it
1373
+ goes on claiming so, in a corpus loam rewrote itself, with nothing anywhere recording that the claim
1374
+ was made about different words. The pin is what makes that visible:
1375
+
1376
+ - `capability.realizes-stale` (**warning**) — a pinned entry whose target has been rewritten since
1377
+ the pin was stamped. The join still resolves; the address is fine and the promise behind it moved.
1378
+
1379
+ It stays a **warning** and never gates. A moved promise has three legitimate outcomes — change the
1380
+ requirement, change the promise, or decide neither needed changing — and only the third is
1381
+ `loam rebase --living` on its own. An error would remove the middle one without `--approve`, and a
1382
+ gate would make an analyst's ordinary edit break other teams' builds.
1383
+
1384
+ Two grammar consequences are load-bearing. `@` is the separator rather than a second `#` because
1385
+ the entry is split at the *last* `#`, so a second one would move that boundary and re-parse every
1386
+ existing entry; `@` is excluded from `Requirement-ID`, which makes the split unambiguous rather than
1387
+ conventional. And a suffix that is not a well-formed digest is **not** a pin and not a second
1388
+ refusal — the whole entry stays the target, so `checkout#CHK-1@nonsense` fails under
1389
+ `capability.realizes-unknown` naming what the author actually typed.
1390
+
1391
+ Finally, a pin is invisible to `requirementDigest`. Pins and `Based-On:` lines are both bookkeeping
1392
+ a requirement carries about its own joins, and a digest that moved when bookkeeping moved would make
1393
+ every pin self-invalidating: one `rebase --living` would go on to invalidate every `Based-On:`
1394
+ baseline in the fleet — the corpus-wide cascade the pin exists to *report* rather than to cause.
1395
+
1396
+ Realizing a REQUIREMENT realizes its capability: a requirement carrying only `Realizes:` counts
1397
+ toward `capability.unrealized`, and a requirement carrying both joins is counted once.
1398
+ `loam list capabilities --json` carries the whole join — each capability's `requirements[]`, and
1399
+ what realizes each one (the key is absent for a capability with no document, and an empty array for
1400
+ a document declaring none).
1401
+
1402
+ #### `#req-` — the join a USE CASE makes, and the only one that crosses services
1403
+
1404
+ `Realizes:` is not the only way to keep a promise, and for a criterion that crosses services it is
1405
+ not enough. "I enter a login and a password and I am in" belongs to no single service's `spec.md`,
1406
+ because each promises only its own part — one announces the order, another sends the message, and
1407
+ neither can promise what happens between them. A use case can, because it IS the hop sequence. So a
1408
+ `dynamic view` already tagged `#cap-<slug>` may carry `#req-<slug>` as well:
242
1409
 
243
- ### Considered and rejected: the rest of the OpenSpec feature set
1410
+ ```
1411
+ dynamic view uc_order_notification {
1412
+ #cap-order-notifications
1413
+ #req-NOTIFY-ONCE
1414
+ title 'Order notification'
1415
+ …
1416
+ }
1417
+ ```
244
1418
 
245
- loam takes OpenSpec's requirement format and leaves most of its machinery, deliberately. OpenSpec and loam optimize different units: a capability-oriented repository versus a contract-linked service fleet. The points below describe the actual v1.7 concepts rather than treating every upstream feature as a workaround for not having a model.
1419
+ The second tag is **scoped by the first**: a `Requirement-ID` is unique only inside its own
1420
+ document, so the view must first say which capability it is about. Several `#req-` tags on one view
1421
+ are legal and normal — a flow commonly keeps two of a capability's promises. Both tags spell their
1422
+ id with every character outside `[A-Za-z0-9_-]` flattened to `-`, because that is all a LikeC4 tag
1423
+ name accepts (measured at the 1.59.2 pin; a rejected character truncates the tag rather than
1424
+ refusing it, which is why the flattening is a whitelist and not a list of substitutions).
1425
+
1426
+ - `usecase.requirement-unresolved` (**error**) — six failures with six fixes, and the message says
1427
+ which: the view resolves no `#cap-` tag, it resolves two, the capability has no document, the
1428
+ document declares no requirements yet, it declares none flattening to this slug (close ids
1429
+ offered, each already spelled as the tag to write), or two of its ids flatten to one slug.
1430
+
1431
+ **Either reserved prefix opts a view in.** A view carrying `#req-` and no `#cap-` is an author who
1432
+ asked to be graded and forgot half the claim; it earns the `unscoped` message rather than silence.
1433
+ That opt-in is **one predicate**, and every reader takes it: `loam validate`, `loam diff`,
1434
+ `loam delta`, `loam status`, the context and explore packs and `loam list capabilities` all decide
1435
+ "is this a use case" the same way, so a flow one of them can see is a flow all of them can see. The
1436
+ byte scan that lets a fleet with no use cases skip the LikeC4 load looks for both prefixes for the
1437
+ same reason — a gate narrower than the predicate is silence, not a smaller answer.
1438
+
1439
+ Being SEEN is not the same as claiming something, and the difference is worth stating because it
1440
+ looks like a gap and is not. A view with no resolved `#cap-` tag claims no capability, so it seeds
1441
+ nothing in `loam explore --capability` and keeps no promise in `loam list capabilities`' `keptBy` —
1442
+ a `#req-` tag is scoped by a `#cap-` tag and means nothing without one. What the shared predicate
1443
+ buys those two is that the flow is read at all: the fleet opts in, the flows are loaded, and
1444
+ `useCases.unreadable` answers honestly instead of the scan reporting a fleet that draws none.
1445
+
1446
+ **A feature can bring a flow with it**: `features/<FEAT>/usecases/<name>.likec4` is a views-only
1447
+ document copied into `architecture/usecases/` by `loam archive` and removed again by
1448
+ `loam unarchive`, so the flow that answers a cross-service promise ships in the same change as the
1449
+ promise. The route is **create-only**, for the glossary's reason exactly: a flow is one ordered hop
1450
+ sequence, the merge is a whole-file copy, and there is nothing to merge partially — a flow the
1451
+ living tree already holds is `usecase.flow-exists` (error, not `--approve`-overridable), and the fix
1452
+ is to edit `architecture/usecases/<name>.likec4` directly in the same pull request.
1453
+
1454
+ Its `#req-` tag resolves against **both corpora** — the living capability tree and this feature's own
1455
+ capability deltas — which is the rule `Realizes:` already follows, and without it the axis's headline
1456
+ case is refused by an error of its own making: an analyst adds `NOTIFY-ONCE`, the architect answers
1457
+ it with a flow, and the tag names a promise that is not living *yet*. The flow itself is graded
1458
+ against the map the same merge would leave behind, so a hop may name a service the feature's
1459
+ `delta.likec4` adds; a hop naming an element the merge does NOT land is `usecase.flow-invalid`
1460
+ (error, not `--approve`-overridable) and stops the archive before anything is written.
1461
+
1462
+ **Only a RESOLVED claim keeps a promise.** A broken `#req-` tag suppresses nothing, so
1463
+ `capability.requirement-unrealized` goes on firing beside the error — a typo that silenced it would
1464
+ turn a mistake into a green fleet. And a run that could not READ the flows (a single-target run, or
1465
+ an `architecture/` that did not parse) suspends `capability.requirement-unrealized` entirely: loam
1466
+ did not look, which is never the same answer as "there is nothing there".
1467
+
1468
+ **Both corpora are reported together, and neither is derived from the other.**
1469
+ `loam list capabilities --json` carries `keptBy` (the flows keeping a promise) beside `realizedBy`
1470
+ (the service requirements realizing it) on every `requirements[]` row, with the same three-state
1471
+ rule one level down — present and non-empty, present and empty, or **absent** when nobody looked —
1472
+ and an additive top-level `useCases` key saying which kind of absence it was.
1473
+ `capability.unrealized` counts flows as well: a capability whose criteria all cross services can be
1474
+ kept entirely by flows and named by no service requirement, and calling that "a word nobody adopted"
1475
+ would contradict loam's own answer in the same report. It stays asymmetric with
1476
+ `capability.requirement-unrealized`, which suspends when the flows are unreadable — a capability has
1477
+ independent evidence in its `Capability:`/`Realizes:` lines, while a cross-service promise often has
1478
+ no realizer but a flow.
1479
+
1480
+ The fleet's own files are the opt-in — either side of the union. A fleet holding neither
1481
+ `architecture/capabilities.yaml` nor `capabilities/` gets no capability findings at all, and once
1482
+ one exists the join is graded both ways:
246
1483
 
247
- - **Workflow schema / DAG engine** — OpenSpec derives artifact completion from whether each schema-declared output exists; it is not a separate mutable workflow-state database. loam still declines arbitrary project graphs because its fleet checks depend on one versioned lifecycle and one meaning of ready. The trade-off is lower workflow customizability, not duplicate state.
248
- - **Stores** — OpenSpec Stores are standalone Git planning roots with explicit root selection, optional project pointers and read-only references to other Stores. They solve shared planning ownership, not caching. loam keeps one docs root because it also owns the fleet topology; an external reference index should be reconsidered if several independent fleet-doc roots become common.
249
- - **Worksets** — upstream Worksets are personal sets of folders/editor state reopened together, not hand-curated evidence bundles consumed by validation. loam has no editor-session manager; `loam delta <FEAT> --service <id>` separately derives a model-scoped work view.
250
- - **Profiles** — upstream profiles select which workflow commands are installed for a user; project artifact behavior comes from config/schema. loam keeps a smaller fixed command surface. The invariant to protect is fleet validation semantics, not a claim that an OpenSpec profile itself changes them per repository.
251
- - **TUI** — agents and CI get `--json`; humans get the files and the forge. A third surface would be a third thing to keep truthful.
252
- - **Authored `tasks.md` as authoritative state** — loam derives its actionable checklist (`loam delta`, `loam verify`), so it cannot drift from the delta. During migration, however, upstream tasks may still contain implementation order and progress, especially for `skip_specs: true` changes; the audit requires an explicit disposition and preserves them as non-authoritative legacy checklists until reviewed.
253
- - **Per-tool adapter matrix** — mostly. `AGENTS.md` travels with the docs as the one tool-agnostic contract, so any runner that can read a file and exec a binary is already supported. `loam init --tools` does emit the slash commands for several tools, but each adapter is only a path plus a format wrapper around the one shared body — never per-tool prose, profiles, delivery modes, or a managed marker block in someone else's file, and like everything init writes, never overwritten once on disk.
1484
+ ```yaml
1485
+ capabilities:
1486
+ payments:
1487
+ description: take money for an order
1488
+ owner: payments-team
1489
+ payments/refunds: {}
1490
+ ```
254
1491
 
255
- The compatibility stance follows the same one-way logic: **loam reads OpenSpec, never writes it.** Serializing drops `## Purpose` and the `## Requirements` wrapper, both of which OpenSpec's parser requires — so loam output is not OpenSpec input, and pretending otherwise would corrupt someone's repo politely. [MIGRATING-from-OpenSpec.md](MIGRATING-from-OpenSpec.md) has the full account, with the evidence pinned in `test/openspec-compat.test.ts`.
1492
+ - `capability.unknown` (**error**) — a `Capability:` entry neither side declares, with close-name
1493
+ suggestions; in a feature delta it gates `loam archive` (`--approve` overrides). Silent when the
1494
+ fleet holds neither file, or when the YAML is invalid.
1495
+ - `capability.invalid` (**error**, fleet scope) — `capabilities.yaml` exists but does not read as a
1496
+ vocabulary; reported exactly once per `validate --all` run (single-target runs stay silent about
1497
+ the file). Every grade that RESOLVES against the vocabulary is suspended behind it, because
1498
+ grading a fleet against a file nobody can read is a cascade rather than a diagnosis — but the
1499
+ authored tree's own grades are not, since none of them consults the YAML.
1500
+ - `capability.unrealized` (**warning**, fleet scope) — a declared capability no living non-`REMOVED`
1501
+ requirement names, one warning per capability, naming the side that declared it: either a promise
1502
+ nobody implemented or a word nobody adopted.
1503
+
1504
+ The total is readable — `loam list capabilities` reports each capability's realizing requirements,
1505
+ services and draft/verified split, and `loam explore --capability <id>` seeds an exploration from
1506
+ the realizing services. `migrate-openspec` preserves capability identity through this same file:
1507
+ every routed requirement carries a `Capability:` line, and the staged target declares every living
1508
+ and active-horizon OpenSpec capability id (empty bodies — descriptions are not invented, the
1509
+ authored `## Purpose` prose stays verbatim under `legacy/`).
1510
+
1511
+ #### A feature can change a capability
1512
+
1513
+ `features/<FEAT>/capabilities/<cap>/spec.md` carries a delta against the living document, in the
1514
+ requirement grammar and delta algebra every service spec already uses —
1515
+ `## ADDED|MODIFIED|REMOVED Requirements`, `Requirement-ID:` identity, `Based-On:` pins — with
1516
+ nesting spelled by the tree exactly as the living corpus spells it
1517
+ (`features/FEAT-9/capabilities/payments/refunds/spec.md`). `loam archive` merges it through the same
1518
+ transactional path as every other document and **creates `capabilities/<cap>/spec.md`, and the
1519
+ `capabilities/` tree itself, when the feature is the first to name that capability**;
1520
+ `loam unarchive` takes it back and removes what the archive created; `loam rebase` pins its
1521
+ `MODIFIED`/`REMOVED` requirements.
1522
+
1523
+ **Both lifecycles coexist and neither is deprecated.** A capability document may still be edited
1524
+ directly under PR review, like `capabilities.yaml` and the use cases. What changes for a document a
1525
+ FEATURE touches is that a `MODIFIED` or `REMOVED` needs a pin: the first such feature in a fleet
1526
+ that has been editing by hand reports `delta.baseline-missing` (a warning that gates) and is fixed
1527
+ by `loam rebase <FEAT>`. What that buys is the protection the corpus had none of — two PRs rewriting
1528
+ one capability requirement were a git conflict at best and a silent overwrite at worst, and are now
1529
+ `delta.baseline-stale`.
1530
+
1531
+ **The document's three rules are graded on the DELTA too**, not only on the living file, and there
1532
+ they gate archive. Without that the feature-local delta becomes a second way to write service
1533
+ requirements: a capability requirement carrying `Operations:` would merge into the living tree and
1534
+ only then earn its error, against a document whoever reads the finding did not write.
1535
+
1536
+ **The archive refuses a promise nothing keeps, in both directions.**
1537
+
1538
+ - `capability.uncovered` (**warning that gates**, `--approve`-overridable) — the feature ADDS a
1539
+ capability requirement and nothing in the same feature keeps it: no `Realizes:` line in its own
1540
+ service deltas, and no `#req-` tag in its own `usecases/` flows. The severity is the judgement:
1541
+ the document is legal, the merge is what is unsafe, because once merged the only thing that will
1542
+ mention the promise again is a fleet-scope warning nobody reads. **Both carriers count, and only
1543
+ a resolved claim does** — the `#cap-` tag to exactly one declared capability and the `#req-` tag
1544
+ to exactly one of its requirements, resolved against the vocabulary and the requirement ids this
1545
+ feature's own merge would leave behind. A broken tag of either kind is an error in its own right
1546
+ and never also marks a promise kept; a typo that silenced this gate would turn a mistake into a
1547
+ green archive. A **living** flow does not count either — a use case already in `architecture/`
1548
+ cannot be keeping a promise this feature has not merged yet, and the flow that keeps it ships in
1549
+ the same change.
1550
+ - `usecase.flow-invalid` (**error**, never `--approve`-overridable) — the feature's `usecases/`
1551
+ could not be read against that post-merge map: a parse error, a hop naming an element the merge
1552
+ does not land, or a landscape merge that itself refuses. The flows were not graded, so the archive
1553
+ would copy them into `architecture/` for somebody else's `validate --all` to fail on. `--approve`
1554
+ overrides loam's judgement about coherence, never its ability to read an axis.
1555
+ - `usecase.flow-exists` (**error**, never `--approve`-overridable) — a flow this feature introduces
1556
+ that `architecture/usecases/` already holds. The merge is a whole-file copy, so it would replace
1557
+ an authored hop sequence wholesale; the same shape and the same severity as `glossary.term-exists`,
1558
+ and the fix is the same — edit the living document directly in this change, where git produces an
1559
+ ordinary conflict.
1560
+ - `capability.remove-requirement-realized` (**error**) — the same join in the removal direction.
1561
+ Unlike the addition it has no legal reading: after the merge a living service document nobody
1562
+ touched would carry a pointer at nothing. Graded against the POST-MERGE fleet, so the escape
1563
+ hatches fall out rather than being special-cased — retiring the realizing requirement in the same
1564
+ feature, or restating it without the line, is no breach; a `MODIFIED` that keeps the line still
1565
+ is.
1566
+
1567
+ Two `--json` notes. `loam rebase` gains the additive `capabilities` and `capabilityPins` keys
1568
+ (`pins[]`/`services[]` untouched), and `loam status` gains `capabilities` rows in `artifacts[]`. And
1569
+ `Issue.subject` on a `delta.*` code no longer always names a service — a capability delta files
1570
+ under the capability id, so a consumer matching `subject` against the service list finds no match
1571
+ rather than the wrong one.
1572
+
1573
+ `loam new <FEAT> --capability <cap>` scaffolds the delta, and `loam show` and `loam delta` project
1574
+ it. The context pack still says nothing about a capability delta — it reports the fleet's
1575
+ capabilities and the flows claiming them, not what a feature is changing about one. A capability
1576
+ document also remains editable directly under PR review, which is the same lifecycle
1577
+ `capabilities.yaml`, `permissions.yaml` and the living use cases already have; the feature route
1578
+ does not replace it. Two rules keep the corpus from becoming a second copy of the living specs and
1579
+ neither may be softened: a capability requirement must be observable outside the fleet (above), and **neither
1580
+ corpus is derived from the other** — `gherkin` and `verify` keep computing from service
1581
+ requirements, so a service repository can still validate itself with nothing but its own files.
1582
+
1583
+ For a theme that crosses services and matches no structural unit, a **LikeC4 tag** also remains
1584
+ available and checkable: tags are declared in a `specification` block, so a misspelling is a parse
1585
+ error rather than quiet drift.
1586
+
1587
+ ### Deliberate limits of the workflow surface
1588
+
1589
+ loam keeps one fixed lifecycle because its cross-artifact checks need one versioned meaning of
1590
+ ready. Several useful product shapes remain outside that boundary on purpose:
1591
+
1592
+ - **Configurable workflow DAGs** — arbitrary project graphs offer more workflow freedom, but would
1593
+ make readiness project-specific. loam derives state from one fixed artifact graph instead.
1594
+ - **External planning roots** — loam keeps one docs root because that root also owns the system
1595
+ topology. Reconsider an external reference index if several independently governed docs roots
1596
+ become a common deployment shape.
1597
+ - **Worksets and editor sessions** — loam has no workspace manager. `loam delta <FEAT> --service
1598
+ <id>` derives a model-scoped work view without owning folders, windows or editor state.
1599
+ - **Configurable lifecycle profiles** — project behavior does not change with the agent or user.
1600
+ `init --agent-profile` narrows only which shared workflow entry points are installed; it does not
1601
+ fork the artifact graph, gates or command semantics. The incident-reporting support entry stays
1602
+ installed in every profile because it is not part of that graph.
1603
+ - **TUI** — agents and CI get `--json`; humans get the files and the forge. A third surface would be
1604
+ a third thing to keep truthful.
1605
+ - **Authored `tasks.md` as authoritative state** — loam derives its actionable checklist (`loam
1606
+ delta`, `loam verify`), so it cannot drift from the delta. Imported task lists are preserved as
1607
+ non-authoritative migration evidence until a human chooses their disposition.
1608
+ - **Per-tool workflow forks** — `AGENTS.md` is the tool-agnostic contract. Generated adapters wrap
1609
+ shared bodies and are never independent process definitions.
1610
+
1611
+ Separately, the compatibility stance is one-way: **loam reads OpenSpec, never writes it.**
1612
+ Serializing drops `## Purpose` and the `## Requirements` wrapper, both of which OpenSpec's parser
1613
+ requires — so loam output is not OpenSpec input, and pretending otherwise would corrupt someone's
1614
+ repo politely. [MIGRATING-from-OpenSpec.md](MIGRATING-from-OpenSpec.md) has the full account, with
1615
+ the evidence pinned in the
1616
+ [compatibility test](https://github.com/ybotok/loam/blob/main/test/openspec-compat.test.ts).
256
1617
 
257
1618
  ### How archive writes, and how unarchive undoes it
258
1619
 
259
- The merge is computed in full before anything is written, and then committed file by file: each new version is staged as a temp file **in the target's own directory** and renamed into place, so a reader sees either the old bytes or the new ones and never a half-written document. If any file fails — including the final move into `features/archive/` — the files already swapped are put back from the bytes read before the swap, and the command says so.
260
-
261
- **The whole path moves bytes, not strings.** Content is encoded once when a write is staged, the compare-and-set before each rename compares bytes, and the snapshot pre-image is written unmodified; text is produced only where a parser needs it. A file loam would have to parse as text and cannot decode as UTF-8 refuses the merge (`merge-failed`, naming the file) rather than being written back with every undecodable byte replaced by U+FFFD — which is what used to happen, to the living document *and* to the snapshot meant to undo it, leaving nothing to restore from. PowerShell's `>` defaulting to UTF-16 is a realistic way to reach this.
262
-
263
- **A process killed between two renames is recoverable.** Before the first swap, `archive`/`unarchive` write `.loam-commit` at the docs-repo root — an intent record naming the command, the feature, the host and pid, the final move, and a before/after digest per path — and fsync it (and its directory where the platform allows). It is deliberately not a source of truth and never disagrees with the files: every judgement it supports is made by re-reading the living bytes and comparing digests, and every byte a repair writes comes from the snapshot and is verified against the digest captured before the crash. The next `archive`/`unarchive` recovers from it first, under the lock: `completed` when the final move landed, `consistent` when no file was left in the other state, `repaired` when files were put back, and `commit-interrupted` (a refusal, exit 1) when a half-written file now holds bytes that are neither state or the repairing pre-image is gone or altered. An archive is **undone**; an unarchive is **finished**, because the merged text it was replacing is written down nowhere else. The record is removed after the final rename and on every rollback path, so outside the commit window it does not exist. `.loam-commit` and `.loam-lock` are both transient writer state and both belong in a docs-repo `.gitignore`; `loam doctor` grades them (`doctor.commit-interrupted`, `doctor.commit-unreadable`, `doctor.docs-locked`, `doctor.staging-temps`) instead of reporting a half-merged repo as healthy.
264
-
265
- **One writer at a time, and only over the bytes it read.** `archive` and `unarchive` hold an advisory lock on the docs repo (`.loam-lock`, created `O_EXCL`, released in a `finally`) across the whole plan-and-commit; a second one refuses with `docs-busy` having read and written nothing, and re-running once the first finishes works. A lock naming a process that is no longer alive on this host is broken once and retried, so a crash cannot wedge the repo. Inside that lock the swap is still guarded per file: each target is re-read immediately before its rename and compared with the pre-image the plan was computed from, and a mismatch fails the commit (`merge-failed`) rather than overwriting an edit that landed in between. Rollback is guarded the same way — a file whose current bytes are not the ones the merge wrote is not restored over, and is named in the failures instead, which correctly promotes the answer to `rollback-incomplete`. Cleanup after a failed create prunes only the directories it created and only while they are empty (`rmdir` up to the first `ENOTEMPTY`), never a recursive delete: the directory that would have been removed is a directory somebody else may have just put a file into.
266
-
267
- Undoing that merge is not a matter of inverting it. **A `MODIFIED` requirement's previous text is recorded nowhere** — the delta says what the requirement became, never what it was — and the landscape merge drops the feature tags, so the lines it added stop being identifiable the moment they land. Anything reconstructed would be a plausible guess at the old docs, which is the kind of quiet fiction the rest of loam exists to prevent.
268
-
269
- So archive writes the bytes down. Before it swaps anything it copies every file it is about to overwrite into `<feature>/.loam-before/`, with a `manifest.json` naming each one, whether it existed at all (a file the merge *created* is restored by deleting it), and **two** hashes: `after`, of what the merge wrote, and `before`, of the pre-image sitting beside the manifest (null when archive created the file). That directory travels with the feature into `features/archive/`, and `loam unarchive` puts it back.
270
-
271
- The manifest is **version 2**, and `before` is what the bump is for. A version-1 snapshot described only what archive *wrote*; nothing described the bytes it would *restore*, so a pre-image altered inside the archived feature directory — a bad merge, a rebase, a stray editor — was written back verbatim and every loam surface certified the result as "the living docs are back to what they said before the archive". `unarchive` now hashes every pre-image before staging anything and refuses on a mismatch (`snapshot-corrupt`), and a version-1 snapshot is refused as `snapshot-missing` with the answer being version control. Correspondingly, `archive` refuses to replace a `.loam-before/` whose pre-images no longer match the living bytes: the previous archive's writes are still in the docs and those files are the only way back.
272
-
273
- Some checks run at plan time rather than in the gate, because only the computed merge can see them. A **living** requirement sitting outside `## Requirements` refuses the whole archive (`living.requirement-outside-requirements`, error): the merge rewrites only that section, so the requirement would land in the file twice — and `--approve` does not override it, because the duplication is mechanical, not a judgment call; re-home the requirement first. A delta that redefines a path+method the living OpenAPI already has (`openapi.op-modified`, warning) merges anyway, but the plan says so — the overwrite is wholesale, and the living definition it replaces deserved an eye. An unparseable `delta.likec4` fails the archive at plan time regardless of `--approve`: the alternative was archiving with the architecture axis silently dropped, which is the data loss this tool exists to prevent.
274
-
275
- The landscape merge **splices authored source, it does not re-serialize**. Each new element and relationship is copied into the living landscape byte-for-byte from `delta.likec4` — technology, style, icons, links, metadata, nested children, every tag — with exactly one edit: the feature's own tag is stripped (the additions are baseline now — the drop above is why unarchive restores bytes), and a construct the strip empties goes with it (`x = kind 'y' { #FEAT-1 }` lands as `x = kind 'y'`). Placement is deterministic and **service-grouped**, and never moves an authored byte — it only chooses where the new blocks go. A top-level element whose service the model already draws (the `metadata { service }` binding first, the title as fallback) lands immediately after the last element resolving to that service — it joins its service's neighborhood; an element opening a new service lands in the trailing region before the model's closing brace, sorted among its fellow no-neighborhood insertions. A relationship lands after the last top-level relationship touching its source's service (either endpoint counts), ordered by a stable key past a shared anchor, with the same sorted trailing region as the fallback. The unit of that sort is the **neighborhood, not the statement**: a contiguous same-service run of elements — or the cluster of relationships grown around the statements touching one service — moves through the trailing walks as a block, keyed by its leading statement (an anchored join sits where its anchor is, wherever its own id or key would sort, so comparing per statement would make placement depend on which archive ran first). That is what makes the scheme **order-independent**: archiving feature A then feature B (touching different services) yields the byte-identical landscape to B then A, so concurrent archive PRs usually splice different lines and git merges them without help. The insertions carry no marker comment — which archive added what is the git history's to say. A **nested** element lands inside its parent's block — inside the living parent when the landscape already has it, riding verbatim inside the spliced parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level. LikeC4 reference resolution is not order-dependent, so a relationship placed before an element it names still parses — and because splicing is text surgery, the computed landscape is **parsed in memory before anything is written**: a result LikeC4 rejects — a splice bug, or legal inputs the living document cannot absorb, like an element kind or tag its `specification` block never declares — refuses the archive at plan time (`merge-failed`, nothing written), the same discipline as the unparseable delta. A living declaration sharing its line with another statement (legal LikeC4 the splice map cannot see around — its statement scan runs to the end of the line) refuses the same way: give each top-level declaration its own line, then re-run.
276
-
277
- The OpenAPI merge carries components along with operations: every `#/components/<kind>/<name>` the merged path items reference — recursively, a component's own `$ref`s pulling in more — is copied from the feature document into the living one, so an operation never lands pointing at a schema that stayed behind. A needed component the living document already holds **identically** is left alone; one it holds **differently** is overwritten wholesale under the same discipline as an operation (`openapi.component-modified`, warning — the merge proceeds, the plan says so). A `$ref` reachable from the merged content that resolves in **neither** document blocks the archive (`openapi.ref-unresolved`, error): merging it would write a dangling reference. `--approve` overrides it — unlike the mechanical refusals, this one is a judgment call. External references — URLs, file paths, anything not starting `#/` — are out of scope: left untouched, never gated.
278
-
279
- `loam archive --json` speaks the same machine contract as everything else: on success (or `--dry-run`) the plan (`{path, action, to?}` per file), the non-blocking warnings, and whatever `--approve` overrode; on refusal a stable `error.code` — `not-coherent` and `living-outside-requirements` carry the issues, `archive-exists` names a taken destination, `docs-busy` means another archive or unarchive holds the lock and nothing was touched, `merge-failed` means nothing was written (or everything was rolled back), `rollback-incomplete` lists the files that need a hand.
1620
+ The merge is computed in full before anything is written, and then committed file by file: each new
1621
+ version is staged as a temp file **in the target's own directory** and renamed into place, so a
1622
+ reader sees either the old bytes or the new ones and never a half-written document. If any file
1623
+ fails — including the final move into `features/archive/` — the files already swapped are put back
1624
+ from the bytes read before the swap, and the command says so.
1625
+
1626
+ **The whole path moves bytes, not strings.** Content is encoded once when a write is staged, the
1627
+ compare-and-set before each rename compares bytes, and the snapshot pre-image is written unmodified;
1628
+ text is produced only where a parser needs it. A file loam would have to parse as text and cannot
1629
+ decode as UTF-8 refuses the merge (`merge-failed`, naming the file) rather than being written back
1630
+ with every undecodable byte replaced by U+FFFD — which is what used to happen, to the living
1631
+ document *and* to the snapshot meant to undo it, leaving nothing to restore from. PowerShell's `>`
1632
+ defaulting to UTF-16 is a realistic way to reach this.
1633
+
1634
+ **A process killed between two renames is recoverable.** Before the first swap,
1635
+ `archive`/`unarchive` write `.loam-commit` at the docs-repo root — an intent record naming the
1636
+ command, the feature, the host and pid, the final move, and a before/after digest per path — and
1637
+ fsync it (and its directory where the platform allows). It is deliberately not a source of truth and
1638
+ never disagrees with the files: every judgement it supports is made by re-reading the living bytes
1639
+ and comparing digests, and every byte a repair writes comes from the snapshot and is verified
1640
+ against the digest captured before the crash. The next `archive`/`unarchive` recovers from it first,
1641
+ under the lock: `completed` when the final move landed, `consistent` when no file was left in the
1642
+ other state, `repaired` when files were put back, and `commit-interrupted` (a refusal, exit 1) when
1643
+ a half-written file now holds bytes that are neither state or the repairing pre-image is gone or
1644
+ altered. An archive is **undone**; an unarchive is **finished**, because the merged text it was
1645
+ replacing is written down nowhere else. The record is removed after the final rename and on every
1646
+ rollback path, so outside the commit window it does not exist. `.loam-commit` and `.loam-lock` are
1647
+ both transient writer state and both belong in a docs-repo `.gitignore`; `loam doctor` grades them
1648
+ (`doctor.commit-interrupted`, `doctor.commit-unreadable`, `doctor.docs-locked`,
1649
+ `doctor.staging-temps`) instead of reporting a half-merged repo as healthy.
1650
+
1651
+ **One writer at a time, and only over the bytes it read.** `archive` and `unarchive` hold an
1652
+ advisory lock on the docs repo (`.loam-lock`, created `O_EXCL`, released in a `finally`) across the
1653
+ whole plan-and-commit; a second one refuses with `docs-busy` having read and written nothing, and
1654
+ re-running once the first finishes works. A lock naming a process that is no longer alive on this
1655
+ host is broken once and retried, so a crash cannot wedge the repo. Inside that lock the swap is
1656
+ still guarded per file: each target is re-read immediately before its rename and compared with the
1657
+ pre-image the plan was computed from, and a mismatch fails the commit (`merge-failed`) rather than
1658
+ overwriting an edit that landed in between. Rollback is guarded the same way — a file whose current
1659
+ bytes are not the ones the merge wrote is not restored over, and is named in the failures instead,
1660
+ which correctly promotes the answer to `rollback-incomplete`. Cleanup after a failed create prunes
1661
+ only the directories it created and only while they are empty (`rmdir` up to the first `ENOTEMPTY`),
1662
+ never a recursive delete: the directory that would have been removed is a directory somebody else
1663
+ may have just put a file into.
1664
+
1665
+ Undoing that merge is not a matter of inverting it. **A `MODIFIED` requirement's previous text is
1666
+ recorded nowhere** — the delta says what the requirement became, never what it was — and the
1667
+ landscape merge drops the feature tags, so the lines it added stop being identifiable the moment
1668
+ they land. Anything reconstructed would be a plausible guess at the old docs, which is the kind of
1669
+ quiet fiction the rest of loam exists to prevent.
1670
+
1671
+ So archive writes the bytes down. Before it swaps anything it copies every file it is about to
1672
+ overwrite into `<feature>/.loam-before/`, with a `manifest.json` naming each one, whether it existed
1673
+ at all (a file the merge *created* is restored by deleting it), and **two** hashes: `after`, of what
1674
+ the merge wrote, and `before`, of the pre-image sitting beside the manifest (null when archive
1675
+ created the file). That directory travels with the feature into `features/archive/`, and
1676
+ `loam unarchive` puts it back.
1677
+
1678
+ The manifest is **version 3**. Version 2 added `before`: a version-1 snapshot described only what
1679
+ archive *wrote*; nothing described the bytes it would *restore*, so a pre-image altered inside the
1680
+ archived feature directory — a bad merge, a rebase, a stray editor — was written back verbatim and
1681
+ every loam surface certified the result as "the living docs are back to what they said before the
1682
+ archive". `unarchive` now hashes every pre-image before staging anything and refuses on a mismatch
1683
+ (`snapshot-corrupt`), and a version-1 snapshot is refused as `snapshot-missing` with the answer
1684
+ being version control. Version 3 re-keys every entry under `services/` by **`(service, artifact)`**
1685
+ — the service id (the leaf directory name, the one thing a subsystem re-cut never changes) and the
1686
+ artifact's path inside the service directory — beside the as-archived literal `path`, which keeps
1687
+ naming the pre-image under `.loam-before/files/`. At restore time the write target resolves through
1688
+ the **current services enumeration**: the bytes land wherever the service lives *today*, not
1689
+ wherever it lived on the day of the archive, and an id the enumeration no longer answers falls back
1690
+ to the literal path (an absent service restores as a create, exactly as before). Entries under
1691
+ `architecture/` and `features/` stay literal-path-only — they do not move. Version-2 manifests are
1692
+ still read, forever, under their own literal-path rules: they already sit inside archived features,
1693
+ which carry them across loam upgrades. Correspondingly, `archive` refuses to replace a
1694
+ `.loam-before/` whose pre-images no longer match the living bytes — version 2 or 3 alike, with a
1695
+ version-3 claim graded at its service's current directory: the previous archive's writes are still
1696
+ in the docs and those files are the only way back.
1697
+
1698
+ Some checks run at plan time rather than in the gate, because only the computed merge can see them.
1699
+ A **living** requirement sitting outside `## Requirements` refuses the whole archive
1700
+ (`living.requirement-outside-requirements`, error): the merge rewrites only that section, so the
1701
+ requirement would land in the file twice — and `--approve` does not override it, because the
1702
+ duplication is mechanical, not a judgment call; re-home the requirement first. A delta that
1703
+ redefines a path+method the living OpenAPI already has (`openapi.op-modified`, warning) merges
1704
+ anyway, but the plan says so — the overwrite is wholesale, and the living definition it replaces
1705
+ deserved an eye. An unparseable `delta.likec4` fails the archive at plan time regardless of
1706
+ `--approve`: the alternative was archiving with the architecture axis silently dropped, which is the
1707
+ data loss this tool exists to prevent.
1708
+
1709
+ The landscape merge **splices authored source, it does not re-serialize**. Each new element and
1710
+ relationship is copied into the living landscape byte-for-byte from `delta.likec4` — technology,
1711
+ style, icons, links, metadata, nested children, every tag — with exactly one edit: the feature's own
1712
+ tag is stripped (the additions are baseline now — the drop above is why unarchive restores bytes),
1713
+ and a construct the strip empties goes with it (`x = kind 'y' { #FEAT-1 }` lands as `x = kind 'y'`).
1714
+ Placement is deterministic and **service-grouped**, and never moves an authored byte — it only
1715
+ chooses where the new blocks go. A top-level element whose service the model already draws (the
1716
+ `metadata { service }` binding first, the title as fallback) lands immediately after the last
1717
+ element resolving to that service — it joins its service's neighborhood; an element opening a new
1718
+ service lands in the trailing region before the model's closing brace, sorted among its fellow
1719
+ no-neighborhood insertions. A relationship lands after the last top-level relationship touching its
1720
+ source's service (either endpoint counts), ordered by a stable key past a shared anchor, with the
1721
+ same sorted trailing region as the fallback. The unit of that sort is the **neighborhood, not the
1722
+ statement**: a contiguous same-service run of elements — or the cluster of relationships grown
1723
+ around the statements touching one service — moves through the trailing walks as a block, keyed by
1724
+ its leading statement (an anchored join sits where its anchor is, wherever its own id or key would
1725
+ sort, so comparing per statement would make placement depend on which archive ran first). That is
1726
+ what makes the scheme **order-independent**: archiving feature A then feature B (touching different
1727
+ services) yields the byte-identical landscape to B then A, so concurrent archive PRs usually splice
1728
+ different lines and git merges them without help. The insertions carry no marker comment — which
1729
+ archive added what is the git history's to say. A **nested** element lands inside its parent's block
1730
+ — inside the living parent when the landscape already has it, riding verbatim inside the spliced
1731
+ parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level. LikeC4
1732
+ reference resolution is not order-dependent, so a relationship placed before an element it names
1733
+ still parses — and because splicing is text surgery, the computed landscape is **parsed in memory
1734
+ before anything is written**: a result LikeC4 rejects — a splice bug, or legal inputs the living
1735
+ document cannot absorb, like an element kind or tag its `specification` block never declares —
1736
+ refuses the archive at plan time (`merge-failed`, nothing written), the same discipline as the
1737
+ unparseable delta. A living declaration sharing its line with another statement (legal LikeC4 the
1738
+ splice map cannot see around — its statement scan runs to the end of the line) refuses the same way:
1739
+ give each top-level declaration its own line, then re-run.
1740
+
1741
+ The OpenAPI merge carries components along with operations, and since the path-item/component
1742
+ baselines it decides each one **by verdict rather than by reachability**: a component the feature
1743
+ declares with a pin equal to its own content is a quote and is skipped (the living document keeps
1744
+ whatever landed since); one whose pin matches the living value is an edit and is copied; a stale one
1745
+ reaches the merge only under `--approve` and is copied with the plan saying so; an unpinned one
1746
+ identical to living is left alone, and an unpinned one that differs is copied
1747
+ (`openapi.component-modified`, warning) — including a differing component nothing in the feature
1748
+ references, which the old reachability closure silently dropped. A genuinely **new** component is
1749
+ copied only when reachable from content actually written — a fixpoint over the copies' own `$ref`s,
1750
+ which is what keeps cycles terminating and unrelated namespaces uncopied. A `$ref` reachable from
1751
+ the written content and copied components that resolves in **neither** document blocks the archive
1752
+ (`openapi.ref-unresolved`, error): merging it would write a dangling reference. `--approve`
1753
+ overrides it — unlike the mechanical refusals, this one is a judgment call. External references —
1754
+ URLs, file paths, anything not starting `#/` — are out of scope: left untouched, never gated.
1755
+
1756
+ `loam archive --json` speaks the same machine contract as everything else: on success (or
1757
+ `--dry-run`) the plan (`{path, action, to?}` per file), the non-blocking warnings, and whatever
1758
+ `--approve` overrode; on refusal a stable `error.code` — `not-coherent` and
1759
+ `living-outside-requirements` carry the issues, `archive-exists` names a taken destination,
1760
+ `docs-busy` means another archive or unarchive holds the lock and nothing was touched,
1761
+ `merge-failed` means nothing was written (or everything was rolled back), `rollback-incomplete`
1762
+ lists the files that need a hand.
280
1763
 
281
1764
  `unarchive` refuses rather than guesses, each refusal under its own `--json` `error.code`:
282
1765
 
283
- - `feature-active` — a feature of that id is active again; restoring over it would bury work in flight;
284
- - `snapshot-missing` — archived before snapshots existed, or by an older layout version (a version-1 manifest, which records nothing about its restore source); the living docs have to come back from version control instead;
285
- - `snapshot-stale` — a merged file changed after the archive, so this would be a revert of someone else's work rather than an undo. `--force` says that was meant. The test is "the file holds neither what archive wrote nor what this restore would write", so a file already holding its own pre-image — what an interrupted restore leaves behind — is not mistaken for a later change;
286
- - `snapshot-corrupt` — a pre-image no longer hashes to the `before` digest archive recorded for it, so restoring it would write text nobody authored. **`--force` does not override this**, and the difference is the point: `--force` discards *later* changes to the living docs, while the damage here is to the undo itself;
287
- - `commit-interrupted` — a previous `archive`/`unarchive` was killed mid-commit and this run cannot repair it on its own.
288
-
289
- Rules (`loam validate`): every requirement has ≥1 scenario; every C4 edge `op` resolves to an OpenAPI operation governed by a requirement; and **the diff applies** — a `MODIFIED`/`REMOVED` requirement exists in the living spec, an `ADDED` one does not, and a section heading matches `## ADDED|MODIFIED|REMOVED Requirements` exactly. A near-miss heading (`## ADDED Requirement`, singular) parses as prose, so without this check archive merges nothing and reports nothing. `## RENAMED Requirements` — OpenSpec's fourth delta operation — gets the same error (`delta.unknown-section`): loam's rename is a stable `Requirement-ID` carried through a `MODIFIED` requirement (legacy documents can still use `REMOVED` + `ADDED`). And an `ADDED` legacy name that differs from a living requirement's only in case is a warning (`delta.added-near-duplicate`): without a stable ID, both spellings would coexist in the living spec.
290
-
291
- **The section heading is what gives a requirement its kind.** A requirement under any other H2 (`## Behavior`, `## Error Handling` — a historically accepted legacy OpenSpec "complete future state" shape) has no kind, and archive merges nothing for it; `delta.requirement-not-merged` names each one and the heading that stranded it — a warning for adopted history that **gates archive** (authored content must not vanish silently; `--approve` overrides). OpenSpec v1.7's default spec-driven schema requires a real delta unless `.openspec.yaml` explicitly uses `skip_specs: true`; migration applies that zero-spec opt-out only from a valid marker whose named built-in or direct project schema resolves, never merely because a custom artifact graph omits specs. Legacy archive presence is not proof that the shape is currently valid. When *no* requirement in the file sits under a delta section — prose headings only, requirements above every heading, or a file that only quotes the living state — the grade goes up to an **error**, `delta.no-delta-sections`: a delta that would merge nothing as a whole is not a valid delta. `## Requirements` stays exempt from the per-requirement warning: quoting the living state inside a delta is legal and merges nothing by design. The heading must be reachable, too — a leading UTF-8 BOM used to hide `## MODIFIED Requirements` on line 1 and void the entire delta, so the parser strips one.
1766
+ - `feature-active` — a feature of that id is active again; restoring over it would bury work in
1767
+ flight;
1768
+ - `snapshot-missing` — archived before snapshots existed, or by an older layout version (a version-1
1769
+ manifest, which records nothing about its restore source); the living docs have to come back from
1770
+ version control instead;
1771
+ - `snapshot-stale` — a merged file changed after the archive, so this would be a revert of someone
1772
+ else's work rather than an undo. `--force` says that was meant. The test is "the file holds
1773
+ neither what archive wrote nor what this restore would write", so a file already holding its own
1774
+ pre-image — what an interrupted restore leaves behind — is not mistaken for a later change;
1775
+ - `snapshot-corrupt` — a pre-image no longer hashes to the `before` digest archive recorded for it,
1776
+ so restoring it would write text nobody authored. **`--force` does not override this**, and the
1777
+ difference is the point: `--force` discards *later* changes to the living docs, while the damage
1778
+ here is to the undo itself;
1779
+ - `commit-interrupted` — a previous `archive`/`unarchive` was killed mid-commit and this run cannot
1780
+ repair it on its own.
1781
+
1782
+ Rules (`loam validate`): every requirement has ≥1 scenario; every C4 edge `op` resolves to an
1783
+ OpenAPI operation governed by a requirement; and **the diff applies** — a `MODIFIED`/`REMOVED`
1784
+ requirement exists in the living spec, an `ADDED` one does not, and a section heading matches
1785
+ `## ADDED|MODIFIED|REMOVED Requirements` exactly. A near-miss heading (`## ADDED Requirement`,
1786
+ singular) parses as prose, so without this check archive merges nothing and reports nothing.
1787
+ `## RENAMED Requirements` — OpenSpec's fourth delta operation — gets the same error
1788
+ (`delta.unknown-section`): loam's rename is a stable `Requirement-ID` carried through a `MODIFIED`
1789
+ requirement (legacy documents can still use `REMOVED` + `ADDED`). And an `ADDED` legacy name that
1790
+ differs from a living requirement's only in case is a warning (`delta.added-near-duplicate`):
1791
+ without a stable ID, both spellings would coexist in the living spec.
1792
+
1793
+ **The section heading is what gives a requirement its kind.** A requirement under any other H2 (`##
1794
+ Behavior`, `## Error Handling` — a historically accepted legacy OpenSpec "complete future state"
1795
+ shape) has no kind, and archive merges nothing for it; `delta.requirement-not-merged` names each one
1796
+ and the heading that stranded it — a warning for adopted history that **gates archive** (authored
1797
+ content must not vanish silently; `--approve` overrides). OpenSpec v1.7's default spec-driven schema
1798
+ requires a real delta unless `.openspec.yaml` explicitly uses `skip_specs: true`; migration applies
1799
+ that zero-spec opt-out only from a valid marker whose named built-in or direct project schema
1800
+ resolves, never merely because a custom artifact graph omits specs. Legacy archive presence is not
1801
+ proof that the shape is currently valid. When *no* requirement in the file sits under a delta
1802
+ section — prose headings only, requirements above every heading, or a file that only quotes the
1803
+ living state — the grade goes up to an **error**, `delta.no-delta-sections`: a delta that would
1804
+ merge nothing as a whole is not a valid delta. `## Requirements` stays exempt from the
1805
+ per-requirement warning: quoting the living state inside a delta is legal and merges nothing by
1806
+ design. The heading must be reachable, too — a leading UTF-8 BOM used to hide
1807
+ `## MODIFIED Requirements` on line 1 and void the entire delta, so the parser strips one.
292
1808
 
293
1809
  ## The verification record
294
1810
 
295
- `features/<FEAT>/verification.yaml` is the done-check written down. `loam verify <FEAT>` derives a **checklist** from the feature's own artifacts — the same files `validate` reads, never the code:
1811
+ `features/<FEAT>/verification.yaml` is the done-check written down. `loam verify <FEAT>` derives a
1812
+ **checklist** from the feature's own artifacts — the same files `validate` reads, never the code:
296
1813
 
297
1814
  | kind | one claim per | reads |
298
1815
  |---|---|---|
299
1816
  | `service.exists` | tagged top-level element the delta introduces | `delta.likec4` |
300
1817
  | `api.exposes` | operationId the feature's openapi delta adds that the living one lacks | `specs/<svc>/openapi.yaml` |
1818
+ | `event.declares` | (direction, message) the feature's asyncapi delta declares that the living contract's send/receive sets lack — quotes and removals are never questions | `specs/<svc>/asyncapi.yaml` |
301
1819
  | `c4.calls` | tagged edge carrying `metadata { op }` | `delta.likec4` |
302
1820
  | `scenario.tested` | scenario of every ADDED/MODIFIED requirement | `specs/<svc>/spec.md` + `specs/<svc>/arch.spec.md` |
303
1821
 
304
- Each claim's **id** is `<kind>-<8 hex>`, hashed from the feature id and what the claim says (with an occurrence counter for genuine duplicates). For `scenario.tested` "what the claim says" includes the requirement's stable `Requirement-ID` when present (otherwise its legacy heading), plus a hash of the scenario's **body**, not just its title. So a heading-only rename under one stable ID preserves its evidence, while the same feature otherwise yields the same ids on every run — two runs are diffable — reordering the artifacts renames nothing, and **rewording a scenario renames its claim, even a Given/When/Then rewrite under an unchanged title**, which is the point: an answer about text nobody wrote must not carry over as if it still applied.
1822
+ Each claim's **id** is `<kind>-<8 hex>`, hashed from the feature id and what the claim says (with an
1823
+ occurrence counter for genuine duplicates). For `scenario.tested` "what the claim says" includes the
1824
+ requirement's stable `Requirement-ID` when present (otherwise its legacy heading), plus a hash of
1825
+ the scenario's **body**, not just its title. So a heading-only rename under one stable ID preserves
1826
+ its evidence, while the same feature otherwise yields the same ids on every run — two runs are
1827
+ diffable — reordering the artifacts renames nothing, and **rewording a scenario renames its claim,
1828
+ even a Given/When/Then rewrite under an unchanged title**, which is the point: an answer about text
1829
+ nobody wrote must not carry over as if it still applied.
1830
+
1831
+ `loam verify <FEAT> --service <id> --record <answers.json>` takes the answers back and refuses
1832
+ anything that does not answer the *current* checklist, each under its own `--json` `error.code`:
1833
+ `answers-unreadable` (not JSON, or a verdict outside `confirmed` / `unconfirmed`),
1834
+ `answers-mismatch` (an id nobody asked about, a claim with no answer, or one answered twice),
1835
+ `answers-unevidenced` (a `confirmed` with no `file:line` behind it). An unchecked claim must never
1836
+ be able to masquerade as checked.
1837
+
1838
+ **`--results <report.json>` answers the `scenario.tested` claims mechanically, from a cucumber JSON
1839
+ test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave and SpecFlow
1840
+ speak too. The contract is exactly this and nothing more: a top-level array of features, each
1841
+ `elements[]` (scenarios) with `name`, `tags[] {name}` and `steps[] {result {status}}` — plus the two
1842
+ places those dialects put a failure the steps never see: cucumber-jvm's per-element
1843
+ `before[]`/`after[]` hook results and behave's element-level `status`. Every other field is ignored,
1844
+ tagless elements (backgrounds, hand-written scenarios) are invisible, and a file that is not that
1845
+ array at all refuses under `answers-unreadable` rather than quietly answering every claim "not
1846
+ found".
1847
+
1848
+ Matching is by digest and nothing else: a claim is answered by the report scenarios carrying its
1849
+ `@loam-digest-<16hex>` tag — the tag `loam gherkin` stamped, the same 16 hex of the same body hash
1850
+ the claim id folds in; the axis rides in that hash (the arch recipe salts the body with its file
1851
+ name), so a business run can never answer an arch claim however identically the two scenarios are
1852
+ worded. Names never match anything: a reworded spec scenario matches nothing until the suite is
1853
+ regenerated and re-run — an agent must not be able to SAY a scenario is tested; only a green run
1854
+ may. The verdicts: every matching occurrence (a digest the report holds twice is a re-run, and all
1855
+ occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`, and
1856
+ the element-level status (when the dialect carries one) is `passed` → `confirmed`, with evidence
1857
+ `<report-path>: <feature uri or name> › <scenario name>` per occurrence; a failed / undefined /
1858
+ pending / ambiguous step (`failed at step N`), a failing hook (`failed in a before/after hook`), a
1859
+ failed element status, a skipped-only run (`skipped`), or no match at all (`not found in report`) →
1860
+ `unconfirmed` with the reason as the note.
1861
+
1862
+ `--results` OWNS every `scenario.tested` claim **in the same run**: an answers-file entry for one is
1863
+ refused (`answers-mismatch` — the runner owns it), `--record` alongside must answer exactly the
1864
+ non-scenario claims, and `--results` alone is legal only when the checklist is all scenarios
1865
+ (otherwise the refusal lists the unanswered ids). On an archived feature `--results` refuses exactly
1866
+ as `--record` does — frozen history.
1867
+
1868
+ The record also writes down **which report it read**: a `report:` block with the file's path, the
1869
+ sha256 of its bytes, its mtime and the count of digest-tagged scenarios — on the record for the
1870
+ all-at-once form, and filed with that repository's attestation in the federated form, so it is
1871
+ pruned with its answers. In federated mode the report must resolve *inside* the attesting
1872
+ repository, the same rule evidence paths obey, and a report that IS committed there and differs from
1873
+ the attested commit is refused by name. Be precise about what that proves: it identifies which file
1874
+ was consumed and, when the file is committed, that it matches the attested commit. It does not prove
1875
+ the file came from executing that commit. No digest can.
1876
+
1877
+ #### Any runner can answer, through loam's own shape
1878
+
1879
+ Cucumber's JSON is not the contract; the **digest** is. `--results` therefore also accepts loam's
1880
+ own runner-neutral shape, chosen by its marker key:
305
1881
 
306
- `loam verify <FEAT> --service <id> --record <answers.json>` takes the answers back and refuses anything that does not answer the *current* checklist, each under its own `--json` `error.code`: `answers-unreadable` (not JSON, or a verdict outside `confirmed` / `unconfirmed`), `answers-mismatch` (an id nobody asked about, a claim with no answer, or one answered twice), `answers-unevidenced` (a `confirmed` with no `file:line` behind it). An unchecked claim must never be able to masquerade as checked.
307
-
308
- **`--results <report.json>` answers the `scenario.tested` claims mechanically, from a cucumber JSON test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave and SpecFlow speak too. The contract is exactly this and nothing more: a top-level array of features, each `elements[]` (scenarios) with `name`, `tags[] {name}` and `steps[] {result {status}}` — plus the two places those dialects put a failure the steps never see: cucumber-jvm's per-element `before[]`/`after[]` hook results and behave's element-level `status`. Every other field is ignored, tagless elements (backgrounds, hand-written scenarios) are invisible, and a file that is not that array at all refuses under `answers-unreadable` rather than quietly answering every claim "not found".
309
-
310
- Matching is by digest and nothing else: a claim is answered by the report scenarios carrying its `@loam-digest-<16hex>` tag — the tag `loam gherkin` stamped, the same 16 hex of the same body hash the claim id folds in; the axis rides in that hash (the arch recipe salts the body with its file name), so a business run can never answer an arch claim however identically the two scenarios are worded. Names never match anything: a reworded spec scenario matches nothing until the suite is regenerated and re-run — an agent must not be able to SAY a scenario is tested; only a green run may. The verdicts: every matching occurrence (a digest the report holds twice is a re-run, and all occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`, and the element-level status (when the dialect carries one) is `passed` → `confirmed`, with evidence `<report-path>: <feature uri or name> › <scenario name>` per occurrence; a failed / undefined / pending / ambiguous step (`failed at step N`), a failing hook (`failed in a before/after hook`), a failed element status, a skipped-only run (`skipped`), or no match at all (`not found in report`) → `unconfirmed` with the reason as the note.
1882
+ ```json
1883
+ {
1884
+ "loamScenarioReport": 1,
1885
+ "results": [
1886
+ { "digest": "9f2c1a4b6d0e5713", "status": "passed", "test": "RefundPaymentTest#capturedOrder" }
1887
+ ]
1888
+ }
1889
+ ```
311
1890
 
312
- `--results` OWNS every `scenario.tested` claim **in the same run**: an answers-file entry for one is refused (`answers-mismatch` — the runner owns it), `--record` alongside must answer exactly the non-scenario claims, and `--results` alone is legal only when the checklist is all scenarios (otherwise the refusal lists the unanswered ids). On an archived feature `--results` refuses exactly as `--record` does — frozen history.
1891
+ `digest` is the 16 lowercase hex of the `@loam-digest-…` tag `loam gherkin` stamped on the scenario.
1892
+ `status` is `passed` or `failed` — nothing else. `test` is optional free text: the runner's own name
1893
+ for the run, used as the evidence string a reader follows. `loamScenarioReport: 1` is the format
1894
+ version this loam reads; any other value refuses rather than being partly understood.
1895
+
1896
+ This exists so a fleet on JUnit, pytest, Playwright, Vitest or a house runner can reach `verified` at
1897
+ all. Before it, a team whose evidence was a real green run was held at `attested` **by a file
1898
+ format** — which is the one thing the attested/verified distinction was never meant to mean. An
1899
+ answer from this shape is `answered_by: runner`, identical to a cucumber one, because it is the same
1900
+ claim answered to the same standard by the same identity: a content-derived digest, and a status
1901
+ saying a run reported it green. **Adapting a runner's output into this shape is a build step you
1902
+ own**; loam parses no vendor format it has not documented.
1903
+
1904
+ It adds no forgeability. loam cannot prove any JSON came from executing a commit — that is why the
1905
+ record stores the file's sha256 and mtime and claims nothing more — and a hand-written cucumber
1906
+ array was always exactly as easy to write as this is.
1907
+
1908
+ Parsing is strict where the cucumber reader is tolerant, for the reason `--contract-results` is: a
1909
+ cucumber report is another tool's file full of entries loam has no business judging, while this shape
1910
+ exists for one purpose, so a malformed entry is a malformed report rather than scenery to skip. A
1911
+ `status` of `skipped` or `pending` is **refused, not guessed** — a scenario that did not run has no
1912
+ place in an answer sheet, and omitting it leaves its claim unanswered rather than confirmed. A file
1913
+ carrying the marker is always graded as this shape, even when malformed: falling through to the
1914
+ cucumber parser would tell an author their report is not a cucumber array, which is true, useless,
1915
+ and hides the real mistake.
1916
+
1917
+ **Two services wording a scenario identically share one digest**, and a single report then cannot
1918
+ say whose suite ran it. Those claims are left `unconfirmed` with `verify.digest-contested` (warn)
1919
+ naming both services and the affected claim ids, rather than confirming both from one run. Record
1920
+ each service's claims from its own repository with `--service`, which narrows the checklist before
1921
+ matching. Scenario digests are salted by *axis* (business versus arch) but **not** by service, so a
1922
+ digest identifies scenario words; `--service` is what gives a report an owner.
1923
+
1924
+ ### Contract-test reports answer the `api.exposes` claims
1925
+
1926
+ **`--contract-results <contract.json>` answers the `api.exposes` claims mechanically, from an API
1927
+ contract-test report.** The accepted format is loam's own documented shape — a JSON object with the
1928
+ marker key and one entry per exercised operation:
313
1929
 
314
- The record also writes down **which report it read**: a `report:` block with the file's path, the sha256 of its bytes, its mtime and the count of digest-tagged scenarios — on the record for the all-at-once form, and filed with that repository's attestation in the federated form, so it is pruned with its answers. In federated mode the report must resolve *inside* the attesting repository, the same rule evidence paths obey, and a report that IS committed there and differs from the attested commit is refused by name. Be precise about what that proves: it identifies which file was consumed and, when the file is committed, that it matches the attested commit. It does not prove the file came from executing that commit. No digest can.
1930
+ ```json
1931
+ {
1932
+ "loamContractReport": 1,
1933
+ "results": [
1934
+ { "operationId": "createSplit", "status": "passed", "test": "POST /splits responds 201 per contract" },
1935
+ { "operationId": "getSplit", "status": "failed" }
1936
+ ]
1937
+ }
1938
+ ```
315
1939
 
316
- **Two services wording a scenario identically share one digest**, and a single report then cannot say whose suite ran it. Those claims are left `unconfirmed` with `verify.digest-contested` (warn) naming both services and the affected claim ids, rather than confirming both from one run. Record each service's claims from its own repository with `--service`, which narrows the checklist before matching. Scenario digests are salted by *axis* (business versus arch) but **not** by service, so a digest identifies scenario words; `--service` is what gives a report an owner.
1940
+ `loamContractReport: 1` is the format version this loam reads (any other value refuses — a later
1941
+ version may redefine what an entry means, and reading it with this parser could confirm something
1942
+ the report never said). Every entry must carry a non-empty string `operationId` and `status`; `test`
1943
+ is optional free text that rides into the evidence line; extra keys pass untouched. Unlike the
1944
+ cucumber reader, which tolerates everything except the shape it matches on because a cucumber report
1945
+ is another tool's file, this shape exists for exactly one purpose, so a malformed entry refuses the
1946
+ whole file (`answers-unreadable`, naming the entry) — an unparseable report never silently answers
1947
+ zero claims as if none matched.
1948
+
1949
+ **Matching is by operationId and nothing else**, against the operationId each `api.exposes` claim
1950
+ asserts — and that join is only half an identity, which the rules below are honest about. An
1951
+ operationId is unique within *one* contract document, not across a fleet, and a report entry names
1952
+ no service; what makes the match safe is scope, not string precision: `--service` narrows the
1953
+ checklist to one subject before the matching starts, exactly as it does for scenario digests. **An
1954
+ operationId that more than one service on the checklist exposes is contested**: no report can say
1955
+ whose suite exercised it, so those claims are left `unconfirmed` with `verify.operation-contested`
1956
+ (warn) naming the services and the affected claim ids — `verify.digest-contested`'s rule on this
1957
+ axis — and recording each service's claims from its own repository with `--service` is the
1958
+ resolution. Within one service the match is exact, and renaming an operation renames its claim, so
1959
+ rewording cannot carry an answer over. The verdicts: every entry naming the claim's operationId (an
1960
+ operationId held twice is two test cases, and all of them count) has status exactly `"passed"` →
1961
+ `confirmed`, with evidence `<report-path>: <test text, or "operationId '…' passed">` per entry; a
1962
+ `"failed"` or unknown status → `unconfirmed` with the status in the note; no entry at all →
1963
+ `unconfirmed` ("not exercised"). Entries naming operations outside the checklist are skipped
1964
+ silently — a real contract suite exercises the whole living API, and the claims only cover what the
1965
+ feature adds.
1966
+
1967
+ The vendor formats were surveyed and none is parsed directly, by decision recorded here so it is not
1968
+ re-litigated by accident: **Specmatic's JSON coverage report is declined by name** because its
1969
+ `coverageStatus` value `"covered"` means *exercised*, not *passed* — a red contract-test run still
1970
+ writes `"covered"`, so an adapter over it would confirm exactly what a failing suite refuted;
1971
+ Specmatic's newer CTRF test report carries honest pass/fail but no structured operation identity
1972
+ loam could join on. Pact is keyed by interaction descriptions and provider states, Dredd emits
1973
+ TAP/JUnit first, and no property-based framework's native output carries an operation join key at
1974
+ all. Emit the generic shape from any of them instead — from a report that knows the operationId and
1975
+ whether its test passed, it is a one-line transform (for example, over a Specmatic CTRF report: map
1976
+ each test to `{"operationId": …, "status": "passed"|"failed"}` and wrap in
1977
+ `{"loamContractReport": 1, "results": […]}`).
1978
+
1979
+ **`--contract-results` OWNS every `api.exposes` claim in the same run**, exactly as `--results` owns
1980
+ the scenario claims: an answers-file entry for one is refused (`answers-mismatch` — the report
1981
+ answers those), and a mechanical flag alone is refused while claims outside its kind are
1982
+ outstanding. The ownership is deliberately total: a partial contract suite leaves its unexercised
1983
+ operations `unconfirmed` rather than agent-attestable in the same run, because silence must not read
1984
+ as checked — the escape is a `--record` run without the flag. On an archived feature the flag
1985
+ refuses exactly as `--record` does.
1986
+
1987
+ The record pins **which contract report it read**: a `contractReport:` block with the file's path,
1988
+ the sha256 of its bytes, its mtime, the count of distinct operationIds it names and the format it
1989
+ was read as — on the record for the all-at-once form, and filed with that repository's attestation
1990
+ in the federated form, pruned with its answers, beside `report:` and under its rules: in federated
1991
+ mode the file must resolve *inside* the attesting repository, and one that is committed there and
1992
+ differs from the attested commit is refused by name. The same honesty applies: the digest identifies
1993
+ which file was consumed; nothing proves the file came from executing the attested commit, and no
1994
+ digest can.
1995
+
1996
+ **The verdict derivation does not move.** A contract-confirmed `api.exposes` claim counts toward
1997
+ `verified` exactly as an agent-confirmed one always has — with strictly more provenance behind it:
1998
+ `answered_by: external-runner` on the claim, `[contract]` on the printed line, and the pinned
1999
+ `contractReport:` block. The attested boundary is exclusively about `scenario.tested` claims, which
2000
+ a contract report can never answer, so `attested` versus `verified` turns on scenario claims alone,
2001
+ before and after this flag.
2002
+
2003
+ ### Cross-examination: `--diff-answers` compares two blind answer sets
2004
+
2005
+ **`--diff-answers a.json b.json` mechanically cross-examines two independently written answer sets
2006
+ against the same current checklist, and writes nothing.** Both files pass the full `--record`
2007
+ validation — `answers-unreadable` / `answers-mismatch` / `answers-unevidenced`, the refusal naming
2008
+ which file ("first answer set (a.json): …") — so each must answer **every** claim in the compared
2009
+ scope, `scenario.tested` included: the diff consults no test report, which makes the
2010
+ cross-examination file a different artifact from the answers file a `--results` recording takes
2011
+ (that one must omit the runner's claims). `--service <id>` narrows the compared claims to one
2012
+ service's as a pure lens — no repository binding, usable from the docs repo, refusing
2013
+ `unknown-service` when the checklist has no claims owned by that id — and both agents must have
2014
+ answered the same scope. Combining the flag with `--record`, `--results` or `--contract-results`
2015
+ refuses `invalid-option`, as does any arity other than exactly two files and an archived feature
2016
+ (frozen history has no current checklist to answer). A `verification.yaml` too damaged to read does
2017
+ **not** block the diff — the lens never reads the record, and the moment a cross-examination is most
2018
+ needed is before re-recording over a broken one.
2019
+
2020
+ The join is by claim id and nothing else — the same deterministic ids that make two runs diffable
2021
+ make two blind answer sets joinable. Per claim the payload reports `cross.agree-confirmed` (with
2022
+ `evidenceDisjoint: true` when the two cited file sets share no path; a trailing `:line` or
2023
+ `:start-end` suffix is normalized away first, so two citations into one file never read as
2024
+ disjoint), `cross.agree-unconfirmed`, or `cross.disagree`. `disagreements[]` repeats the disagreeing
2025
+ rows first-class, and the `cross.evidence-disjoint` notice (warn) lists the agreed-confirmed claims
2026
+ whose citations do not overlap — the second thing to read, after the disagreements. The exit code is
2027
+ 0 with `ok: true` whenever both files validate — disagreement is data, not failure — and the `files`
2028
+ block pins each file's path and the sha256 of the exact bytes read.
2029
+
2030
+ **Under `--diff-answers`, `claims[]` and `summary` are different shapes from ordinary
2031
+ `verify --json` under the same keys**: a row carries the comparison code and both sides' answers
2032
+ (`a` / `b`) instead of a single verdict, and `summary` counts agreements and disagreements, not
2033
+ confirmed/unconfirmed/unanswered. The payload also deliberately carries **no `verified`, `verdict`
2034
+ or `attested` key at all**: two answer sets agreeing — usually two contexts of the same model, so
2035
+ correlated — ranks where a reviewer reads first; it is not verification, it upgrades nothing, and it
2036
+ leaves `verification.yaml` byte-identical (no docs lock is taken, and the diff runs while a writer
2037
+ holds one).
317
2038
 
318
2039
  ### Attested is not verified
319
2040
 
320
- A service with no runnable suite yet must still be able to record its answers, so `--record` may confirm a `scenario.tested` claim too. That path is kept and made **visible**, which is the whole of the decision:
321
-
322
- - the claim's `answered_by` reads `agent` rather than `runner` (an answer written before `--results` existed points at no run, so an absent `answered_by` counts as `agent`);
323
- - the record's **`verdict`** is one of three values, recomputed from `claims[]` on every read and never taken from the `summary:` block: `verified` (every claim confirmed, every scenario claim from a run, and at least one claim exists), `attested` (every claim confirmed, but at least one scenario claim rests on an agent's word), `unverified` (anything else). `verified` in `--json` is exactly `verdict === "verified"`, so a record with zero claims can no longer read as verified by arithmetic;
324
- - `attested` (a count) and `notices[]` ride in the same payload, `verify` prints `verify.scenario-attested` (**warning**, gating nothing) naming each claim, and the closing line says the record travels into `features/archive/` saying so;
325
- - `loam status` re-reports the same finding in `checks.issues[]` and offers `next.verify-attested`; a feature in that state reaches `stage: ready`, never `done`;
326
- - the frozen post-archive view recounts from `claims[]` as well. The record's own `summary:` is still echoed verbatim in `--json` — that is the frozen view's promise — but nothing is *derived* from it, and a `summary` that contradicts its own `claims[]` makes the whole record `record-unreadable` (`verify.record-miscounted`): the file contradicts itself, so neither half can be believed and neither is reported as fact.
327
-
328
- So "a green run is the only thing that confirms a scenario" is a claim about the **record**, not about the CLI's flags: the short path still works, and it is legible afterwards as the short path.
2041
+ A service with no runnable suite yet must still be able to record its answers, so `--record` may
2042
+ confirm a `scenario.tested` claim too. That path is kept and made **visible**, which is the whole of
2043
+ the decision:
2044
+
2045
+ - the claim's `answered_by` reads `agent` rather than `runner` (an answer written before `--results`
2046
+ existed points at no run, so an absent `answered_by` counts as `agent`);
2047
+ - the record's **`verdict`** is one of three values, recomputed from `claims[]` on every read and
2048
+ never taken from the `summary:` block: `verified` (every claim confirmed, every scenario claim
2049
+ from a run, and at least one claim exists), `attested` (every claim confirmed, but at least one
2050
+ scenario claim rests on an agent's word), `unverified` (anything else). `verified` in `--json` is
2051
+ exactly `verdict === "verified"`, so a record with zero claims can no longer read as verified by
2052
+ arithmetic;
2053
+ - `attested` (a count) and `notices[]` ride in the same payload, `verify` prints
2054
+ `verify.scenario-attested` (**warning**, gating nothing) naming each claim, and the closing line
2055
+ says the record travels into `features/archive/` saying so. A record that leaves claims
2056
+ unconfirmed or unanswered carries `verify.claims-open` (**warning**, gating nothing) in the same
2057
+ `notices[]`, on the read view and the frozen post-archive view alike — the one-line "not a clean
2058
+ result" summary with all four counts; a feature with no record never carries it, because
2059
+ not-started is not partial;
2060
+ - `loam status` re-reports the same finding in `checks.issues[]` and offers `next.verify-attested`;
2061
+ a feature in that state reaches `stage: ready`, never `done`;
2062
+ - the frozen post-archive view recounts from `claims[]` as well. The record's own `summary:` is
2063
+ still echoed verbatim in `--json` — that is the frozen view's promise — but nothing is *derived*
2064
+ from it, and a `summary` that contradicts its own `claims[]` makes the whole record
2065
+ `record-unreadable` (`verify.record-miscounted`): the file contradicts itself, so neither half can
2066
+ be believed and neither is reported as fact.
2067
+
2068
+ So "a green run is the only thing that confirms a scenario" is a claim about the **record**, not
2069
+ about the CLI's flags: the short path still works, and it is legible afterwards as the short path.
329
2070
 
330
2071
  ### The record is federated: one file, one attestation per service
331
2072
 
332
- A cross-service feature's claims belong to several services, and each service's code lives in its own repository — so no single place can honestly answer all of them. `loam verify <FEAT> --service <id> --record answers.json`, run **in that service's own repo**, narrows the checklist to that service's claims and binds the answers to this repository's commit. The result is one `schema: 2` `verification.yaml` holding a list of **`attestations`**, one per service: the service id, the commit the answers were bound to, and that service's answered claims.
333
-
334
- The merge semantics are additive and non-destructive. Recording for service B keeps service A's attestation exactly as it stood — the new record is the old attestations minus B's previous one, plus B's new one. Answers the new record does *not* carry are reported in `discarded`, tagged `unattested` (a schema-1 answer, or one whose attestation is gone) or `off-checklist` (a claim the current checklist no longer asks). Nothing is dropped silently.
2073
+ A cross-service feature's claims belong to several services, and each service's code lives in its
2074
+ own repository — so no single place can honestly answer all of them.
2075
+ `loam verify <FEAT> --service <id> --record answers.json`, run **in that service's own repo**,
2076
+ narrows the checklist to that service's claims and binds the answers to this repository's commit.
2077
+ The result is one `schema: 2` `verification.yaml` holding a list of **`attestations`**, one per
2078
+ service: the service id, the commit the answers were bound to, and that service's answered claims.
2079
+
2080
+ The merge semantics are additive and non-destructive. Recording for service B keeps service A's
2081
+ attestation exactly as it stood — the new record is the old attestations minus B's previous one,
2082
+ plus B's new one. Answers the new record does *not* carry are reported in `discarded`, tagged
2083
+ `unattested` (a schema-1 answer, or one whose attestation is gone) or `off-checklist` (a claim the
2084
+ current checklist no longer asks). Nothing is dropped silently.
2085
+
2086
+ The write behind that merge holds the same advisory lock archive does, and holds it from before the
2087
+ feature is resolved to after the record lands — two `--record` runs for different services serialize
2088
+ (the later one waits briefly, then merges over what the first left, so **both** attestations
2089
+ survive), and a record can no longer be written into a path an `archive` is renaming out from under
2090
+ it. The commit itself is staged beside the target and swapped in by one rename over the exact bytes
2091
+ the merge was computed from, so a kill mid-record leaves the previous record whole — never truncated
2092
+ YAML — and an edit that landed in between refuses instead of being buried:
2093
+
2094
+ - `docs-busy` — another writer held the lock longer than the record was willing to wait (5 s).
2095
+ Nothing was read or written; re-run once it finishes. The contention runs both ways: `archive`,
2096
+ `unarchive` and `rebase` refuse on first contact, so any of them can now answer `docs-busy`
2097
+ because a record is landing.
2098
+ - `record-raced` — the file changed between the locked read and the swap: an editor, or a writer
2099
+ that skipped the lock. Their bytes are untouched; re-running merges over the record as it now
2100
+ stands.
2101
+ - `merge-failed` / `rollback-incomplete` — the shared commit path's own two answers, with exactly
2102
+ the meanings archive gives them: the swap failed and nothing was recorded (re-running can work),
2103
+ or the cleanup after that failure also failed and the named file needs a human.
335
2104
 
336
2105
  Recording refuses rather than guesses, each under its own `--json` `error.code`:
337
2106
 
338
- - `service-mismatch` — `--service` names a service this repository's `loam.json` does not declare. A repo may only attest for itself.
2107
+ - `service-mismatch` — `--service` names a service this repository's `loam.json` does not declare. A
2108
+ repo may only attest for itself.
339
2109
  - `unknown-service` — the named service is not part of this feature's checklist.
340
- - `repository-unavailable` — `--service` from a repo that declares none, or one whose commit cannot be read. An attestation with nothing to bind to is not an attestation.
341
- - **`record-federated`** — the legacy all-at-once form (`--record` with **no** `--service`) over a record that already carries attestations. That form writes the whole file from one place, so it would erase every other repository's evidence; loam refuses outright and names the attestors, and never silently migrates or merges. Re-run with `--service`.
2110
+ - `repository-unavailable` — `--service` from a repo that declares none, or one whose commit cannot
2111
+ be read. An attestation with nothing to bind to is not an attestation.
2112
+ - **`record-federated`** — the legacy all-at-once form (`--record` with **no** `--service`) over a
2113
+ record that already carries attestations. That form writes the whole file from one place, so it
2114
+ would erase every other repository's evidence; loam refuses outright and names the attestors, and
2115
+ never silently migrates or merges. Re-run with `--service`.
342
2116
  - `record-unreadable` — see below.
343
2117
 
344
- Reading is unrestricted: `loam verify <FEAT> --service <id>` **without** `--record` is a pure lens on the checklist, writes nothing, and works from the docs repo too. Every run reports all subjects in `services`, which is how you find out which repositories still owe an attestation.
345
-
346
- The record is YAML, like the other data artifacts, and holds the claim text next to each verdict so it reads without loam. Because it has to survive without loam, human edits are expected — and read back defensively. A file that exists but does not read as a record (broken YAML, or a shape missing what the readers use — `summary` with its counts, `recorded`, `checklist`, claims each with an `id` and a known `verdict`) is **`record-unreadable`**: reported with the YAML line, and **never overwritten**. It used to count as *no record at all*, which was the wrong direction on the one artifact loam cannot reconstruct — "no record" invites writing a fresh one over evidence somebody's edit merely broke. Only a genuinely absent file is "no verification record". Extra keys a human adds still pass through untouched. Every verdict names who answered it — `answered_by: runner` (a report's green run, mechanical) or `answered_by: agent` (somebody's word about the code) — so a reviewer can tell the two apart at a glance:
2118
+ Reading is unrestricted: `loam verify <FEAT> --service <id>` **without** `--record` is a pure lens
2119
+ on the checklist, writes nothing, and works from the docs repo too. Every run reports all subjects
2120
+ in `services`, which is how you find out which repositories still owe an attestation.
2121
+
2122
+ The record is YAML, like the other data artifacts, and holds the claim text next to each verdict so
2123
+ it reads without loam. Because it has to survive without loam, human edits are expected — and read
2124
+ back defensively. A file that exists but does not read as a record (broken YAML, or a shape missing
2125
+ what the readers use — `summary` with its counts, `recorded`, `checklist`, claims each with an `id`
2126
+ and a known `verdict`) is **`record-unreadable`**: reported with the YAML line, and **never
2127
+ overwritten**. It used to count as *no record at all*, which was the wrong direction on the one
2128
+ artifact loam cannot reconstruct — "no record" invites writing a fresh one over evidence somebody's
2129
+ edit merely broke. Only a genuinely absent file is "no verification record". Extra keys a human adds
2130
+ still pass through untouched. Every verdict names who answered it — `answered_by: runner` (a
2131
+ cucumber report's green run, mechanical), `answered_by: external-runner` (a contract-test report's
2132
+ operationId-matched green entry, mechanical too), or `answered_by: agent` (somebody's word about the
2133
+ code) — so a reviewer can tell them apart at a glance:
347
2134
 
348
2135
  ```yaml
349
2136
  feature: FEAT-101
@@ -365,36 +2152,290 @@ claims:
365
2152
  note: "failed at step 3 (report.json: features/loam/split-a-payment.feature › Split across two payees)"
366
2153
  ```
367
2154
 
368
- It lives inside the feature, so `archive` carries it into `features/archive/<FEAT>/` with everything else. `loam verify` re-run later compares `checklist` against the current digest and reports the record as **stale** if the feature moved under it — while the feature is active. Once archived there is no current checklist to be stale against (the merge itself moved the feature's operations into the living OpenAPI, so a re-derived checklist could only disagree): `loam verify` on an archived feature renders the record verbatim as **frozen history** (`frozen: true` under `--json`, no staleness judgment), and `--record` refuses (`invalid-option` — there is no current checklist for the answers to answer): the record is about the words that shipped, and it stays that way.
369
-
370
- **Verification does not gate `archive`.** Coherence gates because loam computed it from the documents; a verdict is an agent's word about code loam never read, and a gate in front of shipping teaches everyone that the cheapest way past it is to say yes. The record is for the reviewer who comes later, and an `unconfirmed` claim with a note is worth more there than a `confirmed` nobody can back up. `loam verify` exits 0 either way; branch on `verified` in the `--json` payload, or on `verdict` when you need to tell `attested` from `unverified`.
371
-
372
- ## Operating at fleet scale
373
-
374
- One docs repo, a hundred services, many teams. The repo scales the way any shared codebase does — through the forge, not through loam features:
375
-
376
- - **The docs repo bootstraps itself.** In the empty directory that will hold it, `loam init --docs . --create` scaffolds `services/`, `features/`, `architecture/landscape.likec4`, `AGENTS.md` **and the docs repo's own `loam.json`** (`{"docsDir": "."}`) — which is what makes `loam validate --all`, `loam list` and `loam archive` work when run *inside* the docs repo, rather than only from a service repo pointing at it. Commit all of it. Each service repo then joins with `loam init --docs ../docs-repo --service <id>` (no `--create`: it must find an existing docs repo) and confirms the wiring with `loam doctor`. There is no batch onboarding command and no fleet manifest: the wiring is one committed file per repo, reviewable like anything else, and N repos are N independent one-line commits rather than one command nobody can audit.
377
- - **Ownership is CODEOWNERS.** `services/<svc>/**` belongs to the owning team; `features/**` to whoever drives cross-service change, with the owners of each touched service pulled in through the feature's `specs/<svc>/` paths. loam has no permission model and should not grow one — the forge already has it.
378
- - **`vouch` and `archive` land through reviewed PRs.** Both rewrite the source of truth, and the PR supplies exactly what loam deliberately does not record: who approved, when, and the diff they saw. The voucher's identity is the PR author's git identity — no `vouched_by:` field, because git already refuses to forget.
379
- - **Provenance runs where the code is.** `sources` resolve only inside a service's own repo, so each service repo's CI runs `loam validate --service <id> --json` and branches on the finding codes (`sources.current`, `sources.stale`, `sources.unvouched`). The docs repo's CI runs `loam validate --all`, which counts what it cannot check from there (`sourcesUnverifiableFromHere`). Aggregating the per-repo results into a fleet view happens outside loam, on purpose: the stable codes are the interface, and any CI system can pivot on them.
380
- - **What it costs, measured.** On a generated 120-service fleet with 400 and 800 op-linked landscape edges (compiled binary, 8-core laptop, wall clock including Node startup): `loam list --json` 1.1–1.5 s, and `loam validate --service <id> --json` — the command above, the one every service repo runs — 1.4–1.6 s at either edge count. Both are flat in edge count because loam reads LikeC4's **parsed** model and never computes a view: it renders nothing, and view computation is superlinear in edges. (If you want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start` in the docs repo for the fleet map, `npx likec4 start services/<id>` for one service model — and scope the views, because an `include *` over a fleet-sized landscape takes minutes. `loam init` scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it never draws. It also scaffolds a `likec4.config.json` that scopes the root LikeC4 project to `architecture/`: loam parses every `.likec4` file **alone**, so each one declares its own `specification` block, and a renderer given the whole tree at once merges them and reports every declaration as a duplicate.) `loam validate --all --json` over the same 120 services is **~30 s**, an order of magnitude worse than the rest, because it parses a fresh LikeC4 workspace per service rather than sharing one. That is a known, unclosed cost. It is the docs repo's own CI job, and it is nobody's inner loop.
381
-
382
- Four read-only fleet/transition views make that filesystem contract inspectable without creating or repairing anything:
2155
+ **Federated agent answers carry `evidence_pins`.** When `--service --record` accepts a `confirmed`
2156
+ agent answer, each of its `file:line` citations is stamped with a pin computed from the attested
2157
+ commit's own blob — the same fetch that just validated the evidence, zero extra git reads:
383
2158
 
384
- - **`loam status [<FEAT>] [--json]`** answers "where does this stand and what do I do next", derived every run from the files — there is no state file to go stale. Per feature it grades each artifact `missing`/`blocked`/`draft`/`ready`/`done`, and `next[]` is ordered most-unblocking first, each entry a stable `next.*` code plus the literal command. It is a **projection over the gates, never a second opinion**: `stage` is taken from the union of what `validate --feature` errors on and what `archive` refuses to merge (severity *or* `gates`), so status may be more pessimistic than either and is never greener than both — an invariant with a test behind it. The fleet form is capped at ten substantive steps plus a `next.elided` notice saying how many were left out, and whenever anything is outstanding its **last** entry is `next.fleet-gate` → `loam validate --all --json` (a repository with work in it must never get a list that omits the command CI runs; a fleet with nothing outstanding says so instead, with `next.fleet-clean`). The cap is applied before the gate entry, so the gate is never the thing elided. It deliberately does not parse the landscape: that is a LikeC4 workspace spin, which is exactly the cost the fleet form refuses to pay, and `next.fleet-gate` is the handoff.
385
-
386
- - **`loam doctor [--json]`** reads `loam.json` defensively (including corrupt JSON), resolves its `docsDir` against the config location, probes existence/read/write access, checks `services/`, **reads** the landscape rather than merely stat-ing it, inventories service/active-feature counts, and reports the current `service` binding. Every finding carries a `fix`. `healthy: false` and exit 1 are reserved for blockers: missing/invalid config, missing docsDir, missing `services/`, and the three states in which the fleet map cannot be read at all — `doctor.landscape-merge-conflict` (it still holds `<<<<<<<` markers: it is two halves of two different maps, and LikeC4 may even parse it), `doctor.landscape-invalid` (it does not parse) and `doctor.landscape-unreadable` (it exists but could not be read). An *absent* landscape stays a doctor warning — `loam validate --all` is where that becomes the error `landscape.missing`, because only there is it known whether any service exists to be drawn. Portability, absent/unknown service binding and access limitations are also warnings, not attempted repairs: `doctor.docs-absolute` flags a `docsDir` stored as an absolute path in a committed `loam.json`, which resolves only on the machine that ran `loam init`. The JSON result is a successful v1.0 envelope even when unhealthy because the diagnosis itself ran; branch on `healthy` and `findings[].severity`.
2159
+ ```yaml
2160
+ evidence: [src/split/Api.ts:42]
2161
+ evidence_pins:
2162
+ - path: src/split/Api.ts
2163
+ line: 42
2164
+ file_sha256: 9f2c… # sha256 of the whole file at the attested commit, CRLF-normalized
2165
+ text: "createSplit(request: SplitRequest): Promise<Split>"
2166
+ token: createSplit # the literal the claim asserts
2167
+ ```
387
2168
 
388
- Doctor also reports the **write path**, as state and as findings, so a repo whose last write did not finish is never called healthy: `doctor.commit-interrupted` (blocker — an interrupted `archive`/`unarchive`; the fix re-runs that exact command, which recovers under the lock), `doctor.commit-unreadable` (blocker — `.loam-commit` exists and cannot be parsed, so nothing can grade it), `doctor.docs-locked` (a warning while the lock's holder is alive, a blocker once the holder is a process that no longer exists on this host, because nothing will ever release it) and `doctor.staging-temps` (warning — orphaned `.loam-*.tmp` files a killed writer never linked into place; litter, not damage). The whole picture rides in `--json` as a `writePath` block.
2169
+ The recipe, exactly as implemented: `file_sha256` is sha256 hex over the blob's text with CRLF/CR
2170
+ normalized to LF — git's eol config makes blob bytes and working-tree bytes legally differ on a
2171
+ Windows checkout, and a pin must convict content change, never checkout config (the symmetric cost
2172
+ is accepted: a pure line-ending change is invisible to the lint). `text` is the cited line, trimmed,
2173
+ capped at 200 characters. `token` is stamped from the claim's own structural fields at record time —
2174
+ the operationId for `api.exposes`, the message name for `event.declares`, the edge's op for
2175
+ `c4.calls` — never parsed back out of claim prose, absent for the kinds that assert no literal, and
2176
+ absent when the attested blob does not contain it: that absence is warned once, at record time,
2177
+ where the answerer can still act, and is deliberately not re-litigated by every later `validate` (a
2178
+ call site that never spelled the provider's operationId would otherwise keep a repo yellow forever,
2179
+ with a re-record that re-derives the same state as the only offered repair). Runner-answered claims
2180
+ carry no pins (their evidence names a report entry, not a file), and the legacy all-at-once form
2181
+ stamps none (it binds to no commit). Be precise about what a pin proves: it identifies WHAT was
2182
+ cited at the attested commit; it does not prove the claim true — a faithful pin of the wrong line is
2183
+ still a wrong answer.
2184
+
2185
+ Reading pins back is the report-pin rule again: absent is fine — every record written before pins
2186
+ existed, and every runner answer — but present must be whole, and a pins block that does not read
2187
+ makes the whole record `record-unreadable`, reported and never overwritten. One notice rides beside
2188
+ the stamp at record time: `verify.evidence-token-missing` (warn, verdict-neutral, gating nothing)
2189
+ says a cited blob at the attested commit does not contain the claim's token — fired there because
2190
+ that is when the answerer can still re-read the evidence.
2191
+
2192
+ Later, `loam validate` run in the service's own repository re-checks every pin on the ACTIVE feature
2193
+ records that service has attested, against the working tree, and reports drift: ok
2194
+ `evidence.checked` (every pin resolved clean) and `evidence.unpinned` (a record predating pins — the
2195
+ next `--record` makes it drift-checkable), warn `evidence.unresolved` (the cited file is gone,
2196
+ unsafe, not a regular file, or the cited line is past its end), `evidence.moved` (the file's digest
2197
+ differs while the cited line still reads as pinned), `evidence.line-changed` (the cited line no
2198
+ longer says what was recorded), `evidence.token-missing` (the file contained the token at the
2199
+ attested commit and no longer does) and `evidence.record-unreadable` (a verification.yaml that
2200
+ exists but cannot be read — none of its evidence was checked, and a silent skip would read as clean,
2201
+ on the very file a hand edit to the pins most plausibly breaks). Archived records are deliberately
2202
+ not linted — frozen history, and post-ship code drift is normal evolution. From the docs repo the
2203
+ family is silent: nothing there can resolve the citations, and `sources.unverifiable-from-here`
2204
+ already names that blind spot. The lints demote reviewer confidence only — no pin state moves a
2205
+ verdict in either direction, `attested` never silently becomes `verified`, and the warnings never
2206
+ fail `validate`; `--strict` remains the CI lever it always was.
2207
+
2208
+ It lives inside the feature, so `archive` carries it into `features/archive/<FEAT>/` with everything
2209
+ else. `loam verify` re-run later compares `checklist` against the current digest and reports the
2210
+ record as **stale** if the feature moved under it — while the feature is active. Once archived there
2211
+ is no current checklist to be stale against (the merge itself moved the feature's operations into
2212
+ the living OpenAPI, so a re-derived checklist could only disagree): `loam verify` on an archived
2213
+ feature renders the record verbatim as **frozen history** (`frozen: true` under `--json`, no
2214
+ staleness judgment), and `--record` refuses (`invalid-option` — there is no current checklist for
2215
+ the answers to answer): the record is about the words that shipped, and it stays that way.
2216
+
2217
+ **Verification does not gate `archive`.** Coherence gates because loam computed it from the
2218
+ documents; a verdict is an agent's word about code loam never read, and a gate in front of shipping
2219
+ teaches everyone that the cheapest way past it is to say yes. The record is for the reviewer who
2220
+ comes later, and an `unconfirmed` claim with a note is worth more there than a `confirmed` nobody
2221
+ can back up. `loam verify` exits 0 either way; branch on `verified` in the `--json` payload, or on
2222
+ `verdict` when you need to tell `attested` from `unverified`.
389
2223
 
390
- And it reports the **agent surface**: which tool the repo is wired for, how many command and skill files it holds, how many a newer loam would lay down (`doctor.agent-files-missing`), and how many have fallen behind (`doctor.agent-files-stale`). The staleness check reads exactly one thing — the `<!-- generated by loam vX.Y.Z -->` line every generated body opens with — and nothing else. Comparing bodies would yield "your file differs from the template", which is one step from offering to rewrite it, and loam never rewrites a generated file; a stamp is a different and smaller claim, and it can be false while every edit around it is legitimate. Which deliveries a repo holds is asked of the files rather than of a record: `loam.json` stores tools, not deliveries, and a repo initialized `--no-skills` is not reported as missing six skills.
391
- - **`loam dependencies [<FEAT>] [--json]`** derives a deterministic graph from parsed active artifacts, never from validator message prose. A MODIFIED/REMOVED requirement depends on the active feature that ADDS the same stable `Requirement-ID` (or legacy exact name); an operation reference/removal depends on the active feature whose OpenAPI delta introduces that `operationId`. Two active features adding the same requirement identity or operationId are conflicts. JSON exposes `nodes`, grouped `edges[].reasons`, `conflicts`, dependency-first `order`, and strongly-connected `cycles`; a focused feature includes all of its transitive prerequisites.
392
- - **`loam audit-openspec <root> [--write-mapping <path>] [--json]`** inventories modern config/Store roots, the union of nested living and active-only capabilities, active and archived changes, per-change schema/`skip_specs`, RENAMED FROM/TO pairs and artifact dispositions. Living/active blockers determine `mechanicallyCompatible`; frozen `archiveDiagnostics` never do. A completed audit exits successfully even with findings. An explicit `--write-mapping` writes a non-overwriting versioned skeleton outside the source, bound to the canonical root and a SHA-256 inventory digest **over the living tree and active changes only** — frozen history never gates, so it must not be able to invalidate a completed mapping either. Its `changes.<OpenSpec-id>` entries require a unique loam feature id and title; suggestions remain null decisions. A root holding no living spec and no active change is never `ready` (`openspec.workspace-empty`): a verdict over a corpus nobody read is the one answer this command must not be able to give.
393
- - **`loam migrate-openspec <root> --map <path> [--apply --target <empty-dir>] [--json]`** repeats the audit, verifies that source digest, resolves capability→one-or-many-service mappings (with per-requirement allocation across living and active requirements), explicit change→feature decisions, validated rename identities, and proposal/design/tasks dispositions. RENAMED FROM must resolve to exactly one living requirement; an existing `Requirement-ID` is reused, otherwise the mapping supplies one, and conflicting rename chains are refused. It is dry-run by default and never creates a target. `--apply --target` transactionally materializes **staged migration docs** — draft living service specs, active feature intents and routed delta specs, deterministic ADR/legacy outputs, an exact feature-local copy of every source change artifact, a verbatim copy of the living capability tree under `legacy/openspec/specs/`, normalized mapping, plan and follow-up blockers — into a separate empty directory, together with that directory's own `loam.json`, `AGENTS.md` and an empty `architecture/landscape.likec4`, so the staged target is a standalone docs repo the follow-up instructions can actually be run inside. The target may not sit inside a live loam fleet (`docsDir` of a governing `loam.json`, or any directory holding `architecture/landscape.likec4`); otherwise every staged feature would be enumerated as a phantom feature of the real fleet. A `skip_specs: true` change gets intent/legacy material but no spec delta. The result remains deliberately non-green because C4/OpenAPI/provenance/vouch work is not invented.
2224
+ ## Operating at fleet scale
394
2225
 
395
- **The adoption readout measures presence, not truth.** `loam list --json` grades every service on one monotone ladder — `empty` (a directory and nothing else) → `partial` (some artifacts, but not the required triple) → `documented` (`model.likec4` + `spec.md` + `openapi.yaml` all present) → `sourced` (the living spec declares `sources`) → `vouched` (`status: verified` with a `sources_digest` behind it) — as a `maturity` string per service and a fleet rollup of counts per rung, with the same rollup as a line in the text view. Every rung is derived from artifact presence and provenance state alone; COMPLETENESS of what was written is on the adopt brief's unchecked list, so no rung is called "adopted" — a service with one endpoint documented out of thirty reaches `vouched` exactly as fast as a thorough one, and only a reader can tell them apart.
2226
+ One docs repo, a hundred services, many teams. The repo scales the way any shared codebase does —
2227
+ through the forge, not through loam features:
2228
+
2229
+ - **The docs repo bootstraps itself.** In the empty directory that will hold it,
2230
+ `loam init --docs . --create` scaffolds `services/`, `features/`, `architecture/landscape.likec4`,
2231
+ `AGENTS.md` **and the docs repo's own `loam.json`** (`{"docsDir": "."}`) — which is what makes
2232
+ `loam validate --all`, `loam list` and `loam archive` work when run *inside* the docs repo, rather
2233
+ than only from a service repo pointing at it. Commit all of it. Each service repo then joins with
2234
+ `loam init --docs ../docs-repo --service <id>` (no `--create`: it must find an existing docs repo)
2235
+ and confirms the wiring with `loam doctor`. There is no batch onboarding command and no fleet
2236
+ manifest: the wiring is one committed file per repo, reviewable like anything else, and N repos
2237
+ are N independent one-line commits rather than one command nobody can audit.
2238
+ - **Ownership is CODEOWNERS.** `services/<svc>/**` belongs to the owning team; `features/**` to
2239
+ whoever drives cross-service change, with the owners of each touched service pulled in through the
2240
+ feature's `specs/<svc>/` paths. loam has no permission model and should not grow one — the forge
2241
+ already has it.
2242
+ - **`vouch` and `archive` land through reviewed PRs.** Both rewrite the source of truth, and the PR
2243
+ supplies what loam does not record: who *accepted* the change, and the diff they saw.
2244
+ `vouched_by:` records the voucher's own git identity — the person who ran `loam vouch` in the
2245
+ service repo (see the frontmatter contract above) — which is a different person from the reviewer
2246
+ whenever the vouch lands through a PR, and the PR review is where that second identity lives.
2247
+ - **Provenance runs where the code is.** `sources` resolve only inside a service's own repo, so each
2248
+ service repo's CI runs `loam validate --service <id> --json` and branches on the finding codes
2249
+ (`sources.current`, `sources.stale`, `sources.unvouched`). The docs repo's CI runs
2250
+ `loam validate --all`, which counts what it cannot check from there
2251
+ (`sourcesUnverifiableFromHere`). Aggregating the per-repo results into a fleet view happens
2252
+ outside loam, on purpose: the stable codes are the interface, and any CI system can pivot on them.
2253
+ - **A finding is addressable data.** JSON findings always carry `locations[]`. A producer that can
2254
+ prove a file, line, column or structural pointer emits a `primary` location and may add `related`
2255
+ ones; otherwise the serializer supplies the smallest `scope` path of the service or feature target.
2256
+ Consumers never need to mine a path out of `message` prose.
2257
+ - **What it costs, measured.** On a generated 120-service fleet with 400 and 800 op-linked landscape
2258
+ edges (measured 2026-08: compiled binary, 8-core laptop, wall clock including Node startup):
2259
+ `loam list --json` ~0.45 s, and `loam validate --service <id> --json` — the command above, the one
2260
+ every service repo runs — ~0.6 s at either edge count. (The README's "Fleet scale" section is the
2261
+ canonical home of these numbers and the measurement method; this bullet restates them for the
2262
+ operating model and must move with it.) Both are flat in edge count because loam reads LikeC4's
2263
+ **parsed** model and never computes a view — it reads what a view **declares** (a `dynamic view`'s
2264
+ tags and ordered steps, already materialized by the parse loam pays for) and never what a view
2265
+ **shows**. It renders nothing, and view *computation* is what is superlinear in edges. (If you
2266
+ want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start` in
2267
+ the docs repo for the fleet map, `npx likec4 start services/<id>` for one service model — and
2268
+ scope the views, because an `include *` over a fleet-sized landscape takes minutes. `loam init`
2269
+ scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it
2270
+ never draws **— and never asks for one: a fleet that draws nothing owes no views block.** It also
2271
+ scaffolds a `likec4.config.json` that scopes the root LikeC4 project to `architecture/`: loam
2272
+ parses every `.likec4` file **alone**, so each one declares its own `specification` block, and a
2273
+ renderer given the whole tree at once merges them and reports every declaration as a duplicate.)
2274
+ `loam validate --all --json` over the same 120 services used to be the outlier at ~13–14 s — one
2275
+ fresh LikeC4 workspace per service; since the batched loader it parses the run's C4 documents in
2276
+ one shared workspace and answers in **~0.7 s median** on the committed 120-service benchmark
2277
+ (measured 2026-08-19, method and runs in docs/BENCHMARKS.md, canonically restated in the README's
2278
+ "Fleet scale" section). It is the docs repo's own CI job, and it is nobody's inner loop.
2279
+
2280
+ ### The fleet scorecard
2281
+
2282
+ `loam validate --all --json` carries an additive `scorecard` key: ceiling-vs-actual pairs per axis —
2283
+ operations defined → governed (plus deprecated, and deprecated operations something still joins to),
2284
+ messages defined → linked, drawn C4 systems → covered by an arch requirement, the maturity rollup,
2285
+ verification verdicts with the confirmed claims split by who answered them (`claims.answered` under
2286
+ the verification aggregate: runner · external-runner · agent, off each answer's own `answered_by`),
2287
+ and each active feature's stage. The `--all` text report prints the same table after its summary
2288
+ footer. Every number is derived from that run's own reads; loam has no state, so week-over-week is
2289
+ your pipeline's job — capture the key per gate run into your metrics store and diff there:
2290
+
2291
+ ```bash
2292
+ loam validate --all --json | jq '{at: now | todate} + .scorecard' >> fleet-scorecard.ndjson
2293
+ ```
396
2294
 
397
- **The per-service summary a CI pipeline publishes.** Staleness and validity are computable only one service repo at a time, and loam ships no aggregator — but a fleet view needs a defined thing to aggregate, and without one "no report" is indistinguishable from "clean". So the contract is a shape, not a tool. Each service repo's CI derives this from `loam validate --service <id> --json` and publishes it wherever the fleet view reads:
2295
+ Two numbers are honest about a blind spot: stale source digests count only what the run standing in
2296
+ that repo could verify, and from the docs repo most services' `sources` are checkable only from
2297
+ their own repos — the companion count (sources unverifiable from here) says how many, and each
2298
+ service repo's own CI run is where staleness actually surfaces.
2299
+
2300
+ Four read-only fleet/transition views make that filesystem contract inspectable without creating or
2301
+ repairing anything:
2302
+
2303
+ - **`loam status [<FEAT>] [--json]`** answers "where does this stand and what do I do next", derived
2304
+ every run from the files — there is no state file to go stale. Per feature it grades each artifact
2305
+ `missing`/`blocked`/`draft`/`ready`/`done`, and `next[]` is ordered most-unblocking first, each
2306
+ entry a stable `next.*` code plus the compatible `command` hint and a typed `execution` plan:
2307
+ `kind`, `cwd`, `runnable`, a concrete `command` only when no placeholder remains, otherwise
2308
+ `needs` and (for authored files) the grading command in `after`. It is a **projection over the gates, never
2309
+ a second opinion**: `stage` is taken from the union of what `validate --feature` errors on and
2310
+ what `archive` refuses to merge (severity *or* `gates`), so status may be more pessimistic than
2311
+ either and is never greener than both — an invariant with a test behind it. The fleet form is
2312
+ capped at ten substantive steps plus a `next.elided` notice saying how many were left out, and
2313
+ whenever anything is outstanding its **last** entry is `next.fleet-gate` →
2314
+ `loam validate --all --json` (a repository with work in it must never get a list that omits the
2315
+ command CI runs; a fleet whose services exist and owe nothing says so instead, with
2316
+ `next.fleet-clean` — while a docs repo with no services and no features gets the first-hour ladder
2317
+ in place of that vacuously green sentence: `next.author-landscape` when the fleet map is missing
2318
+ or still the scaffold's untouched bytes, `next.bind-service`, `next.adopt-first`).
2319
+ `next.author-landscape` carries the command its own arm names — `loam init --create` when the map
2320
+ is absent, `loam seed --from fleet.yaml` when it is the untouched stub — and never the gate: over a
2321
+ repository with no services `validate --all` answers `ok: true`, so an agent running the documented
2322
+ loop against it would change nothing and read the identical `next[0]` on the next run. The cap is
2323
+ applied before the gate entry, so the gate is never the thing elided. It deliberately does not
2324
+ parse the landscape: that is a LikeC4 workspace spin, which is exactly the cost the fleet form
2325
+ refuses to pay, and `next.fleet-gate` is the handoff.
2326
+
2327
+ - **`loam show <FEAT> --json`** includes a semantic `review` pack in addition to its original feature
2328
+ summary: intent excerpt, exact tagged C4 objects and relationships, per-service requirement/API/
2329
+ event slices, dependency blockers, artifact and verification states, use-case blast radius,
2330
+ gating blockers, advisories and the same executable next actions. It is assembled from the shared
2331
+ projections and status functions, so review does not invent a second readiness verdict.
2332
+
2333
+ - **`loam doctor [--json]`** reads `loam.json` defensively (including corrupt JSON), resolves its
2334
+ `docsDir` against the config location, probes existence/read/write access, checks `services/`,
2335
+ **reads** the landscape rather than merely stat-ing it, inventories service/active-feature counts,
2336
+ and reports the current `service` binding. Every finding carries a `fix`. `healthy: false` and
2337
+ exit 1 are reserved for blockers: missing/invalid config, missing docsDir, missing `services/`,
2338
+ and the three states in which the fleet map cannot be read at all —
2339
+ `doctor.landscape-merge-conflict` (it still holds `<<<<<<<` markers: it is two halves of two
2340
+ different maps, and LikeC4 may even parse it), `doctor.landscape-invalid` (it does not parse) and
2341
+ `doctor.landscape-unreadable` (it exists but could not be read). An *absent* landscape stays a
2342
+ doctor warning — `loam validate --all` is where that becomes the error `landscape.missing`,
2343
+ because only there is it known whether any service exists to be drawn. Portability, absent/unknown
2344
+ service binding and access limitations are also warnings, not attempted repairs:
2345
+ `doctor.docs-absolute` flags a `docsDir` stored as an absolute path in a committed `loam.json`,
2346
+ which resolves only on the machine that ran `loam init`. The JSON result is a successful v1.0
2347
+ envelope even when unhealthy because the diagnosis itself ran; branch on `healthy` and
2348
+ `findings[].severity`.
2349
+
2350
+ Doctor also reports the **write path**, as state and as findings, so a repo whose last write did
2351
+ not finish is never called healthy: `doctor.commit-interrupted` (blocker — an interrupted
2352
+ `archive`/`unarchive`; the fix re-runs that exact command, which recovers under the lock),
2353
+ `doctor.commit-unreadable` (blocker — `.loam-commit` exists and cannot be parsed, so nothing can
2354
+ grade it), `doctor.docs-locked` (a warning while the lock's holder is alive, a blocker once the
2355
+ holder is a process that no longer exists on this host, because nothing will ever release it) and
2356
+ `doctor.staging-temps` (warning — orphaned `.loam-*.tmp` files a killed writer never linked into
2357
+ place; litter, not damage). The whole picture rides in `--json` as a `writePath` block.
2358
+
2359
+ And it reports the **agent surface**: which tool and profile the repo is wired for, how many command and skill
2360
+ files it holds, how many a newer loam would lay down (`doctor.agent-files-missing`), and how many
2361
+ have fallen behind (`doctor.agent-files-stale`). The staleness check reads exactly one thing — the
2362
+ `<!-- generated by loam vX.Y.Z -->` line every generated body opens with — and nothing else.
2363
+ The digest manifest is what authorizes `init` to refresh an unchanged pointer; a different body is
2364
+ treated as human-owned and never overwritten. A stamp is a separate claim and can be stale while
2365
+ every edit around it is legitimate. Which deliveries a repo holds is asked of the files rather
2366
+ than of a record: `loam.json` stores tools and profile, not deliveries, and a repo initialized
2367
+ `--no-skills` is not reported as missing the selected skills.
2368
+
2369
+ Commands and skills are two entry points into one protocol. The explicit command is the
2370
+ recommended deterministic chat path (`/loam-feature` → `/loam-implement` → `/loam-check` →
2371
+ `/loam-verify` → `/loam-ship`, with host-specific punctuation); a natural-language request may
2372
+ load the matching skill. Both generated files point to the same binary-owned
2373
+ `loam instructions` page, so choosing convenience cannot select different lifecycle rules. Once
2374
+ loaded, the agent drives `status.next[].execution` itself and returns to the user only for a
2375
+ decision or human-only action.
2376
+
2377
+ `loam-report` is a separate support protocol, generated as a command and skill in every profile
2378
+ but never appended to the lifecycle. It writes
2379
+ `loam-reports/YYYY-MM-DD-<short-slug>.md` in the current repository with expected/actual behavior,
2380
+ version, stable envelope and finding codes, relevant locations, write state, a minimal safe
2381
+ reproduction, classification and missing evidence. Before writing, the agent redacts secrets and
2382
+ home paths and reduces command output to the excerpt that proves the symptom. It never retries a
2383
+ writer merely to reproduce it, repairs files, uploads telemetry or submits the report. The output
2384
+ is an ordinary Markdown artifact a person may review, commit or share deliberately.
2385
+ - **`loam dependencies [<FEAT>] [--json]`** derives a deterministic graph from parsed active
2386
+ artifacts, never from validator message prose. A MODIFIED/REMOVED requirement depends on the
2387
+ active feature that ADDS the same stable `Requirement-ID` (or legacy exact name); an operation
2388
+ reference/removal depends on the active feature whose OpenAPI delta introduces that `operationId`.
2389
+ Two active features adding the same requirement identity or operationId are conflicts. JSON
2390
+ exposes `nodes`, grouped `edges[].reasons`, `conflicts`, dependency-first `order`, and
2391
+ strongly-connected `cycles`; a focused feature includes all of its transitive prerequisites.
2392
+ - **`loam audit-openspec <root> [--write-mapping <path>] [--json]`** inventories modern config/Store
2393
+ roots, the union of nested living and active-only capabilities, active and archived changes,
2394
+ per-change schema/`skip_specs`, RENAMED FROM/TO pairs and artifact dispositions. Living/active
2395
+ blockers determine `mechanicallyCompatible`; frozen `archiveDiagnostics` never do. A completed
2396
+ audit exits successfully even with findings. An explicit `--write-mapping` writes a
2397
+ non-overwriting versioned skeleton outside the source, bound to the canonical root and a SHA-256
2398
+ inventory digest **over the living tree and active changes only** — frozen history never gates, so
2399
+ it must not be able to invalidate a completed mapping either. Its `changes.<OpenSpec-id>` entries
2400
+ require a unique loam feature id and title; suggestions remain null decisions. A root holding no
2401
+ living spec and no active change is never `ready` (`openspec.workspace-empty`): a verdict over a
2402
+ corpus nobody read is the one answer this command must not be able to give.
2403
+ - **`loam migrate-openspec <root> --map <path> [--apply --target <empty-dir>] [--json]`** repeats
2404
+ the audit, verifies that source digest, resolves capability→one-or-many-service mappings (with
2405
+ per-requirement allocation across living and active requirements), explicit change→feature
2406
+ decisions, validated rename identities, and proposal/design/tasks dispositions. RENAMED FROM must
2407
+ resolve to exactly one living requirement; an existing `Requirement-ID` is reused, otherwise the
2408
+ mapping supplies one, and conflicting rename chains are refused. It is dry-run by default and
2409
+ never creates a target. `--apply --target` transactionally materializes **staged migration docs**
2410
+ — draft living service specs, active feature intents and routed delta specs, deterministic
2411
+ ADR/legacy outputs, an exact feature-local copy of every source change artifact, a verbatim copy
2412
+ of the living capability tree under `legacy/openspec/specs/`, normalized mapping, plan and
2413
+ follow-up blockers — into a separate empty directory, together with that directory's own
2414
+ `loam.json`, `AGENTS.md` and an empty `architecture/landscape.likec4`, so the staged target is a
2415
+ standalone docs repo the follow-up instructions can actually be run inside. The target may not sit
2416
+ inside a live loam fleet (`docsDir` of a governing `loam.json`, or any directory holding
2417
+ `architecture/landscape.likec4`); otherwise every staged feature would be enumerated as a phantom
2418
+ feature of the governing fleet. A `skip_specs: true` change gets intent/legacy material but no
2419
+ spec delta. The result remains deliberately non-green because C4/OpenAPI/provenance/vouch work is
2420
+ not invented.
2421
+
2422
+ **The adoption readout measures presence, not truth.** `loam list --json` grades every service on
2423
+ one monotone ladder — `empty` (a directory and nothing else) → `partial` (some artifacts, but not
2424
+ the required triple) → `documented` (`model.likec4` + `spec.md` + `openapi.yaml` all present) →
2425
+ `sourced` (the living spec declares `sources`) → `vouched` (`status: verified` with a
2426
+ `sources_digest` behind it; a vouch given after reading a recorded sample of the document reaches
2427
+ this rung too, and carries `vouchScope: "sampled"` on the row saying so) — as a `maturity` string
2428
+ per service and a fleet rollup of counts per rung, with the same rollup as a line in the text view.
2429
+ Every rung is derived from artifact presence and provenance state alone; COMPLETENESS of what was
2430
+ written is on the adopt brief's unchecked list, so no rung is called "adopted" — a service with one
2431
+ endpoint documented out of thirty reaches `vouched` exactly as fast as a thorough one, and only a
2432
+ reader can tell them apart.
2433
+
2434
+ **The per-service summary a CI pipeline publishes.** Staleness and validity are computable only one
2435
+ service repo at a time, and loam ships no aggregator — but a fleet view needs a defined thing to
2436
+ aggregate, and without one "no report" is indistinguishable from "clean". So the contract is a
2437
+ shape, not a tool. Each service repo's CI derives this from `loam validate --service <id> --json`
2438
+ and publishes it wherever the fleet view reads:
398
2439
 
399
2440
  ```json
400
2441
  {
@@ -425,19 +2466,77 @@ loam validate --service "$SVC" --json | jq '{
425
2466
  }'
426
2467
  ```
427
2468
 
428
- `generatedAt` is the CI's clock, never loam's — loam output carries no timestamps, by design, so the same repo state always yields the same bytes. That is also what makes the timestamp the liveness signal: the rule is that every service repo's CI publishes this summary on every run, so a summary that is missing or whose `generatedAt` is old IS the "no report" state, and a fleet view renders it as unknown, never as clean. The derivation is pinned in `test/validate-contract.test.ts`, so the payload cannot drift out from under this recipe silently.
429
-
430
- **One `landscape.likec4`, by decision.** A shared file every feature merges into sounds like a conflict factory, but archive's insertion is service-grouped and order-independent: additions land beside their service's existing elements and edges (new services in an id-sorted trailing region), so two concurrent archives touching **different** services usually splice different lines and git merges their PRs without help. What that does not eliminate is two archives touching the **same** service — those land adjacently by design, and their PRs conflict. The accepted process for now is AI-assisted human review in the PR flow — the same posture OpenSpec takes for same-capability conflicts, left to the AI and human at archive time: the resolver reads both features and hand-merges the block. That resolution has a documented cost, not a surprise: a hand-resolved landscape moves the file away from the bytes the archive's `.loam-before/` snapshot recorded, so a later `loam unarchive` of that feature refuses with `snapshot-stale` and needs `--force` — the resolver is trading undo precision for the merge, and should know it. The trigger for revisiting is written down so nobody relitigates it early: when landscape merge conflicts become routine — weekly, not monthly — move per-service internals fully into `services/<svc>/model.likec4` and thin the landscape down to top-level elements plus cross-service edges. Until then, one file keeps the fleet map one diffable document.
2469
+ `generatedAt` is the CI's clock, never loam's — loam output carries no timestamps, by design, so the
2470
+ same repo state always yields the same bytes. That is also what makes the timestamp the liveness
2471
+ signal: the rule is that every service repo's CI publishes this summary on every run, so a summary
2472
+ that is missing or whose `generatedAt` is old IS the "no report" state, and a fleet view renders it
2473
+ as unknown, never as clean. The derivation is pinned in `test/validate-contract.test.ts`, so the
2474
+ payload cannot drift out from under this recipe silently.
2475
+
2476
+ **One `landscape.likec4`, by decision.** A shared file every feature merges into sounds like a
2477
+ conflict factory, but archive's insertion is service-grouped and order-independent: additions land
2478
+ beside their service's existing elements and edges (new services in an id-sorted trailing region),
2479
+ so two concurrent archives touching **different** services usually splice different lines and git
2480
+ merges their PRs without help. What that does not eliminate is two archives touching the **same**
2481
+ service — those land adjacently by design, and their PRs conflict. The accepted process for now is
2482
+ AI-assisted human review in the PR flow — the same posture OpenSpec takes for same-capability
2483
+ conflicts, left to the AI and human at archive time: the resolver reads both features and
2484
+ hand-merges the block. That resolution has a documented cost, not a surprise: a hand-resolved
2485
+ landscape moves the file away from the bytes the archive's `.loam-before/` snapshot recorded, so a
2486
+ later `loam unarchive` of that feature refuses with `snapshot-stale` and needs `--force` — the
2487
+ resolver is trading undo precision for the merge, and should know it. The trigger for revisiting is
2488
+ written down so nobody relitigates it early: when landscape merge conflicts become routine — weekly,
2489
+ not monthly — move per-service internals fully into `services/<svc>/model.likec4` and thin the
2490
+ landscape down to top-level elements plus cross-service edges. Until then, one file keeps the fleet
2491
+ map one diffable document.
431
2492
 
432
2493
  ## Two flows
433
2494
 
434
- - **Bootstrap (reverse):** `loam adopt --service <id>` emits a **brief**, not an extraction — the target paths (and which already exist, to be diffed and never overwritten), the grammar of each artifact, what the living landscape already says about the service, the frontmatter to write, the checks `loam validate --service <id>` will then run, and the ones that do not exist. One target is not under `services/<id>/`: `architecture/landscape.likec4`, carried with `action: "edit"` and a `landscape.instruction` naming the block this service owes the fleet map, and present only while nothing in the map resolves to the service. An agent reads the code and writes draft `model.likec4` + `spec.md` + `openapi.yaml` + `adrs/`, `runbook.md`, `health.yaml`, and makes that one addition to the shared map. A human promotes `draft` -> `verified` with `loam vouch`.
435
- - **Forward (generative):** author `features/<FEAT>/delta.likec4` -> `loam delta <FEAT>` projects it per-service into work -> `loam gherkin <FEAT>` emits the scenarios as the executable `.feature` skeleton in the service's repo -> tests -> code -> `loam verify <FEAT> --service <id>` records, from each affected service's own repo, what was actually built.
436
-
437
- There is no code extractor on either side, by decision. Nothing deterministic reads a service and says what its architecture means, and two generated models of the same code disagree in wording every run — a done-check built on diffing them would flap until somebody turned it off. What is deterministic is the **question**: which files, in which grammar, bound to which existing elements; which operations, which edges, which scenarios. loam owns the questions and the checking; an agent owns the reading. `sources` and `verification.yaml` are where the answers are written down.
2495
+ - **Bootstrap (reverse):** `loam adopt --service <id>` emits a **brief**, not an extraction — the
2496
+ target paths (and which already exist, to be diffed and never overwritten), the grammar of each
2497
+ artifact, what the living landscape already says about the service, the frontmatter to write, the
2498
+ checks `loam validate --service <id>` will then run, and the ones that do not exist. One target is
2499
+ not under `services/<id>/`: `architecture/landscape.likec4`, carried with `action: "edit"` and a
2500
+ `landscape.instruction` naming the block this service owes the fleet map, and present only while
2501
+ nothing in the map resolves to the service. An agent reads the code and writes draft
2502
+ `model.likec4` + `spec.md` + `openapi.yaml` + `adrs/`, `runbook.md`, `health.yaml`, and makes that
2503
+ one addition to the shared map. A human promotes `draft` -> `verified` with `loam vouch`.
2504
+ - **Forward (generative):** author `features/<FEAT>/delta.likec4` -> `loam delta <FEAT>` projects it
2505
+ per-service into work -> `loam gherkin <FEAT>` emits the scenarios as the executable `.feature`
2506
+ skeleton in the service's repo -> tests -> code -> `loam verify <FEAT> --service <id>` records,
2507
+ from each affected service's own repo, what was actually built.
2508
+
2509
+ There is no code extractor on either side, by decision. Nothing deterministic reads a service and
2510
+ says what its architecture means, and two generated models of the same code disagree in wording
2511
+ every run — a done-check built on diffing them would flap until somebody turned it off. What is
2512
+ deterministic is the **question**: which files, in which grammar, bound to which existing elements;
2513
+ which operations, which edges, which scenarios. loam owns the questions and the checking; an agent
2514
+ owns the reading. `sources` and `verification.yaml` are where the answers are written down.
438
2515
 
439
2516
  ## Status
440
2517
 
441
- `init`, `list` / `show` / `status` (navigation and what-to-do-next), `doctor` / `dependencies` (preflight and the active-feature graph), `adopt` (the baseline brief), `new` (feature scaffolding), `validate` (C4 + requirement + API coverage + cross-axis coherence + the landscape ↔ `services/` cross-check, single target or `--all`), `delta` (per-service projection), `gherkin` (the generated `.feature` suite in the service repo), `rebase` (pin a delta to the living text it was written against), `verify` (the done-check checklist + its record — scenario claims answered by the cucumber report via `--results`, or attested via `--record`), `archive` (three-axis merge, gated on gating coherence issues) / `unarchive` (put it back from the snapshot archive left behind), `vouch` (stamp a spec verified against the code it describes), and `audit-openspec` / `migrate-openspec` (the one-way on-ramp) are implemented, each with a `--json` contract. Remaining: `render` (diagrams — delegated to LikeC4's own tooling), `health` compose, UI-prototype generation.
442
-
443
- Known and unclosed, so nobody has to discover them: `loam validate --all` costs ~30 s on a 120-service fleet (one LikeC4 workspace per service — see "Operating at fleet scale"); `delta.service-unknown` reddens `validate` but does not yet gate `archive`, which gates on coherence rather than on the feature validator; a path-level `x-loam-remove` already sitting in a *living* contract is gated at archive time but not yet reported by `validate`; and `loam list`'s verification column shows a confirmed/claims count without distinguishing an attested record from a verified one — `loam verify` and `loam status` are where that distinction is currently legible.
2518
+ `init`, `list` / `show` / `status` (navigation and what-to-do-next), `doctor` / `dependencies`
2519
+ (preflight and the active-feature graph), `adopt` (the baseline brief), `new` (feature scaffolding),
2520
+ `validate` (C4 + requirements + OpenAPI + AsyncAPI + permissions + cross-axis coherence, single
2521
+ target or `--all`), `delta` (per-service projection), `gherkin` (the generated `.feature` suite,
2522
+ including Scenario Outlines), `rebase` (requirement, operation, asyncapi slot and asyncapi
2523
+ component-surface pins), `verify`,
2524
+ transactional `archive` / `unarchive`, `vouch`, and the OpenSpec audit/migration on-ramp are
2525
+ implemented, each with a `--json` contract.
2526
+
2527
+ Known and unclosed, so nobody has to discover them: the complete gate still needs repeatable CI and
2528
+ installed-package evidence observed from an actual push, even though behavior, coverage and the
2529
+ local stress runner pass. Both contract axes now merge a components-only delta: a feature
2530
+ asyncapi.yaml declaring no channel/operation/message slot (`components.schemas`-only, say) merges
2531
+ the component surfaces it declares, pinned by the root `x-loam-baselines` record, and they enter the
2532
+ ref sweep like anything else the merge wrote — so a `$ref` whose target the delta itself defines
2533
+ resolves instead of gating `asyncapi.ref-unresolved`, while one nothing anywhere defines still does.
2534
+ (The verification record, the multi-file writers, the OpenAPI
2535
+ path-item/component surfaces, the `validate --all` cost — the ~13–14 s that used to be listed here
2536
+ is measured at ~0.7 s, docs/BENCHMARKS.md — and the AsyncAPI feature lifecycle, all previously
2537
+ listed here, are closed: lock/CAS records, the journaled roll-forward transaction,
2538
+ `x-loam-baselines`, the batched LikeC4 workspace, and the slot-pinned asyncapi delta/merge/verify
2539
+ axis.) Living path-level remove markers are already reported by `validate`, and `loam list` already
2540
+ distinguishes `attested` from `verified`; neither is an open gap. [ROADMAP.md](ROADMAP.md) orders
2541
+ the remaining work by risk and gives completion criteria. `render`, health composition and UI
2542
+ generation are later work, not prerequisites for the integrity fixes.