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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (648) hide show
  1. package/CHANGELOG.md +1315 -1
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +40 -22
  5. package/QUICKSTART.md +181 -9
  6. package/README.md +256 -57
  7. package/ROADMAP.md +83 -7
  8. package/SCHEMA.md +569 -98
  9. package/WORKFLOW.md +272 -62
  10. package/dist/cli.js +26 -8
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt/adopt.js +5 -10
  13. package/dist/commands/adopt/adopt.js.map +1 -1
  14. package/dist/commands/archive/archive.js +15 -18
  15. package/dist/commands/archive/archive.js.map +1 -1
  16. package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
  17. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  18. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  19. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/openapi.js +25 -42
  21. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
  23. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  24. package/dist/commands/archive/plan/gate.js +3 -7
  25. package/dist/commands/archive/plan/gate.js.map +1 -1
  26. package/dist/commands/archive/plan/landscape.js +4 -4
  27. package/dist/commands/archive/plan/landscape.js.map +1 -1
  28. package/dist/commands/archive/plan/refusal.js +8 -18
  29. package/dist/commands/archive/plan/refusal.js.map +1 -1
  30. package/dist/commands/archive/plan/specs.js +134 -25
  31. package/dist/commands/archive/plan/specs.js.map +1 -1
  32. package/dist/commands/archive/plan/state.js +1 -0
  33. package/dist/commands/archive/plan/state.js.map +1 -1
  34. package/dist/commands/archive/run.js +33 -11
  35. package/dist/commands/archive/run.js.map +1 -1
  36. package/dist/commands/archive/shipped.js +49 -0
  37. package/dist/commands/archive/shipped.js.map +1 -0
  38. package/dist/commands/context/context.js +4 -7
  39. package/dist/commands/context/context.js.map +1 -1
  40. package/dist/commands/context/print.js +2 -2
  41. package/dist/commands/context/print.js.map +1 -1
  42. package/dist/commands/delta/delta.js +44 -12
  43. package/dist/commands/delta/delta.js.map +1 -1
  44. package/dist/commands/delta/print.js +1 -1
  45. package/dist/commands/delta/print.js.map +1 -1
  46. package/dist/commands/dependencies.js +3 -7
  47. package/dist/commands/dependencies.js.map +1 -1
  48. package/dist/commands/diff/diff.js +20 -27
  49. package/dist/commands/diff/diff.js.map +1 -1
  50. package/dist/commands/explore.js +4 -8
  51. package/dist/commands/explore.js.map +1 -1
  52. package/dist/commands/gate/gate.js +45 -8
  53. package/dist/commands/gate/gate.js.map +1 -1
  54. package/dist/commands/gate/print.js +15 -3
  55. package/dist/commands/gate/print.js.map +1 -1
  56. package/dist/commands/gherkin/commit.js +13 -54
  57. package/dist/commands/gherkin/commit.js.map +1 -1
  58. package/dist/commands/gherkin/gherkin.js +36 -20
  59. package/dist/commands/gherkin/gherkin.js.map +1 -1
  60. package/dist/commands/gherkin/reconcile.js +10 -13
  61. package/dist/commands/gherkin/reconcile.js.map +1 -1
  62. package/dist/commands/init/first-hour.js +44 -0
  63. package/dist/commands/init/first-hour.js.map +1 -1
  64. package/dist/commands/init/init.js +18 -22
  65. package/dist/commands/init/init.js.map +1 -1
  66. package/dist/commands/list/campaign/campaign.js +4 -6
  67. package/dist/commands/list/campaign/campaign.js.map +1 -1
  68. package/dist/commands/list/json.js +0 -4
  69. package/dist/commands/list/json.js.map +1 -1
  70. package/dist/commands/list/list.js +38 -23
  71. package/dist/commands/list/list.js.map +1 -1
  72. package/dist/commands/list/print.js +38 -6
  73. package/dist/commands/list/print.js.map +1 -1
  74. package/dist/commands/list/suites/suites.js +140 -0
  75. package/dist/commands/list/suites/suites.js.map +1 -0
  76. package/dist/commands/list/views.js +28 -33
  77. package/dist/commands/list/views.js.map +1 -1
  78. package/dist/commands/mcp/dispatch.js +91 -36
  79. package/dist/commands/mcp/dispatch.js.map +1 -1
  80. package/dist/commands/mcp/mcp.js +56 -15
  81. package/dist/commands/mcp/mcp.js.map +1 -1
  82. package/dist/commands/mcp/serve.js.map +1 -1
  83. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  84. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  85. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  86. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  87. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  88. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  89. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  90. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  91. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  92. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  93. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  94. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  95. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  96. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  97. package/dist/commands/new/capability-template.js +118 -0
  98. package/dist/commands/new/capability-template.js.map +1 -0
  99. package/dist/commands/new/commit.js +8 -22
  100. package/dist/commands/new/commit.js.map +1 -1
  101. package/dist/commands/new/modifies.js +206 -0
  102. package/dist/commands/new/modifies.js.map +1 -0
  103. package/dist/commands/new/new.js +59 -14
  104. package/dist/commands/new/new.js.map +1 -1
  105. package/dist/commands/new/templates.js +48 -124
  106. package/dist/commands/new/templates.js.map +1 -1
  107. package/dist/commands/open.js +3 -7
  108. package/dist/commands/open.js.map +1 -1
  109. package/dist/commands/policy/gate.js +27 -0
  110. package/dist/commands/policy/gate.js.map +1 -1
  111. package/dist/commands/rebase/plan.js +2 -2
  112. package/dist/commands/rebase/plan.js.map +1 -1
  113. package/dist/commands/rebase/rebase.js +60 -35
  114. package/dist/commands/rebase/rebase.js.map +1 -1
  115. package/dist/commands/seed/commit.js +21 -25
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/seed.js +6 -9
  118. package/dist/commands/seed/seed.js.map +1 -1
  119. package/dist/commands/show/show.js +3 -7
  120. package/dist/commands/show/show.js.map +1 -1
  121. package/dist/commands/status/print.js +13 -4
  122. package/dist/commands/status/print.js.map +1 -1
  123. package/dist/commands/status/status.js +3 -7
  124. package/dist/commands/status/status.js.map +1 -1
  125. package/dist/commands/steps/steps.js +3 -7
  126. package/dist/commands/steps/steps.js.map +1 -1
  127. package/dist/commands/subsystem/edit.js +3 -5
  128. package/dist/commands/subsystem/edit.js.map +1 -1
  129. package/dist/commands/subsystem/move.js +5 -8
  130. package/dist/commands/subsystem/move.js.map +1 -1
  131. package/dist/commands/subsystem/query.js +2 -4
  132. package/dist/commands/subsystem/query.js.map +1 -1
  133. package/dist/commands/subsystem/subsystem.js +4 -7
  134. package/dist/commands/subsystem/subsystem.js.map +1 -1
  135. package/dist/commands/subsystem/sync.js +14 -41
  136. package/dist/commands/subsystem/sync.js.map +1 -1
  137. package/dist/commands/subsystem/txn/txn.js +17 -38
  138. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  139. package/dist/commands/subsystem/txn/views.js +32 -5
  140. package/dist/commands/subsystem/txn/views.js.map +1 -1
  141. package/dist/commands/tasks/render.js +116 -0
  142. package/dist/commands/tasks/render.js.map +1 -0
  143. package/dist/commands/tasks/tasks.js +113 -0
  144. package/dist/commands/tasks/tasks.js.map +1 -0
  145. package/dist/commands/unarchive/manifest.js +6 -9
  146. package/dist/commands/unarchive/manifest.js.map +1 -1
  147. package/dist/commands/unarchive/restore.js +17 -28
  148. package/dist/commands/unarchive/restore.js.map +1 -1
  149. package/dist/commands/unarchive/unarchive.js +22 -30
  150. package/dist/commands/unarchive/unarchive.js.map +1 -1
  151. package/dist/commands/validate/arch-coverage.js +6 -15
  152. package/dist/commands/validate/arch-coverage.js.map +1 -1
  153. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  154. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  155. package/dist/commands/validate/checks/requirements.js +18 -8
  156. package/dist/commands/validate/checks/requirements.js.map +1 -1
  157. package/dist/commands/validate/checks/sources.js +6 -2
  158. package/dist/commands/validate/checks/sources.js.map +1 -1
  159. package/dist/commands/validate/feature.js +23 -19
  160. package/dist/commands/validate/feature.js.map +1 -1
  161. package/dist/commands/validate/fleet/landscape.js +23 -23
  162. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  163. package/dist/commands/validate/fleet/map/attest.js +6 -10
  164. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  165. package/dist/commands/validate/fleet/map/bindings.js +1 -3
  166. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  167. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  168. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  169. package/dist/commands/validate/fleet/obligations.js +1 -5
  170. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  171. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  172. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  173. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  174. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  175. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  176. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  177. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  178. package/dist/commands/validate/fleet/views/root-documents.js +8 -28
  179. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  180. package/dist/commands/validate/links/corpus.js +1 -4
  181. package/dist/commands/validate/links/corpus.js.map +1 -1
  182. package/dist/commands/validate/links/glossary.js.map +1 -1
  183. package/dist/commands/validate/report.js +5 -4
  184. package/dist/commands/validate/report.js.map +1 -1
  185. package/dist/commands/validate/scope.js +113 -0
  186. package/dist/commands/validate/scope.js.map +1 -0
  187. package/dist/commands/validate/service/api.js +16 -104
  188. package/dist/commands/validate/service/api.js.map +1 -1
  189. package/dist/commands/validate/service/contracts/generated.js +134 -0
  190. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  191. package/dist/commands/validate/service/events/events.js +1 -1
  192. package/dist/commands/validate/service/events/events.js.map +1 -1
  193. package/dist/commands/validate/service/events/message-producers.js +1 -1
  194. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  195. package/dist/commands/validate/service/evidence-pins.js +56 -10
  196. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  197. package/dist/commands/validate/service/service.js +16 -26
  198. package/dist/commands/validate/service/service.js.map +1 -1
  199. package/dist/commands/validate/service/specs.js +31 -38
  200. package/dist/commands/validate/service/specs.js.map +1 -1
  201. package/dist/commands/validate/service/spine.js +8 -11
  202. package/dist/commands/validate/service/spine.js.map +1 -1
  203. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  204. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  205. package/dist/commands/validate/validate.js +44 -26
  206. package/dist/commands/validate/validate.js.map +1 -1
  207. package/dist/commands/verify/cross/diff.js +2 -6
  208. package/dist/commands/verify/cross/diff.js.map +1 -1
  209. package/dist/commands/verify/evidence/contract.js +1 -1
  210. package/dist/commands/verify/evidence/contract.js.map +1 -1
  211. package/dist/commands/verify/evidence/read.js +12 -3
  212. package/dist/commands/verify/evidence/read.js.map +1 -1
  213. package/dist/commands/verify/frozen.js +45 -23
  214. package/dist/commands/verify/frozen.js.map +1 -1
  215. package/dist/commands/verify/record.js +69 -17
  216. package/dist/commands/verify/record.js.map +1 -1
  217. package/dist/commands/verify/report.js +27 -44
  218. package/dist/commands/verify/report.js.map +1 -1
  219. package/dist/commands/verify/results.js +112 -30
  220. package/dist/commands/verify/results.js.map +1 -1
  221. package/dist/commands/verify/verify.js +54 -51
  222. package/dist/commands/verify/verify.js.map +1 -1
  223. package/dist/commands/vouch/pack/pack.js +2 -2
  224. package/dist/commands/vouch/pack/pack.js.map +1 -1
  225. package/dist/commands/vouch/pack/print.js +1 -6
  226. package/dist/commands/vouch/pack/print.js.map +1 -1
  227. package/dist/commands/vouch/pack/sections.js +1 -2
  228. package/dist/commands/vouch/pack/sections.js.map +1 -1
  229. package/dist/commands/vouch/run.js +26 -81
  230. package/dist/commands/vouch/run.js.map +1 -1
  231. package/dist/commands/vouch/sample/plan.js +20 -34
  232. package/dist/commands/vouch/sample/plan.js.map +1 -1
  233. package/dist/commands/vouch/vet/verify.js +39 -7
  234. package/dist/commands/vouch/vet/verify.js.map +1 -1
  235. package/dist/commands/vouch/vouch.js +10 -17
  236. package/dist/commands/vouch/vouch.js.map +1 -1
  237. package/dist/core/agent/agents-md/command-map.js +56 -101
  238. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  239. package/dist/core/agent/agents-md/map/explain.js +4 -3
  240. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  241. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  242. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  243. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  244. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  245. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  246. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  247. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  248. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  249. package/dist/core/agent/agents-md/map/status.js +9 -3
  250. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  251. package/dist/core/agent/agents-md/map/subsystem.js +4 -3
  252. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  253. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  254. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  255. package/dist/core/agent/agents-md/refusals.js +32 -18
  256. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  257. package/dist/core/agent/agents-md.js +9 -3
  258. package/dist/core/agent/agents-md.js.map +1 -1
  259. package/dist/core/agent/agents-stamp.js +10 -0
  260. package/dist/core/agent/agents-stamp.js.map +1 -1
  261. package/dist/core/agent/scaffold.js +10 -15
  262. package/dist/core/agent/scaffold.js.map +1 -1
  263. package/dist/core/agent/tools/dialects.js +1 -0
  264. package/dist/core/agent/tools/dialects.js.map +1 -1
  265. package/dist/core/agent/tools/registry.js +41 -76
  266. package/dist/core/agent/tools/registry.js.map +1 -1
  267. package/dist/core/agent/workflows/adopt.js +5 -3
  268. package/dist/core/agent/workflows/adopt.js.map +1 -1
  269. package/dist/core/agent/workflows/check.js +20 -16
  270. package/dist/core/agent/workflows/check.js.map +1 -1
  271. package/dist/core/agent/workflows/closing.js +54 -21
  272. package/dist/core/agent/workflows/closing.js.map +1 -1
  273. package/dist/core/agent/workflows/feature.js +79 -11
  274. package/dist/core/agent/workflows/feature.js.map +1 -1
  275. package/dist/core/agent/workflows/implement.js +91 -13
  276. package/dist/core/agent/workflows/implement.js.map +1 -1
  277. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  278. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  279. package/dist/core/agent/workflows/reference/codes.js +8 -6
  280. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  281. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  282. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  283. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  284. package/dist/core/agent/workflows/reference/spine.js +50 -18
  285. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  286. package/dist/core/asyncapi/baseline/plan.js +14 -81
  287. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  288. package/dist/core/asyncapi/digest.js +30 -25
  289. package/dist/core/asyncapi/digest.js.map +1 -1
  290. package/dist/core/asyncapi/merge/components.js +5 -4
  291. package/dist/core/asyncapi/merge/components.js.map +1 -1
  292. package/dist/core/asyncapi/merge/error.js +7 -2
  293. package/dist/core/asyncapi/merge/error.js.map +1 -1
  294. package/dist/core/asyncapi/merge/markers.js +30 -50
  295. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  296. package/dist/core/asyncapi/merge/merge.js +31 -53
  297. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  298. package/dist/core/asyncapi/merge/pin.js +10 -36
  299. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  300. package/dist/core/asyncapi/model.js.map +1 -1
  301. package/dist/core/asyncapi/read.js +36 -12
  302. package/dist/core/asyncapi/read.js.map +1 -1
  303. package/dist/core/brief/brief.js +6 -18
  304. package/dist/core/brief/brief.js.map +1 -1
  305. package/dist/core/brief/landscape.js +8 -12
  306. package/dist/core/brief/landscape.js.map +1 -1
  307. package/dist/core/brief/map/extend.js +8 -5
  308. package/dist/core/brief/map/extend.js.map +1 -1
  309. package/dist/core/brief/map/owed.js +8 -26
  310. package/dist/core/brief/map/owed.js.map +1 -1
  311. package/dist/core/brief/targets.js +15 -5
  312. package/dist/core/brief/targets.js.map +1 -1
  313. package/dist/core/c4/likec4.js +147 -16
  314. package/dist/core/c4/likec4.js.map +1 -1
  315. package/dist/core/c4/locate/unlocated.js +150 -0
  316. package/dist/core/c4/locate/unlocated.js.map +1 -0
  317. package/dist/core/c4/parsed/values.js +54 -1
  318. package/dist/core/c4/parsed/values.js.map +1 -1
  319. package/dist/core/c4/prefetch/fleet.js +1 -9
  320. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  321. package/dist/core/c4/project/documents.js +31 -6
  322. package/dist/core/c4/project/documents.js.map +1 -1
  323. package/dist/core/c4/project/load.js +6 -17
  324. package/dist/core/c4/project/load.js.map +1 -1
  325. package/dist/core/c4/resolve/external.js +51 -0
  326. package/dist/core/c4/resolve/external.js.map +1 -0
  327. package/dist/core/c4/resolve/service.js +25 -1
  328. package/dist/core/c4/resolve/service.js.map +1 -1
  329. package/dist/core/c4/seed/template.js +15 -1
  330. package/dist/core/c4/seed/template.js.map +1 -1
  331. package/dist/core/c4/service-model/shape.js +2 -22
  332. package/dist/core/c4/service-model/shape.js.map +1 -1
  333. package/dist/core/c4/source-mask.js +288 -4
  334. package/dist/core/c4/source-mask.js.map +1 -1
  335. package/dist/core/c4/source-scan.js +203 -147
  336. package/dist/core/c4/source-scan.js.map +1 -1
  337. package/dist/core/c4/splice/authored-source.js +19 -0
  338. package/dist/core/c4/splice/authored-source.js.map +1 -1
  339. package/dist/core/c4/splice/delta-blocks.js +5 -18
  340. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  341. package/dist/core/c4/splice/landscape-merge.js +13 -8
  342. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  343. package/dist/core/c4/splice/model/merge.js +10 -6
  344. package/dist/core/c4/splice/model/merge.js.map +1 -1
  345. package/dist/core/c4/splice/placement.js +108 -18
  346. package/dist/core/c4/splice/placement.js.map +1 -1
  347. package/dist/core/c4/workspace.js +13 -22
  348. package/dist/core/c4/workspace.js.map +1 -1
  349. package/dist/core/capabilities/capabilities.js +6 -6
  350. package/dist/core/capabilities/capabilities.js.map +1 -1
  351. package/dist/core/capabilities/delta/doc.js +7 -10
  352. package/dist/core/capabilities/delta/doc.js.map +1 -1
  353. package/dist/core/capabilities/findings.js +15 -20
  354. package/dist/core/capabilities/findings.js.map +1 -1
  355. package/dist/core/capabilities/realizes/findings.js +12 -19
  356. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  357. package/dist/core/capabilities/realizes/join.js +0 -20
  358. package/dist/core/capabilities/realizes/join.js.map +1 -1
  359. package/dist/core/capabilities/rollup.js +6 -0
  360. package/dist/core/capabilities/rollup.js.map +1 -1
  361. package/dist/core/capabilities/tree.js +6 -28
  362. package/dist/core/capabilities/tree.js.map +1 -1
  363. package/dist/core/capabilities/usecase-join.js +3 -13
  364. package/dist/core/capabilities/usecase-join.js.map +1 -1
  365. package/dist/core/coherence/authoring/scaffold.js +11 -3
  366. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  367. package/dist/core/coherence/authoring/sentinels.js +2 -1
  368. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  369. package/dist/core/coherence/coherence.js +30 -16
  370. package/dist/core/coherence/coherence.js.map +1 -1
  371. package/dist/core/coherence/consumed/operations.js +70 -0
  372. package/dist/core/coherence/consumed/operations.js.map +1 -0
  373. package/dist/core/coherence/consumed/refusal.js +38 -0
  374. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  375. package/dist/core/coherence/declared.js +60 -34
  376. package/dist/core/coherence/declared.js.map +1 -1
  377. package/dist/core/coherence/events/declared.js +44 -59
  378. package/dist/core/coherence/events/declared.js.map +1 -1
  379. package/dist/core/coherence/events/events.js +119 -8
  380. package/dist/core/coherence/events/events.js.map +1 -1
  381. package/dist/core/coherence/events/grades.js +31 -8
  382. package/dist/core/coherence/events/grades.js.map +1 -1
  383. package/dist/core/coherence/events/lookups.js +14 -82
  384. package/dist/core/coherence/events/lookups.js.map +1 -1
  385. package/dist/core/coherence/lookups.js +18 -78
  386. package/dist/core/coherence/lookups.js.map +1 -1
  387. package/dist/core/coherence/retired/archived.js +285 -0
  388. package/dist/core/coherence/retired/archived.js.map +1 -0
  389. package/dist/core/coherence/retired/event-pair.js +62 -0
  390. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  391. package/dist/core/coherence/retired/merged.js +79 -0
  392. package/dist/core/coherence/retired/merged.js.map +1 -0
  393. package/dist/core/coherence/retired/retirement.js +49 -0
  394. package/dist/core/coherence/retired/retirement.js.map +1 -0
  395. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  396. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  397. package/dist/core/coherence/retired/revival/revived.js +297 -0
  398. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  399. package/dist/core/consumers/after.js +113 -0
  400. package/dist/core/consumers/after.js.map +1 -0
  401. package/dist/core/consumers/consumers.js +296 -0
  402. package/dist/core/consumers/consumers.js.map +1 -0
  403. package/dist/core/consumers/current.js +211 -0
  404. package/dist/core/consumers/current.js.map +1 -0
  405. package/dist/core/consumers/scan.js +46 -0
  406. package/dist/core/consumers/scan.js.map +1 -0
  407. package/dist/core/consumers/state.js +27 -0
  408. package/dist/core/consumers/state.js.map +1 -0
  409. package/dist/core/delta/delta.js +1 -1
  410. package/dist/core/delta/delta.js.map +1 -1
  411. package/dist/core/deployment/delta.js +3 -6
  412. package/dist/core/deployment/delta.js.map +1 -1
  413. package/dist/core/diff/base-git.js +94 -12
  414. package/dist/core/diff/base-git.js.map +1 -1
  415. package/dist/core/diff/base-state.js +28 -16
  416. package/dist/core/diff/base-state.js.map +1 -1
  417. package/dist/core/diff/joins.js +136 -0
  418. package/dist/core/diff/joins.js.map +1 -0
  419. package/dist/core/diff/scope/changed-paths.js +22 -6
  420. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  421. package/dist/core/diff/scope/edges.js +82 -0
  422. package/dist/core/diff/scope/edges.js.map +1 -0
  423. package/dist/core/diff/semantic.js +100 -31
  424. package/dist/core/diff/semantic.js.map +1 -1
  425. package/dist/core/doctor/agents.js +34 -18
  426. package/dist/core/doctor/agents.js.map +1 -1
  427. package/dist/core/doctor/report.js.map +1 -1
  428. package/dist/core/document/apply.js +10 -2
  429. package/dist/core/document/apply.js.map +1 -1
  430. package/dist/core/document/frontmatter.js +55 -0
  431. package/dist/core/document/frontmatter.js.map +1 -1
  432. package/dist/core/document/parse.js +29 -6
  433. package/dist/core/document/parse.js.map +1 -1
  434. package/dist/core/document/scenarios.js +17 -0
  435. package/dist/core/document/scenarios.js.map +1 -1
  436. package/dist/core/document/spec.js +4 -11
  437. package/dist/core/document/spec.js.map +1 -1
  438. package/dist/core/envelope/config.js +10 -0
  439. package/dist/core/envelope/config.js.map +1 -1
  440. package/dist/core/envelope/json.js +34 -0
  441. package/dist/core/envelope/json.js.map +1 -1
  442. package/dist/core/explain/families.js +17 -12
  443. package/dist/core/explain/families.js.map +1 -1
  444. package/dist/core/explain/refusals.js +3 -3
  445. package/dist/core/explain/refusals.js.map +1 -1
  446. package/dist/core/explain/terms.js +18 -9
  447. package/dist/core/explain/terms.js.map +1 -1
  448. package/dist/core/explore/explore.js +2 -7
  449. package/dist/core/explore/explore.js.map +1 -1
  450. package/dist/core/fleet-context.js +34 -95
  451. package/dist/core/fleet-context.js.map +1 -1
  452. package/dist/core/gate/checks.js +5 -2
  453. package/dist/core/gate/checks.js.map +1 -1
  454. package/dist/core/gate/partners.js +4 -2
  455. package/dist/core/gate/partners.js.map +1 -1
  456. package/dist/core/gate/report.js.map +1 -1
  457. package/dist/core/gate/verification.js +75 -12
  458. package/dist/core/gate/verification.js.map +1 -1
  459. package/dist/core/gherkin/stale.js +13 -37
  460. package/dist/core/gherkin/stale.js.map +1 -1
  461. package/dist/core/glossary/tree.js +6 -19
  462. package/dist/core/glossary/tree.js.map +1 -1
  463. package/dist/core/kernel/concurrency.js +19 -0
  464. package/dist/core/kernel/concurrency.js.map +1 -1
  465. package/dist/core/kernel/records.js +11 -0
  466. package/dist/core/kernel/records.js.map +1 -1
  467. package/dist/core/links/backlinks.js +1 -3
  468. package/dist/core/links/backlinks.js.map +1 -1
  469. package/dist/core/links/corpus.js +5 -36
  470. package/dist/core/links/corpus.js.map +1 -1
  471. package/dist/core/mcp/argv.js +101 -0
  472. package/dist/core/mcp/argv.js.map +1 -1
  473. package/dist/core/mcp/author-tools.js +2 -1
  474. package/dist/core/mcp/author-tools.js.map +1 -1
  475. package/dist/core/mcp/protocol.js +2 -2
  476. package/dist/core/mcp/protocol.js.map +1 -1
  477. package/dist/core/mcp/tools.js +43 -33
  478. package/dist/core/mcp/tools.js.map +1 -1
  479. package/dist/core/openapi/baseline/gate.js +53 -12
  480. package/dist/core/openapi/baseline/gate.js.map +1 -1
  481. package/dist/core/openapi/baseline/plan.js +22 -8
  482. package/dist/core/openapi/baseline/plan.js.map +1 -1
  483. package/dist/core/openapi/baseline/record.js +0 -1
  484. package/dist/core/openapi/baseline/record.js.map +1 -1
  485. package/dist/core/openapi/contract-text.js +33 -0
  486. package/dist/core/openapi/contract-text.js.map +1 -0
  487. package/dist/core/openapi/contract-tree.js +56 -0
  488. package/dist/core/openapi/contract-tree.js.map +1 -0
  489. package/dist/core/openapi/depth.js +38 -24
  490. package/dist/core/openapi/depth.js.map +1 -1
  491. package/dist/core/openapi/digest.js +52 -2
  492. package/dist/core/openapi/digest.js.map +1 -1
  493. package/dist/core/openapi/doc.js +12 -4
  494. package/dist/core/openapi/doc.js.map +1 -1
  495. package/dist/core/openapi/merge/components.js +25 -60
  496. package/dist/core/openapi/merge/components.js.map +1 -1
  497. package/dist/core/openapi/merge/error.js +3 -13
  498. package/dist/core/openapi/merge/error.js.map +1 -1
  499. package/dist/core/openapi/merge/markers.js +26 -30
  500. package/dist/core/openapi/merge/markers.js.map +1 -1
  501. package/dist/core/openapi/merge/merge.js +26 -52
  502. package/dist/core/openapi/merge/merge.js.map +1 -1
  503. package/dist/core/openapi/merge/pin.js +15 -35
  504. package/dist/core/openapi/merge/pin.js.map +1 -1
  505. package/dist/core/openspec/decide/verdict.js.map +1 -1
  506. package/dist/core/openspec/inventory.js +4 -5
  507. package/dist/core/openspec/inventory.js.map +1 -1
  508. package/dist/core/openspec/model/mapping.js +30 -28
  509. package/dist/core/openspec/model/mapping.js.map +1 -1
  510. package/dist/core/openspec/model/model.js +12 -7
  511. package/dist/core/openspec/model/model.js.map +1 -1
  512. package/dist/core/openspec/scan/changes.js +10 -4
  513. package/dist/core/openspec/scan/changes.js.map +1 -1
  514. package/dist/core/openspec/scan/config.js +7 -16
  515. package/dist/core/openspec/scan/config.js.map +1 -1
  516. package/dist/core/openspec/scan/shape.js +63 -19
  517. package/dist/core/openspec/scan/shape.js.map +1 -1
  518. package/dist/core/openspec/scan/walk.js +21 -38
  519. package/dist/core/openspec/scan/walk.js.map +1 -1
  520. package/dist/core/pack/living.js +2 -16
  521. package/dist/core/pack/living.js.map +1 -1
  522. package/dist/core/provenance/findings.js +48 -12
  523. package/dist/core/provenance/findings.js.map +1 -1
  524. package/dist/core/provenance/git.js +33 -57
  525. package/dist/core/provenance/git.js.map +1 -1
  526. package/dist/core/provenance/gitq/moves.js +71 -114
  527. package/dist/core/provenance/gitq/moves.js.map +1 -1
  528. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  529. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  530. package/dist/core/provenance/sources.js +23 -6
  531. package/dist/core/provenance/sources.js.map +1 -1
  532. package/dist/core/provenance/stamp.js +5 -17
  533. package/dist/core/provenance/stamp.js.map +1 -1
  534. package/dist/core/repo/entries.js.map +1 -1
  535. package/dist/core/repo/repo.js +24 -14
  536. package/dist/core/repo/repo.js.map +1 -1
  537. package/dist/core/repo/tree/find.js +12 -3
  538. package/dist/core/repo/tree/find.js.map +1 -1
  539. package/dist/core/repo/tree/fs.js +27 -1
  540. package/dist/core/repo/tree/fs.js.map +1 -1
  541. package/dist/core/repo/tree/walk.js +8 -30
  542. package/dist/core/repo/tree/walk.js.map +1 -1
  543. package/dist/core/results.js +47 -12
  544. package/dist/core/results.js.map +1 -1
  545. package/dist/core/staging/commit.js +10 -12
  546. package/dist/core/staging/commit.js.map +1 -1
  547. package/dist/core/staging/lock.js +74 -12
  548. package/dist/core/staging/lock.js.map +1 -1
  549. package/dist/core/staging/recovery/intent.js +2 -5
  550. package/dist/core/staging/recovery/intent.js.map +1 -1
  551. package/dist/core/staging/recovery/recover.js +60 -1
  552. package/dist/core/staging/recovery/recover.js.map +1 -1
  553. package/dist/core/staging/snapshot/manifest.js +152 -0
  554. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  555. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  556. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  557. package/dist/core/status/actions/execution.js +11 -0
  558. package/dist/core/status/actions/execution.js.map +1 -1
  559. package/dist/core/status/feature/artifacts.js +2 -2
  560. package/dist/core/status/feature/artifacts.js.map +1 -1
  561. package/dist/core/status/feature/feature.js +5 -11
  562. package/dist/core/status/feature/feature.js.map +1 -1
  563. package/dist/core/status/fleet/fleet.js +6 -2
  564. package/dist/core/status/fleet/fleet.js.map +1 -1
  565. package/dist/core/status/fleet/next.js +26 -3
  566. package/dist/core/status/fleet/next.js.map +1 -1
  567. package/dist/core/status/report.js.map +1 -1
  568. package/dist/core/status/verification.js +30 -15
  569. package/dist/core/status/verification.js.map +1 -1
  570. package/dist/core/tasks/packet.js +174 -0
  571. package/dist/core/tasks/packet.js.map +1 -0
  572. package/dist/core/tasks/record.js +43 -0
  573. package/dist/core/tasks/record.js.map +1 -0
  574. package/dist/core/tasks/sources.js +181 -0
  575. package/dist/core/tasks/sources.js.map +1 -0
  576. package/dist/core/tasks/stamps.js +76 -0
  577. package/dist/core/tasks/stamps.js.map +1 -0
  578. package/dist/core/tasks/unit.js +0 -0
  579. package/dist/core/tasks/unit.js.map +1 -0
  580. package/dist/core/usecases/capability.js +11 -14
  581. package/dist/core/usecases/capability.js.map +1 -1
  582. package/dist/core/usecases/delta/flows.js +5 -7
  583. package/dist/core/usecases/delta/flows.js.map +1 -1
  584. package/dist/core/usecases/fleet.js +178 -31
  585. package/dist/core/usecases/fleet.js.map +1 -1
  586. package/dist/core/usecases/operations.js +5 -4
  587. package/dist/core/usecases/operations.js.map +1 -1
  588. package/dist/core/usecases/service/flows.js +9 -6
  589. package/dist/core/usecases/service/flows.js.map +1 -1
  590. package/dist/core/usecases/suites/participants.js +83 -0
  591. package/dist/core/usecases/suites/participants.js.map +1 -0
  592. package/dist/core/usecases/suites/suites.js +45 -0
  593. package/dist/core/usecases/suites/suites.js.map +1 -0
  594. package/dist/core/verify/answers.js +12 -5
  595. package/dist/core/verify/answers.js.map +1 -1
  596. package/dist/core/verify/build.js +102 -82
  597. package/dist/core/verify/build.js.map +1 -1
  598. package/dist/core/verify/checklist.js +3 -0
  599. package/dist/core/verify/checklist.js.map +1 -1
  600. package/dist/core/verify/location.js +67 -0
  601. package/dist/core/verify/location.js.map +1 -0
  602. package/dist/core/verify/pins/lint.js +3 -2
  603. package/dist/core/verify/pins/lint.js.map +1 -1
  604. package/dist/core/verify/pins/pin.js +1 -1
  605. package/dist/core/verify/pins/pin.js.map +1 -1
  606. package/dist/core/verify/read/file.js +224 -0
  607. package/dist/core/verify/read/file.js.map +1 -0
  608. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  609. package/dist/core/verify/read/shape.js.map +1 -0
  610. package/dist/core/verify/read/union.js +105 -0
  611. package/dist/core/verify/read/union.js.map +1 -0
  612. package/dist/core/verify/record.js +35 -12
  613. package/dist/core/verify/record.js.map +1 -1
  614. package/dist/core/verify/store/commit.js +40 -30
  615. package/dist/core/verify/store/commit.js.map +1 -1
  616. package/dist/core/verify/store/render.js +8 -4
  617. package/dist/core/verify/store/render.js.map +1 -1
  618. package/dist/core/vocabulary/issue.js +1 -0
  619. package/dist/core/vocabulary/issue.js.map +1 -1
  620. package/dist/core/vocabulary/maturity.js +12 -2
  621. package/dist/core/vocabulary/maturity.js.map +1 -1
  622. package/examples/README.md +21 -6
  623. package/examples/docs/architecture/landscape.likec4 +8 -0
  624. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  625. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  626. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  627. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  628. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  629. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  630. package/package.json +1 -1
  631. package/dist/commands/validate/checks/vocabulary.js +0 -16
  632. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  633. package/dist/core/agent/agents-md/artifacts.js +0 -221
  634. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  635. package/dist/core/agent/agents-md/cycle.js +0 -180
  636. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  637. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  638. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  639. package/dist/core/agent/agents-md/spine.js +0 -40
  640. package/dist/core/agent/agents-md/spine.js.map +0 -1
  641. package/dist/core/coherence/events/merged.js +0 -40
  642. package/dist/core/coherence/events/merged.js.map +0 -1
  643. package/dist/core/diff/current-state.js +0 -96
  644. package/dist/core/diff/current-state.js.map +0 -1
  645. package/dist/core/diff/victims.js +0 -256
  646. package/dist/core/diff/victims.js.map +0 -1
  647. package/dist/core/staging/snapshot.js.map +0 -1
  648. package/dist/core/verify/file.js.map +0 -1
package/SCHEMA.md CHANGED
@@ -89,24 +89,25 @@ docs/
89
89
  delta.likec4 C4 delta (architecture) [authored]
90
90
  specs/<svc>/spec.md requirement delta (ADDED/MODIFIED/REMOVED + scenarios) [authored]
91
91
  specs/<svc>/arch.spec.md architectural requirement delta, same algebra [authored]
92
- specs/<svc>/openapi.yaml the endpoints this feature adds [authored]
93
- specs/<svc>/asyncapi.yaml the event-contract delta, slot-pinned [authored]
92
+ specs/<svc>/openapi.yaml only the endpoints it adds/changes/removes [authored]
93
+ specs/<svc>/asyncapi.yaml only the event slots it adds/changes/removes [authored]
94
94
  capabilities/<cap>/spec.md capability delta, same requirement grammar [authored]
95
95
  glossary/<term>.md a word this feature brings with it; create-only [authored]
96
96
  usecases/<name>.likec4 a FLOW this feature brings; views-only, create-only [authored]
97
97
  adrs/NNNN-*.md feature-level decisions [authored]
98
- verification.yaml the done-check: claims + verdicts + evidence, one attestation per service [loam verify --service <id> --record]
98
+ verification/<svc>.yaml the done-check, one file per attesting service: claims + verdicts + evidence [loam verify --service <id> --record]
99
+ verification.yaml the same answered all at once, and every record from before the directory — read forever [loam verify --record]
99
100
  features/archive/<FEAT>/ a shipped change — the same files, plus:
100
- .loam-before/ the bytes the merge overwrote [written by loam]
101
+ .loam-before/ the bytes the merge overwrote, and the checklist it shipped with [written by loam]
101
102
  .loam-lock transient: one writer at a time [archive/unarchive only]
102
103
  .loam-commit transient: the commit intent journal [archive/unarchive only]
103
104
  ```
104
105
 
105
106
  There is no `flows/` tree, and there will not be one. A cross-service **interaction flow** is a
106
107
  `dynamic view` under `architecture/` — the one place that already holds every cross-service edge —
107
- tagged with the capability it realizes. A flow that spans services does not belong under one
108
- service's directory, and the view's steps join to the landscape's own relationships, which is what
109
- makes the flow checkable rather than merely drawn.
108
+ tagged with the capability it realizes, or with the suites it runs in (below). A flow that spans
109
+ services does not belong under one service's directory, and the view's steps join to the
110
+ landscape's own relationships, which is what makes the flow checkable rather than merely drawn.
110
111
 
111
112
  A flow that stays INSIDE one service — the hop sequence behind an arch.spec.md requirement whose
112
113
  `Covers:` names containers — cannot live under `architecture/`: loam parses that directory as its
@@ -147,6 +148,41 @@ would leave behind — so a flow may draw a hop into a service the feature's own
147
148
  and a hop into an element the merge does NOT land is refused at plan time rather than archived into
148
149
  a map the next reader's `loam validate --all` fails on.
149
150
 
151
+ **A fleet flow can also say which SUITE it runs with: `#suite-<name>`.** A suite is a set of
152
+ journeys a test run exercises together — a smoke suite, a payments suite — and a journey joins one
153
+ by carrying the tag on its `dynamic view`, declared like every LikeC4 tag in the landscape's one
154
+ `specification` block (`tag suite-smoke`), so a mistyped suite is a parse error rather than a second,
155
+ empty suite. It is a SEPARATE prefix, not `#cap-`: a capability says what a flow is FOR, a suite says
156
+ what it is RUN WITH, and membership is **many-to-many** — one view may carry several `#suite-` tags,
157
+ and a suite holds as many views as carry its tag, which is why it is a tag and not a directory.
158
+
159
+ **Tagging is the opt-in, and it grades.** A view carrying only `#suite-` tags — no `#cap-`, no
160
+ `#req-` — IS a use case: every reader takes it (the predicate and the byte gate below now read three
161
+ prefixes), and `loam validate --all` grades its hops like any other's (`usecase.step-unbacked`,
162
+ `usecase.step-contested`, `usecase.step-unlinked`). A journey a test stand is provisioned from must
163
+ be held to the model it is drawn over. The suite tag claims no capability and no requirement, so it
164
+ earns nothing from the capability axis. An UNTAGGED view stays ungraded, exactly as before.
165
+
166
+ `loam list suites [<name>] [--json]` is the answer the tag exists for: **which services must be up
167
+ for suite X.** For each suite it lists the views carrying the tag, the enumerated services their
168
+ steps resolve to — through the same element→service resolver the step grades use, so a step into
169
+ one of order-service's containers counts order-service — and every participant that resolves to NO
170
+ service, by element id, with whether it (or anything it is drawn inside) carries `#external`: the
171
+ customer who drives the flow, a topic, a partner's API. The walk up stops at the first element
172
+ bound to or titled for a service, as `loam gate`'s partner reading does, so a service drawn inside
173
+ an `#external` grouping is still ours. Those are named and never dropped, because an environment
174
+ list that is silently short comes up, and the journey then fails against something nobody was
175
+ told to start. The payload is `suites[]` (`name`, `views[]` of `{id, file}`, `services[]`, `unresolved[]`
176
+ of `{element, external}`), `ungrouped[]` — the use cases carrying no `#suite-` tag, which every
177
+ per-suite line would otherwise leave out — and `useCases` (`state`: `absent` when the fleet
178
+ declares no use case, `ok`, or `unreadable` with `error`, beside the `unreadable` boolean
179
+ `list capabilities` spells). Over an unreadable `architecture/` the section degrades rather than
180
+ refusing, and `suites` and `ungrouped` are ABSENT rather than `[]`. A `<name>` narrows `suites[]`
181
+ only; an unknown one refuses `unknown-target` with the close names. The listing reads the FLEET
182
+ map's use cases; a `#suite-` tag on a flow inside one service's own project is graded there like any
183
+ other use-case tag, and is not listed — that flow runs inside one service, whose environment is the
184
+ service itself.
185
+
150
186
  ### Two shapes of a service model
151
187
 
152
188
  `services/<…>/<svc>/model.likec4` comes in two shapes, and **the file's own grammar decides which**
@@ -708,7 +744,10 @@ so running loam from a subdirectory works; `loam init` writes the file where it
708
744
  this repo, digests that document and compares it with the committed
709
745
  `services/<id>/openapi.yaml`: `openapi.generated-stale` (warn) when they differ,
710
746
  `contracts.source-missing` (error) when nothing is there, `contracts.source-invalid` (error) when
711
- the path escapes the repository or the file does not parse.
747
+ the path escapes the repository or the file does not parse. The stale warning's `details` name
748
+ the operationIds that differ (served but not in the committed copy, committed but no longer
749
+ served), and its message says when the two sets agree, so a reworded description no longer reads
750
+ the same as twenty vanished endpoints.
712
751
 
713
752
  **loam reads it and never writes it.** The copy into the docs repo stays a human `cp` reviewed in
714
753
  a pull request — that is what keeps the committed contract a document somebody agreed to rather
@@ -922,7 +961,12 @@ two checked files:
922
961
  `--all` alike, from the docs repo included. A `verified` spec whose current body hash mismatches
923
962
  gets `content.stale` (**warning**, the `sources.stale` grading: the doc changed since it was
924
963
  vouched, and only a person can say whether verified still holds). A verified doc with no
925
- `content_digest` predates the stamp and stays quiet — the fix is re-vouching.
964
+ `content_digest` predates the stamp and stays quiet — the fix is re-vouching. Every fleet readout
965
+ carries the same fact, because it is computable from the docs repo: `loam list` shows
966
+ `vouched (stale)` and owes the row `re-vouch` in `missing` (so `--needs-work` lists it), `loam
967
+ status` counts it under `services.contentStale` and emits `next.revouch` instead of
968
+ `next.fleet-clean`, and `validate --all`'s scorecard counts it under `provenance.contentStale`.
969
+ The rung stays `vouched` — a person did stamp the document — exactly as for `vouch_scope` below.
926
970
  - `vouch_scope` — written by `loam vouch --sample <n>`, never by hand, and present only when the
927
971
  person who vouched read a recorded SAMPLE of the document rather than all of it. One flat string,
928
972
  `sampled <k>/<n> seed=<16 hex>`: k sections read of n, and the seed that chose them. `status`
@@ -1197,9 +1241,64 @@ under any flag — `x-loam-remove` and `x-loam-based-on` are stripped at path le
1197
1241
  level alike, on every merge branch. A path item consisting of nothing but a marker publishes no
1198
1242
  phantom path. The same feature must carry a `REMOVED` requirement whose `Operations:` line names
1199
1243
  that id. Coherence gates an absent path+method, an id mismatch, an unjustified marker, a removed
1200
- requirement with no marker, and a new tagged C4 edge consuming the operation being removed.
1244
+ requirement with no marker, a new tagged C4 edge consuming the operation being removed, and a
1245
+ removal anything would still consume after the merge (`openapi.remove-op-consumed`, error,
1246
+ `--approve` overrides — **Who still consumes it** below says what counts). A `REMOVED`
1247
+ requirement owes a marker only while there is a living operation for it to retire: not for an id
1248
+ the service's living contract does not define (a consumer's `REMOVED` requirement naming another
1249
+ service's operation), and not for one this delta already replaces at its address (the overwrite
1250
+ below — no marker could be written there, since the replacement holds the slot). A living
1251
+ contract that does not parse proves neither exemption, so the marker stays owed.
1201
1252
  `serviceOperationIds` evaluates the feature as living upserts minus removals, so other same-feature
1202
- checks see the post-archive contract. Archive deletes only the matching method, reports removals in
1253
+ checks see the post-archive contract.
1254
+
1255
+ **An overwrite can remove an operation too, and is graded as one.** The merge writes per method and
1256
+ path, so a delta defining ANOTHER operationId where a living operation lives replaces it there — and
1257
+ when the contract the merge would write then defines the living one nowhere, it has left the
1258
+ contract with no marker for any of the checks above to read. Coherence asks the removal's own
1259
+ consumer question of it, on the merged contract: `openapi.remove-op-consumed` (error, `--approve`
1260
+ overrides, exactly as for a marker) while anything would still consume it after the merge. With no
1261
+ marker there is no `REMOVED` requirement either, so the provider's own requirement naming the old id
1262
+ stays unless the feature `MODIFIES` it to name the new operation or `REMOVES` it — and a `REMOVED`
1263
+ one owes no marker here. A new tagged edge onto the replaced operation is `c4-api.op-removing`. The
1264
+ fix is the delta's choice: define the replaced operation at another address in the same delta if it
1265
+ is to stay; for a rename, redraw the living edge and restate the governing requirement. An
1266
+ overwrite of an operation nothing names stays the advisory `api.op-unconsumed` it always was.
1267
+
1268
+ **Who still consumes it — one answer for the gate, `loam diff` and `loam archive`.** The consumers
1269
+ of an operation are read once, the same way for every reader (`core/consumers/`):
1270
+
1271
+ - every edge of the `architecture/` **project** whose `metadata { op }` names it and whose target
1272
+ resolves to the provider — any document the spine grades, never `landscape.likec4` alone (when
1273
+ only a sibling document fails to parse, the map file's own edges still count);
1274
+ - another service's requirement — in `spec.md` **or** `arch.spec.md` — whose `Operations:` line
1275
+ names it, **unless that service's own contract defines the same id** — then the line names that
1276
+ service's own operation: `validate` resolves a spec.md `Operations:` line against its own
1277
+ service's contract, and `loam dependencies` reads a line in either file naming an operation its
1278
+ own service provides as no dependency on anybody (an unreadable or absent contract proves no
1279
+ ownership, and the line counts);
1280
+ - the use-case hops the model attributes to it (a contested hop is a doubt, never a consumer);
1281
+ - the provider's OWN documents still naming it — its `spec.md` requirements, and edges it draws to
1282
+ itself. Not its `arch.spec.md` lines: `validate` grades an `Operations:` line in spec.md only, so
1283
+ one in arch.spec.md strands nothing (another service's does count — it is a dependency on this
1284
+ contract, as `loam dependencies` reads it). The gate counts these, because the merge strands them as surely; `loam diff` and the archive's
1285
+ `openapi.op-modified` warnings answer who ELSE is affected, and leave them out (a service owes no
1286
+ operationId to itself).
1287
+
1288
+ The gate reads all of it **after the feature's merge**: a requirement the same feature `MODIFIES`
1289
+ to drop the id, or `REMOVES`, no longer counts — another service's too, so retiring the consumer in
1290
+ the same feature is a fix the gate sees — and one of its own `ADDED`/`MODIFIED` requirements naming
1291
+ the id does. A delta the merge would refuse to apply fails closed: the living requirements count,
1292
+ plus every one the delta adds or modifies. Edges and hops are read as they stand: the landscape
1293
+ merge only ADDS edges and a feature's flows are create-only, so no feature document can take one
1294
+ away — drop or redraw a living edge by editing the living map in the same change. The refusal lists
1295
+ the consumers in its `details[]`, in the order `loam diff` lists the victims of the same removal.
1296
+ A consumer document nobody could read refuses too, and is named there: a spec file that does not
1297
+ decode (another service's, or the provider's own), or a service id two directories claim — the gate
1298
+ cannot prove such a document does not name what is leaving, so it says it could not check, never
1299
+ that the fleet still consumes it. An edge drawn with no `metadata { op }` and a contested use-case
1300
+ hop are doubts about a drawing: `loam diff` lists them, and they never refuse, since `loam seed`
1301
+ draws every edge without an op. Archive deletes only the matching method, reports removals in
1203
1302
  text and `openapiRemovals` JSON, never copies the marker into living OpenAPI, and deliberately does
1204
1303
  not garbage-collect components. A removal marker in a living `openapi.yaml` is
1205
1304
  `openapi.remove-marker-living` (error). The usual staged-write rollback and `unarchive` snapshot
@@ -1211,9 +1310,11 @@ A `paymentService -> kafka 'Publishes PaymentAuthorized'` edge used to read as f
1211
1310
  while naming no contract anybody could check. The event axis is the API axis's mirror, in **AsyncAPI
1212
1311
  3.0** — only 3.0, because its operations are named top-level objects carrying
1213
1312
  `action: send|receive`, the exact analog of an `operationId`, where 2.x spells the same thing as
1214
- `publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A 2.x
1215
- document declares no `operations` and reads as a contract with no messages rather than as a
1216
- mis-parsed one. Like OpenAPI it is read with a shallow structural walk over the `yaml` parse, never
1313
+ `publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A document
1314
+ whose `asyncapi:` field says 1.x or 2.x is refused as one `asyncapi.invalid` naming the version, and
1315
+ nothing on the event axis is graded against it: reading it as a 3.0 contract with no messages used
1316
+ to turn every edge and requirement into a misleading `spine.message-undefined`. The AsyncAPI CLI's
1317
+ `asyncapi convert --target-version=3.0.0` upgrades it. Like OpenAPI it is read with a shallow structural walk over the `yaml` parse, never
1217
1318
  a real AsyncAPI parser: the slice loam joins on is small enough that the parser's model would be
1218
1319
  dead weight, and adding it would be a fourth runtime dependency.
1219
1320
 
@@ -1276,9 +1377,14 @@ the edges point at channels it is the channels that collect three consumers each
1276
1377
  broker leaves a row of untagged hubs behind it.
1277
1378
 
1278
1379
  **The feature lifecycle, mirrored from the OpenAPI axis.** A feature changes this contract through
1279
- `features/<FEAT>/specs/<svc>/asyncapi.yaml` — a **complete AsyncAPI 3.0 document**, exactly as an
1280
- openapi delta is a complete document: the author restates the living contract around the part they
1281
- are changing, and the machinery tells restatement from edit. Identity is the **slot** — the pair
1380
+ `features/<FEAT>/specs/<svc>/asyncapi.yaml` — a self-contained AsyncAPI 3.0 document holding **only
1381
+ the slots the feature adds, changes or removes**, exactly as an openapi delta holds only its own
1382
+ operations: the merge walks only the slots the delta declares, so a slot it leaves out stays as the
1383
+ living contract has it, and an edited slot is written out whole. A slot restated without being
1384
+ changed is legal and the machinery tells it from an edit, but it buys nothing and costs a pin,
1385
+ which a parallel feature's removal turns into `asyncapi.baseline-invalid` and `asyncapi.slot-revived`
1386
+ — or, unpinned, into `asyncapi.slot-revived` alone, the contract axis's revival check (under *Living
1387
+ spec vs delta* below) on this axis's identity. Identity is the **slot** — the pair
1282
1388
  (section, key) over the three named sections `channels.<key>`, `operations.<key>` and
1283
1389
  `components.messages.<key>` — and the join token stays the message name. Three feature-only keys,
1284
1390
  spelled and shaped exactly as on the OpenAPI axis: `x-loam-based-on` inside a slot's value (written
@@ -1330,7 +1436,36 @@ decision that reverses that stance: the removal family exists, a message-slot re
1330
1436
  justified by a `REMOVED` requirement's `Publishes:`/`Consumes:` line (channel and operation slots
1331
1437
  need exactness only — requirements join on message names and never on channel or operation keys, so
1332
1438
  demanding a justifying requirement there would invent a join the corpus cannot express), and
1333
- `asyncapi.remove-message-consumed` gates a removal the living fleet still consumes. The lag argument
1439
+ `asyncapi.remove-message-consumed` gates a removal the fleet would still consume after the merge —
1440
+ a `consumes` edge of the `architecture/` project, another service's `Consumes:` line in either spec
1441
+ file (no ownership rule here: a consumer declares the message in its own contract as `receive` by
1442
+ construction), a use-case hop backed by such an edge — or whose provider's own `Publishes:` line the
1443
+ merge would leave behind, or its own `publishes` edge (the spine grades that edge on its source,
1444
+ `spine.message-undefined`; drop or redraw it in the living map), which covers a message slot re-keyed to another name or a send operation
1445
+ turned around as surely as a marker: read after the merge, as on the HTTP axis, and a `REMOVED`
1446
+ requirement owes no marker for a message the merge already takes out. **A consumer counts only
1447
+ while nothing would still send the message after the merge** — no service, and no `#external`
1448
+ element whose edge publishes it: the join is fleet-global, and a consumer with another producer is
1449
+ not stranded — `spine.message-unproduced` grades a consumer by exactly that, an outside producer
1450
+ included (*The direction is not decoration* above), and `loam diff` reports such a removal as
1451
+ `diff.message-removed`, "still sent by …", naming an outside producer as `<title> (#external)`.
1452
+ The senders are read after the merge too: a service this feature carries an event delta for sends
1453
+ what the merged contract would (the delta itself, where it creates the contract), and vouches for
1454
+ nothing where the merge cannot be simulated, so a feature retiring the message from every sender
1455
+ is still refused; an unreadable contract never vouches. The outside producer is read off the
1456
+ living `architecture/` project with validate's own rule, and only while that project parses. The
1457
+ landscape merge only adds to the map, so no feature takes its edge or tag away — but the rule also
1458
+ asks that no `services/<id>/` directory stand for the element, and the merge creates one from each
1459
+ `specs/<id>/` the feature carries. So the rule is asked over the directories as the merge leaves
1460
+ them: a feature that onboards the outside system as a service (`specs/legacy-gateway/`, its element
1461
+ titled or bound `legacy-gateway`, or a topic nested inside it) takes the producer away, and a
1462
+ retirement in it is refused unless something else still sends the message — the new service's own
1463
+ contract, in its `specs/legacy-gateway/` delta, for one. A `publishes` edge
1464
+ from an `#external` element that only the feature's own C4 delta draws does not count either —
1465
+ draw it in the living map, or archive with `--approve`. Another sender does not excuse the
1466
+ provider's own `Publishes:` lines and `publishes` edges — those are graded against its own
1467
+ contract, whoever else sends the message — nor its own unreadable spec files; another service's
1468
+ unreadable spec file, or an id two directories claim, refuses only while nothing would send. The lag argument
1334
1469
  survives as an **operational warning**, not a veto: passing the gate proves the *documents* agree
1335
1470
  nobody consumes the message — it does not prove no consumer is still reading the topic with lag, so
1336
1471
  sequence the runtime retirement accordingly.
@@ -1444,19 +1579,35 @@ the source for tests.
1444
1579
  never the resolution itself — a pin says "I read this version", so `loam rebase` reports every
1445
1580
  requirement that moved and says plainly that restamping without re-reading merges your text over
1446
1581
  theirs. There is deliberately no three-way merge: requirement prose does not merge.
1447
- - **The same baseline on the contract axis, where it matters more.** A feature's `openapi.yaml` is a
1448
- **complete document, not a patch** — authors restate the living contract around the slot they
1449
- change — and the merge upserts every operation the document spells. The collision therefore needs
1450
- no overlap between the two features at all: two deltas over one service, editing *different*
1451
- operations, destroy each other, because whichever archives second pushes its authoring-time copy
1452
- of the operation it never meant to touch back over what landed in between. On a service with
1453
- thirty operations a delta quotes twenty-nine of them, and each is a candidate. So every operation
1454
- in a feature contract carries **`x-loam-based-on`** — the same 16-hex digest, in the
1582
+ - **The same baseline on the contract axis.** A feature's `openapi.yaml` is a **self-contained
1583
+ document holding only what the feature adds, changes or removes** — not a copy of the living
1584
+ contract. The merge walks only the paths and methods the delta spells: an operation it leaves out
1585
+ stays exactly as the living contract has it, a local `$ref` (`#/components/schemas/Order`)
1586
+ resolves into the living document, and a brand-new operation, path-level key or component has no
1587
+ living version to pin, so an add-only delta never trips `openapi.baseline-missing` and needs no
1588
+ `loam rebase` at all. What the merge does upsert is every operation the document *does* spell,
1589
+ so an operation quoted without being changed — copied for context, or a whole contract pasted in
1590
+ — is a collision waiting for somebody else's edit: two deltas over one service, each quoting the
1591
+ operation the other edits, destroy each other, because whichever archives second pushes its
1592
+ authoring-time copy back over what landed in between. A quote also costs something even pinned:
1593
+ it is one more pin, and a pinned quote whose living operation a parallel feature removes is
1594
+ `openapi.baseline-invalid` — the living slot is gone, so the pin is founded on nothing — an
1595
+ error that blocks the archive until the quote is deleted. Unpinned, the same quote has no living
1596
+ operation behind it, so the baseline axis cannot tell it from an addition — and it used to
1597
+ archive as one, at exit 0, putting the retired operation back. The **revival check** below is
1598
+ what refuses it now. So a delta quotes nothing it does not change, and every operation in it that
1599
+ the living contract already holds carries
1600
+ **`x-loam-based-on`** — the same 16-hex digest, in the
1455
1601
  vendor-extension shape this axis already uses for `x-loam-remove`, written by `loam rebase` and
1456
1602
  stripped on merge exactly like `Based-On:`. The digest is taken over the operation's *canonical*
1457
1603
  form (object keys sorted), which is the same comparison `isDeepStrictEqual` makes when the merge
1458
1604
  decides an operation "differs" — so a pin goes stale exactly when the merge would say something
1459
- moved, never over a reordered key. The pin is always the digest of the **living** operation, and
1605
+ moved, never over a reordered key. The pin is keyed by the slot, method and path, on both sides,
1606
+ so an operation written with no `operationId` — living or in the delta — is pinned, graded and
1607
+ quoted exactly as a named one: `loam rebase` always pinned against whatever stood at the slot,
1608
+ and the gate once read named operations alone, so it refused the pin rebase had just written as
1609
+ one over "no operation at that slot" and never asked an unnamed quote for a pin. The pin is
1610
+ always the digest of the **living** operation, and
1460
1611
  that one rule yields both merge verdicts by itself: an operation the author only quoted is
1461
1612
  byte-equal to living, so its pin equals its own content and **the merge skips it entirely**
1462
1613
  (reported as `· quotes …` in the plan, so the plan never silently writes less than the delta
@@ -1466,7 +1617,8 @@ the source for tests.
1466
1617
  decision, reverting an operation nobody edited is a bug. `openapi.baseline-missing` is a warning
1467
1618
  that gates the archive, exactly like its requirement-axis sibling — every unpinned restatement is
1468
1619
  a candidate rollback — and it is reported **once per service** rather than per operation: the fix
1469
- is one command, and twenty-nine identical findings teach people to filter the code out. **Covered
1620
+ is one command, and the twenty-nine identical findings a contract pasted whole from a
1621
+ thirty-operation service would raise teach people to filter the code out. **Covered
1470
1622
  since the path-item/component baselines landed:** the same discipline now reaches the two surfaces
1471
1623
  the operation pin cannot — path-level keys (`parameters`, `servers`, `summary`, `x-*`) and every
1472
1624
  `components/<kind>/<name>` — through a single feature-only root record **`x-loam-baselines`** (`{
@@ -1481,6 +1633,93 @@ the source for tests.
1481
1633
  component copying is verdict-driven, with genuinely new components copied only when reachable from
1482
1634
  content actually written. Removal markers still carry no pin, since
1483
1635
  `openapi.remove-target-mismatch` already checks that exact slot.
1636
+ - **A retired operation does not come back by accident.** An operation the living contract does
1637
+ not hold is either new or *retired*, and the living contract alone cannot say which: both are
1638
+ simply absent. So the feature gate also reads what every **archived** feature took out — the
1639
+ `x-loam-remove: true` markers left in `features/archive/<FEAT>/specs/<svc>/openapi.yaml` and
1640
+ `asyncapi.yaml`, through the same relocation rule the gate applies to an in-flight delta (a
1641
+ marker on an id the same delta redefines elsewhere moved it, and retired nothing). A delta entry
1642
+ that is not itself a removal, that its service's living contract does not hold, and that an
1643
+ archived feature retired is **`openapi.op-revived`** (by operationId — a
1644
+ retired operation quoted at a new path is the same operation back) or **`asyncapi.slot-revived`**
1645
+ (a `channels` or `operations` slot by its key; a message by its *name*, wherever it is declared).
1646
+ A relocation retires nothing, but it VACATES an address — and so does a retirement, once a later
1647
+ feature re-adds the id somewhere else — so the index records, per id, every address an archived
1648
+ feature's executed marker vacated, which feature, and for a move where its delta put the id. The
1649
+ finding is judged on the contract the archive would WRITE: the check runs the archive's own
1650
+ merge of the delta over the living contract and compares where each operationId stands there with
1651
+ where it stands now. An (operationId, address) the merged contract has and the living one lacks
1652
+ is `openapi.op-revived` when the living contract holds the id nowhere and an archived feature
1653
+ retired it (anywhere — by identity) or moved it away from that very address, or when the merged
1654
+ contract answers the id at two or more addresses and an archived feature retired it at, or moved
1655
+ it away from, that very address: archived, it would answer the id at both. An id that the living
1656
+ contract still holds and the merged contract answers at exactly one address is never a revival —
1657
+ a delta moving it back with its own removal marker where it lives now, or swapping another
1658
+ operation in there (named or not: the merge writes per method and path), leaves it at one
1659
+ address — while a marker standing where the id does not live deletes nothing, and excuses
1660
+ nothing. One address is no exemption for an id the living contract holds nowhere: that is the
1661
+ first case, so a quote of an address a move vacated is the finding even after a hand edit took
1662
+ the id out of where the move put it. Reading the merged contract replaced a
1663
+ case-by-case re-derivation of the merge that each review found a new hole in.
1664
+ On the event axis the same rule runs on the message name, over the event merge: the addresses a
1665
+ feature vacates are `components.messages` keys (a marker there) and the inline declarations a
1666
+ channel removal took. A delta that restates the channel an inline declaration stands in (with
1667
+ the message as a `$ref`) or retires that channel takes the name out, since a channel upsert
1668
+ replaces the channel's interior; a marker nested on the inline message does not
1669
+ (`asyncapi.remove-marker-inline`), so the finding names the channel as the remedy. A channel or
1670
+ operation slot's identity is its key, so moving one is a retirement plus an addition, graded as
1671
+ such. A message declared inline under a channel the delta brings back is that channel's
1672
+ interior, and the channel's finding stands for it — unless the merged contract declares the
1673
+ name somewhere else too (an archived feature re-declared it under `components` when it retired
1674
+ the channel): then the inline declaration is a second place of its own, and gets its own
1675
+ finding, since the channel's says nothing of the duplicate `--approve` would archive.
1676
+ Both are warnings that **gate** the archive, `openapi.baseline-missing`'s doctrine: the document
1677
+ is legal, and re-adding a retired operation can be exactly what a feature means — a retirement
1678
+ rolled back — so `--approve` carries the call, and the archive output then names what came back
1679
+ and whose retirement it reverses. The message offers both remedies: delete the quote if it only
1680
+ restated the operation; archive with `--approve` if the feature brings it back on purpose (a pin
1681
+ over nothing is `openapi.baseline-invalid`, so a deliberate re-add drops it first). At a vacated
1682
+ address beside a living one it names the address to drop instead: delete the quote, or, to move
1683
+ the id back on purpose, take it out of where it lives now in the same delta (a removal marker
1684
+ there; for a message declared inline, restate or retire its channel; for a `components.messages`
1685
+ key, also restate every channel or operation of the merged contract still `$ref`-ing it, with
1686
+ the `$ref` pointed at the quoted key — the message names them, since a marker alone would leave
1687
+ them pointing at nothing and their send would stop sending the message) — and where the delta
1688
+ already spells that address, so no marker can stand there, archive with `--approve`. A slot
1689
+ holds one entry, so where the living contract has ANOTHER operation (or message) at the address
1690
+ the delta spells the id at, the merge replaces it there: the message names it ("now holds
1691
+ 'requestCancellation', and archiving this delta replaces it there"), and the move-back remedy
1692
+ says to define it at another address (another key, for a message) in the same delta if it is
1693
+ to stay — never an address
1694
+ "the living contract no longer has", a remedy that once led to a clean gate and an archive that
1695
+ deleted the other entry unnamed. `loam rebase`
1696
+ never pins such an entry as new: it prints the same finding (`revived[]` in `--json`) and leaves
1697
+ the decision to the author. It asks the question of the delta as it will WRITE it, pins
1698
+ included: a quote pinned to its own content is skipped by the merge, and the same quote
1699
+ repinned to whatever now lives at its address is an edit the merge writes — so a revival
1700
+ rebase's own repin creates is in that list, `--dry-run` included, and the next validate agrees. A channel-slot removal retires the messages its living channel
1701
+ declared inline, which the delta never spells, so those names are read from the archived
1702
+ feature's undo snapshot (`.loam-before/`, the pre-image of the contract the merge ran against) —
1703
+ only while that pre-image still hashes to what the manifest recorded. A marker is a request, not a
1704
+ record of what happened: the OpenAPI merge deletes a living operation only when the one at the
1705
+ marker's own method and path carries the marker's `operationId`, and the event merge deletes a
1706
+ `channels` or `operations` slot only when the living contract has that key, and a
1707
+ `components.messages` slot only when the message declared there carries the marker's name. A
1708
+ marker that missed — archived past `remove-target-missing` or `remove-target-mismatch` under
1709
+ `--approve` — deleted nothing, so each marker counts only if the same pre-image held its target
1710
+ (a contract the archive created, or never wrote, lost nothing to it); a feature with no intact
1711
+ snapshot cannot be checked, and its markers count as written. Two marker
1712
+ shapes are never sources, because neither can match anything: a marker with no `operationId`
1713
+ (`openapi.remove-marker-anonymous`) and one written at path level
1714
+ (`openapi.remove-marker-path-level`) — a feature archived past either under `--approve` left the
1715
+ operation living.
1716
+ **The honest limit:** the check knows only what the files under `features/archive/` record. A
1717
+ removal made by hand-editing a living contract leaves no marker anywhere, and an archived feature
1718
+ deleted from disk takes its markers with it; both read as "never retired", and a quote of such an
1719
+ operation grades as an addition exactly as it did before the check existed. A channel's inline
1720
+ messages drop out the same way when its feature carries no intact pre-image (archived before the
1721
+ snapshot existed, or the snapshot edited or removed since) — the channel slot itself is still
1722
+ caught. No git history is read: the archive directory is the record.
1484
1723
 
1485
1724
  A requirement may carry a stable identity in its body: `Requirement-ID: <id>`, where the ID matches
1486
1725
  `[A-Za-z][A-Za-z0-9._-]{0,127}`. It is optional for backward compatibility. When present in a delta,
@@ -1533,7 +1772,10 @@ scenarios exercise. Comma-separated, same keep-last-line quirk (and the same rep
1533
1772
 
1534
1773
  An entry that resolves to nothing is `covers.unknown` (**warning**, with close ids offered where
1535
1774
  they exist): the typo guard, because a mistyped entry silently costs exactly the coverage it was
1536
- written for.
1775
+ written for. It is suspended — every form at once — for a service whose `model.likec4` could not be
1776
+ read, whether the model itself does not parse (`c4.invalid`) or it extends a map that does not
1777
+ (`spine.landscape-invalid`): an entry naming a container of an unread model is not a typo, and the
1778
+ one repair brings the whole grade back on the next run.
1537
1779
 
1538
1780
  **Coverage obligations** — both warnings, deliberately: they never gate `archive`, and `--strict` is
1539
1781
  the CI escalation.
@@ -1649,12 +1891,15 @@ digest:
1649
1891
  - stamps: line 1 is `# generated by loam v<version> — …` (the AGENTS.md stamp pattern — the file is
1650
1892
  never to be hand-edited), and each scenario carries the tag `@loam-digest-<16hex>` on the line
1651
1893
  above its `Scenario:` keyword: the first 16 hex characters of `loam verify`'s scenario body hash —
1652
- sha256 of the body lines joined and edge-trimmed, salted with the file name on the arch axis, the
1653
- exact recipe `scenario.tested` claim ids fold in — so the stamp, the claim and the spec can never
1654
- disagree about what a scenario says, and identically-worded scenarios across `spec.md` and
1655
- `arch.spec.md` can never share a digest (a business-axis test run cannot answer for an arch
1656
- scenario). A tag rather than a comment because cucumber's JSON report carries tags per scenario
1657
- (`elements[].tags`): the stamp rides through the runner untouched, which is what
1894
+ sha256 of the body lines joined and edge-trimmed, salted with the owning SERVICE and, on the arch
1895
+ axis, with that axis's file name — the exact recipe `scenario.tested` claim ids fold in — so the
1896
+ stamp, the claim and the spec can never disagree about what a scenario says. Both salts are there
1897
+ for one reason: a digest that spanned two namespaces would let a green run answer a question
1898
+ nobody ran a test for. Identically-worded scenarios across `spec.md` and `arch.spec.md` can never
1899
+ share a digest (a business-axis test run cannot answer for an arch scenario), and neither can two
1900
+ services wording a scenario the same way (one repository's green run cannot answer another
1901
+ repository's claim). A tag rather than a comment because cucumber's JSON report carries tags per
1902
+ scenario (`elements[].tags`): the stamp rides through the runner untouched, which is what
1658
1903
  `loam verify --results` matches on.
1659
1904
 
1660
1905
  **Staleness** (`loam validate --service <id>`, service-repo-scoped like the `sources.*` chain):
@@ -1727,6 +1972,12 @@ requirement also needs a current `Based-On:` pin before archive; `loam rebase <F
1727
1972
  Omitting or leaving the intent as scaffold text raises the gating `intent.empty` /
1728
1973
  `scaffold.placeholder` warning, so the old one-file shape is not archive-ready.
1729
1974
 
1975
+ Below that floor is no feature at all. A change that touches no requirement, no operation and
1976
+ no fleet edge — a refactor, a dependency bump, a fix that brings the code back to behaviour the
1977
+ living spec already documents — leaves the docs repo untouched: it is a code PR, then
1978
+ `loam validate --service <id>` in the service repo, where `sources.stale` names the moved paths, and
1979
+ the re-vouch they owe (`loam vouch --pack` is the reading list; the vouch is a person's).
1980
+
1730
1981
  Each axis may be legitimately empty, and the checks grade absence differently from
1731
1982
  emptiness-by-accident:
1732
1983
 
@@ -1737,7 +1988,8 @@ emptiness-by-accident:
1737
1988
  declared-but-untagged is almost always a forgotten tag, while declaring nothing is a legible
1738
1989
  statement that the architecture is unchanged.
1739
1990
  - **openapi delta** — absent whenever the feature adds no operations; a requirement change that
1740
- stays inside the existing contract has no API axis to speak on.
1991
+ stays inside the existing contract has no API axis to speak on. Present, it holds only the
1992
+ operations the feature adds, changes or removes.
1741
1993
  - **requirement delta** — the axis that is nearly always present, because a feature that changes no
1742
1994
  requirement changes no promised behaviour. The exception is architecture-only work: a new service
1743
1995
  in the C4 delta with no `specs/<svc>/spec.md` gets `service.no-requirement-delta` as a warning,
@@ -1750,6 +2002,16 @@ a feature cannot fold into the living docs with its reason unwritten. Its siblin
1750
2002
  `scaffold.placeholder` refuses the rest of an unedited `loam new` scaffold the same way — the exact
1751
2003
  template strings, a requirement or description nobody authored, must not become living truth.
1752
2004
 
2005
+ The scaffolded `intent.md` also carries an optional `## Open questions` heading over a comment.
2006
+ It is where `loam-feature`'s analyze step — between authoring and `loam rebase` — writes what the
2007
+ documents leave open: a SHALL two engineers could build two ways, a value or window two services
2008
+ state differently and which side refuses, a `Realizes:` that does not deliver its capability, an
2009
+ edge with no stated timeout, retry, partial-failure or compensation answer, a neighbour left out of
2010
+ `--touches`. A person answers by editing the file. Nothing grades the section: `intent.empty` reads
2011
+ past it, so questions written there are never mistaken for the Why, and they never gate the archive —
2012
+ every check loam has is a join between exact tokens, and whether two services MEAN the same thing by
2013
+ one is the part no join reads.
2014
+
1753
2015
  ### Where a capability lives (a declared name, and the document behind it)
1754
2016
 
1755
2017
  A business capability — "payment splitting" — is spread across the living specs of every service
@@ -1916,13 +2178,19 @@ refusing it, which is why the flattening is a whitelist and not a list of substi
1916
2178
  document declares no requirements yet, it declares none flattening to this slug (close ids
1917
2179
  offered, each already spelled as the tag to write), or two of its ids flatten to one slug.
1918
2180
 
1919
- **Either reserved prefix opts a view in.** A view carrying `#req-` and no `#cap-` is an author who
2181
+ **Any reserved prefix opts a view in.** A view carrying `#req-` and no `#cap-` is an author who
1920
2182
  asked to be graded and forgot half the claim; it earns the `unscoped` message rather than silence.
1921
- That opt-in is **one predicate**, and every reader takes it: `loam validate`, `loam diff`,
1922
- `loam delta`, `loam status`, the context and explore packs and `loam list capabilities` all decide
1923
- "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
1924
- byte scan that lets a fleet with no use cases skip the LikeC4 load looks for both prefixes for the
1925
- same reason — a gate narrower than the predicate is silence, not a smaller answer.
2183
+ A view carrying only `#suite-<name>` is a use case too (see the layout section above). That opt-in
2184
+ is **one predicate**, and every reader takes it: `loam validate`, `loam diff`, `loam delta`,
2185
+ `loam status`, the context and explore packs, `loam list capabilities` and `loam list suites` all
2186
+ decide "is this a use case" the same way, so a flow one of them can see is a flow all of them can
2187
+ see. The byte scan that lets a fleet with no use cases skip the LikeC4 load looks for every prefix
2188
+ — `cap-`, `req-` and `suite-` — for the same reason: a gate narrower than the predicate is silence,
2189
+ not a smaller answer. It looks for each written as a tag REFERENCE: a `#`, then only what LikeC4
2190
+ skips between the `#` and a tag's name (whitespace, line breaks, comments), then the prefix — the
2191
+ only way a view carries a tag. A prefix in prose, a title or a `tag cap-…` declaration does not open
2192
+ it, so a hand-drawn `.likec4` beside a service's model that names `suite-` in its title, carries no
2193
+ tagged view and does not parse is not loaded and earns no `usecase.flow-invalid`.
1926
2194
 
1927
2195
  Being SEEN is not the same as claiming something, and the difference is worth stating because it
1928
2196
  looks like a gap and is not. A view with no resolved `#cap-` tag claims no capability, so it seeds
@@ -2107,7 +2375,10 @@ ready. Several useful product shapes remain outside that boundary on purpose:
2107
2375
  a third thing to keep truthful.
2108
2376
  - **Authored `tasks.md` as authoritative state** — loam derives its actionable checklist (`loam
2109
2377
  delta`, `loam verify`), so it cannot drift from the delta. Imported task lists are preserved as
2110
- non-authoritative migration evidence until a human chooses their disposition.
2378
+ non-authoritative migration evidence until a human chooses their disposition. The refusal stands,
2379
+ and the derived projection now exists as `loam tasks`, which answers the same question the file
2380
+ would have — recomputed from the delta, the generated suite and the verification record on every
2381
+ run, so there is nothing to tick and nothing to go stale.
2111
2382
  - **Per-tool workflow forks** — `AGENTS.md` is the tool-agnostic contract. Generated adapters wrap
2112
2383
  shared bodies and are never independent process definitions.
2113
2384
 
@@ -2198,6 +2469,19 @@ which carry them across loam upgrades. Correspondingly, `archive` refuses to rep
2198
2469
  version-3 claim graded at its service's current directory: the previous archive's writes are still
2199
2470
  in the docs and those files are the only way back.
2200
2471
 
2472
+ The manifest also carries one key that is not about bytes, added without a version bump because
2473
+ nothing that restores reads it: **`checklist`**, the digest of the checklist a fresh `loam verify`
2474
+ derived for the feature just before the merge — written when the feature carried a readable
2475
+ verification record then, absent otherwise and in every manifest an older loam wrote. It is the
2476
+ question the feature asked as it shipped. Once archived, a record is frozen history and the checklist
2477
+ cannot be re-derived (the merge moved the feature's operations into the living contract, so a
2478
+ derivation after it can only mismatch), so this is the one thing an archived record can still be
2479
+ held to: `loam gate --since` grades the record against it, and a record that answered an older
2480
+ checklist reads `stale`. `unarchive` takes the snapshot away with the rest of it, so a feature
2481
+ archived again gets a fresh capture. `list`, `status` and `verify` keep reading an archived record as
2482
+ frozen history; a manifest with no `checklist` leaves the gate reading it that way too, and saying
2483
+ so.
2484
+
2201
2485
  Some checks run at plan time rather than in the gate, because only the computed merge can see them.
2202
2486
  A **living** requirement sitting outside `## Requirements` refuses the whole archive
2203
2487
  (`living.requirement-outside-requirements`, error): the merge rewrites only that section, so the
@@ -2205,7 +2489,9 @@ requirement would land in the file twice — and `--approve` does not override i
2205
2489
  duplication is mechanical, not a judgment call; re-home the requirement first. A delta that
2206
2490
  redefines a path+method the living OpenAPI already has (`openapi.op-modified`, warning) merges
2207
2491
  anyway, but the plan says so — the overwrite is wholesale, and the living definition it replaces
2208
- deserved an eye. An unparseable `delta.likec4` fails the archive at plan time regardless of
2492
+ deserved an eye. One that defines a different operationId there, leaving a consumed operation
2493
+ defined nowhere, never reaches the plan without `--approve`: the gate refuses it first
2494
+ (`openapi.remove-op-consumed`, the overwrite shape). An unparseable `delta.likec4` fails the archive at plan time regardless of
2209
2495
  `--approve`: the alternative was archiving with the architecture axis silently dropped, which is the
2210
2496
  data loss this tool exists to prevent.
2211
2497
 
@@ -2213,7 +2499,8 @@ The landscape merge **splices authored source, it does not re-serialize**. Each
2213
2499
  relationship is copied into the living landscape byte-for-byte from `delta.likec4` — technology,
2214
2500
  style, icons, links, metadata, nested children, every tag — with exactly one edit: the feature's own
2215
2501
  tag is stripped (the additions are baseline now — the drop above is why unarchive restores bytes),
2216
- and a construct the strip empties goes with it (`x = kind 'y' { #FEAT-1 }` lands as `x = kind 'y'`).
2502
+ together with the `,` or `;` that set it apart in a tag list (`#FEAT-1, #x` lands as `#x`), and a
2503
+ construct the strip empties goes with it (`x = kind 'y' { #FEAT-1 }` lands as `x = kind 'y'`).
2217
2504
  Placement is deterministic and **service-grouped**, and never moves an authored byte — it only
2218
2505
  chooses where the new blocks go. A top-level element whose service the model already draws (the
2219
2506
  `metadata { service }` binding first, the title as fallback) lands immediately after the last
@@ -2231,22 +2518,32 @@ services) yields the byte-identical landscape to B then A, so concurrent archive
2231
2518
  different lines and git merges them without help. The insertions carry no marker comment — which
2232
2519
  archive added what is the git history's to say. A **nested** element lands inside its parent's block
2233
2520
  — inside the living parent when the landscape already has it, riding verbatim inside the spliced
2234
- parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level. LikeC4
2521
+ parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level. A
2522
+ landscape may be written in **several top-level `model { }` blocks**, which LikeC4 reads as one
2523
+ model, and the merge reads all of them: an ANCHORED addition — nested in a parent, joining its
2524
+ service's neighborhood, or joining its source's edges — lands in its anchor's block, and an
2525
+ unanchored one lands in the trailing region of the **first** block, the one every addition went to
2526
+ before a second block was readable. The walks never cross a block boundary, so the scheme stays
2527
+ order-independent, and a one-block map places exactly as before. LikeC4
2235
2528
  reference resolution is not order-dependent, so a relationship placed before an element it names
2236
2529
  still parses — and because splicing is text surgery, the computed landscape is **parsed in memory
2237
2530
  before anything is written**: a result LikeC4 rejects — a splice bug, or legal inputs the living
2238
2531
  document cannot absorb, like an element kind or tag its `specification` block never declares —
2239
2532
  refuses the archive at plan time (`merge-failed`, nothing written), the same discipline as the
2240
- unparseable delta. A living declaration sharing its line with another statement (legal LikeC4 the
2241
- splice map cannot see around — its statement scan runs to the end of the line) refuses the same way:
2242
- give each top-level declaration its own line, then re-run.
2533
+ unparseable delta. A living declaration the splice map cannot locate refuses the same way rather
2534
+ than be spliced around blind — most often one sharing its line with another statement, which is
2535
+ legal LikeC4 the splice map cannot see around, since it reads one statement per line. The refusal
2536
+ names the line LikeC4 parsed the declaration on and what the splice map read there instead, and
2537
+ says "shares that line" only when the line shows it: give each top-level declaration its own line,
2538
+ then re-run.
2243
2539
 
2244
2540
  **Which document an addition lands in.** The C4 merge asks it per declaration, not per feature. An
2245
2541
  element whose PARENT resolves to a service whose `model.likec4` extends the map is spliced into that
2246
2542
  model — and so is the interior a delta writes inside a service element the same archive introduces,
2247
2543
  since those containers carry no feature tag and would otherwise ride onto the map inside their
2248
2544
  parent's bytes. The parent chain has to be spelled in the map's own ids for either to happen. The
2249
- splice goes inside the `extend <fqn> { … }` block for the parent — appended when the model has none, as
2545
+ splice goes inside the `extend <fqn> { … }` block for the parent — in whichever `model { }` block
2546
+ holds it, and appended to the first one when the model has none, as
2250
2547
  the `model { }` block itself is appended to a model that has none (an empty file, a `views { }`-only
2251
2548
  file and a tags-only `specification { }` are all extending models, and all three are legal to
2252
2549
  archive into). A
@@ -2349,7 +2646,9 @@ design. The heading must be reachable, too — a leading UTF-8 BOM used to hide
2349
2646
 
2350
2647
  ## The verification record
2351
2648
 
2352
- `features/<FEAT>/verification.yaml` is the done-check written down. `loam verify <FEAT>` derives a
2649
+ The done-check is written down inside the feature: one `features/<FEAT>/verification/<svc>.yaml` per
2650
+ attesting service, or — for the all-at-once form, and every record written before the directory —
2651
+ the one file `features/<FEAT>/verification.yaml`. `loam verify <FEAT>` derives a
2353
2652
  **checklist** from the feature's own artifacts — the same files `validate` reads, never the code:
2354
2653
 
2355
2654
  | kind | one claim per | reads |
@@ -2373,18 +2672,26 @@ nobody wrote must not carry over as if it still applied.
2373
2672
  anything that does not answer the *current* checklist, each under its own `--json` `error.code`:
2374
2673
  `answers-unreadable` (not JSON, or a verdict outside `confirmed` / `unconfirmed`),
2375
2674
  `answers-mismatch` (an id nobody asked about, a claim with no answer, or one answered twice),
2376
- `answers-unevidenced` (a `confirmed` with no `file:line` behind it). An unchecked claim must never
2377
- be able to masquerade as checked.
2675
+ `answers-unevidenced` (a `confirmed` with no `file:line` behind it, or one citing a file or a line
2676
+ that does not exist in the repository the record is written from — the all-at-once form included,
2677
+ which binds to no commit but no longer takes a citation on trust). The all-at-once form checks what
2678
+ is shaped like a citation and only that: an evidence entry that is one token ending in `:<digits>`
2679
+ and is not a URL must name a regular file inside the repository with that line, while prose ("manual
2680
+ QA by Alice"), a URL or a path with no line is recorded as it always was. The federated form keeps
2681
+ its stricter contract — every agent entry a `file:line` bound to the attested commit. An unchecked
2682
+ claim must never be able to masquerade as checked.
2378
2683
 
2379
2684
  **`--results <report.json>` answers the `scenario.tested` claims mechanically, from a cucumber JSON
2380
- test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave and SpecFlow
2381
- speak too. The contract is exactly this and nothing more: a top-level array of features, each
2685
+ test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave,
2686
+ SpecFlow/Reqnroll, pytest-bdd (`--cucumberjson`) and godog's cucumber formatter speak too. The contract is exactly this and nothing more: a top-level array of features, each
2382
2687
  `elements[]` (scenarios) with `name`, `tags[] {name}` and `steps[] {result {status}}` — plus the two
2383
2688
  places those dialects put a failure the steps never see: cucumber-jvm's per-element
2384
2689
  `before[]`/`after[]` hook results and behave's element-level `status`. Every other field is ignored,
2385
2690
  tagless elements (backgrounds, hand-written scenarios) are invisible, and a file that is not that
2386
2691
  array at all refuses under `answers-unreadable` rather than quietly answering every claim "not
2387
- found".
2692
+ found". A file that is not JSON at all — a JUnit XML report is the usual one — refuses the same way,
2693
+ and the refusal names both shapes `--results` reads, because the second one exists for exactly that
2694
+ runner.
2388
2695
 
2389
2696
  Matching is by digest and nothing else: a claim is answered by the report scenarios carrying its
2390
2697
  `@loam-digest-<16hex>` tag — the tag `loam gherkin` stamped, the same 16 hex of the same body hash
@@ -2393,12 +2700,21 @@ name), so a business run can never answer an arch claim however identically the
2393
2700
  worded. Names never match anything: a reworded spec scenario matches nothing until the suite is
2394
2701
  regenerated and re-run — an agent must not be able to SAY a scenario is tested; only a green run
2395
2702
  may. The verdicts: every matching occurrence (a digest the report holds twice is a re-run, and all
2396
- occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`, and
2397
- the element-level status (when the dialect carries one) is `passed` → `confirmed`, with evidence
2703
+ occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`, the
2704
+ element-level status (when the dialect carries one) is `passed`, and **it ran exactly as many steps
2705
+ as the stamped scenario has** — the count `loam gherkin` emits for it, `And`/`But` included →
2706
+ `confirmed`, with evidence
2398
2707
  `<report-path>: <feature uri or name> › <scenario name>` per occurrence; a failed / undefined /
2399
2708
  pending / ambiguous step (`failed at step N`), a failing hook (`failed in a before/after hook`), a
2400
2709
  failed element status, a skipped-only run (`skipped`), or no match at all (`not found in report`) →
2401
- `unconfirmed` with the reason as the note.
2710
+ `unconfirmed` with the reason as the note — and a run whose step count differs from the stamped
2711
+ scenario's is `unconfirmed` too (`ran N step(s) where the stamped scenario has M`). The digest is
2712
+ only a tag, and a tag is text: a one-step stub carrying the right digest used to confirm five-step
2713
+ scenarios, because nothing compared what ran with what was stamped. The count is what every dialect
2714
+ can be held to; the step TEXT is not compared, because an outline's steps reach the report with
2715
+ their placeholders filled in and dialects disagree about keyword spacing and localisation. The hook
2716
+ entries cucumber-js lists among `steps[]` (marked `hidden`, keyword `Before`/`After`) are read as
2717
+ hooks, never counted as steps — and a failing one still sinks the run.
2402
2718
 
2403
2719
  `--results` OWNS every `scenario.tested` claim **in the same run**: an answers-file entry for one is
2404
2720
  refused (`answers-mismatch` — the runner owns it), `--record` alongside must answer exactly the
@@ -2430,6 +2746,8 @@ own runner-neutral shape, chosen by its marker key:
2430
2746
  ```
2431
2747
 
2432
2748
  `digest` is the 16 lowercase hex of the `@loam-digest-…` tag `loam gherkin` stamped on the scenario.
2749
+ An entry carries no steps, so it is held to its `status` alone — the step-count check above is a
2750
+ property of the cucumber shape.
2433
2751
  `status` is `passed` or `failed` — nothing else. `test` is optional free text: the runner's own name
2434
2752
  for the run, used as the evidence string a reader follows. `loamScenarioReport: 1` is the format
2435
2753
  version this loam reads; any other value refuses rather than being partly understood.
@@ -2455,12 +2773,18 @@ carrying the marker is always graded as this shape, even when malformed: falling
2455
2773
  cucumber parser would tell an author their report is not a cucumber array, which is true, useless,
2456
2774
  and hides the real mistake.
2457
2775
 
2458
- **Two services wording a scenario identically share one digest**, and a single report then cannot
2459
- say whose suite ran it. Those claims are left `unconfirmed` with `verify.digest-contested` (warn)
2460
- naming both services and the affected claim ids, rather than confirming both from one run. Record
2461
- each service's claims from its own repository with `--service`, which narrows the checklist before
2462
- matching. Scenario digests are salted by *axis* (business versus arch) but **not** by service, so a
2463
- digest identifies scenario words; `--service` is what gives a report an owner.
2776
+ **Scenario digests are salted by both the owning service and the axis** (business versus arch), so
2777
+ two services wording a scenario identically do not share one: the hash folds in the service the
2778
+ claim is filed under. The service salt is what makes ordinary repeated wording CORRECT rather than
2779
+ merely refused — "the service returns 404 for an unknown id" is a sentence a fleet writes many
2780
+ times, and before the salt those scenarios shared a digest, so one repository's green run could
2781
+ appear to confirm claims from suites that never ran each other's tests, and the only defence was to
2782
+ decline to answer. `verify.digest-contested` (warn) survives as the guard on that invariant rather
2783
+ than as a diagnosis of ordinary authoring: if a digest is ever claimed by two services it is a
2784
+ truncated-hash collision, a single report still cannot say whose suite ran it, and those claims are
2785
+ left `unconfirmed` naming both services and the affected claim ids rather than confirmed from one
2786
+ run. Record each service's claims from its own repository with `--service`, which narrows the
2787
+ checklist before matching.
2464
2788
 
2465
2789
  ### Contract-test reports answer the `api.exposes` claims
2466
2790
 
@@ -2554,7 +2878,7 @@ service's as a pure lens — no repository binding, usable from the docs repo, r
2554
2878
  `unknown-service` when the checklist has no claims owned by that id — and both agents must have
2555
2879
  answered the same scope. Combining the flag with `--record`, `--results` or `--contract-results`
2556
2880
  refuses `invalid-option`, as does any arity other than exactly two files and an archived feature
2557
- (frozen history has no current checklist to answer). A `verification.yaml` too damaged to read does
2881
+ (frozen history has no current checklist to answer). A verification record too damaged to read does
2558
2882
  **not** block the diff — the lens never reads the record, and the moment a cross-examination is most
2559
2883
  needed is before re-recording over a broken one.
2560
2884
 
@@ -2574,7 +2898,7 @@ block pins each file's path and the sha256 of the exact bytes read.
2574
2898
  confirmed/unconfirmed/unanswered. The payload also deliberately carries **no `verified`, `verdict`
2575
2899
  or `attested` key at all**: two answer sets agreeing — usually two contexts of the same model, so
2576
2900
  correlated — ranks where a reviewer reads first; it is not verification, it upgrades nothing, and it
2577
- leaves `verification.yaml` byte-identical (no docs lock is taken, and the diff runs while a writer
2901
+ leaves the verification record byte-identical (no docs lock is taken, and the diff runs while a writer
2578
2902
  holds one).
2579
2903
 
2580
2904
  ### Attested is not verified
@@ -2609,28 +2933,79 @@ the decision:
2609
2933
  So "a green run is the only thing that confirms a scenario" is a claim about the **record**, not
2610
2934
  about the CLI's flags: the short path still works, and it is legible afterwards as the short path.
2611
2935
 
2612
- ### The record is federated: one file, one attestation per service
2936
+ ### The record is federated: one attestation per service, one file each
2613
2937
 
2614
2938
  A cross-service feature's claims belong to several services, and each service's code lives in its
2615
2939
  own repository — so no single place can honestly answer all of them.
2616
2940
  `loam verify <FEAT> --service <id> --record answers.json`, run **in that service's own repo**,
2617
2941
  narrows the checklist to that service's claims and binds the answers to this repository's commit.
2618
- The result is one `schema: 2` `verification.yaml` holding a list of **`attestations`**, one per
2619
- service: the service id, the commit the answers were bound to, and that service's answered claims.
2620
-
2621
- The merge semantics are additive and non-destructive. Recording for service B keeps service A's
2622
- attestation exactly as it stood — the new record is the old attestations minus B's previous one,
2623
- plus B's new one. Answers the new record does *not* carry are reported in `discarded`, tagged
2624
- `unattested` (a schema-1 answer, or one whose attestation is gone) or `off-checklist` (a claim the
2625
- current checklist no longer asks). Nothing is dropped silently.
2626
-
2627
- The write behind that merge holds the same advisory lock archive does, and holds it from before the
2628
- feature is resolved to after the record lands — two `--record` runs for different services serialize
2629
- (the later one waits briefly, then merges over what the first left, so **both** attestations
2630
- survive), and a record can no longer be written into a path an `archive` is renaming out from under
2631
- it. The commit itself is staged beside the target and swapped in by one rename over the exact bytes
2632
- the merge was computed from, so a kill mid-record leaves the previous record whole — never truncated
2633
- YAML — and an edit that landed in between refuses instead of being buried:
2942
+ The result is that service's **attestation**, written to a file of its own:
2943
+ `features/<FEAT>/verification/<id>.yaml` — a `schema: 2` record holding exactly that service's
2944
+ attestation (the service id, the commit the answers were bound to, the checklist digest it answered,
2945
+ and its answered claims) and nothing of anybody else's. **The directory is the record**, the rule
2946
+ `glossary/` and `capabilities/` already follow: loam reads every file in it and adds them up, and
2947
+ the verdict and the counts are computed from the union at read time — exactly as they always were,
2948
+ since nothing ever trusted a stored `summary`. Each file's own `summary` counts the whole checklist,
2949
+ with the claims other services own honestly `unanswered` in it.
2950
+
2951
+ **Why one file per service.** The record used to be ONE `verification.yaml` for every service, and
2952
+ every attestation rewrote its `summary`, its `recorded` date and its checklist-ordered `claims`. Two
2953
+ teams recording two different services on two docs-repo branches therefore always conflicted when
2954
+ the PRs merged — add/add from an empty start, content conflicts on the summary, the claims and the
2955
+ attestations once one had landed — and sorting the attestations did not help, because two
2956
+ insertions at one anchor are a conflict whatever the order. The advisory lock below serializes
2957
+ writers on one checkout only; a fleet records from many. With a file per service, two teams' records
2958
+ touch two different paths and merge cleanly. Recording a service again replaces that service's file
2959
+ and nothing else; another service's file is never rewritten, and its pins never touched.
2960
+
2961
+ The record answers **one version of the checklist**: the one the feature asks now — `loam verify`
2962
+ (read and record alike), `tasks`, and `status`, `list` and `gate` for an in-flight feature all
2963
+ derive it, and the files that answered it are the ones that count. Only where no reader knows the
2964
+ current version (the frozen view of an archived feature) or no file answered it does the newest
2965
+ file decide (by `recorded`, a later service id breaking a same-day tie). Newest-by-date used to
2966
+ decide always, and `recorded` is a date: a service that recorded the current checklist lost a
2967
+ same-day tie to a service with a later id that had recorded an older one, so the current answers
2968
+ were dropped and every reader called the record stale. A file that answered a different version
2969
+ keeps its attestation in the list — `verify.checklist-forked` names it, on the recording run's
2970
+ output as well as the read view — but its answers do not count until that service records again.
2971
+ The one shared file used to settle this by rewriting: the last writer pruned everybody's answers to
2972
+ its own checklist. Per-service files are never rewritten by anyone else, so nothing can prune them,
2973
+ and counting an answer filed against another question set would let an id the feature no longer
2974
+ asks fill the slot of the question that replaced it.
2975
+
2976
+ **`features/<FEAT>/verification/` is loam's.** Every non-dot `*.yaml` in it is read as one
2977
+ service's attestation file — the file name is the claim about whose word it holds — so a file there
2978
+ that is not one (a hand-made `k6.yaml`, say) makes the record `record-unreadable`, exactly like a
2979
+ damaged attestation. Keep anything else beside the feature, not inside that directory.
2980
+
2981
+ **The one-file record is read forever.** A `verification.yaml` written before the directory existed
2982
+ — a federated record with several attestations in it, active or archived — reads exactly as it
2983
+ always did while no service has a file of its own. Once services do, it is read beside them: a
2984
+ service that records again supersedes its share of the one-file record (that attestation and its
2985
+ answers drop out of the union; the file itself is not rewritten), and the write that leaves nothing
2986
+ in it counting removes it. An all-at-once record (no `schema`, no attestations) never counts once a
2987
+ service has a file — its answers carry no commit attestation — so the first `--service` write
2988
+ removes it in the same commit and lists its answers in `discarded`. Answers a write takes off the
2989
+ disk that its new file does not carry are reported in `discarded`, tagged `unattested` (an
2990
+ all-at-once answer) or `off-checklist` (a claim the current checklist no longer asks). Nothing is
2991
+ dropped silently.
2992
+
2993
+ **The all-at-once form keeps the one file, by decision.** `--record` without `--service` answers the
2994
+ whole checklist from one place on one word, with no commit and no service — so there is no second
2995
+ writer for it to collide with and no service to name a file after, and every record it ever wrote
2996
+ (the example fleet's included) stays byte-for-byte what it is. There is one reader either way:
2997
+ `loam verify`, `status`, `list`, `tasks`, `gate` and `validate` all read the record through the same
2998
+ union, whichever files it is made of.
2999
+
3000
+ The write holds the same advisory lock archive does, and holds it from before the feature is
3001
+ resolved to after the record lands — two `--record` runs for different services serialize (the
3002
+ later one waits briefly, then writes its own file, so **both** attestations survive), and a record
3003
+ can no longer be written into a path an `archive` is renaming out from under it. The commit itself
3004
+ is staged beside the target and swapped in by one rename over the exact bytes the read found, so a
3005
+ kill mid-record leaves the previous record whole — never truncated YAML — and an edit that landed in
3006
+ between refuses instead of being buried. A write that also removes a superseded one-file record
3007
+ holds that removal to the same compare, and lands the service's own file first, so a kill between
3008
+ the two leaves a record that still reads whole:
2634
3009
 
2635
3010
  - `docs-busy` — another writer held the lock longer than the record was willing to wait (5 s).
2636
3011
  Nothing was read or written; re-run once it finishes. The contention runs both ways: `archive`,
@@ -2651,9 +3026,10 @@ Recording refuses rather than guesses, each under its own `--json` `error.code`:
2651
3026
  - `repository-unavailable` — `--service` from a repo that declares none, or one whose commit cannot
2652
3027
  be read. An attestation with nothing to bind to is not an attestation.
2653
3028
  - **`record-federated`** — the legacy all-at-once form (`--record` with **no** `--service`) over a
2654
- record that already carries attestations. That form writes the whole file from one place, so it
2655
- would erase every other repository's evidence; loam refuses outright and names the attestors, and
2656
- never silently migrates or merges. Re-run with `--service`.
3029
+ record that already carries attestations, in per-service files or in a one-file record. That form
3030
+ writes the whole record from one place, so it would put one unattested answer set over every
3031
+ other repository's evidence; loam refuses outright and names the attestors, and never silently
3032
+ migrates or merges. Re-run with `--service`.
2657
3033
  - `record-unreadable` — see below.
2658
3034
 
2659
3035
  Reading is unrestricted: `loam verify <FEAT> --service <id>` **without** `--record` is a pure lens
@@ -2662,12 +3038,26 @@ in `services`, which is how you find out which repositories still owe an attesta
2662
3038
 
2663
3039
  The record is YAML, like the other data artifacts, and holds the claim text next to each verdict so
2664
3040
  it reads without loam. Because it has to survive without loam, human edits are expected — and read
2665
- back defensively. A file that exists but does not read as a record (broken YAML, or a shape missing
3041
+ back defensively. A record file that exists but does not read as a record (git conflict markers a
3042
+ merge left in it — named before the YAML parser sees them, since the parser either fails on a line
3043
+ that is not the problem or reads both sides as one plausible document; broken YAML; a shape missing
2666
3044
  what the readers use — `summary` with its counts, `recorded`, `checklist`, claims each with an `id`
2667
- and a known `verdict`) is **`record-unreadable`**: reported with the YAML line, and **never
3045
+ and a known `verdict`; or a `verification/<svc>.yaml` holding anything but that one service's
3046
+ attestation and answers) is **`record-unreadable`**: reported with the file and the line, and **never
2668
3047
  overwritten**. It used to count as *no record at all*, which was the wrong direction on the one
2669
3048
  artifact loam cannot reconstruct — "no record" invites writing a fresh one over evidence somebody's
2670
- edit merely broke. Only a genuinely absent file is "no verification record". Extra keys a human adds
3049
+ edit merely broke. Only a genuinely absent file is "no verification record".
3050
+
3051
+ The refusal names the ONE file that failed and the repair that is safe for it, because the two kinds
3052
+ of file call for opposite advice. A service's own `verification/<svc>.yaml` holds that service's
3053
+ word and nobody else's — after a merge, both sides of a conflict in it are that service's own
3054
+ attestations — so repairing it by hand, or deleting it and recording that service again from its
3055
+ repository, loses no other team's evidence. The one-file `verification.yaml` may hold every other
3056
+ service's attestation, so its advice is to repair it or **restore it from version control** (after a
3057
+ merge, the copy on the branch you merged into) and then record again — **never to delete it**. The
3058
+ old one-size advice, "delete it and record again", erased other teams' attestations when followed on
3059
+ a shared record, and the miscount advice, "record the answers again", sent the reader straight back
3060
+ into this refusal: `--record` will not write over a file it could not read. Both now end in a record. Extra keys a human adds
2671
3061
  still pass through untouched. Every verdict names who answered it — `answered_by: runner` (a
2672
3062
  cucumber report's green run, mechanical), `answered_by: external-runner` (a contract-test report's
2673
3063
  operationId-matched green entry, mechanical too), or `answered_by: agent` (somebody's word about the
@@ -2737,7 +3127,7 @@ next `--record` makes it drift-checkable), warn `evidence.unresolved` (the cited
2737
3127
  unsafe, not a regular file, or the cited line is past its end), `evidence.moved` (the file's digest
2738
3128
  differs while the cited line still reads as pinned), `evidence.line-changed` (the cited line no
2739
3129
  longer says what was recorded), `evidence.token-missing` (the file contained the token at the
2740
- attested commit and no longer does) and `evidence.record-unreadable` (a verification.yaml that
3130
+ attested commit and no longer does) and `evidence.record-unreadable` (a verification record file that
2741
3131
  exists but cannot be read — none of its evidence was checked, and a silent skip would read as clean,
2742
3132
  on the very file a hand edit to the pins most plausibly breaks). Archived records are deliberately
2743
3133
  not linted — frozen history, and post-ship code drift is normal evolution. From the docs repo the
@@ -2747,13 +3137,18 @@ verdict in either direction, `attested` never silently becomes `verified`, and t
2747
3137
  fail `validate`; `--strict` remains the CI lever it always was.
2748
3138
 
2749
3139
  It lives inside the feature, so `archive` carries it into `features/archive/<FEAT>/` with everything
2750
- else. `loam verify` re-run later compares `checklist` against the current digest and reports the
3140
+ else — the `verification/` directory moves with the feature it sits in, and `unarchive` brings it
3141
+ back the same way; the frozen view reads it through the same union. `loam verify` re-run later compares `checklist` against the current digest and reports the
2751
3142
  record as **stale** if the feature moved under it — while the feature is active. Once archived there
2752
3143
  is no current checklist to be stale against (the merge itself moved the feature's operations into
2753
3144
  the living OpenAPI, so a re-derived checklist could only disagree): `loam verify` on an archived
2754
3145
  feature renders the record verbatim as **frozen history** (`frozen: true` under `--json`, no
2755
3146
  staleness judgment), and `--record` refuses (`invalid-option` — there is no current checklist for
2756
- the answers to answer): the record is about the words that shipped, and it stays that way.
3147
+ the answers to answer): the record is about the words that shipped, and it stays that way. One
3148
+ reader holds it to something all the same: `loam gate --since <ref>` grades an archived record
3149
+ against the checklist digest `archive` captured as the feature merged (the snapshot manifest's
3150
+ `checklist`, above), because the deploy question is whether what shipped was what was checked — a
3151
+ record that had gone stale before the archive is `stale` there, never `verified` again.
2757
3152
 
2758
3153
  **Verification does not gate `archive`.** Coherence gates because loam computed it from the
2759
3154
  documents; a verdict is an agent's word about code loam never read, and a gate in front of shipping
@@ -2762,6 +3157,72 @@ comes later, and an `unconfirmed` claim with a note is worth more there than a `
2762
3157
  can back up. `loam verify` exits 0 either way; branch on `verified` in the `--json` payload, or on
2763
3158
  `verdict` when you need to tell `attested` from `unverified`.
2764
3159
 
3160
+ ### The same checklist read forwards (`loam tasks`)
3161
+
3162
+ `loam verify` answers *answered, and by whom*. `loam tasks <FEAT> [--service <id>] [--next]
3163
+ [--json]` answers the other half of the same question — *still open, at which step, and what fact
3164
+ closes it* — and it is the identical checklist read in the other direction. It is derived on every
3165
+ run from the feature's deltas, the generated suite and the verification record; it writes nothing,
3166
+ anywhere, ever, and it adds no artifact for a later run to disagree with.
3167
+
3168
+ **One unit per promise, and a unit's id is the claim's id.** Every claim the checklist derives is
3169
+ one work unit, under the same identity on both commands, so the thing to do and the answer that
3170
+ closes it can never be two different objects. One kind of unit has no claim behind it: an ADDED or
3171
+ MODIFIED requirement with no scenario under it asks the checklist nothing — there is no scenario to
3172
+ test — so `loam tasks` mints a unit of its own for it, of the kind requirement.described. That is
3173
+ the promise nobody has written acceptance criteria for yet, and it is where the loop starts.
3174
+
3175
+ **Where a unit stands** is its `at` field, and the four values are the steps of the loop this
3176
+ document already describes rather than a percentage: `describe` (the requirement carries no scenario
3177
+ at all, so there is nothing to generate a test from), `generate` (the scenario exists and this
3178
+ repository's generated suite does not carry its stamp yet), `implement` (everything else still
3179
+ open), `attested` (an agent's word confirmed the claim and no run has). It is spelled `at`
3180
+ deliberately, and not `stage` or `verdict`: both of those already mean something else on payloads an
3181
+ agent reads beside this one, and a third meaning for either word is how a consumer comes to branch
3182
+ on the wrong field.
3183
+
3184
+ **Done is a fact, not a tick.** A closed unit simply stops appearing, and exactly one thing closes
3185
+ one: a claim recorded `confirmed` in a verification record that is **not stale** against the current
3186
+ checklist. A stale record closes nothing — the words moved under the answers, which is the state
3187
+ `loam verify` already reports. An agent-attested scenario never vanishes either: it stays visible at
3188
+ `attested`, because attested is not verified, and a work list that hid the difference would be
3189
+ exactly the gate that teaches everyone the cheapest way past it is to say yes.
3190
+
3191
+ **The frontier** is the first ten open units in derived order, providers before consumers — ordered
3192
+ on the exact token join `c4.calls.token == api.exposes.operation`, never on a reading of prose.
3193
+ **The order is advice and never a lock**: nothing is assigned, nothing is reserved, and working out
3194
+ of order is legal and invisible to every other command.
3195
+
3196
+ **`--next` is the leaf**: the same packet with `units` narrowed to the frontier head, which is what
3197
+ an orchestrator hands a coding agent. Everything else — the counts, the frontier, the review
3198
+ notes — is left whole on purpose, so a cheap model handed one unit cannot read it as the whole job.
3199
+ The text view prints that leaf in full: the scenario's body verbatim and never reflowed (the bytes
3200
+ the digest was taken over), the digest itself — the same `@loam-digest-<16hex>` join key
3201
+ `loam gherkin` stamps and `loam verify --results` matches on — the `done` block whose `check` is the
3202
+ fact that closes the unit, and the literal follow-up command to run next, spelled out rather than
3203
+ left as a shape to fill in.
3204
+
3205
+ **`--service <id>` is a pure lens.** It narrows the units to one service's without binding a
3206
+ repository, and adds `waitingOn[]`: the open units in OTHER services that this service's units
3207
+ depend on. A team learns what it is blocked by from the same run that tells it what to do.
3208
+
3209
+ **What the payload says about what it could not see.** `stamps` is `read` or `unknown`, and
3210
+ `unknown` means the bound service repo's generated suite could not be read — could-not-look, never
3211
+ nothing-wrong, because a question loam was unable to ask must not read as one that came back clean.
3212
+ Beside it the payload carries `checklist` (the digest of the question set these units derive from),
3213
+ `record` (`absent`, `recorded`, `stale` or `unreadable`), `intent`, `services[]`,
3214
+ `size{requirements,scenarios,operations,messages,calls}` — the size of the DELTA and never of the
3215
+ open work, so that "this feature is small" and "this feature is nearly done" cannot become one
3216
+ sentence — `contracts{expose[],call[]}`, `counts{open,done,attested}` and `review[]`, the four
3217
+ instructions for re-deriving this answer and reviewing it, because a projection that says what to do
3218
+ must also say how to check it was read correctly. A broken `delta.likec4` or contract axis names
3219
+ what it could not read in `unreadable` and exits 1 with `ok: true` — `loam delta`'s own rule rather
3220
+ than a new one: a feeder nothing could read empties the packet, an empty packet reads as "nothing to
3221
+ do", so the envelope stays true while the exit code stops a pipeline building on it.
3222
+
3223
+ The command mints no finding or refusal code of its own, and `loam_tasks` serves the same projection
3224
+ read-only over MCP.
3225
+
2765
3226
  ## Operating at fleet scale
2766
3227
 
2767
3228
  One docs repo, a hundred services, many teams. The repo scales the way any shared codebase does —
@@ -2913,11 +3374,14 @@ repairing anything:
2913
3374
 
2914
3375
  And it reports the **agent surface**: which tool and profile the repo is wired for, how many command and skill
2915
3376
  files it holds, how many a newer loam would lay down (`doctor.agent-files-missing`), and how many
2916
- have fallen behind (`doctor.agent-files-stale`). The staleness check reads exactly one thing — the
2917
- `<!-- generated by loam vX.Y.Z -->` line every generated body opens with — and nothing else.
3377
+ have fallen behind (`doctor.agent-files-stale`). A file has fallen behind when BOTH hold: its
3378
+ bytes, the `<!-- generated by loam vX.Y.Z -->` line every generated body opens with set aside, are
3379
+ not what this binary writes; and that stamp is missing or older than this binary. A stamp-only
3380
+ difference is not staleness — the stamp moves in every file at every release, and a warning that
3381
+ fires on every upgrade hides the release where a pointer's body really changed. A customized body
3382
+ under a current stamp is not staleness either: the stamp is the record that somebody reviewed it.
2918
3383
  The digest manifest is what authorizes `init` to refresh an unchanged pointer; a different body is
2919
- treated as human-owned and never overwritten. A stamp is a separate claim and can be stale while
2920
- every edit around it is legitimate. Which deliveries a repo holds is asked of the files rather
3384
+ treated as human-owned and never overwritten. Which deliveries a repo holds is asked of the files rather
2921
3385
  than of a record: `loam.json` stores tools and profile, not deliveries, and a repo initialized
2922
3386
  `--no-skills` is not reported as missing the selected skills.
2923
3387
 
@@ -3020,6 +3484,7 @@ and publishes it wherever the fleet view reads:
3020
3484
  "errors": 0,
3021
3485
  "warnings": 2,
3022
3486
  "sources": { "current": 1, "stale": 0, "unvouched": 0, "absent": 0 },
3487
+ "contentStale": 0,
3023
3488
  "generatedAt": "2026-08-03T12:00:00Z"
3024
3489
  }
3025
3490
  ```
@@ -3038,10 +3503,16 @@ loam validate --service "$SVC" --json | jq '{
3038
3503
  unvouched: [.targets[0].findings[] | select(.code == "sources.unvouched")] | length,
3039
3504
  absent: [.targets[0].findings[] | select(.code == "sources.absent")] | length
3040
3505
  },
3506
+ contentStale: [.targets[0].findings[] | select(.code == "content.stale")] | length,
3041
3507
  generatedAt: (now | todate)
3042
3508
  }'
3043
3509
  ```
3044
3510
 
3511
+ `contentStale` counts the spec documents whose words moved after their vouch — the doc-side half of
3512
+ staleness, beside the code-side `sources.stale`. Unlike that half it is not owed to the service repo
3513
+ alone (the docs repo's own `validate --all` scorecard counts it too), but a summary that left it out
3514
+ would publish "0 stale" over a document nobody re-read.
3515
+
3045
3516
  `generatedAt` is the CI's clock, never loam's — loam output carries no timestamps, by design, so the
3046
3517
  same repo state always yields the same bytes. That is also what makes the timestamp the liveness
3047
3518
  signal: the rule is that every service repo's CI publishes this summary on every run, so a summary
@@ -3111,7 +3582,7 @@ says what its architecture means, and two generated models of the same code disa
3111
3582
  every run — a done-check built on diffing them would flap until somebody turned it off. What is
3112
3583
  deterministic is the **question**: which files, in which grammar, bound to which existing elements;
3113
3584
  which operations, which edges, which scenarios. loam owns the questions and the checking; an agent
3114
- owns the reading. `sources` and `verification.yaml` are where the answers are written down.
3585
+ owns the reading. `sources` and the verification record are where the answers are written down.
3115
3586
 
3116
3587
  ## Status
3117
3588