@ybotok/loam 0.2.0-alpha.6 → 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 (691) hide show
  1. package/CHANGELOG.md +1477 -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 +108 -11
  8. package/SCHEMA.md +721 -130
  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 +82 -33
  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 +55 -26
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/plan.js +64 -13
  118. package/dist/commands/seed/plan.js.map +1 -1
  119. package/dist/commands/seed/seed.js +6 -9
  120. package/dist/commands/seed/seed.js.map +1 -1
  121. package/dist/commands/show/show.js +3 -7
  122. package/dist/commands/show/show.js.map +1 -1
  123. package/dist/commands/status/print.js +13 -4
  124. package/dist/commands/status/print.js.map +1 -1
  125. package/dist/commands/status/status.js +3 -7
  126. package/dist/commands/status/status.js.map +1 -1
  127. package/dist/commands/steps/steps.js +3 -7
  128. package/dist/commands/steps/steps.js.map +1 -1
  129. package/dist/commands/subsystem/edit.js +7 -7
  130. package/dist/commands/subsystem/edit.js.map +1 -1
  131. package/dist/commands/subsystem/move.js +60 -25
  132. package/dist/commands/subsystem/move.js.map +1 -1
  133. package/dist/commands/subsystem/query.js +2 -4
  134. package/dist/commands/subsystem/query.js.map +1 -1
  135. package/dist/commands/subsystem/subsystem.js +4 -7
  136. package/dist/commands/subsystem/subsystem.js.map +1 -1
  137. package/dist/commands/subsystem/sync.js +141 -71
  138. package/dist/commands/subsystem/sync.js.map +1 -1
  139. package/dist/commands/subsystem/txn/report.js +55 -5
  140. package/dist/commands/subsystem/txn/report.js.map +1 -1
  141. package/dist/commands/subsystem/txn/txn.js +121 -45
  142. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  143. package/dist/commands/subsystem/txn/views.js +82 -12
  144. package/dist/commands/subsystem/txn/views.js.map +1 -1
  145. package/dist/commands/tasks/render.js +116 -0
  146. package/dist/commands/tasks/render.js.map +1 -0
  147. package/dist/commands/tasks/tasks.js +113 -0
  148. package/dist/commands/tasks/tasks.js.map +1 -0
  149. package/dist/commands/unarchive/manifest.js +6 -9
  150. package/dist/commands/unarchive/manifest.js.map +1 -1
  151. package/dist/commands/unarchive/restore.js +17 -28
  152. package/dist/commands/unarchive/restore.js.map +1 -1
  153. package/dist/commands/unarchive/unarchive.js +22 -30
  154. package/dist/commands/unarchive/unarchive.js.map +1 -1
  155. package/dist/commands/validate/arch-coverage.js +6 -15
  156. package/dist/commands/validate/arch-coverage.js.map +1 -1
  157. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  158. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  159. package/dist/commands/validate/checks/health-deps.js +9 -6
  160. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  161. package/dist/commands/validate/checks/requirements.js +18 -8
  162. package/dist/commands/validate/checks/requirements.js.map +1 -1
  163. package/dist/commands/validate/checks/sources.js +6 -2
  164. package/dist/commands/validate/checks/sources.js.map +1 -1
  165. package/dist/commands/validate/feature.js +23 -19
  166. package/dist/commands/validate/feature.js.map +1 -1
  167. package/dist/commands/validate/fleet/census.js +26 -3
  168. package/dist/commands/validate/fleet/census.js.map +1 -1
  169. package/dist/commands/validate/fleet/landscape.js +35 -32
  170. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  171. package/dist/commands/validate/fleet/map/attest.js +7 -12
  172. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  173. package/dist/commands/validate/fleet/map/bindings.js +16 -5
  174. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  175. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  176. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  177. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  178. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  179. package/dist/commands/validate/fleet/obligations.js +1 -5
  180. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  181. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  182. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  183. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  184. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  185. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  186. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  187. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  188. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  189. package/dist/commands/validate/fleet/views/ids.js +33 -7
  190. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  191. package/dist/commands/validate/fleet/views/root-documents.js +17 -35
  192. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  193. package/dist/commands/validate/fleet/views/stale.js +31 -14
  194. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  195. package/dist/commands/validate/links/corpus.js +1 -4
  196. package/dist/commands/validate/links/corpus.js.map +1 -1
  197. package/dist/commands/validate/links/glossary.js.map +1 -1
  198. package/dist/commands/validate/report.js +5 -4
  199. package/dist/commands/validate/report.js.map +1 -1
  200. package/dist/commands/validate/scope.js +113 -0
  201. package/dist/commands/validate/scope.js.map +1 -0
  202. package/dist/commands/validate/service/api.js +16 -104
  203. package/dist/commands/validate/service/api.js.map +1 -1
  204. package/dist/commands/validate/service/contracts/generated.js +134 -0
  205. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  206. package/dist/commands/validate/service/events/events.js +1 -1
  207. package/dist/commands/validate/service/events/events.js.map +1 -1
  208. package/dist/commands/validate/service/events/message-producers.js +1 -1
  209. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  210. package/dist/commands/validate/service/evidence-pins.js +56 -10
  211. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  212. package/dist/commands/validate/service/service.js +16 -26
  213. package/dist/commands/validate/service/service.js.map +1 -1
  214. package/dist/commands/validate/service/specs.js +31 -38
  215. package/dist/commands/validate/service/specs.js.map +1 -1
  216. package/dist/commands/validate/service/spine.js +8 -11
  217. package/dist/commands/validate/service/spine.js.map +1 -1
  218. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  219. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  220. package/dist/commands/validate/validate.js +44 -26
  221. package/dist/commands/validate/validate.js.map +1 -1
  222. package/dist/commands/verify/cross/diff.js +2 -6
  223. package/dist/commands/verify/cross/diff.js.map +1 -1
  224. package/dist/commands/verify/evidence/contract.js +1 -1
  225. package/dist/commands/verify/evidence/contract.js.map +1 -1
  226. package/dist/commands/verify/evidence/read.js +12 -3
  227. package/dist/commands/verify/evidence/read.js.map +1 -1
  228. package/dist/commands/verify/frozen.js +45 -23
  229. package/dist/commands/verify/frozen.js.map +1 -1
  230. package/dist/commands/verify/record.js +69 -17
  231. package/dist/commands/verify/record.js.map +1 -1
  232. package/dist/commands/verify/report.js +27 -44
  233. package/dist/commands/verify/report.js.map +1 -1
  234. package/dist/commands/verify/results.js +112 -30
  235. package/dist/commands/verify/results.js.map +1 -1
  236. package/dist/commands/verify/verify.js +54 -51
  237. package/dist/commands/verify/verify.js.map +1 -1
  238. package/dist/commands/vouch/pack/pack.js +2 -2
  239. package/dist/commands/vouch/pack/pack.js.map +1 -1
  240. package/dist/commands/vouch/pack/print.js +1 -6
  241. package/dist/commands/vouch/pack/print.js.map +1 -1
  242. package/dist/commands/vouch/pack/sections.js +1 -2
  243. package/dist/commands/vouch/pack/sections.js.map +1 -1
  244. package/dist/commands/vouch/run.js +26 -81
  245. package/dist/commands/vouch/run.js.map +1 -1
  246. package/dist/commands/vouch/sample/plan.js +20 -34
  247. package/dist/commands/vouch/sample/plan.js.map +1 -1
  248. package/dist/commands/vouch/vet/verify.js +39 -7
  249. package/dist/commands/vouch/vet/verify.js.map +1 -1
  250. package/dist/commands/vouch/vouch.js +10 -17
  251. package/dist/commands/vouch/vouch.js.map +1 -1
  252. package/dist/core/agent/agents-md/command-map.js +57 -102
  253. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  254. package/dist/core/agent/agents-md/map/explain.js +4 -3
  255. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  256. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  257. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  258. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  259. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  260. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  261. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  262. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  263. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  264. package/dist/core/agent/agents-md/map/status.js +9 -3
  265. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  266. package/dist/core/agent/agents-md/map/subsystem.js +31 -17
  267. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  268. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  269. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  270. package/dist/core/agent/agents-md/refusals.js +37 -21
  271. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  272. package/dist/core/agent/agents-md.js +25 -10
  273. package/dist/core/agent/agents-md.js.map +1 -1
  274. package/dist/core/agent/agents-stamp.js +10 -0
  275. package/dist/core/agent/agents-stamp.js.map +1 -1
  276. package/dist/core/agent/scaffold.js +10 -15
  277. package/dist/core/agent/scaffold.js.map +1 -1
  278. package/dist/core/agent/tools/dialects.js +1 -0
  279. package/dist/core/agent/tools/dialects.js.map +1 -1
  280. package/dist/core/agent/tools/registry.js +41 -76
  281. package/dist/core/agent/tools/registry.js.map +1 -1
  282. package/dist/core/agent/workflows/adopt.js +5 -3
  283. package/dist/core/agent/workflows/adopt.js.map +1 -1
  284. package/dist/core/agent/workflows/check.js +25 -22
  285. package/dist/core/agent/workflows/check.js.map +1 -1
  286. package/dist/core/agent/workflows/closing.js +54 -21
  287. package/dist/core/agent/workflows/closing.js.map +1 -1
  288. package/dist/core/agent/workflows/feature.js +79 -11
  289. package/dist/core/agent/workflows/feature.js.map +1 -1
  290. package/dist/core/agent/workflows/implement.js +91 -13
  291. package/dist/core/agent/workflows/implement.js.map +1 -1
  292. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  293. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  294. package/dist/core/agent/workflows/reference/codes.js +8 -6
  295. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  296. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  297. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  298. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  299. package/dist/core/agent/workflows/reference/spine.js +62 -24
  300. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  301. package/dist/core/asyncapi/baseline/plan.js +14 -81
  302. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  303. package/dist/core/asyncapi/digest.js +30 -25
  304. package/dist/core/asyncapi/digest.js.map +1 -1
  305. package/dist/core/asyncapi/merge/components.js +5 -4
  306. package/dist/core/asyncapi/merge/components.js.map +1 -1
  307. package/dist/core/asyncapi/merge/error.js +7 -2
  308. package/dist/core/asyncapi/merge/error.js.map +1 -1
  309. package/dist/core/asyncapi/merge/markers.js +30 -50
  310. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  311. package/dist/core/asyncapi/merge/merge.js +31 -53
  312. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  313. package/dist/core/asyncapi/merge/pin.js +10 -36
  314. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  315. package/dist/core/asyncapi/model.js.map +1 -1
  316. package/dist/core/asyncapi/read.js +36 -12
  317. package/dist/core/asyncapi/read.js.map +1 -1
  318. package/dist/core/brief/brief.js +30 -25
  319. package/dist/core/brief/brief.js.map +1 -1
  320. package/dist/core/brief/landscape.js +8 -12
  321. package/dist/core/brief/landscape.js.map +1 -1
  322. package/dist/core/brief/map/extend.js +8 -5
  323. package/dist/core/brief/map/extend.js.map +1 -1
  324. package/dist/core/brief/map/owed.js +8 -26
  325. package/dist/core/brief/map/owed.js.map +1 -1
  326. package/dist/core/brief/targets.js +79 -5
  327. package/dist/core/brief/targets.js.map +1 -1
  328. package/dist/core/brief/unchecked.js +1 -1
  329. package/dist/core/brief/unchecked.js.map +1 -1
  330. package/dist/core/c4/likec4.js +147 -16
  331. package/dist/core/c4/likec4.js.map +1 -1
  332. package/dist/core/c4/locate/unlocated.js +150 -0
  333. package/dist/core/c4/locate/unlocated.js.map +1 -0
  334. package/dist/core/c4/parsed/values.js +54 -1
  335. package/dist/core/c4/parsed/values.js.map +1 -1
  336. package/dist/core/c4/prefetch/fleet.js +1 -9
  337. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  338. package/dist/core/c4/project/architecture.js +31 -9
  339. package/dist/core/c4/project/architecture.js.map +1 -1
  340. package/dist/core/c4/project/documents.js +36 -10
  341. package/dist/core/c4/project/documents.js.map +1 -1
  342. package/dist/core/c4/project/load.js +14 -21
  343. package/dist/core/c4/project/load.js.map +1 -1
  344. package/dist/core/c4/project/staged.js +7 -3
  345. package/dist/core/c4/project/staged.js.map +1 -1
  346. package/dist/core/c4/resolve/external.js +51 -0
  347. package/dist/core/c4/resolve/external.js.map +1 -0
  348. package/dist/core/c4/resolve/service.js +25 -1
  349. package/dist/core/c4/resolve/service.js.map +1 -1
  350. package/dist/core/c4/seed/template.js +15 -1
  351. package/dist/core/c4/seed/template.js.map +1 -1
  352. package/dist/core/c4/service-model/shape.js +2 -22
  353. package/dist/core/c4/service-model/shape.js.map +1 -1
  354. package/dist/core/c4/source-mask.js +288 -4
  355. package/dist/core/c4/source-mask.js.map +1 -1
  356. package/dist/core/c4/source-scan.js +203 -147
  357. package/dist/core/c4/source-scan.js.map +1 -1
  358. package/dist/core/c4/splice/authored-source.js +19 -0
  359. package/dist/core/c4/splice/authored-source.js.map +1 -1
  360. package/dist/core/c4/splice/delta-blocks.js +5 -18
  361. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  362. package/dist/core/c4/splice/landscape-merge.js +13 -8
  363. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  364. package/dist/core/c4/splice/model/merge.js +10 -6
  365. package/dist/core/c4/splice/model/merge.js.map +1 -1
  366. package/dist/core/c4/splice/placement.js +108 -18
  367. package/dist/core/c4/splice/placement.js.map +1 -1
  368. package/dist/core/c4/workspace.js +13 -22
  369. package/dist/core/c4/workspace.js.map +1 -1
  370. package/dist/core/capabilities/capabilities.js +6 -6
  371. package/dist/core/capabilities/capabilities.js.map +1 -1
  372. package/dist/core/capabilities/delta/doc.js +7 -10
  373. package/dist/core/capabilities/delta/doc.js.map +1 -1
  374. package/dist/core/capabilities/findings.js +15 -20
  375. package/dist/core/capabilities/findings.js.map +1 -1
  376. package/dist/core/capabilities/realizes/findings.js +12 -19
  377. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  378. package/dist/core/capabilities/realizes/join.js +0 -20
  379. package/dist/core/capabilities/realizes/join.js.map +1 -1
  380. package/dist/core/capabilities/rollup.js +6 -0
  381. package/dist/core/capabilities/rollup.js.map +1 -1
  382. package/dist/core/capabilities/tree.js +6 -28
  383. package/dist/core/capabilities/tree.js.map +1 -1
  384. package/dist/core/capabilities/usecase-join.js +3 -13
  385. package/dist/core/capabilities/usecase-join.js.map +1 -1
  386. package/dist/core/coherence/authoring/scaffold.js +11 -3
  387. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  388. package/dist/core/coherence/authoring/sentinels.js +2 -1
  389. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  390. package/dist/core/coherence/coherence.js +30 -16
  391. package/dist/core/coherence/coherence.js.map +1 -1
  392. package/dist/core/coherence/consumed/operations.js +70 -0
  393. package/dist/core/coherence/consumed/operations.js.map +1 -0
  394. package/dist/core/coherence/consumed/refusal.js +38 -0
  395. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  396. package/dist/core/coherence/declared.js +60 -34
  397. package/dist/core/coherence/declared.js.map +1 -1
  398. package/dist/core/coherence/events/declared.js +44 -59
  399. package/dist/core/coherence/events/declared.js.map +1 -1
  400. package/dist/core/coherence/events/events.js +119 -8
  401. package/dist/core/coherence/events/events.js.map +1 -1
  402. package/dist/core/coherence/events/grades.js +31 -8
  403. package/dist/core/coherence/events/grades.js.map +1 -1
  404. package/dist/core/coherence/events/lookups.js +14 -82
  405. package/dist/core/coherence/events/lookups.js.map +1 -1
  406. package/dist/core/coherence/lookups.js +18 -78
  407. package/dist/core/coherence/lookups.js.map +1 -1
  408. package/dist/core/coherence/retired/archived.js +285 -0
  409. package/dist/core/coherence/retired/archived.js.map +1 -0
  410. package/dist/core/coherence/retired/event-pair.js +62 -0
  411. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  412. package/dist/core/coherence/retired/merged.js +79 -0
  413. package/dist/core/coherence/retired/merged.js.map +1 -0
  414. package/dist/core/coherence/retired/retirement.js +49 -0
  415. package/dist/core/coherence/retired/retirement.js.map +1 -0
  416. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  417. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  418. package/dist/core/coherence/retired/revival/revived.js +297 -0
  419. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  420. package/dist/core/consumers/after.js +113 -0
  421. package/dist/core/consumers/after.js.map +1 -0
  422. package/dist/core/consumers/consumers.js +296 -0
  423. package/dist/core/consumers/consumers.js.map +1 -0
  424. package/dist/core/consumers/current.js +211 -0
  425. package/dist/core/consumers/current.js.map +1 -0
  426. package/dist/core/consumers/scan.js +46 -0
  427. package/dist/core/consumers/scan.js.map +1 -0
  428. package/dist/core/consumers/state.js +27 -0
  429. package/dist/core/consumers/state.js.map +1 -0
  430. package/dist/core/delta/delta.js +1 -1
  431. package/dist/core/delta/delta.js.map +1 -1
  432. package/dist/core/deployment/delta.js +15 -16
  433. package/dist/core/deployment/delta.js.map +1 -1
  434. package/dist/core/diff/base-git.js +94 -12
  435. package/dist/core/diff/base-git.js.map +1 -1
  436. package/dist/core/diff/base-state.js +28 -16
  437. package/dist/core/diff/base-state.js.map +1 -1
  438. package/dist/core/diff/joins.js +136 -0
  439. package/dist/core/diff/joins.js.map +1 -0
  440. package/dist/core/diff/scope/changed-paths.js +22 -6
  441. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  442. package/dist/core/diff/scope/edges.js +82 -0
  443. package/dist/core/diff/scope/edges.js.map +1 -0
  444. package/dist/core/diff/semantic.js +100 -31
  445. package/dist/core/diff/semantic.js.map +1 -1
  446. package/dist/core/doctor/agents.js +34 -18
  447. package/dist/core/doctor/agents.js.map +1 -1
  448. package/dist/core/doctor/report.js.map +1 -1
  449. package/dist/core/document/apply.js +10 -2
  450. package/dist/core/document/apply.js.map +1 -1
  451. package/dist/core/document/frontmatter.js +55 -0
  452. package/dist/core/document/frontmatter.js.map +1 -1
  453. package/dist/core/document/parse.js +29 -6
  454. package/dist/core/document/parse.js.map +1 -1
  455. package/dist/core/document/scenarios.js +17 -0
  456. package/dist/core/document/scenarios.js.map +1 -1
  457. package/dist/core/document/spec.js +4 -11
  458. package/dist/core/document/spec.js.map +1 -1
  459. package/dist/core/envelope/config.js +10 -0
  460. package/dist/core/envelope/config.js.map +1 -1
  461. package/dist/core/envelope/json.js +34 -0
  462. package/dist/core/envelope/json.js.map +1 -1
  463. package/dist/core/explain/families.js +17 -12
  464. package/dist/core/explain/families.js.map +1 -1
  465. package/dist/core/explain/refusals.js +3 -3
  466. package/dist/core/explain/refusals.js.map +1 -1
  467. package/dist/core/explain/terms.js +18 -9
  468. package/dist/core/explain/terms.js.map +1 -1
  469. package/dist/core/explore/explore.js +2 -7
  470. package/dist/core/explore/explore.js.map +1 -1
  471. package/dist/core/fleet-context.js +34 -95
  472. package/dist/core/fleet-context.js.map +1 -1
  473. package/dist/core/gate/checks.js +10 -3
  474. package/dist/core/gate/checks.js.map +1 -1
  475. package/dist/core/gate/partners.js +64 -19
  476. package/dist/core/gate/partners.js.map +1 -1
  477. package/dist/core/gate/report.js.map +1 -1
  478. package/dist/core/gate/verification.js +75 -12
  479. package/dist/core/gate/verification.js.map +1 -1
  480. package/dist/core/gherkin/stale.js +13 -37
  481. package/dist/core/gherkin/stale.js.map +1 -1
  482. package/dist/core/glossary/tree.js +6 -19
  483. package/dist/core/glossary/tree.js.map +1 -1
  484. package/dist/core/kernel/concurrency.js +19 -0
  485. package/dist/core/kernel/concurrency.js.map +1 -1
  486. package/dist/core/kernel/records.js +11 -0
  487. package/dist/core/kernel/records.js.map +1 -1
  488. package/dist/core/links/backlinks.js +1 -3
  489. package/dist/core/links/backlinks.js.map +1 -1
  490. package/dist/core/links/corpus.js +5 -36
  491. package/dist/core/links/corpus.js.map +1 -1
  492. package/dist/core/mcp/argv.js +101 -0
  493. package/dist/core/mcp/argv.js.map +1 -1
  494. package/dist/core/mcp/author-tools.js +2 -1
  495. package/dist/core/mcp/author-tools.js.map +1 -1
  496. package/dist/core/mcp/protocol.js +2 -2
  497. package/dist/core/mcp/protocol.js.map +1 -1
  498. package/dist/core/mcp/tools.js +43 -33
  499. package/dist/core/mcp/tools.js.map +1 -1
  500. package/dist/core/openapi/baseline/gate.js +53 -12
  501. package/dist/core/openapi/baseline/gate.js.map +1 -1
  502. package/dist/core/openapi/baseline/plan.js +22 -8
  503. package/dist/core/openapi/baseline/plan.js.map +1 -1
  504. package/dist/core/openapi/baseline/record.js +0 -1
  505. package/dist/core/openapi/baseline/record.js.map +1 -1
  506. package/dist/core/openapi/contract-text.js +33 -0
  507. package/dist/core/openapi/contract-text.js.map +1 -0
  508. package/dist/core/openapi/contract-tree.js +56 -0
  509. package/dist/core/openapi/contract-tree.js.map +1 -0
  510. package/dist/core/openapi/depth.js +38 -24
  511. package/dist/core/openapi/depth.js.map +1 -1
  512. package/dist/core/openapi/digest.js +52 -2
  513. package/dist/core/openapi/digest.js.map +1 -1
  514. package/dist/core/openapi/doc.js +12 -4
  515. package/dist/core/openapi/doc.js.map +1 -1
  516. package/dist/core/openapi/merge/components.js +25 -60
  517. package/dist/core/openapi/merge/components.js.map +1 -1
  518. package/dist/core/openapi/merge/error.js +3 -13
  519. package/dist/core/openapi/merge/error.js.map +1 -1
  520. package/dist/core/openapi/merge/markers.js +26 -30
  521. package/dist/core/openapi/merge/markers.js.map +1 -1
  522. package/dist/core/openapi/merge/merge.js +26 -52
  523. package/dist/core/openapi/merge/merge.js.map +1 -1
  524. package/dist/core/openapi/merge/pin.js +15 -35
  525. package/dist/core/openapi/merge/pin.js.map +1 -1
  526. package/dist/core/openspec/decide/verdict.js.map +1 -1
  527. package/dist/core/openspec/inventory.js +4 -5
  528. package/dist/core/openspec/inventory.js.map +1 -1
  529. package/dist/core/openspec/model/mapping.js +30 -28
  530. package/dist/core/openspec/model/mapping.js.map +1 -1
  531. package/dist/core/openspec/model/model.js +12 -7
  532. package/dist/core/openspec/model/model.js.map +1 -1
  533. package/dist/core/openspec/scan/changes.js +10 -4
  534. package/dist/core/openspec/scan/changes.js.map +1 -1
  535. package/dist/core/openspec/scan/config.js +7 -16
  536. package/dist/core/openspec/scan/config.js.map +1 -1
  537. package/dist/core/openspec/scan/shape.js +63 -19
  538. package/dist/core/openspec/scan/shape.js.map +1 -1
  539. package/dist/core/openspec/scan/walk.js +21 -38
  540. package/dist/core/openspec/scan/walk.js.map +1 -1
  541. package/dist/core/pack/living.js +2 -16
  542. package/dist/core/pack/living.js.map +1 -1
  543. package/dist/core/provenance/findings.js +48 -12
  544. package/dist/core/provenance/findings.js.map +1 -1
  545. package/dist/core/provenance/git.js +33 -57
  546. package/dist/core/provenance/git.js.map +1 -1
  547. package/dist/core/provenance/gitq/moves.js +71 -114
  548. package/dist/core/provenance/gitq/moves.js.map +1 -1
  549. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  550. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  551. package/dist/core/provenance/sources.js +23 -6
  552. package/dist/core/provenance/sources.js.map +1 -1
  553. package/dist/core/provenance/stamp.js +5 -17
  554. package/dist/core/provenance/stamp.js.map +1 -1
  555. package/dist/core/repo/entries.js.map +1 -1
  556. package/dist/core/repo/paths.js +34 -29
  557. package/dist/core/repo/paths.js.map +1 -1
  558. package/dist/core/repo/repo.js +24 -14
  559. package/dist/core/repo/repo.js.map +1 -1
  560. package/dist/core/repo/tree/find.js +12 -3
  561. package/dist/core/repo/tree/find.js.map +1 -1
  562. package/dist/core/repo/tree/fs.js +27 -1
  563. package/dist/core/repo/tree/fs.js.map +1 -1
  564. package/dist/core/repo/tree/render/body.js +45 -6
  565. package/dist/core/repo/tree/render/body.js.map +1 -1
  566. package/dist/core/repo/tree/render/service/header.js +125 -0
  567. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  568. package/dist/core/repo/tree/render/service/peers.js +28 -0
  569. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  570. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  571. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  572. package/dist/core/repo/tree/render/service/views.js +78 -0
  573. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  574. package/dist/core/repo/tree/render/stale.js +19 -2
  575. package/dist/core/repo/tree/render/stale.js.map +1 -1
  576. package/dist/core/repo/tree/render/views.js +102 -33
  577. package/dist/core/repo/tree/render/views.js.map +1 -1
  578. package/dist/core/repo/tree/walk.js +8 -30
  579. package/dist/core/repo/tree/walk.js.map +1 -1
  580. package/dist/core/results.js +47 -12
  581. package/dist/core/results.js.map +1 -1
  582. package/dist/core/staging/commit.js +10 -12
  583. package/dist/core/staging/commit.js.map +1 -1
  584. package/dist/core/staging/lock.js +74 -12
  585. package/dist/core/staging/lock.js.map +1 -1
  586. package/dist/core/staging/recovery/intent.js +2 -5
  587. package/dist/core/staging/recovery/intent.js.map +1 -1
  588. package/dist/core/staging/recovery/recover.js +60 -1
  589. package/dist/core/staging/recovery/recover.js.map +1 -1
  590. package/dist/core/staging/snapshot/manifest.js +152 -0
  591. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  592. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  593. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  594. package/dist/core/status/actions/execution.js +11 -0
  595. package/dist/core/status/actions/execution.js.map +1 -1
  596. package/dist/core/status/feature/artifacts.js +2 -2
  597. package/dist/core/status/feature/artifacts.js.map +1 -1
  598. package/dist/core/status/feature/feature.js +5 -11
  599. package/dist/core/status/feature/feature.js.map +1 -1
  600. package/dist/core/status/fleet/fleet.js +6 -2
  601. package/dist/core/status/fleet/fleet.js.map +1 -1
  602. package/dist/core/status/fleet/next.js +26 -3
  603. package/dist/core/status/fleet/next.js.map +1 -1
  604. package/dist/core/status/report.js.map +1 -1
  605. package/dist/core/status/verification.js +30 -15
  606. package/dist/core/status/verification.js.map +1 -1
  607. package/dist/core/tasks/packet.js +174 -0
  608. package/dist/core/tasks/packet.js.map +1 -0
  609. package/dist/core/tasks/record.js +43 -0
  610. package/dist/core/tasks/record.js.map +1 -0
  611. package/dist/core/tasks/sources.js +181 -0
  612. package/dist/core/tasks/sources.js.map +1 -0
  613. package/dist/core/tasks/stamps.js +76 -0
  614. package/dist/core/tasks/stamps.js.map +1 -0
  615. package/dist/core/tasks/unit.js +0 -0
  616. package/dist/core/tasks/unit.js.map +1 -0
  617. package/dist/core/usecases/capability.js +11 -14
  618. package/dist/core/usecases/capability.js.map +1 -1
  619. package/dist/core/usecases/delta/flows.js +5 -7
  620. package/dist/core/usecases/delta/flows.js.map +1 -1
  621. package/dist/core/usecases/fleet.js +178 -31
  622. package/dist/core/usecases/fleet.js.map +1 -1
  623. package/dist/core/usecases/operations.js +5 -4
  624. package/dist/core/usecases/operations.js.map +1 -1
  625. package/dist/core/usecases/service/flows.js +9 -6
  626. package/dist/core/usecases/service/flows.js.map +1 -1
  627. package/dist/core/usecases/suites/participants.js +83 -0
  628. package/dist/core/usecases/suites/participants.js.map +1 -0
  629. package/dist/core/usecases/suites/suites.js +45 -0
  630. package/dist/core/usecases/suites/suites.js.map +1 -0
  631. package/dist/core/verify/answers.js +12 -5
  632. package/dist/core/verify/answers.js.map +1 -1
  633. package/dist/core/verify/build.js +102 -82
  634. package/dist/core/verify/build.js.map +1 -1
  635. package/dist/core/verify/checklist.js +45 -10
  636. package/dist/core/verify/checklist.js.map +1 -1
  637. package/dist/core/verify/location.js +67 -0
  638. package/dist/core/verify/location.js.map +1 -0
  639. package/dist/core/verify/pins/lint.js +3 -2
  640. package/dist/core/verify/pins/lint.js.map +1 -1
  641. package/dist/core/verify/pins/pin.js +1 -1
  642. package/dist/core/verify/pins/pin.js.map +1 -1
  643. package/dist/core/verify/read/file.js +224 -0
  644. package/dist/core/verify/read/file.js.map +1 -0
  645. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  646. package/dist/core/verify/read/shape.js.map +1 -0
  647. package/dist/core/verify/read/union.js +105 -0
  648. package/dist/core/verify/read/union.js.map +1 -0
  649. package/dist/core/verify/record.js +35 -12
  650. package/dist/core/verify/record.js.map +1 -1
  651. package/dist/core/verify/store/commit.js +40 -30
  652. package/dist/core/verify/store/commit.js.map +1 -1
  653. package/dist/core/verify/store/render.js +8 -4
  654. package/dist/core/verify/store/render.js.map +1 -1
  655. package/dist/core/vocabulary/issue.js +1 -0
  656. package/dist/core/vocabulary/issue.js.map +1 -1
  657. package/dist/core/vocabulary/maturity.js +53 -3
  658. package/dist/core/vocabulary/maturity.js.map +1 -1
  659. package/examples/README.md +28 -9
  660. package/examples/docs/architecture/landscape.likec4 +21 -7
  661. package/examples/docs/architecture/services.likec4 +70 -0
  662. package/examples/docs/architecture/subsystems.likec4 +3 -0
  663. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  664. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  665. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  666. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  667. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  668. package/examples/docs/services/order-service/model.likec4 +10 -8
  669. package/examples/docs/services/payment-service/model.likec4 +6 -4
  670. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  671. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  672. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  673. package/package.json +1 -1
  674. package/dist/commands/validate/checks/vocabulary.js +0 -16
  675. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  676. package/dist/core/agent/agents-md/artifacts.js +0 -221
  677. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  678. package/dist/core/agent/agents-md/cycle.js +0 -180
  679. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  680. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  681. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  682. package/dist/core/agent/agents-md/spine.js +0 -40
  683. package/dist/core/agent/agents-md/spine.js.map +0 -1
  684. package/dist/core/coherence/events/merged.js +0 -40
  685. package/dist/core/coherence/events/merged.js.map +0 -1
  686. package/dist/core/diff/current-state.js +0 -96
  687. package/dist/core/diff/current-state.js.map +0 -1
  688. package/dist/core/diff/victims.js +0 -256
  689. package/dist/core/diff/victims.js.map +0 -1
  690. package/dist/core/staging/snapshot.js.map +0 -1
  691. package/dist/core/verify/file.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,1479 @@ case for a change — the alternative that was rejected, the defect it came from
10
10
  generalises — lives where it is maintained: [SCHEMA.md](SCHEMA.md) for the rule,
11
11
  [ROADMAP.md](ROADMAP.md) for the priority and its exit criteria, and the commit that landed it._
12
12
 
13
+ ## [0.2.0-alpha.9] - 2026-10-20
14
+
15
+ The first publish since `0.2.0-alpha.7`. `0.2.0-alpha.8` was tagged but never reached npm (its
16
+ release run failed at `npm ci` on a stripped lockfile), so a repository upgrading from alpha.7 takes
17
+ alpha.8's section below as well as this one. Still a prerelease on the `alpha` line.
18
+
19
+ Most of it is fixes. `loam archive` merges into a landscape written in any form LikeC4 parses —
20
+ several model blocks, markdown strings, split heads, hyphenated ids — and no longer writes a wrong
21
+ merge at exit 0 for the shapes listed under "Fixed — `loam archive` merges into a landscape…",
22
+ which alpha.7 did. Consumers are counted once, by one index, so `loam archive`, `loam validate` and
23
+ `loam diff` agree; the PR gate sees a changed schema, an edited `op` on an edge and a caller drawn
24
+ with no `op`; `loam gate --since` keeps an archived feature in view; the trust readouts show stale
25
+ vouches; a waiting writer no longer deletes the next writer's lock.
26
+
27
+ Several entries change an exit code, listed in each: refusals that are new (an overwrite or
28
+ retirement a consumer still names, a delta reviving a retired operation, an unreadable consumer
29
+ document) and refusals that were false and are gone. A federated verification record is now one
30
+ file per service, and an older loam does not read it: upgrade every repository that records or
31
+ gates a feature together. New: `loam list suites`, `loam new --modifies`, `loam gate --since`,
32
+ `loam_adopt` on MCP; six new stable codes — `openapi.op-revived`, `asyncapi.slot-revived`,
33
+ `next.revouch`, `frontmatter.owner-mismatch`, `diff.op-modified` and `diff.message-modified`.
34
+
35
+ ### Changed — one consumer index: `loam archive`, `loam validate` and `loam diff` agree about who still consumes an operation or a message
36
+
37
+ No code or flag was added; one additive `--json` key on two existing codes. Who still consumes what
38
+ a change takes out was answered three ways — the coherence gate (`openapi.remove-op-consumed`,
39
+ `asyncapi.remove-message-consumed`), `loam diff`'s victims (`diff.op-removed-consumed`,
40
+ `diff.message-removed-consumed`, the `-deprecated`/`-modified` details, `diff.consumer-added` /
41
+ `diff.consumer-removed`) and `loam archive`'s `openapi.op-modified` / `asyncapi.message-modified`
42
+ warnings — and the three copies read different documents. They are one index now; SCHEMA.md's
43
+ "Who still consumes it" states the rules.
44
+
45
+ - **More consumers are found, so more removals are refused:**
46
+ - **Edges come from the whole `architecture/` project**, not `architecture/landscape.likec4`
47
+ alone, for the gate as for `loam diff` and the archive's overwrite warnings: an edge with `metadata { op }` (or `consumes`) drawn in
48
+ `architecture/calls.likec4` or in a use-case document counts. The gate passed such a removal
49
+ and the next `validate --all` reported `spine.op-undefined` on the edge. Where only a sibling
50
+ document fails to parse, the map file's own edges still count.
51
+ - **The gate names use-case hops** attributed to the operation (or backed by a `consumes` edge),
52
+ as `loam diff` already did.
53
+ - **An `arch.spec.md` `Operations:` line** of another service counts, as a spec.md one does —
54
+ for the gate, `loam diff`'s victims, `diff.consumer-added` / `diff.consumer-removed` and the
55
+ archive's overwrite warnings. So an
56
+ operation's `diff.consumer-added` / `-removed` is not reported while the consumer's
57
+ `arch.spec.md` cannot be read on the other side, as it never was over an unreadable `spec.md`.
58
+ The consumer's own `openapi.yaml` does not suppress it: an unreadable contract proves no
59
+ ownership, so the line counts on that side too. The exception is a line whose join exists only
60
+ because that contract did not parse on one side, while on the other side it reads and defines
61
+ the id: there the line names the consumer's own operation, and only the contract's readability
62
+ changed. No `diff.consumer-added` / `diff.consumer-removed` is reported for it — before, one per
63
+ provider of the id, each counted in the `--json` summary's `added` / `removed`. The contract's own
64
+ `unreadable[]` entry is still reported, and `loam diff` still exits 1 on it.
65
+ - **The provider's own `spec.md` requirements count for a marker retirement too**, as they do for
66
+ the overwrite of the entry below: one still naming the retired operation after the merge (a
67
+ second one beside the REMOVED requirement) archived at exit 0 and left `spec-api.op-undefined`.
68
+ Its `arch.spec.md` `Operations:` lines count for neither shape: `validate` grades such a line in
69
+ spec.md only, so one in arch.spec.md strands nothing. An edge the provider draws to itself
70
+ counted already, and still does — the spine grades it like any other.
71
+ `loam diff` and the archive's overwrite warnings still leave the provider's own documents out:
72
+ they name who ELSE is affected.
73
+ - **On the event axis the provider's own `Publishes:` lines count**, as the merge would leave
74
+ them. A message slot re-keyed to another name, or a send operation turned to receive, while the
75
+ provider's requirement still publishes the message, archived at exit 0 and the next
76
+ `validate --all` reported `spec-event.message-undefined`; it is `asyncapi.remove-message-consumed`
77
+ now. So do its own `publishes` edges, which the spine grades on their source: a full retirement
78
+ over a living `paymentService -> billingService { metadata { publishes '…' } }` archived at exit
79
+ 0 and left `spine.message-undefined` on the provider. The remedy names "drop or redraw its
80
+ publishes edge".
81
+ - **Fail closed**: a spec the merge refuses to apply — a malformed `Requirement-ID:` in the
82
+ living spec or the delta — no longer answers "no consumers". The living requirements count,
83
+ plus every requirement the delta ADDS or MODIFIES.
84
+ - **A consumer document loam cannot read refuses the removal**, as the gate did before it asked
85
+ one index: another service's `spec.md` or `arch.spec.md` that does not decode as UTF-8, the
86
+ provider's own `spec.md` (either spec file, for a message), or a service id two directories
87
+ claim. `openapi.remove-op-consumed` /
88
+ `asyncapi.remove-message-consumed` name each in `details[]` and say the gate could not check —
89
+ never that the fleet still consumes it. An edge drawn with no `metadata { op }` and a contested
90
+ use-case hop still never refuse; `loam diff` lists them as doubts, as before.
91
+ - **`loam diff` counts only a service that SENDS a message as still producing it.** A removed
92
+ message another service declares only with `action: receive` — a consumer's own declaration,
93
+ which validate requires — was `diff.message-removed` (warn), "still declared by" that consumer,
94
+ while the gate refused the same removal and the next `validate --all` reported
95
+ `spine.message-unproduced` there. It is `diff.message-removed-consumed` now, naming the gate's
96
+ consumers; the warning for a message another service does send reads "still sent by".
97
+ - **Fewer refusals where the consumer was false or already retired:**
98
+ - **Another service's `Operations:` line naming an id its own contract defines** names its own
99
+ operation, and is no consumer (gate, `loam diff`, `diff.consumer-added` / `-removed`, the
100
+ archive's overwrite warnings): shared
101
+ ids such as `healthCheck` no longer refuse a rename, and `--approve` is no longer the only way
102
+ past a fleet `validate --all` calls clean.
103
+ - **Read after the feature's merge**: a consumer requirement the same feature MODIFIES to drop
104
+ the id, or REMOVES, no longer counts — another service's included, which is the remedy the
105
+ refusal itself names.
106
+ - **A message another service would still send after the merge strands no consumer.**
107
+ `asyncapi.remove-message-consumed` counted every `consumes` edge, `Consumes:` line and use-case
108
+ hop of a message whoever sent it, so retiring it from one of two senders was refused over the
109
+ other sender's consumers. Its remedy said to drop that sender's own `consumes` edge. Meanwhile
110
+ `loam diff` reported the same removal as `diff.message-removed` (warn), "still sent by …", and
111
+ the next `validate --all` was clean. Now a consumer refuses only while no service would send the
112
+ message after this feature's merge. The senders are read as the merge leaves them: a service
113
+ the feature carries an event delta for sends what its merged contract would (the delta itself
114
+ where it creates that contract), so moving production to another service in the same feature
115
+ passes, and a feature that retires the message from every sender is still refused. An unreadable contract never counts as
116
+ a sender. Beside another sender, the provider's own `Publishes:` lines, its own `publishes`
117
+ edges and its own unreadable spec files still refuse; another service's unreadable spec file,
118
+ or an id two directories claim, does not. This predates the one index: the gate behaved the
119
+ same before it. The message is reworded to say which case applies. With no sender left it reads
120
+ "no readable contract would still send it after this merge" and adds the route "keep another
121
+ service sending it". With a sender left it names the senders, lists only the provider's own
122
+ documents, and gives only their remedy.
123
+ - **An `#external` element that publishes the message keeps its consumers produced too**, for
124
+ the gate and for `loam diff` as for `validate`. Both counted only senders inside the fleet, so
125
+ retiring a message from its only fleet sender was refused while an `#external` element still
126
+ published it (an edge from it with `metadata { publishes }`), and after `--approve` `loam diff`
127
+ reported `diff.message-removed-consumed` (error) — while `spine.message-unproduced` accepts such
128
+ a producer and the next `validate --all` was clean. Now the outside producer counts as a
129
+ sender, read off the living `architecture/` project with validate's own rule: only while the
130
+ project parses, and not from an edge the feature's own C4 delta draws. The gate asks the rule
131
+ over the service directories as the merge leaves them, so an element the same feature makes a
132
+ service — a `specs/<id>/` directory for the id the element, or the element it is nested in,
133
+ resolves to, which the archive turns into `services/<id>/` — no longer counts, and the
134
+ retirement is refused, as the next `loam diff` and `validate --all` would read it; `loam diff`
135
+ asks over the directories as they stand. `loam diff` reports
136
+ `diff.message-removed` (warn), "still sent by `<title>` (#external)", and a refusal for the
137
+ provider's own documents, which it still does not excuse, names it the same way. With no sender
138
+ left, the refusal now also says "nor does an #external element publish it", and its route
139
+ reads "keep another service sending it (…) or an #external element publishing it (a publishes
140
+ edge from it in the living architecture/ project; one whose element this feature's
141
+ specs/<service>/ makes a service no longer counts)". This predates the one index too.
142
+ - **`openapi.remove-marker-missing` is owed only while there is a living operation to retire**:
143
+ not for an id the service's living contract does not define (a consumer's REMOVED requirement
144
+ naming another service's operation), and not for one the delta already replaces at its method
145
+ and path (no marker could be written there). A living `openapi.yaml` that does not parse proves
146
+ neither, so the marker is still owed there, as before. The overwrite's "REMOVE the requirement"
147
+ route can now be followed; its message says a REMOVED requirement owes no marker there.
148
+ `asyncapi.remove-marker-missing` likewise skips a message the merge already takes out.
149
+ - **`details[]`** (additive) on `openapi.remove-op-consumed` and `asyncapi.remove-message-consumed`:
150
+ the consumers, one per line, in the order `loam diff` lists the victims of the same removal.
151
+ - **Messages reworded**: the remedies name the `architecture/` project instead of
152
+ `architecture/landscape.likec4`, and say a requirement is retired in the feature's
153
+ `specs/<service>/` delta; `diff.op-removed`'s no-consumer sentence reads "no current edge,
154
+ requirement or use-case hop names it".
155
+ - **Exit code:** where a consumer is newly found, `loam archive` exits 1 (`not-coherent`) without
156
+ `--approve`; `loam validate --feature`, `loam validate <FEAT>` and `loam validate --all` (for every
157
+ active feature in that shape) exit 1; `loam status` counts the finding as gating; `loam show
158
+ <FEAT> --json` reports `review.readyToArchive: false`; and `loam diff` exits 1
159
+ (`diff.op-removed-consumed` / `diff.message-removed-consumed`, `breaking: true`) where a removal
160
+ now has a victim — a removed message whose only other declarer receives it included, which exited
161
+ 0. Where another service's spec file does not decode, `loam validate --feature`
162
+ reports `openapi.remove-op-consumed` / `asyncapi.remove-message-consumed` naming the file where it
163
+ reported `feature.unreadable`, at exit 1 as before, and `loam archive` exits 1 as before. Where
164
+ two directories claim a service id, or the provider's own `spec.md` does not decode (either spec
165
+ file, for a message), `loam
166
+ archive` and `loam validate --feature` exit 1 for the removal whatever those documents say —
167
+ before, 0 unless a copy the gate could read named it — until the document reads or `--approve` is
168
+ passed. Where a refusal was false or the consumer is retired in the same feature, each
169
+ of those exits 0 for that shape unless something else gates. That includes a message retired from
170
+ one sender while another service would still send it after the merge, or while an `#external`
171
+ element still publishes it and the feature does not make that element a service, where only
172
+ consumers were named: `loam archive`, `loam validate --feature`, `loam validate <FEAT>` and `loam validate
173
+ --all` exited 1 and now exit 0, `loam status` no longer counts it as gating, and `loam show
174
+ <FEAT> --json` no longer reports `review.readyToArchive: false` for it. `loam diff` reports
175
+ `diff.op-removed` (warn) where its only victim was a same-id requirement, and
176
+ `diff.message-removed` (warn) where an `#external` element still publishes the removed message —
177
+ it exited 1 (`diff.message-removed-consumed`, `breaking: true`) and now exits 0 unless something
178
+ else fails. `--approve` overrides
179
+ every one of these refusals as before. `--strict` changes nothing here beyond that: every finding
180
+ in this entry is an error, or is unchanged.
181
+
182
+ ### Fixed — `loam instructions loam-spine` says what `loam rebase` pins for a revived quote
183
+
184
+ The page said `loam rebase` "pins neither shape". That has been false since rebase began grading
185
+ the delta it will write. Rebase never pins a retired operation as new, but it does repin a quote
186
+ of an address a move vacated over the operation that lives there now, and it reports the revival
187
+ that edit makes. The page now says so, in line with the `loam-codes` command map, WORKFLOW.md
188
+ and SCHEMA.md.
189
+
190
+ ### Fixed — an operation with no `operationId` is a slot like any other
191
+
192
+ No code, flag or payload key was added. The OpenAPI reader used to drop an operation written
193
+ without an `operationId`. It now keeps it by its method and path, never by an id: requirements,
194
+ edges and removal markers still cannot name it, and it gets no finding of its own. The checks that
195
+ work by slot now see it:
196
+
197
+ - **`openapi.op-revived` names it.** When a quote lands where such an operation lives, the message
198
+ says `<METHOD /path> now holds an operation with no operationId, and archiving this delta
199
+ replaces it there`. Its move-back remedy adds that, if that operation is to stay, the same delta
200
+ must define it at another address. The message no longer calls that address one "the living
201
+ contract no longer has". Followed as it used to read, the remedy led to an archive that deleted
202
+ the operation with nothing naming it.
203
+ - **`loam rebase` and `loam validate` agree about a pin over it.** Rebase always pinned against
204
+ whatever stood at the slot, but validate refused that pin with `openapi.baseline-invalid` ("no
205
+ operation at that slot").
206
+ - **Pins are now required at these slots.** An unpinned quote over such an operation counts into
207
+ `openapi.baseline-missing` (warn, gates archive), and so does an unpinned delta operation without
208
+ an `operationId` over a living one. Their pins are graded like any other (`-stale`, `-invalid`).
209
+ **Exit code:** `loam archive` now exits 1 (`not-coherent`) on either shape until `loam rebase`
210
+ pins it, or `--approve` merges unpinned. `openapi.baseline-missing` is a warning that gates:
211
+ `loam validate` exits as before on it, except `--strict` (and `--all --strict`), which fails on
212
+ any warning and so now exits 1; `loam status` counts it as gating, and `loam show <FEAT> --json`
213
+ reports `review.readyToArchive: false` until the pin is written.
214
+ - **A pin on such an operation where nothing lives is `openapi.baseline-invalid` (error)**, as it is
215
+ for a named operation (rebase never writes one; a malformed pin on such an operation is refused
216
+ the same way). **Exit code:** `loam validate --feature`, `loam validate <FEAT>` and `loam validate
217
+ --all` (for every active feature carrying one) now exit 1 on it, where they exited 0 with no
218
+ finding; `loam archive` exits 1 (`not-coherent`) without `--approve`; `loam status` counts it as
219
+ gating; `loam show <FEAT> --json` reports `review.readyToArchive: false`.
220
+ - **`openapi.remove-op-consumed`** also fires when the delta defines an operation with no
221
+ `operationId` where a consumed one lives. Before, a delta holding only such an operation upserted
222
+ over the consumed one with no finding of any kind.
223
+
224
+ ### Changed — `loam archive` refuses an overwrite that takes a consumed operation out of the contract
225
+
226
+ No code, flag or payload key was added: an existing code covers a second shape.
227
+
228
+ - **`openapi.remove-op-consumed` (error, `--approve` overrides) now also fires for an overwrite.**
229
+ A feature's `openapi.yaml` defines another operationId at the method and path where a living
230
+ operation lives, and the contract the archive would write then defines the living one nowhere.
231
+ The merge replaces it there with no removal marker naming it, so before this only the advisory
232
+ `api.op-unconsumed` fired, the archive passed without `--approve`, and the next `validate`
233
+ reported `spine.op-undefined` / `spec-api.op-undefined`. The consumers are a removal's — the one
234
+ consumer index of the entry above — including the provider's own `spec.md` requirements as the
235
+ merge would leave them: one the feature MODIFIES to name the new operation, or REMOVES, no longer counts. The
236
+ message names what replaces the operation and where, and says to define it at another address in
237
+ the same delta if it is to stay.
238
+ - **Exit code:** on this shape `loam archive` now exits 1 (`not-coherent`) without `--approve`,
239
+ and `loam validate --feature`, `loam validate <FEAT>` and `loam validate --all` (for every active
240
+ feature in this shape) exit 1. `loam status` counts it as gating, and `loam show <FEAT> --json`
241
+ reports `review.readyToArchive: false` until it is fixed or archived past with `--approve`. With
242
+ `--approve` the archive merges as before and lists the finding in `overridden[]`.
243
+ - **`c4-api.op-removing`** now also fires for a new tagged edge onto an operation the delta
244
+ replaces that way.
245
+ - An overwrite of an operation nothing names is unchanged: `api.op-unconsumed` stays advisory. On
246
+ the event axis `asyncapi.remove-message-consumed` already graded the simulated merge's sent
247
+ messages, so a channel, operation or message slot redefined under another identity was caught
248
+ like a retirement for its OTHER consumers — but not for the provider's own `Publishes:` line,
249
+ which the consumer-index entry above adds.
250
+
251
+ ### Fixed — `loam archive` merges into a landscape written in any form LikeC4 parses
252
+
253
+ No code, flag or `--json` key was added or changed. Each case below is a landscape (or a delta)
254
+ that `loam validate --all` called valid and that `loam archive` refused with `merge-failed`, nothing
255
+ written — or, where the case says "exit 0", merged wrong and wrote; each now merges as LikeC4
256
+ reads it.
257
+
258
+ - **A landscape with more than one top-level `model { }` block.** The splice map read only the
259
+ first block, so every declaration in a later one was invisible and archive refused with
260
+ "declares '<id>' in a form placement cannot locate — most often two declarations sharing one
261
+ line" about a declaration on a line of its own. Every block is read now. An addition anchored to
262
+ a living element — nested in it, joining its service's neighborhood, or joining its source's
263
+ edges — lands in that element's block; an unanchored one lands in the first block's trailing
264
+ region, where it landed before. A one-block map is placed byte for byte as before, and archive
265
+ order still does not change the bytes. The same reading fixes three neighbours: a delta written
266
+ in two `model { }` blocks (its second-block additions were "cannot locate … in delta.likec4"), a
267
+ service `model.likec4` whose `extend <fqn> { }` frame sits in a later block (the merge minted a
268
+ second frame in the first block, or re-added a container the model already declares there), and
269
+ `loam validate --all --base`, which did not grade a call edge the branch edited in a later block.
270
+ - **A declaration split across lines.** LikeC4's newlines are hidden tokens, so `name =` with its
271
+ kind on the next line, a bare name above `= kind`, a kind above the name it declares, a title on
272
+ the next line, a `/* … */` comment spanning a line break, an edge broken around its arrow, and an
273
+ edge's `#tag` on the next line are all one statement — and the splice map, reading one line per
274
+ statement, could not see them. Worse, `name = kind 'title'` with its `{` on the NEXT line read as
275
+ a bodyless element: an addition anchored after it landed between the head and its body and took
276
+ that body — description, children, `metadata { service }` binding — for its own, in a merge that
277
+ parsed and was written (exit 0). The body stays with its element now.
278
+ - **An `extend <fqn> { }` frame written in the landscape itself, with an edge inside it.** An
279
+ edge the feature added, anchored to the frame's edge, was spliced INSIDE the frame — a scope, so
280
+ its names re-resolved there: the delta's `zulu -> beta` landed in `extend alpha { beta = … }` as
281
+ an edge into alpha's own `beta` container, in a merge that parsed and was written (exit 0). It
282
+ lands outside the frame now. Elements inside such a frame were already read correctly.
283
+ - **A hyphenated id (`entity-service = …`) and a dot-kind edge (`a .async b`).** LikeC4's
284
+ identifiers may carry hyphens and a declared relationship kind may be written as `.kind`; the
285
+ splice map read neither, so such an element was "in a form placement cannot locate" and such an
286
+ edge made the relationship count disagree.
287
+ - **A `link` (or `icon`) URI followed by more code on its line.** LikeC4 reads `https://…` and a
288
+ relative `./…` path as one token; the splice map read its `//` as a comment that ran to the end
289
+ of the line, so `x = kind 'X' { link https://… }` lost its closing brace and archive refused with
290
+ `landscape.likec4 has no model block`. The URI is a token wherever it sits: a `link` or `icon`
291
+ alone on its line ends there, and the child or edge declared on the line below is its own.
292
+ - **A markdown string (`'''…'''` or `"""…"""`) whose prose holds an apostrophe or an odd number
293
+ of quotes.** The splice map read the triple as three ordinary quotes, so the first `it's` in a
294
+ long `description` ended the string and the rest of the file was read inside out — a map with
295
+ one such description above its model had no model block at all, and archive refused with
296
+ `landscape.likec4 has no model block`.
297
+ - **A delta edge whose body holds a key or tag containing `op`, `publishes` or `consumes`.** The
298
+ splice map read those keys anywhere in the edge's body, and took a hyphen for the end of a word,
299
+ so `op-version 'v2'` above `op 'cancelOrder'` read as op `v2`, and a `#op-critical` or `#op` tag
300
+ as an op naming the description below it. The edge then matched none of the delta's statements
301
+ and archive refused with "cannot locate the '<a> -> <b>' relationship in delta.likec4". A key is
302
+ read whole now, and `op`, `publishes` and `consumes` only from `metadata { }`, which is where
303
+ LikeC4 reads them. The same misreading hid such an edge from `validate --all --base` (below).
304
+ - **A bare carriage return (`\r` with no `\n`) between two statements.** LikeC4 breaks a line at
305
+ `\r` as well as `\n`, and the splice map broke only at `\n`. A statement therefore ran on over
306
+ a bare `\r` into the next one, and a `// comment` ran on over it and hid the next declaration.
307
+ Two elements, two edges or a property and a child on either side of a bare `\r` were refused as
308
+ "in a form placement cannot locate". A bare `\r` ends the statement and the comment now. A CRLF
309
+ map reads exactly as before.
310
+ - **An addition anchored to a declaration whose line goes on with more code.** Placement put an
311
+ addition on the line after its anchor. When the next declaration opened a multi-line body on the
312
+ anchor's own line (`alpha = … { … } beta = … {`), that next line was inside beta. An element
313
+ bound to alpha's service was then written inside beta as its child, in a merge that parsed and
314
+ was written (exit 0). A block comment opened on the anchor's line and closed on a later one
315
+ swallowed the addition the same way. The addition now lands right after its anchor whenever
316
+ code, or a block comment the line does not close, follows the anchor on its line. Two archives
317
+ anchored to the two declarations on such a line also write the same bytes in either order now;
318
+ before, whichever ran second landed above the other. A comment trailing the anchor changes
319
+ nothing: the addition lands on the next line, as before — unless a bare `\r` ends that `// comment`
320
+ and the next declaration follows it on the same line, which counts as code following the anchor.
321
+ - **A bodyless declaration with a comment trailing it on its line.** A parent such as
322
+ `api = container 'API' // the api` that gained its first child got its `{` inside the comment,
323
+ and archive refused: the merged landscape "would not parse". The body now opens before the
324
+ comment, which then trails the new closing brace. A block comment that closes on a LATER line
325
+ was worse: the splice map ended the declaration inside it, so a child it gained, or an element
326
+ or edge anchored after it, was written inside the comment, in a merge that parsed and was
327
+ written (exit 0). The declaration now ends before such a comment. A comment that closes on the
328
+ declaration's own line keeps its place, and the bytes come out as before.
329
+ - **A delta whose feature tag sits in a tag list with a separator.** LikeC4 reads a tag list as
330
+ `#a #b, #c;`: one or more tags, then comma groups, then an optional `;`. Stripping the feature's
331
+ own tag left `#FEAT-1, #x` as `, #x` and `#FEAT-1;` as a bare `;`, and archive refused because
332
+ the merged landscape "would not parse". The tag now takes the comma after it with it (or else
333
+ the comma before it), and takes a `;` when no tag is left in front of it. A tag after a comma no
334
+ longer leaves that comma behind either: `#x, #FEAT-1` lands as `#x`, where it used to land as
335
+ `#x,`.
336
+
337
+ What the splice map still cannot locate is still refused (`merge-failed`, nothing written), and
338
+ the message no longer guesses. It used to say "most often two declarations sharing one line"
339
+ whatever the map held — over the reported declaration, which sat on a line of its own. It names the
340
+ declaration (a relationship by its endpoints, where it used to give only a count) and the line
341
+ LikeC4 parsed it on, then the cause the text shows: "it shares that line with '<other>'" only when
342
+ other code precedes it on that line (its own kind keyword, or an edge's own source, is not other
343
+ code); "the splice map read it as part of '<other>', which starts at line
344
+ <n>" when the scan ran a statement on past a line break LikeC4 makes (a bare carriage return was
345
+ the one such break until the splice map learnt to break there too, above); and otherwise that the
346
+ splice map read nothing there, with the rewrite to make and a request to report the shape. Wording
347
+ only; the code is unchanged.
348
+
349
+ ### Changed — `loam archive` refuses a delta that would bring back an operation another feature retired
350
+
351
+ Two codes, both added to the `IssueCode` union, and one additive `--json` key were added.
352
+
353
+ - **New `openapi.op-revived` (warn, gates archive).** A feature's `openapi.yaml` spells an
354
+ operation (by operationId) that its service's living contract no longer holds, and an ARCHIVED
355
+ feature retired it with an `x-loam-remove: true` marker — any archived feature, including one
356
+ whose directory carries the same feature id as the one being graded. Before this, the
357
+ quote read as an addition: a delta that had only quoted `createOrderV1` archived after the
358
+ feature retiring it and put it back into the living contract at exit 0, with the advisory
359
+ `api.op-unconsumed` the only finding. The message names the operation, the retiring feature(s)
360
+ and both remedies — delete the quote if it only restated the operation; archive with `--approve`
361
+ if the feature brings it back on purpose.
362
+ - **New `asyncapi.slot-revived` (warn, gates archive)** — the same on the event axis: a `channels`
363
+ or `operations` slot by its key, or a message by its name, that an archived feature retired. A
364
+ channel-slot removal's inline messages are named from that feature's `.loam-before/` snapshot
365
+ pre-image, read only while it still hashes to the digest its manifest recorded.
366
+ - **Both codes also cover an address an archived feature VACATED.** An archived feature that
367
+ relocated an operation (a marker on the old slot, the same operationId defined at a new one)
368
+ vacated the old address — and so did one that retired it there, once a later feature re-added it
369
+ somewhere else. A later delta quoting the operation at that address — unpinned — archived at
370
+ exit 0 and put the id back beside where it lives, one operationId at two slots (only the
371
+ advisory `openapi.duplicate-operationid` said so afterwards): the slot-keyed baseline gate saw
372
+ nothing living at the slot, and the id-keyed check saw the id living. That quote is now
373
+ `openapi.op-revived` too. Both codes are judged on the contract the archive would write: the
374
+ check runs the archive's own merge of the delta over the living contract and compares where each
375
+ operationId (each message name) stands there with where it stands now. One the merged contract
376
+ answers at two or more addresses — one of them new, and vacated by an archived feature — is the
377
+ finding; one the living contract still holds that it answers at exactly one address is not,
378
+ whether the delta moves it back with its own removal marker where it lives now or swaps another
379
+ operation in there (with or without an operationId — the merge replaces what stands at that
380
+ method and path). A removal marker standing where the id does not live deletes nothing, so it
381
+ no longer excuses the quote. The message says which archived feature moved the operation and
382
+ where to, or retired it there, and — apart from
383
+ that, since anything landed after can have moved it again — where it lives now; its remedy names
384
+ the address to drop: delete the quote, or, to move it back on purpose, add a removal marker
385
+ where it lives now — and where the delta already spells that address, so no marker can be
386
+ written there, archive with `--approve` instead. Where another operation lives at the quoted
387
+ address now (on the event axis, another message is declared at the quoted key), the message
388
+ names it and says archiving replaces it there — the merge writes one entry per address —
389
+ instead of calling the address one the living contract no longer has, and the move-back remedy
390
+ says to define it at another address (key) in the same delta if it is to stay: followed as it
391
+ read before, the remedy reached a clean gate and an archive that deleted the other operation
392
+ with nothing naming it. The retired-shape message names such an entry too. A quote of an
393
+ address an archived feature moved the operation away from is the finding even where the living
394
+ contract holds the operation nowhere (taken out of its new address by a hand edit, which leaves
395
+ no marker): the move is on the record, and the message reads as a retirement brought back,
396
+ naming the move. On the event axis the vacated addresses are `components.messages` keys (a
397
+ marker there) and the inline declarations a channel removal took; a delta that restates the
398
+ channel a name stands inline in with a `$ref`, or retires it, takes the name out of there and
399
+ is not graded. For an inline declaration the remedy names the channel (restate it with a
400
+ `$ref`, or retire it), never a marker nested on the inline message, which retires nothing. For
401
+ a `components.messages` key the remedy also names every channel (or operation) of the merged
402
+ contract still `$ref`-ing that key, to restate with the `$ref` pointed at the quoted key: told
403
+ only to retire the key, an author left the channel pointing at nothing, and its send stopped
404
+ sending the message — `asyncapi.remove-message-consumed`, which gates. `asyncapi.baseline-invalid`'s
405
+ move-back remedy says the same. A channel or operation slot is its key, so a "moved" one is a
406
+ retirement, as before. A message inline under a channel the delta brings back is reported by
407
+ the channel's finding alone only while the merged contract declares the name nowhere else; where
408
+ an archived feature re-declared it under `components` as it retired the channel, the inline
409
+ declaration is a finding of its own (one message name in two places) — it was skipped, and
410
+ `--approve` archived `asyncapi.duplicate-message`.
411
+ - **Exit code: `loam archive` now exits 1 (`not-coherent`) on this shape** without `--approve`;
412
+ with `--approve` it merges as before, and the operation lands in `overridden[]` and the text
413
+ output's "archiving despite" list, naming what came back and whose retirement it reverses.
414
+ `loam validate --feature` and `--all` report both codes as warnings: exit unchanged, except
415
+ under `--strict`, which fails on any warning and so now exits 1 on this shape. `loam status`
416
+ counts them as gating like every warning that gates, and `loam show <FEAT> --json` reports
417
+ `review.readyToArchive: false` for such a feature until the quote is deleted (or the feature is
418
+ archived past it with `--approve`).
419
+ - **`loam rebase` no longer calls a retired operation one "this feature is adding".** It pins
420
+ nothing for it either way, and now prints the same finding after its pin lines; the additive
421
+ `--json` key `revived[]` carries the findings (`severity`, `code`, `gates`, `subject`,
422
+ `message`). The operation's `pins[]` row still reads `unresolved`. The check runs over the
423
+ delta as rebase will write it, pins included, under `--dry-run` too: a quote pinned to its
424
+ own content is skipped by the merge, and repinned over another operation that now lives at an
425
+ address an archived feature moved it away from it becomes an edit that would put it back
426
+ there — that finding is in `revived[]` as well, and its `pins[]` row reads `repinned`. The
427
+ text output's revival banner no longer says "nothing was pinned" for such items.
428
+ - **`openapi.baseline-invalid` and `asyncapi.baseline-invalid` reworded** for a pin whose living
429
+ operation or slot is gone: they used to say "drop the marker", which turned the quote into the
430
+ revival above. They now say to re-read the living contract and delete the quote unless the
431
+ feature deliberately brings it back. When the living contract still holds the operationId (or,
432
+ for a `components.messages` key, the message name) elsewhere, the message instead says a
433
+ parallel feature MOVED it there (or retired it here, and another re-added it there) and where it
434
+ lives now, and to make any change there — never to archive past the revival finding, which would
435
+ answer it at both addresses. The `/loam-check` rows and `loam explain` say the same. `loam
436
+ rebase`'s text says such items were retired by an archived feature "or moved to another address
437
+ by one".
438
+ - **What it reads, and what it cannot see.** Only files under `features/archive/` — the archived
439
+ deltas' removal markers and each feature's `.loam-before/` snapshot; no git. A marker counts only
440
+ if the merge carried it out: the snapshot's pre-image must hold the marker's operationId at its
441
+ method and path (an event marker: its channel or operation key, or its message name at its
442
+ `components.messages` key), and a contract the archive created or never wrote lost nothing. A
443
+ marker archived past `openapi.remove-target-missing` / `-mismatch` (or the `asyncapi.*` twins)
444
+ under `--approve` deleted nothing, so a genuinely new operation it happened to name is not a
445
+ revival. A feature with no intact snapshot cannot be checked, and its markers count as written.
446
+ A removal made by hand-editing a living contract, or an archived feature deleted from disk,
447
+ leaves nothing to read, and a quote of such an operation still grades as an addition. A marker
448
+ with no `operationId` or one written at path level retired nothing when its feature archived, so
449
+ it is never named as the retirer.
450
+
451
+ ### Added — `#suite-<name>` tags and `loam list suites`: which services a test suite needs up
452
+
453
+ No code was added. One reserved tag prefix, one list section and one positional argument were.
454
+
455
+ - **A `dynamic view` may carry `#suite-<name>` tags** to say which test suites the journey runs in —
456
+ a separate prefix from `#cap-`, many-to-many (one view may carry several), each declared as
457
+ `tag suite-<name>` in the landscape's `specification` block like every LikeC4 tag, so a mistyped
458
+ suite is a parse error.
459
+ - **A view carrying only `#suite-` tags is now a use case.** The one opt-in predicate takes three
460
+ prefixes (`cap-`, `req-`, `suite-`), and so does the byte scan that lets a fleet with no use cases
461
+ skip the LikeC4 load. Such a view's hops are graded by `loam validate` like any other use case's
462
+ (`usecase.step-unbacked`, `usecase.step-contested`, `usecase.step-unlinked` — at fleet scope, and
463
+ per service for a flow beside a service's model), and every reader that names use cases sees it:
464
+ `loam diff`'s removed-operation victims, `loam delta` and `loam status`'s use-case blast radius,
465
+ and the context packs. A view that already carried `#cap-` or `#req-` is unaffected, and an
466
+ untagged view is still never graded. On a fleet that had drawn `#suite-`-tagged views before this
467
+ release, those views can now earn findings.
468
+ - **New section `loam list suites [<name>] [--json]`** — the answer to "which services must be up
469
+ for suite X". `[section]` accepts `suites`, and a new optional positional `[name]` narrows the
470
+ listing to one suite. Additive `--json` keys, emitted only with this section: `suites[]` (`name`,
471
+ `views[]` of `{id, file}`, `services[]` — enumerated service ids, sorted — and `unresolved[]` of
472
+ `{element, external}`: the participants that resolve to no service, `external` true when the
473
+ element or anything it is drawn inside carries `#external`, the walk stopping at the first
474
+ element bound to or titled for a service as `loam gate`'s partner reading does), `ungrouped[]` (`{id, file}` of every
475
+ use case carrying no `#suite-` tag, never narrowed by `[name]`), and `useCases` (`state`:
476
+ `absent` | `ok` | `unreadable`, beside `unreadable` and, when unreadable, `error`, spelled as
477
+ `loam list capabilities` spells them). Over an `architecture/` that does not parse the section
478
+ degrades instead of refusing, and `suites` / `ungrouped` are absent rather than `[]`. Only the
479
+ fleet map's use cases are listed. The text view prints one block per suite — its services, the
480
+ unresolved participants marked `!`, its use cases — then the use cases in no suite.
481
+ - **Refusals use existing codes.** An unknown suite name is `unknown-target` with close names; a
482
+ `[name]` after any other section, and `--needs-work` with `suites`, are `invalid-option` (a name
483
+ after another section used to be commander's "too many arguments", also `invalid-option` under
484
+ `--json`). The campaign flags (`--subsystem`, `--owners`) refuse `suites` as they refuse every
485
+ section but `services`.
486
+ - The MCP tool `loam_list` gains the matching optional `name` argument, and its `section`
487
+ description now lists `glossary` and `suites`. `name` without `section` is refused as invalid
488
+ params (`-32602`) rather than read as a section.
489
+ - `examples/docs` tags its one use case `#suite-smoke` and `#suite-notifications`, so
490
+ `loam list suites` there answers checkout-web, notification-service and order-service, with
491
+ `customer` and the `#external` topic `kafka.orderEvents` named as unresolved.
492
+
493
+ ### Changed — the use-case byte scan opens on a `#` tag reference, not on a bare prefix
494
+
495
+ No code, flag or payload key was added, changed or retired.
496
+
497
+ - **loam starts LikeC4 for use cases only when a document references a reserved tag.** The scan in
498
+ front of every reader of the fleet's use cases, and of a service's own flows, used to open on the
499
+ bare text `cap-`, `req-` or `suite-` anywhere in a file. It now opens on `#cap-`, `#req-` or
500
+ `#suite-`, with only whitespace, line breaks or comments between the `#` and the name, as LikeC4
501
+ allows. That is the only way a view carries a tag, so no use case is missed.
502
+ - **A broken `.likec4` beside a service's model that names a prefix only in prose no longer fails
503
+ the service.** In `0.2.0-alpha.8`, a file with no tagged view that mentioned `cap-` or `req-`
504
+ anywhere — a title, a comment — was loaded, and if it did not parse, `validate --service` (and
505
+ `--all`, per service) reported `usecase.flow-invalid`, an error, exit 1. This release's `suite-`
506
+ prefix would have added a third such substring (an id like `test-suite-runner`). Now the file is
507
+ skipped, like any untagged diagram.
508
+ - **The same holds at fleet scope.** Suppose `architecture/` does not parse, carries no tagged view,
509
+ and only declares `tag cap-…` / `tag req-…` or names a prefix in prose. It now reads as declaring
510
+ no use case. `useCases.unreadable` is then `false` where it used to be `true` in
511
+ `loam status --json`, `loam delta --json` and `loam list capabilities --json`, and so is
512
+ `useCaseScan.unreadable` in `loam context --json`. `validate --all` still reports that map as
513
+ `landscape.invalid`.
514
+ - A `#` followed by a reserved prefix still opens the scan wherever it is written, a comment
515
+ included.
516
+
517
+ ### Changed — `loam-feature` asks whether a feature is owed, and analyzes before it pins
518
+
519
+ No code, flag or payload key was added, changed or retired. What changes is the protocol text an
520
+ agent is handed, the `intent.md` a new feature starts from (and which of its text `intent.empty`
521
+ counts), and what the documentation says a feature's contract delta holds.
522
+
523
+ - **`loam instructions loam-feature` opens with a step 0.** A change that touches no requirement,
524
+ operation or fleet edge — a refactor, a dependency bump, a fix back to behaviour the living spec
525
+ already documents — needs no feature: a code PR, then `loam validate --service <id>` and
526
+ `loam vouch --pack --service <id>` in the service repo, and a person's vouch. WORKFLOW.md's "The
527
+ honestly-small change" now opens with the same lane.
528
+ - **A new, ungraded step 4b, "Analyze before you pin", sits between authoring and `loam rebase`.**
529
+ The agent reads `loam show <FEAT> --json`, `loam context <service> --feature <FEAT> --json` and the
530
+ `explore` neighbours, then writes questions under `## Open questions` in `intent.md`: ambiguous
531
+ SHALLs, values, limits and windows two services state differently (and which side refuses),
532
+ `Realizes:` lines that do not deliver their capability, each edge's timeout, retry, partial
533
+ failure and compensation, and neighbours left out of `--touches`. A person answers by editing the
534
+ file. `loam-implement` sends an ambiguity it meets to the same section.
535
+ - **`loam new` scaffolds an optional `## Open questions` heading in `intent.md`**, over a comment
536
+ only. **`intent.empty` now reads past that section:** questions an agent writes there are not the
537
+ feature's Why, so an intent whose only prose is open questions still gates the archive. The generated `loam-feature` command and skill pointers
538
+ list both new steps in their spine; an unedited pointer refreshes on the next `loam init`.
539
+ - **A feature's `openapi.yaml` / `asyncapi.yaml` is documented as holding only what the feature
540
+ adds, changes or removes.** SCHEMA.md, WORKFLOW.md, the `loam-spine` and `loam-feature` pages,
541
+ the `spec.md` scaffold's comment and the example deltas taught "a complete document, not a
542
+ patch". The merge never needed one: it walks only the paths, methods and slots a delta spells, a
543
+ local `$ref` resolves into the living contract, and an add-only delta needs no `loam rebase`.
544
+ Each needless quote is one more pin and, once a parallel feature removes the quoted operation,
545
+ either a blocking `openapi.baseline-invalid` (pinned) or an archive that puts the retired operation
546
+ back (unpinned); `loam-check`'s fix for the first now says to delete the quote. The
547
+ `loam-spine` page also no longer says path-level keys and components go unpinned —
548
+ `x-loam-baselines` covers them.
549
+ - **WORKFLOW.md gains "Build on the envelope":** a tracker export from `loam tasks <FEAT> --json`
550
+ keyed on unit ids (and what rewording re-keys one), a rollup of the per-service CI summaries
551
+ that reads a missing or old report as unknown, and `npx likec4 mcp` beside `loam mcp` on the same
552
+ docs repo.
553
+ - **The `loam-codes` page points at AGENTS.md's "Reference pages — fetch when needed" section** —
554
+ it named a "The reference pages" section that does not exist — and lists `loam-report` among the
555
+ pages `loam instructions` prints.
556
+
557
+ ### Added — `loam new --modifies <Req-ID>`: a small change is new → edit → archive
558
+
559
+ - **New repeatable flag `--modifies <Req-ID>` on `loam new`.** Each value names a living
560
+ requirement, by its `Requirement-ID:`, in a service the feature `--touches`. The scaffold copies
561
+ that requirement into the service's delta, uncommented, under `## MODIFIED Requirements`, with
562
+ its `Based-On:` pin already set. It is the same pin `loam rebase` writes, so a rebase right after
563
+ reports it `unchanged`, and the living text moving before archive is still `delta.baseline-stale`.
564
+ An `arch.spec.md` requirement lands in a scaffolded `specs/<svc>/arch.spec.md`. Before this, the
565
+ author copied the requirement by hand and ran `loam rebase` to pin it.
566
+ - **When every touched service has a `--modifies` and no `--new-service` or `--capability` is
567
+ given, no `delta.likec4` is written.** A feature that only rewrites requirements changes no
568
+ architecture, and the scaffold already told authors to delete the file in that case. `created`
569
+ in `--json` simply omits it. Any other combination scaffolds `delta.likec4` as before.
570
+ - **Refusals use existing codes.** An id that no touched service's living spec carries is
571
+ `unknown-target`. The message names the close ids in those specs, or the untouched service that
572
+ does carry the id (`add --touches <svc>`). A value that is not a Requirement-ID, or an id two
573
+ touched documents both carry, is `invalid-option`. A living spec loam cannot decode is
574
+ `merge-failed`, as `loam rebase` refuses it. Every refusal writes nothing.
575
+ - The opt-in MCP author tool `loam_new` gains the matching `modifies` argument. No code or payload
576
+ key was added.
577
+
578
+ ### Added — `loam_adopt`: the adopt brief on the MCP server
579
+
580
+ - **`loam mcp` serves a sixteenth read tool, `loam_adopt`**, mirroring `loam adopt [--service <id>]
581
+ [--subsystem <name>] [--targets] --json` (JSON arguments `service`, `subsystem`, `targets`). Its
582
+ result is the CLI's `adopt` envelope, verbatim. It is advertised `readOnlyHint: true` like every
583
+ base tool, because `adopt` writes nothing: it hands the agent the brief, and the agent writes the
584
+ baseline. Until now an MCP-only host could fetch the loam-adopt protocol through
585
+ `loam_instructions` and could not run its first step, `loam adopt --service <id> --json`. No
586
+ code, flag or payload key was added to the CLI; `tools/list` returns one more tool.
587
+
588
+ ### Changed — `loam init --create` with no service prints the docs repo's first hour
589
+
590
+ - **A run that creates a docs repo and binds no service now ends with `next — the docs repo's first
591
+ hour:`**: `loam seed --from fleet.yaml` (or draw the map by hand), `loam init --docs <docs-repo>
592
+ --service <id> --agent-profile service` in each service's own repo, `loam adopt --service <id>
593
+ --json` there, and `loam status`. It used to print nothing after the scaffold list. These are
594
+ WORKFLOW.md's Day zero steps and the rungs `loam status` already names on an empty fleet. A run
595
+ that binds a service still prints the single-repo trial's hour, and a join still prints neither.
596
+ Human output only: no code, flag or payload key was added, and the `--json` envelope is unchanged
597
+ for both variants.
598
+
599
+ ### Changed — every service bind loam prints names `--agent-profile service`
600
+
601
+ No code, flag or payload key was added, changed or retired. What changes is printed text and one
602
+ `--json` value.
603
+
604
+ - **The bind command now reads `loam init --docs <path> --service <id> --agent-profile service`**
605
+ in `loam status`'s `next.bind-service` step, its `loam explain` entry, the docs repo's first hour,
606
+ the generated AGENTS.md (`## The cycle`, step 0), and the `loam-adopt` and `loam-implement`
607
+ protocols. README, QUICKSTART and docs/SERVICE-TEAMS.md already told a fleet to bind each service
608
+ checkout this way, because `--agent-profile` defaults to `full`, and `full` writes `loam-feature`
609
+ and `loam-ship` into the service repo. But every bind command loam printed left the flag out, and
610
+ an agent runs the command loam prints. WORKFLOW.md's Day zero step 3 now spells it too.
611
+ - **`next.bind-service`'s `command` in `status --json` `next[]` carries the new text.** Its
612
+ `execution` block still names the same two placeholders.
613
+ - **Two binds keep their old form on purpose.**
614
+ - `loam-adopt` names the single-repo trial (`--create`, used only when the docs repo does not
615
+ exist yet) as the one bind that keeps the full profile.
616
+ - `doctor.service-unbound`'s fix is a re-run in a checkout that is already wired. An omitted
617
+ `--agent-profile` there keeps the profile that checkout recorded.
618
+
619
+ ### Changed — the OpenSpec compatibility pin moves to v1.14.1, and a weekly canary watches the newest release
620
+
621
+ No code, CLI flag or payload key was added, changed or retired. The `audit-openspec` /
622
+ `migrate-openspec` payload shape is unchanged, but the value it reports as the certified release moves.
623
+
624
+ - **`audit-openspec` and `migrate-openspec` now report OpenSpec v1.14.1 as the certified release.**
625
+ The banner line and `baselines.release` under `--json` carry `1.14.1` / `v1.14.1` /
626
+ `87c3595ace6a2e22957f39ebe5b74c2f2316e0cb` (released 2026-10-05) instead of `1.9.0` / `2826b88`.
627
+ `baselines.mainCanary` is unchanged. The corpus gate's `--baseline release` is now the v1.14.1
628
+ corpus: 223 Markdown files, 765 requirements and 2475 scenarios, every file round-tripped. v1.9.0
629
+ stays as the new `--baseline previous` regression pin beside `legacy` (v1.7.0) and `canary`, and
630
+ the scheduled `ci.yml` matrix runs all four. The sweep found no parser gap. The grammar OpenSpec
631
+ 1.13 added — a closing `#` run, `*`/`+` bullets on `REMOVED` and `RENAMED`, repeated delta
632
+ sections — was already read by the migration, and a new test pins the repeated sections.
633
+ `audit-openspec` and `migrate-openspec --apply`, run on OpenSpec's own v1.14.1 workspace, staged
634
+ all 251 living requirements and 747 scenarios across 36 services. The seven vendored fixtures stay
635
+ at v1.9.0, since v1.14.1 has no shape they lack. MIGRATING-from-OpenSpec.md's version matrix
636
+ records all of this.
637
+ - **The corpus sweep reads each file through the migration's own reader.** That reader is the one
638
+ `audit-openspec` and `--apply` use: loam's parser plus the grammar only OpenSpec accepts. The
639
+ sweep used to read with loam's parser alone. Every pinned corpus gives the same totals either way.
640
+ - **A new scheduled workflow, `.github/workflows/openspec-canary.yml`, watches the newest OpenSpec
641
+ release.** It runs weekly and on manual dispatch. It resolves the newest `vX.Y.Z` tag with
642
+ `git ls-remote` and sweeps it with the new `--baseline latest` mode of `npm run
643
+ test:openspec-corpus`. That mode reports how the totals moved from the certified pin instead of
644
+ refusing an unpinned commit, and still fails on a file that does not round-trip. The workflow also
645
+ audits OpenSpec's own workspace and diffs upstream's parser sources against the pin, in its step
646
+ summary. It has no push or pull-request trigger and blocks nothing; CONTRIBUTING.md's "The
647
+ OpenSpec canary" is the runbook. The README's note that `--baseline` defaults to `canary` was
648
+ wrong — it defaults to `release` — and now says so.
649
+
650
+ ### Added — documentation: running loam where service work runs, a CLI-only first ten minutes, and what loam costs an agent
651
+
652
+ No code, flag, command or payload key was added, changed or retired; the binary is unchanged. The
653
+ new paths are `docs/SERVICE-TEAMS.md` and `scripts/payload-sizes.ts`.
654
+
655
+ - **README has a service-repo CI recipe** beside the docs repo's fleet-edges one. The workflow
656
+ checks out the service and the docs repo as sibling subdirectories of the workspace, so a
657
+ committed relative `docsDir` such as `../docs` resolves. It does not use `path: ../docs`, which
658
+ `actions/checkout` refuses. It runs `loam validate --service <id> --json` at the same pinned
659
+ version, and two `jq` programs turn the findings, or a refusal, into `::error` / `::warning`
660
+ annotations and a `$GITHUB_STEP_SUMMARY` table. A deploy pipeline can add
661
+ `loam gate --service <id> --since <ref>`. It is followed by setup for cloud agents in a service
662
+ repo: a `copilot-setup-steps.yml` and a Codex environment setup script, each cloning the docs
663
+ repo into the committed `docsDir` with a read token from a secret.
664
+ - **README's day-zero binding passes `--agent-profile service`**, and says why: the profile
665
+ defaults to `full`, which puts `loam-feature` and `loam-ship` into every service checkout.
666
+ QUICKSTART's command for joining a shared docs repo passes it too.
667
+ - **QUICKSTART opens with ten minutes of CLI only.** A two-service `fleet.yaml`, `loam seed`, one
668
+ provider `openapi.yaml` and one misspelled `op` on an edge give `spine.op-undefined`, with no
669
+ agent and no adoption. A requirements-only `loam new --touches … --touches …` then validates and
670
+ plans its archive before either service is adopted. Every output shown was produced by running
671
+ the commands. The single-project path moves to sections 3–6.
672
+ - **`docs/SERVICE-TEAMS.md`, one screen for a team whose repository is one service in a fleet:**
673
+ four commands, five slash commands, what a person signs when they vouch, and the fifteen codes
674
+ they will actually meet. README, QUICKSTART and the Docs list link to it.
675
+ - **COMPARISON.md names OpenSpec, Spec Kit and Kiro again, in one dated section.** "Spec-driven
676
+ change tools, as of October 2026" sets out what each offers for a change that crosses
677
+ repositories: OpenSpec Stores and worksets, `show --diff` and the Cloud Agent; Spec Kit's
678
+ contract-driven guide, `/speckit.converge` and MCP server; Kiro's Design-First specs and Kiro
679
+ Web. It records what is still open upstream (OpenSpec #725, Spec Kit #4583) and where loam
680
+ differs, including the two gaps it has against them. Each claim links its source. The rest of
681
+ the page stays an ecosystem map, and `test/docs-drift.test.ts` now allows the three names only
682
+ inside that dated section.
683
+ - **`docs/BENCHMARKS.md` has a table of agent-facing payload sizes**, in bytes and ~tokens:
684
+ the generated `AGENTS.md`, `loam adopt --service <id> --json` with and without `--targets`,
685
+ every `loam instructions` page with and without `--no-fix-tables`, and `loam explain <code>
686
+ --json` across the whole vocabulary. `npx tsx scripts/payload-sizes.ts --write` generates it from
687
+ the tree, and `test/docs-facts.test.ts` regenerates it and fails when the committed table
688
+ differs. README's adopt row now points at that table instead of quoting a dated figure.
689
+ `test/docs-drift.test.ts` now also requires every exact `@ybotok/loam@<version>` in README to be
690
+ this checkout's version.
691
+
692
+ ### Changed — `loam archive`'s overwrite warnings name the current consumers
693
+
694
+ No code or flag was added. Existing warnings now fill a payload key that was always empty.
695
+
696
+ - **`openapi.op-modified` and `asyncapi.message-modified` now say who the overwrite affects.**
697
+ Each message ends with one of three phrases: `N current consumer(s) still name it`,
698
+ `whether current consumers name it could not be fully answered`, or
699
+ `no current consumer names it`. The warning's `details` (until now always `[]`) list the
700
+ consumers, in `archive --json` `warnings[]` and, when the contract merge is refused as
701
+ `not-coherent`, in the same warning's entry in `issues[]`:
702
+ - edges of the `architecture/` project;
703
+ - other services' living requirements, in either spec file;
704
+ - use-case hops;
705
+ - and last, any consumer whose documents could not be read, or any caller drawn without an
706
+ `op`.
707
+
708
+ The human plan prints the same lines under the warning. This is the scan `loam diff` runs for
709
+ `diff.op-modified` and `diff.message-modified`. Here it runs over the living fleet as it stands
710
+ before the merge, and only when one of these warnings fires. On the 120-service benchmark fleet
711
+ that costs about 130 ms. Severity is unchanged, and neither warning gates the archive. Whether a
712
+ change breaks a consumer is still not graded (`oasdiff breaking` / `asyncapi diff` do that).
713
+ - `openapi.component-modified` and `asyncapi.component-modified` do not name consumers. A
714
+ component's consumers are the operations or messages whose `$ref` closure reaches it, and loam
715
+ has no index of those.
716
+
717
+ ### Fixed — an MCP tool argument is never read in another argument's slot
718
+
719
+ No code, flag or payload key was added, changed or retired. Two MCP input schemas each gained one
720
+ JSON Schema keyword, named below.
721
+
722
+ - **A `tools/call` that supplies an optional positional argument while leaving out an earlier one is
723
+ refused as invalid params (`-32602`)**, naming the argument left out, as a missing required
724
+ argument already is. Positionals reach the command by position, so the later value used to be read
725
+ as the earlier one: `loam_instructions` with `args` and no `workflow` took the first arg as the
726
+ workflow name. An empty `args` still passes. The defect is present in `0.2.0-alpha.8`.
727
+ - **The MCP tools' advertised input schemas now say that `name` needs `section` and a non-empty
728
+ `args` needs `workflow`**, the rule a tool call already enforces, so a client that validates a
729
+ call against `tools/list` is no longer refused `-32602` for a call the schema allowed.
730
+ `loam_list`'s schema gains `dependentRequired` (`name` → `section`); `loam_instructions`' gains
731
+ `dependentSchemas`, requiring `workflow` once `args` has a member, because an empty `args` still
732
+ passes. Both are standard JSON Schema keywords; no tool, argument or other schema key changed.
733
+
734
+ ### Fixed — `loam validate` reports an unreadable verification record from the docs repo
735
+
736
+ No code, flag or payload key was added. An existing code fires in one more place.
737
+
738
+ - **`evidence.record-unreadable` (warn) now fires on the feature target too**, under
739
+ `validate --feature <FEAT>`, `validate <FEAT>` and every active feature under `validate --all`, run
740
+ from anywhere. A `features/<FEAT>/verification.yaml` or `verification/<svc>.yaml` with git
741
+ conflict markers, broken YAML, a shape that is not a record, or a summary that contradicts its
742
+ claims used to pass `validate --all` in the docs repo with no finding. `verify` refused the file,
743
+ and `status`, `gate` and `tasks` reported it. Until now only the service target raised this code,
744
+ and only from the service's own repository. The finding names the file and gives the same repair
745
+ as the service-target finding. Its `locations` hold that file as `primary`. It is a warning, as
746
+ it is on the service target. `valid`, the exit code and `loam archive` are unchanged, and
747
+ `--strict` fails on it. In a service repository's `validate --all` the same file is reported
748
+ twice: once by the service target and once by the feature target.
749
+
750
+ ### Fixed — `loam diff` reads a spec saved as UTF-16 without a byte-order mark the same way on both sides
751
+
752
+ No code, flag, path or payload key was added, changed or retired.
753
+
754
+ - On disk, a `spec.md` or `arch.spec.md` saved as UTF-16 without a byte-order mark was refused as
755
+ unreadable. At the base ref, the same bytes were read as a readable spec with no requirements.
756
+ That happened because only the working-tree side checked for NUL characters. So an untouched file
757
+ was suspended on one side only, and a branch that re-saved the file as UTF-8 reported every
758
+ requirement in it as `diff.requirement-added`. The base side now applies the same decoding rule as
759
+ the working tree. Both sides report the file as unreadable, and the spec axis is suspended. The
760
+ contract axes are unchanged: their readers on both sides already applied the same rule.
761
+
762
+ ### Fixed — a writer waiting on the lock no longer deletes the next writer's live lock
763
+
764
+ No code, flag, path or payload key was added, changed or retired. What changes is that concurrent
765
+ writers no longer fail, or share one commit window, when a lock holder exits while another writer
766
+ waits for it.
767
+
768
+ - **A stale `.loam-lock` is broken only while the holder judged dead still holds it.** A writer
769
+ that finds the lock held reads the holder, and if that pid no longer exists on this host, it
770
+ breaks the lock. Under CPU load the holder could release normally and exit between that read and
771
+ the pid check, and a third writer could take the lock in the same gap. The waiter then deleted
772
+ the third writer's live lock and took one of its own, so two writers were inside one commit window.
773
+ One of four concurrent `loam gherkin --json` runs failed as `merge-failed` (`ENOENT … rename
774
+ '….feature.loam-<pid>-…tmp'`): the second writer's recovery read the first's in-flight journal as
775
+ an interrupted commit and swept its staged files. The code is shared by every writer that takes
776
+ the lock (`archive`, `unarchive`, `new`, `seed`, `rebase`, `subsystem`, `verify --record`, `vouch`,
777
+ `gherkin`), and the defect is present in `0.2.0-alpha.8`. The waiter now re-reads the lock after
778
+ its verdict, and moves it to a private name before removing it. Anything it moved that is not the
779
+ lock it judged goes back in place. A crashed holder's lock is still reclaimed as before.
780
+
781
+ ### Fixed — `validate --all --base` grades the landscape by what the branch changed in it
782
+
783
+ No code, flag, path or payload key was added, changed or retired; the unscoped `validate --all` is
784
+ unchanged.
785
+
786
+ - **A call edge the branch adds or edits in `architecture/` is graded against its provider's
787
+ contract**, even when the provider is not in scope. README's own example — `authorisePayment` on
788
+ an edge into a contract spelling it `authorizePayment` — used to pass both PR gates on the example
789
+ fleet and, even under `--strict`, on a freshly seeded spine-first fleet. It now fails as
790
+ `spine.op-undefined` (and `spine.op-deprecated`, `spine.op-link-missing` and `spine.resolved` read
791
+ the same way), filed on the `landscape` target. Only those edges are graded: the provider is not
792
+ pulled into scope, so its own documents — on a seeded fleet, its `service.no-model` — stay out of
793
+ the PR's verdict. An edge is new when its (source, target, op) is; a reformat grades nothing.
794
+ - **An edge whose metadata or tags hold a word containing `op` is graded on the op it names.**
795
+ `--base` finds the branch's edges by reading the map's text, and that reading took
796
+ `op-version 'v2'` above `op 'cancelOrdr'` for op `v2`, and a `#op-critical` tag for an op naming
797
+ the description below it. The edge it read matched no edge in the parsed map, so the branch's
798
+ misspelt `cancelOrdr` was graded nowhere: exit 0 and `valid: true`, where the unscoped run
799
+ reports `spine.op-undefined`. A key is read whole now, and the op only from `metadata { }`.
800
+ - **An edge written after a bare carriage return is graded on its own source.** After a `link`,
801
+ `description` or `technology` property ending in a bare `\r`, the parent's sourceless
802
+ `-> target` edge was read as an edge FROM `link` (or `description`, or `technology`), and an edge
803
+ after a `// comment` ending in a bare `\r` was not read at all. Either way the branch's op on that
804
+ edge was graded nowhere, with the same exit 0. LikeC4 breaks a line at a bare `\r`, and the
805
+ reading does too now.
806
+ - **The landscape re-grades a fleet-wide vocabulary only when the file that carries it changed.**
807
+ `permissions.*` rides a scoped run only when `architecture/permissions.yaml` changed,
808
+ `capability.*` only when `architecture/capabilities.yaml` or `capabilities/` did,
809
+ `obligation.invalid` and `obligation.unapplied` only when `architecture/obligations.yaml` did,
810
+ `glossary.*` only when `glossary/` did, and `agents.stale` / `docs.binary-behind` only when
811
+ `AGENTS.md` did. A one-line map edit on the example fleet is no longer red under `--strict` on four
812
+ warnings that were all there at the base.
813
+ - README's spine-first section now says that under the `--base` gate a PR adding a service's
814
+ `openapi.yaml` stays red until that service has a `model.likec4` — `service.no-model` is the
815
+ ratchet working as designed.
816
+ - **Each service target in scope reports a committed map that does not parse, even when the branch
817
+ left `architecture/` alone.** On such a branch the scoped run emits no `landscape` target, yet
818
+ each service in scope still said `spine.landscape-invalid … the parser output is reported once,
819
+ on the landscape target`, with empty `details`. So the parser output was reported nowhere. Each
820
+ service's `spine.landscape-invalid` now carries the parser output in `details` whenever the run
821
+ has no `landscape` target, as `validate --service` always has. A feature target does not report
822
+ the map, so a run whose only targets are features still says nothing about it. The defect
823
+ predates this release's `--base` changes and is present in `0.2.0-alpha.8`.
824
+
825
+ ### Fixed — one LikeC4 typo no longer floods agent-facing output
826
+
827
+ No code, flag, path or payload key was added, changed or removed; what moves is the TEXT of the
828
+ diagnostics LikeC4 hands loam, and one warning that stops cascading.
829
+
830
+ - **A parser's "Expecting: one of these possible Token sequences" message is cut to its first four
831
+ alternatives plus `… (+N more)`**, keeping the `but found: '…'` line — on every surface that
832
+ prints a LikeC4 diagnostic (`validate`, `context`, `show`, `tasks`, `doctor`, `gate`,
833
+ `archive`). One dropped quote in the example fleet's order-service model made `loam validate
834
+ --service order-service` print 18,503 lines (434 KB; 458 KB as `--json`); it now prints 27 lines
835
+ (1.7 KB; 5.5 KB as `--json`).
836
+ - **Each LikeC4 diagnostic is reported once, in line order.** The same message at the same line of
837
+ the same file used to be listed twice, and the lexer's error led the parser's even when it sat a
838
+ dozen lines below the fault. The `N error(s)` count in `c4.invalid`, `delta.invalid` and the other
839
+ parse findings now counts distinct diagnostics.
840
+ - **`covers.unknown` is no longer raised for a service whose own `model.likec4` does not parse.**
841
+ `c4.invalid` is the one finding; the six `covers.unknown` warnings the example fleet showed for one
842
+ dropped quote named ids that were spelled right. The same suspension already applied to a model
843
+ extending a map that does not parse.
844
+ - **A broken document no longer bloats anybody else's briefing.** A dropped quote in a feature's
845
+ `delta.likec4` made `loam context order-service --json` 626 KB instead of 24 KB (now 27 KB). The
846
+ fleet map's parse error reached `useCaseScan.error` on every service's `context --json` and the
847
+ `loam gate` "landscape cannot be used" message raw — 173 KB and 152 KB — and now goes through the
848
+ same spelling as every other diagnostic, which adds its 1-based `L<n>:` line (23 KB and 2 KB).
849
+
850
+ ### Fixed — `doctor.agent-files-stale` fires on a changed body, not on every release
851
+
852
+ No code, flag, path or payload key was added, changed or retired; what a repo NOTICES is that
853
+ `loam doctor` stops warning about generated command and skill files whose only difference is the
854
+ version stamp.
855
+
856
+ - The stamp sits inside every generated command and skill file, so every release used to turn every
857
+ one of them stale in every repo, even when the release had not changed a word of the pointer — a
858
+ warning that is always on, which then hid the release where a pointer really did change. A file is
859
+ now stale only when its body, the stamp line set aside, is not what this loam writes, AND its stamp
860
+ is missing or older. Line endings do not count (a Windows checkout that converts them to CRLF is
861
+ not a change). A customized body under a current stamp stays quiet, as before. Same code, same severity;
862
+ the `agents.staleFiles` list in `loam doctor --json` simply stops naming stamp-only files.
863
+
864
+ ### Fixed — relationship order is the same on every run
865
+
866
+ No code, flag, path or payload key was added, changed or retired; lists built from relationships
867
+ come back in one stable order.
868
+
869
+ - LikeC4 indexes relationships by a hash of the document's path, and loam stages documents under a
870
+ fresh random folder per run, so an edge whose hash happened to come out all digits (about one in
871
+ 2,200) jumped to the front of the list on that run only; inside a project of several documents the
872
+ base order was whichever file finished reading first. A scheduled CI run failed on it. Every loader
873
+ now returns relationships sorted by source, target, title, `op`, `publishes`, `consumes` and tags,
874
+ so anything listed from edges — `loam explore`'s neighbours, the candidates a
875
+ `usecase.step-contested` finding names, findings emitted per edge — keeps one order between runs
876
+ and machines. That order is no longer the declaration order.
877
+
878
+ ### Fixed — the repository no longer claims a version is published
879
+
880
+ No code, flag, path or payload key was added, changed or retired; only documentation moved.
881
+
882
+ - README called the version in `package.json` "the newest published version", by construction of
883
+ the test that pinned it — and kept saying so for `0.2.0-alpha.8`, whose publish failed. It now
884
+ names that version as the version of the checkout and points at the npm dist-tags as the source of
885
+ truth; the drift test forbids the publication claim in README and in the CHANGELOG section of the
886
+ checkout's own version. The `0.2.0-alpha.8` section below carries a dated note that it was never
887
+ published.
888
+ - The `0.2.0-alpha.3` section promised that installing `@ybotok/loam@alpha` gets it. That release was tagged
889
+ and withdrawn before its publish step, which only the `0.2.0-alpha.4` section said. It now carries
890
+ the same kind of dated note.
891
+ - README said Codex reads `.codex/skills/`. That is the folder the `codex` target writes, and Codex
892
+ still loads it, but Codex's skills documentation names only `.agents/skills`. README now says so
893
+ and points at the `agents` target for the documented root. The `codex` target's path is
894
+ unchanged.
895
+ - README and WORKFLOW no longer quote byte sizes and code counts that had gone stale (the
896
+ `loam-check` page, the AGENTS.md orientation, the finding-code total). A finding- or refusal-code
897
+ count quoted on a shipped page is now graded against `loam explain --codes`.
898
+
899
+ ### Changed — `loam archive` moves the delta's bytes and nothing else's
900
+
901
+ - **The requirements merge splices instead of re-serializing.** A living requirement the delta does
902
+ not touch is written back byte for byte; only ADDED requirements and the ones a MODIFIED replaces
903
+ are serialized. Before, the whole `## Requirements` run was re-serialized, so a living spec
904
+ written without a blank line under `### Requirement:` had one inserted under every untouched
905
+ requirement by its first archive — the diff, `loam vouch --pack` and the re-vouch all widened to
906
+ the whole section. `loam unarchive` is unchanged and still restores every byte. Each kept slice
907
+ is matched to its requirement by `Requirement-ID:` (the heading when there is none), never by
908
+ position; if the slices and the living requirements do not match one to one, the whole run is
909
+ re-serialized as before rather than one requirement's text landing in another's place.
910
+ - **`archive --json` gains the additive `revouchOwed` key** (`[{service, path, owner}]`, on the
911
+ dry run and the real merge alike): every vouched living `spec.md` / `arch.spec.md` the merge
912
+ moves under its vouch stamp, with the frontmatter's `owner` (`null` when none is recorded).
913
+ `content.stale` follows on each — unchanged and still a warning; no new code, nothing `--strict`
914
+ lets through. The human view prints the same list under a `re-vouch owed` line, naming
915
+ `loam vouch --service <id>` and saying the re-vouches belong in the archive's own PR, which
916
+ `loam validate --all --base <ref> --strict` keeps red until they land. The `/loam-ship` protocol
917
+ gains that step, and README's docs-repo PR recipe says so.
918
+ - The human view of `loam archive` no longer prints its `archive <id>` header twice.
919
+
920
+ ### Fixed — the adopt brief's spec examples
921
+
922
+ - The `spec.md` and `arch.spec.md` examples in `loam adopt`'s brief name the service being adopted
923
+ in their frontmatter `service:` and title, where they used to say `payment-service` for every
924
+ service — a verbatim paste failed `frontmatter.field-mismatch`. Their requirements now carry the
925
+ canonical blank line under `### Requirement:`, so a spec copied from them is not reflowed by its
926
+ first archive.
927
+
928
+ ### Fixed — fleet readouts tell the truth about trust
929
+
930
+ A vouched spec edited after the vouch (`content.stale`, computable from the docs repo) was reported
931
+ by `loam validate` and by no fleet readout; `sources.unwalked` fired in every service repo straight
932
+ after a fresh vouch; and the human `status`/`list` views printed a verification count without
933
+ saying who answered it. Additive throughout — no code, flag or key changed or was removed, and the
934
+ maturity rung of every service is unchanged.
935
+
936
+ - **New stable code `next.revouch`** in `loam status`'s fleet `next[]` (and under `--service`): one
937
+ step per vouched service whose `spec.md` or `arch.spec.md` changed since the vouch, naming the
938
+ documents. Its `command` is `loam vouch --service <id>` and its `execution` is
939
+ `kind: "human-review"`, `runnable: false`, `cwd: "service-repo"` — a re-vouch is a person's act.
940
+ While one stands, `status` no longer reports `next.fleet-clean`.
941
+ - **New payload key `services.contentStale`** on `loam status --json` (fleet form): of `vouched`, the
942
+ services with a document moved under its stamp — a subset, like `sampledVouched`. The human line
943
+ reads `N vouched (M stale)`.
944
+ - **New payload key `scorecard.provenance.contentStale`** on `loam validate --all --json`: services
945
+ with a `content.stale` finding in that run. The text row reads `N vouched — M stale content ·
946
+ K stale digest(s) · …` (it used to say `0 stale digest(s)` beside the warning).
947
+ - **New payload key `answered` inside `verification`** on `loam status --json` (the feature form, and each
948
+ `features[].verification` of the fleet form) and on each `features[]` row of `loam gate --json`,
949
+ which spreads the same record: the confirmed claims per `answered_by`
950
+ (`runner`, `external-runner`, `agent`), the split `list --json` already carried. The human views
951
+ print it beside the verdict — `status <FEAT>` as `verified 19/19 confirmed (13 runner · 6 agent)`,
952
+ `list`'s verification column as `7/7 (5 runner · 2 agent)`. The verdict and its definition are
953
+ unchanged.
954
+ - **`loam list`** shows such a service as `vouched (stale)` (or `vouched (sampled, stale)`) and counts
955
+ it in the maturity line; its `missing` — in text and in `--json`, and likewise in `explore` and the
956
+ context pack, which share it — now reads `re-vouch (<file> changed since the vouch)`, so
957
+ `list --needs-work` (with `--review-order` and `--owners`) lists it instead of answering
958
+ "nothing to do". The `maturity` value stays `vouched`.
959
+ - **`sources.unwalked`** no longer counts loam's own files — `loam.json` and the agent files recorded
960
+ in its `agentFiles` — or any `.gitignore` toward its denominator. `.gitignore` is the file that
961
+ defines what git tracks, so it is the measuring instrument, never a part of the service a spec
962
+ owes; `package.json`, READMEs and the like stay in. A freshly vouched service repo is no longer
963
+ red under `validate --service --strict` for files it cannot honestly list in `sources`.
964
+ - **`loam list` legend** names the `e` (events / AsyncAPI) column: `[e]vents`.
965
+ - **SCHEMA.md**: the per-service CI summary shape gains `contentStale` (the count of `content.stale`
966
+ findings), and its jq derivation counts it. **WORKFLOW.md** states that `loam gate --strict` also
967
+ fails on a join partner's `content.stale`.
968
+
969
+ ### Added
970
+
971
+ - **New warning `frontmatter.owner-mismatch`** from `loam validate` (`--service` and `--all`) when
972
+ one service's `spec.md` and `arch.spec.md` both name an `owner` and the two differ. `loam show`
973
+ and `loam context` print only `spec.md`'s owner, so the other value was a second answer nobody
974
+ saw. Its `details` list each file's value (`spec.md: <owner>`, `arch.spec.md: <owner>`). Silent
975
+ when either file names no owner (that stays `frontmatter.field-missing`). It compares the two
976
+ files only, never CODEOWNERS. As a warning it fails `loam validate --strict`, so a service that
977
+ passed `--strict` with two different owners now exits 1 there until they agree.
978
+ - **`openapi.generated-stale` now says what moved.** The finding gains `details` naming each
979
+ operationId that differs between the build's contract and the committed copy — `served, not in
980
+ the committed copy: <id>` and `committed, no longer served: <id>` — and its message counts both
981
+ sides, or says every operationId matches when the difference is inside an operation or outside
982
+ `paths`. The digest still decides whether it fires; same code, same severity.
983
+ - **`diff.op-modified` (warn)**: an operationId present at the base ref and now, whose operation
984
+ changed — its own definition, its method or path, or anything its internal `$ref`s reach, so a
985
+ field renamed in a shared `components` schema counts. **`diff.message-modified` (warn)** is the
986
+ same for an AsyncAPI message: its declaration or anything its payload `$ref`s reach. Both name
987
+ the authored consumers in `details[]` (the same scan `diff.op-deprecated` runs) and point at
988
+ oasdiff or `asyncapi diff`; neither grades whether the change is breaking, so neither sets
989
+ `breaking` or changes the exit code. A PR that renamed a required field used to print "no
990
+ fleet-meaningful changes" and exit 0 with nothing else.
991
+ - **The `modified` count in `loam diff --json`'s `summary` now counts both new codes** beside
992
+ `diff.requirement-modified`. The key is unchanged; a diff over a changed contract reports a
993
+ higher number than before.
994
+ - A newly deprecated operation is still reported once, as `diff.op-deprecated`, not also as
995
+ `diff.op-modified`.
996
+
997
+ ### Changed
998
+
999
+ - **An AsyncAPI 1.x or 2.x document is `asyncapi.invalid`, once.** loam reads AsyncAPI 3.0 only,
1000
+ and a document whose `asyncapi:` field says `1.` or `2.` used to read as a 3.0 contract with no
1001
+ messages: one `spine.message-undefined` per landscape edge, one `spec-event.message-undefined` per
1002
+ requirement and a `spine.message-unproduced` on every consumer, none naming the version. It now
1003
+ takes the unreadable path every broken contract takes — one `asyncapi.invalid` whose `details`
1004
+ name the version and `asyncapi convert --target-version=3.0.0`, and nothing on the event axis
1005
+ graded against it. The same applies to a feature's own `asyncapi.yaml` under `--feature`. The
1006
+ `asyncapi.invalid` message, and the `context` / `delta` lines for an unreadable event contract,
1007
+ now say "does not read as AsyncAPI 3.0" rather than "does not parse". **It is an error:
1008
+ `loam validate` now exits 1 on such a document even where no edge or requirement names its
1009
+ messages** — a document that used to pass. `asyncapi convert --target-version=3.0.0` upgrades it.
1010
+ - **`loam diff --base <ref>` exits 1 while a CHANGED `asyncapi.yaml` says 1.x or 2.x.** The
1011
+ document is listed in that service's `unreadable[]` once for each side on which it says 1.x or
1012
+ 2.x, counted in the `unreadable` total of `summary`, and the service reads
1013
+ `change: "changed"`; it used to read as a contract with no messages and exit 0. One the branch
1014
+ did not touch does not do this — see the next entry. `asyncapi convert --target-version=3.0.0`
1015
+ upgrades it.
1016
+ - **`loam diff --base <ref>` skips an unreadable `openapi.yaml` or `asyncapi.yaml` that the base
1017
+ ref holds byte for byte.** Such a contract is no longer listed in `unreadable[]`, counted in
1018
+ `summary`, or the reason a service reads `change: "changed"`, so an unconverted
1019
+ AsyncAPI 2.x file, or a broken OpenAPI file, that the branch never touched no longer fails every
1020
+ PR's diff with exit 1. A contract the branch changed, or one only one side cannot read, still
1021
+ suspends its axis and exits 1. `spec.md` and `arch.spec.md` are not skipped: an unreadable one
1022
+ stays suspended even when untouched, because another service's removal may be the one that
1023
+ could not be checked against it. No key is added or removed.
1024
+ - **Suggested element ids follow `loam seed`'s convention.** For a service the fleet map does not
1025
+ draw yet, the `loam adopt` brief and the `loam new` delta scaffold now suggest `svc_<id>` (dots
1026
+ and dashes folded to `_`, e.g. `svc_payment_service`) instead of a camel-cased name, and
1027
+ `loam new --touches <id>` reuses the element id the map already draws the service as. A service
1028
+ the map nests inside another element is quoted by its last segment with a note naming the parent,
1029
+ since a dotted id is no top-level declaration. The commented example in a freshly seeded
1030
+ `architecture/landscape.likec4` uses the seeded ids too. Element ids stay cosmetic —
1031
+ `metadata { service '<id>' }` is the join — so nothing already written needs to change.
1032
+ - **`seed-landscape-edited` gives advice that can be followed.** The refusal and `loam explain
1033
+ seed-landscape-edited` used to say "fold the edits into fleet.yaml and delete the file", which is
1034
+ impossible once an edge carries `metadata { op }` (fleet.yaml cannot). They now say seed runs
1035
+ once, before any operation is drawn, and that the map grows from then on through
1036
+ `loam adopt --service <id>` and feature C4 deltas. Same code, same exit code.
1037
+ - **`migrate-openspec` and `audit-openspec` read the OpenSpec 1.13 requirement grammar.** A
1038
+ `RENAMED` pair written with a `*` or `+` bullet, or with no bullet, is a pair (it was
1039
+ `openspec.renamed-malformed`); a `REMOVED` section written as a bullet list (`-`, `*` or `+`) of
1040
+ requirement headings is read as those removals (it was `openspec.change-empty`); and a closing
1041
+ `#` run on a `### Requirement:` or `#### Scenario:` heading is dropped at ingest, so
1042
+ `### Requirement: Invoice totals ###` stages as `Invoice totals` and a MODIFIED delta naming it
1043
+ no longer fails `loam validate` as `delta.modified-unknown`. loam's own parser is unchanged.
1044
+ - **An inbound landscape edge into the provider with no `metadata { op }`** (and no
1045
+ `publishes`/`consumes`) is now listed in the `details[]` of `diff.op-removed`,
1046
+ `diff.op-removed-consumed`, `diff.op-deprecated` and `diff.op-modified` as a consumer whose
1047
+ operation is unknown. A removal with such a caller and no named one now says "could NOT be fully
1048
+ answered" instead of "no current landscape edge or living requirement names it". It stays a
1049
+ warning, exit 0: a map `loam seed` drew, every edge without an op, does not fail every removal.
1050
+ - **Without `--json`, a commander usage error** (a missing `--base` on `loam diff`, an unknown flag
1051
+ or command, a missing argument) now prints a fix line naming `loam <command> --help` and the code
1052
+ line `invalid-option · loam explain invalid-option` under commander's own message, like every
1053
+ other refusal. stdout stays empty, exit 1; the `--json` envelope is unchanged.
1054
+ - README's `.github/workflows/fleet-edges.yml` **pins loam to an exact version** (`LOAM`) and says
1055
+ how to move the pin, **adds an oasdiff step** per changed `services/*openapi.yaml`, and no longer
1056
+ labels the `diff` step "did you break a consumer?". `test/docs-drift.test.ts` keeps the pin equal
1057
+ to the version README names.
1058
+
1059
+ ### Contributors — release preflight and test harness
1060
+
1061
+ - `npm run release:check` checks that every dependency a `package-lock.json` entry names resolves
1062
+ to another entry, the way Node looks it up, and then runs `npm ci --dry-run --offline` on a
1063
+ scratch copy of `package.json` and `package-lock.json` (new `scripts/lockfile-check.mjs`): the
1064
+ check that would have refused `0.2.0-alpha.8`'s stripped lockfile before the tag instead of after
1065
+ it. The structural walk comes first because npm's own verdict depends on the npm asked: npm 11
1066
+ refuses that lockfile, npm 10 (Node 22.22.3) installs it.
1067
+ - The test harness creates its temporary directories under the canonical (symlink-resolved) temp
1068
+ root, so the suite passes on macOS without a `TMPDIR` override.
1069
+
1070
+ ### Changed — a federated verification record is one file per service (a file-format event)
1071
+
1072
+ - **New path written: `features/<FEAT>/verification/<svc>.yaml`.** `loam verify <FEAT> --service
1073
+ <svc> --record` (and `--results` / `--contract-results` under `--service`, and the bound form that
1074
+ substitutes `--service` from loam.json) now writes that service's attestation to a file of its
1075
+ own instead of into one shared `verification.yaml`. Each file is a `schema: 2` record holding
1076
+ exactly one attestation and that service's answers; the directory is the record, and the verdict
1077
+ and counts are computed from the union of its files when anything reads it. What a repo notices:
1078
+ two teams recording two different services on two docs-repo branches no longer conflict when
1079
+ their PRs merge — the shared file conflicted every time (add/add from an empty start, content
1080
+ conflicts on `summary:`, `claims:` and `attestations:` once one attestation had landed).
1081
+ - **Old path still read, forever: `features/<FEAT>/verification.yaml`**, active and archived, in
1082
+ every shape loam has written. A one-file federated record reads exactly as before until a service
1083
+ records again; that service's new file then supersedes its share of the old one (the old file is
1084
+ not rewritten), and the write that leaves nothing in it counting removes it. An all-at-once
1085
+ (schema-1) record is removed by the first `--service` write, which lists its answers in
1086
+ `discarded` — the same answers the old overwrite listed.
1087
+ - **The all-at-once form is unchanged on disk.** `loam verify <FEAT> --record` without `--service`
1088
+ still writes the one file `verification.yaml` — it answers the whole checklist from one place, so
1089
+ there is no second writer to collide with and no service to name a file after — and is still
1090
+ refused `record-federated` over any record carrying attestations, now including per-service files.
1091
+ - **When services answered different versions of the checklist**, a record kept as per-service files
1092
+ counts the answers of the files that answered the version the feature asks NOW — `loam verify`
1093
+ (read and `--record`), `loam tasks`, and `status`, `list` and `gate` for an in-flight feature all
1094
+ derive it — and falls back to the newest file (by `recorded` date, a later service id breaking a
1095
+ same-day tie) only for an archived feature's frozen view or when no file answered the current
1096
+ version. The others stay in `attestations` and are named by `verify.checklist-forked` until they
1097
+ record again — now on the recording run's own output (`notices` under `--json`) as well as the
1098
+ read view, so the service whose answers a tally leaves out is named where the tally is printed. A
1099
+ same-day record from a service with a later id against an older checklist no longer drops the
1100
+ current answers and makes every reader call the record stale. A one-file record keeps its old
1101
+ reading. The `verify.checklist-forked` message no longer suggests accepting the split.
1102
+ - **`record-unreadable` names the file that failed and the repair that is safe for it.** Git conflict
1103
+ markers in any record file are detected before the YAML is parsed and named with their lines. A
1104
+ service's own `verification/<svc>.yaml` is that service's word alone, so its advice is to repair
1105
+ it or delete it and record that service again; the one-file `verification.yaml` may hold other
1106
+ services' attestations, so its advice is to repair it or restore it from version control (after a
1107
+ merge, the copy on the branch you merged into) — never to delete it. The old advice ("delete it
1108
+ and record again") erased other teams' attestations when followed on a shared record, and a
1109
+ `verify.record-miscounted` record's "record the answers again" looped straight back into the same
1110
+ refusal. The same per-file advice reaches `evidence.record-unreadable`, and the `status` and
1111
+ `gate` sentences for an unreadable record no longer say "delete it". A per-service file holding
1112
+ anything but its own service's attestation is unreadable too.
1113
+ - **`path` in a recording `verify --json` payload** is the file this run wrote — the service's own
1114
+ `verification/<svc>.yaml` under `--service`. The `path` inside `recorded` on the read and frozen views, and the
1115
+ `verification` artifact's `path` in `status`, name the `verification/` directory once it exists,
1116
+ the one file otherwise. `archive` and `unarchive` carry the directory with the feature they move.
1117
+ No code or flag was added, changed or retired, and no payload key was added or removed. What
1118
+ changes is the VALUE of four `path` keys, and each may now name a DIRECTORY: `path` on a recording
1119
+ `verify --json`, the `path` inside `recorded` on the read and frozen views, the `verification` artifact's
1120
+ `path` in `status --json`, and the `path` on a `next.verify` step in `status --json` (which follows
1121
+ the artifact's). A script that opens one of them as a file must now expect
1122
+ `features/<FEAT>/verification` — a directory — once any service has recorded under `--service`.
1123
+ - **A loam older than this release does not read `verification/<svc>.yaml`.** It sees only
1124
+ `verification.yaml`, so `status`, `gate` and `verify` under an older pin report a feature recorded
1125
+ this way as having no record (`gate --strict` then exits 1 on `gate.feature-unverified`), and an
1126
+ older all-at-once `--record` writes a `verification.yaml` that this release ignores beside the
1127
+ directory (its answers carry no attestation). Upgrade every repository that records or gates a
1128
+ feature together — the docs repo's pipelines and each service repo's pin.
1129
+ - **`features/<FEAT>/verification/` is loam's from this release.** Every non-dot `*.yaml` in it is
1130
+ read as one service's attestation file, and one that is not (a hand-made `k6.yaml`, say) is
1131
+ `record-unreadable`: `verify` refuses and `status` shows the record as unreadable. Keep anything
1132
+ else beside the feature, not inside that directory.
1133
+
1134
+ ### Changed — `verified` means a run of the stamped scenario, and the runner-neutral path is findable
1135
+
1136
+ - **A cucumber run must have run the stamped scenario's step count.** `verify --results` used to
1137
+ confirm a `scenario.tested` claim from any report element carrying its `@loam-digest-…` tag whose
1138
+ steps passed — a one-step stub tagged by hand confirmed five-step scenarios. A matched cucumber
1139
+ element must now have run exactly as many steps as `loam gherkin` emits for the scenario (`And` /
1140
+ `But` included); otherwise the claim is `unconfirmed`, noted `ran N step(s) where the stamped
1141
+ scenario has M`. The step text is not compared (outlines fill their placeholders in, dialects
1142
+ differ on keyword spacing). cucumber-js's hook entries inside `steps[]` (`hidden`, keyword
1143
+ `Before`/`After`) are read as hooks, not steps: they no longer count toward the steps, and a
1144
+ failing one is reported `failed in a before/after hook`. `loamScenarioReport` entries carry no
1145
+ steps and are held to their status, as before. What a repo notices: a report whose elements do
1146
+ not run every stamped step stops confirming.
1147
+ - **A `--results` file that is not JSON is refused naming both accepted shapes.** A JUnit XML report
1148
+ died with the parser's raw "Unexpected token '<'". It is still `answers-unreadable`, and the
1149
+ message now says the file is XML or not JSON and names what `--results` reads: cucumber JSON
1150
+ (cucumber-js, cucumber-jvm, behave, SpecFlow/Reqnroll, pytest-bdd `--cucumberjson`, godog) or
1151
+ loam's runner-neutral `{"loamScenarioReport": 1, …}`. A `--contract-results` file that is not JSON
1152
+ names its one shape the same way.
1153
+ - **`loamScenarioReport` is named wherever cucumber is.** The `--results` description in
1154
+ `loam verify --help` now reads "cucumber JSON, or loam's runner-neutral loamScenarioReport JSON for
1155
+ any other runner"; the `check` in the `done` block `loam tasks` prints for a `scenario.tested` unit names both
1156
+ report shapes; the `verify.scenario-attested` message's suggested command reads
1157
+ `--results <report.json>` and names both; WORKFLOW, the verify, implement and done-check protocols
1158
+ and the cycle `loam explain` teaches say the same. So does the `next.verify-attested` step in
1159
+ `status --json` — its `statement` names both shapes and its `command` now reads
1160
+ `loam verify <FEAT> --results <report.json>` (it was `--results <cucumber-report.json>`) — and
1161
+ `loam explain next.verify-attested`.
1162
+ - **The all-at-once `--record` checks each evidence entry shaped like a `file:line`.** Without
1163
+ `--service` it accepted any non-empty evidence — `src/foo.py:1` for a file that does not exist —
1164
+ and printed `verified`, while the federated form refused the same citation. An evidence entry of
1165
+ an agent-confirmed answer that is shaped like a `file:line` (one token ending in `:<digits>`, not
1166
+ a URL) must now be a canonical relative path to a regular file inside the repository loam.json was
1167
+ found in, with that line in it; otherwise `answers-unevidenced`, exit 1, where it used to be
1168
+ recorded. Every other entry — prose ("manual QA by Alice"), a URL, a path with no line — is
1169
+ recorded exactly as before. The form still binds to no commit and stamps no pins. Evidence that
1170
+ lives in another service's repository is recorded from there, with `--service`. A missing cited
1171
+ file is now named in the repository's own spelling ("does not exist in this repository") in both
1172
+ forms, never through the machine's absolute path.
1173
+ - No code, flag, path or payload key was added, changed or retired.
1174
+
1175
+ ### Added — `loam gate --since <ref>`: the deploy gate no longer forgets a feature at archive
1176
+
1177
+ - **`loam gate` used to grade in-flight features only**, so a feature left the verification check
1178
+ the moment it was archived — and `loam archive` reads no verification record, by design. On the
1179
+ example fleet `gate --service order-service --strict` went from exit 1 (FEAT-112 unverified) to
1180
+ exit 0 after `loam archive FEAT-112`; a contract-only feature escaped even a vouched service,
1181
+ because `content_digest` covers the spec bodies and not `openapi.yaml`/`asyncapi.yaml`. Without
1182
+ the new flag that is still exactly what the gate does, and `--help`, WORKFLOW.md and
1183
+ `loam explain gate.feature-unverified` now say so.
1184
+ - **New flag `--since <ref>`** on `loam gate`: a docs-repo git ref — the docs commit the pipeline's
1185
+ last deploy recorded. Every feature archived after it that carries a `specs/<service>/` delta is
1186
+ graded too, by its frozen verification record, under the existing warn-only
1187
+ `gate.feature-unverified` (or the reused `verify.scenario-attested`). "Archived after" means its
1188
+ `features/archive/<FEAT>/` directory is absent at the ref, or differs from what it held there —
1189
+ committed since or not — so a feature unarchived, edited and archived again is graded as the new
1190
+ ship it is, not skipped as one an earlier deploy answered for. When git will not say what changed
1191
+ under `features/archive/`, every archived feature touching the service is graded. Its message
1192
+ names the ref and points at `loam unarchive <FEAT>` rather than a `loam verify --record` an
1193
+ archived feature refuses. The ref is read from the docs repo's own git exactly as
1194
+ `loam diff --base` reads its base, and refused the same way: `repository-unavailable` when git
1195
+ cannot answer from the docs repo (or cannot list `features/archive/` at the ref), `unknown-target`
1196
+ when the ref names no commit. No new code; no deployment state is stored.
1197
+ - **An archived record is held to the checklist its feature shipped with.** A record gone stale
1198
+ before the archive (a scenario appended after it was written) read `stale`/`unverified` in flight
1199
+ and `verified` with no finding once archived, because an archived record is never judged against
1200
+ a checklist re-derived after the merge. `loam archive` now writes the digest of the checklist a
1201
+ fresh `loam verify` derives just before the merge — when the feature carries a readable record —
1202
+ into its snapshot manifest as a new key, **`checklist`**, in
1203
+ `features/archive/<FEAT>/.loam-before/manifest.json` (additive: the manifest stays version 3,
1204
+ `unarchive` ignores the key, and an older loam reading the manifest is unaffected). `gate --since`
1205
+ grades the frozen record against it: a record answering an older checklist is `state: "stale"`,
1206
+ `verdict: "unverified"`, with a `gate.feature-unverified` saying the feature moved after its
1207
+ record was written. A manifest without the key — an archive made by an older loam — is graded as
1208
+ before, as frozen history, and the row says so. `list`, `status` and `verify` keep reading an
1209
+ archived record as frozen history, never stale.
1210
+ - **New `--json` payload keys:** top-level `since` — `{ ref, commit }` under `--since`, `null`
1211
+ without it — and, only on a row `--since` added, `features[].archived` (`true`) and
1212
+ `features[].checklistAtArchive` (the captured digest, or `null` when the archive captured none).
1213
+ An in-flight row carries neither key, so a payload without the flag is unchanged apart from
1214
+ `since: null`. The text view titles the block "verification — in flight, and archived since
1215
+ <ref>", marks those rows `· archived`, and adds a line under a recorded row whose archive
1216
+ captured no checklist.
1217
+ - **The MCP `loam_gate` tool** gains the matching `since` string argument.
1218
+
1219
+ ## [0.2.0-alpha.8] - 2026-09-21
1220
+
1221
+ The work packet: the verify checklist read forwards. Still a prerelease on the `alpha` line.
1222
+ **Not published (noted 2026-10-10):** the `v0.2.0-alpha.8` tag was cut, but its release run failed at
1223
+ `npm ci` — npm on Windows had stripped other platforms' optional packages from the tagged commit's
1224
+ `package-lock.json` during the version bump (restored on `main` the same day) — so the publish never
1225
+ ran. `npm i @ybotok/loam@alpha` does not install this version, and `npm view @ybotok/loam dist-tags`
1226
+ is the record of what does. Everything in it is additive — no stable code was added,
1227
+ changed or retired, no existing flag, path or payload key moved, and nothing a repository has
1228
+ committed needs to change. What a repo NOTICES after upgrading: one new read-only command
1229
+ (`loam tasks`), new keys on two `--json` payloads it may already consume, and a corrected SCHEMA
1230
+ sentence about how scenario digests are salted — the code did not move, the sentence did.
1231
+
1232
+ ### Added — `loam tasks`, the derived answer to "what do I do next, and what closes it"
1233
+
1234
+ The question an authored task file answers is a good one; the file is the wrong place to answer it,
1235
+ because a second mutable state drifts from the spec it was copied out of. So this is that answer
1236
+ DERIVED — the `loam verify` checklist read forwards, one work unit per promise the feature makes,
1237
+ each carrying the step it is waiting on and the fact that retires it, recomputed on every run from
1238
+ the delta, the generated suite and the verification record. SCHEMA's "Deliberate limits of the
1239
+ workflow surface" row refusing an authored `tasks.md` as authoritative state **stands unchanged**;
1240
+ this is the projection that row has always pointed at, and it is a projection precisely so that
1241
+ nothing here can disagree with the documents it was derived from.
1242
+
1243
+ - **New read-only command `loam tasks <FEAT> [--service <id>] [--next] [--json]`.** It writes
1244
+ nothing, anywhere, ever, and creates no artifact. Every unit it reports is one claim of the same
1245
+ checklist `loam verify` derives, **under the same id** — a unit and the answer that closes it are
1246
+ one identity on both commands. The one unit with no claim behind it is minted for an ADDED or
1247
+ MODIFIED requirement carrying no scenario at all (kind requirement.described): nothing to test
1248
+ means nothing for the checklist to ask, and a promise with no acceptance criteria is still work.
1249
+ - **`units[]` is the projection**, one entry per unit, carrying `id`, `kind`, `subject`, `at`,
1250
+ `claim`, `dependsOn[]`, a `done` block, and — where the ledger has them — `digest`, `operation`,
1251
+ `token` and a `source` naming the document, the 1-based line and, for a scenario, its body
1252
+ verbatim.
1253
+ - **`at` is the step the unit is waiting at**, one of `describe`, `generate`, `implement` or
1254
+ `attested` — never a percentage. It is spelled `at` rather than `stage` or `verdict` on purpose:
1255
+ both of those already mean something else on payloads an agent reads beside this one.
1256
+ - **Done is a fact, not a tick.** A closed unit stops appearing, and exactly one thing closes one: a
1257
+ claim recorded `confirmed` in a `verification.yaml` that is **not stale** against the current
1258
+ checklist. A stale record closes nothing — the words moved under the answers. An agent-attested
1259
+ scenario never vanishes: it stays visible at `attested`, because attested is not verified and a
1260
+ work list that hid the difference would be the gate that teaches everyone to say yes.
1261
+ - **`frontier` is the ids of the first ten open units in derived order**, providers before consumers
1262
+ on the exact token join `c4.calls.token == api.exposes.operation` — never a reading of prose —
1263
+ with `frontierElided` saying how many open units it left off. **The order is advice and never a
1264
+ lock**: nothing is assigned, nothing is reserved, and working out of order is legal and invisible
1265
+ to every other command.
1266
+ - **`--next` is the leaf packet**: the same payload with `units` narrowed to the frontier head, and
1267
+ what an orchestrator hands one coding agent. Everything else is left whole on purpose, so a cheap
1268
+ model handed one unit cannot read it as the whole job. The text view prints that leaf in full —
1269
+ the scenario's Given/When/Then verbatim and never reflowed (those are the bytes the digest was
1270
+ taken over), its digest, the `done` block whose `check` is the fact that closes the unit, and the
1271
+ literal follow-up command. Literal rather than a shape to fill in, because "the appropriate verify
1272
+ invocation" is a guess the reader will get wrong.
1273
+ - **`--service <id>` is a pure lens** over the same derivation, and adds `waitingOn[]`: the open
1274
+ units in OTHER services that this service's units depend on.
1275
+ - **`stamps` is `read` or `unknown`.** `unknown` means the bound service repo's generated suite could
1276
+ not be read — could-not-look, and never nothing-wrong.
1277
+ - **The rest of the payload**: `feature`, `service` (the lens, or null), `services[]`, `path`,
1278
+ `intent`, `checklist` (the digest of the question set the units derive from), `record` (`absent`,
1279
+ `recorded`, `stale` or `unreadable`), `size{requirements,scenarios,operations,messages,calls}` —
1280
+ the size of the DELTA and never of the open work, so that "this feature is small" and "this
1281
+ feature is nearly done" cannot become one sentence — `contracts{expose[],call[]}`,
1282
+ `counts{open,done,attested}`, and `review[]`, the four instructions for re-deriving this answer
1283
+ and reviewing it.
1284
+ - **A broken `delta.likec4` or contract axis exits 1 with `ok: true`**, naming what it could not read
1285
+ in `unreadable` (`architecture`, `contracts[]`). That is `loam delta`'s own rule rather than a new
1286
+ one: a feeder that could not be read empties the packet, an agent consuming an empty packet reads
1287
+ "nothing to do", so the envelope stays informative and true while the exit code stops a pipeline
1288
+ building on it.
1289
+ - **New MCP read-only tool `loam_tasks`**, declaring `readOnlyHint` like every other read tool; the
1290
+ base table is fifteen tools where it was fourteen.
1291
+ - **No stable code was added, changed or retired, and no existing flag, path or payload key moved.**
1292
+ The change is purely additive: everything `loam tasks` reports is a projection of state other
1293
+ commands already derive and grade.
1294
+
1295
+ ### Added — `loam delta --json` carries the joins and the lines it had been dropping
1296
+
1297
+ - **New additive keys per requirement**: `line` (where it sits in the document it was parsed from, so
1298
+ a consumer can send an author to the line rather than to the file), `basedOn`, `requires`,
1299
+ `capabilities`, `realizes`, `publishes` and `consumes`. Every one of them is already written on the
1300
+ requirement and every one names work — which permission gates it, which capability it belongs to,
1301
+ which named promise it realizes, which messages it produces and consumes — and an implementer
1302
+ handed this projection had to go and read the Markdown it claimed to have projected to find them.
1303
+ - **New additive keys per scenario**: `line`, and `digest`, the 16-hex scenario body hash. That
1304
+ digest is the exact join key the rest of the toolchain already uses — the `@loam-digest-<16hex>`
1305
+ tag `loam gherkin` stamps and the string `loam verify --results` matches a cucumber report on — so
1306
+ a consumer of this brief can now name the test that answers an acceptance criterion instead of
1307
+ regenerating the suite to find out. It is salted by the owning service and the axis, like every
1308
+ other spelling of it, so an arch requirement's scenarios digest differently from identically
1309
+ worded business ones.
1310
+ - **Nothing existing moved.** Every key this payload already carried is untouched, the text output is
1311
+ unchanged, and no stable code was added or changed.
1312
+
1313
+ ### Added — `loam gherkin --json` says which stamp belongs to which scenario
1314
+
1315
+ - **New additive key `files[].stamped`**, a list of `{scenario, digest}` pairs. `digests` was a bare
1316
+ array and `scenarios` a bare count, so the one thing a consumer needed — which stamp belongs to
1317
+ which scenario — was the one thing this payload could not say, and the only way to learn it was to
1318
+ read the generated file back off disk. A `kept` row reads its pairs off the file that is really on
1319
+ disk, because its numbers describe the in-flight emission that stayed rather than the one that was
1320
+ withheld.
1321
+ - **`digests` and `scenarios` are unchanged beside it**, and no stable code was added or changed: the
1322
+ envelope permits adding a key and never changing one.
1323
+
1324
+ ## [0.2.0-alpha.7] - 2026-09-07
1325
+
1326
+ Infrastructure belongs on the fleet map, and the render is navigable at fleet scale. Still a
1327
+ prerelease on the `alpha` line: `npm i @ybotok/loam@alpha` installs it. One code is REMOVED —
1328
+ `landscape.datastore-private`, the first code this project has ever retired — and no flag, path or
1329
+ payload key is. What a repo NOTICES on its first `validate --all` after upgrading: a
1330
+ `subsystem.views-stale` error against the new `architecture/services.likec4`, in every fleet whose
1331
+ map draws a service, cleared by one `loam subsystem sync`; a second one against
1332
+ `architecture/subsystems.likec4` wherever subsystems exist, because those views gained boundary
1333
+ edges and an `of` subject; and silence where a datastore drawn beside the services used to be told
1334
+ to move.
1335
+
1336
+ **Upgrading: one `loam subsystem sync` per docs repo, once.** `loam validate --all` reports
1337
+ `subsystem.views-stale` (error) against the new `architecture/services.likec4` in EVERY existing
1338
+ docs repo whose fleet map draws a service — flat fleets with no subsystems included, because the
1339
+ file is new everywhere and an absent generated file is a state of its own. This is not a
1340
+ subsystems-only upgrade: a fleet that has never run `loam subsystem new` gets the finding too. A
1341
+ repo that DOES have subsystems gets a second finding on the same run, against
1342
+ `architecture/subsystems.likec4`, because the subsystem views gained boundary edges and an `of`
1343
+ subject and every existing copy therefore re-renders differently. One `loam subsystem sync` writes
1344
+ both files and clears both findings. `loam archive` names the same repair rather than claiming
1345
+ "complete + current". Nothing else in a repository has to change, and no document a team wrote is
1346
+ touched — and a document a team wrote that is already standing at one of the two generated paths is
1347
+ refused by name rather than overwritten (below).
1348
+
1349
+ ### Removed — `landscape.datastore-private`, because where a store is drawn is the team's choice
1350
+
1351
+ - **`landscape.datastore-private` (warn) is retired.** It fired on a `database` element drawn at
1352
+ fleet level with exactly ONE consumer, and its whole message prescribed deleting the peer and
1353
+ writing the store inside its owner. The identical store nested was silent, so the check graded the
1354
+ DRAWING and not the architecture — a restriction rather than a fact about the fleet. A store one
1355
+ service owns may now be nested under that service's element or drawn beside the services, and loam
1356
+ grades neither. The code is not in the frozen `ErrorCode` or `IssueCode` unions, so removing it
1357
+ breaks no contract; `loam explain landscape.datastore-private` no longer answers, and the row is
1358
+ gone from the agent-facing check table.
1359
+ - **`landscape.datastore-shared` (warn) is unchanged**, both arms. Two or more services on one store
1360
+ is a fact about coupling, not a restriction, and it fires at two consumers for both placements
1361
+ alike. What survives from the retired code's message is the one clause that was a fact about
1362
+ LikeC4 rather than a preference, and SCHEMA's "Private stores" now leads with it: a second
1363
+ consumer's edge resolves only against a declaration the MAP holds, because a per-service project
1364
+ is `architecture/` plus one model.
1365
+ - **Infrastructure is not a service.** A new `INFRA_KINDS` set in `core/vocabulary/maturity.ts` —
1366
+ `database`, `queue`, `topic`, `cache`, `bucket`, compared case-insensitively — takes such an
1367
+ element out of four surfaces at once: the drawn-service census behind
1368
+ `landscape.service-undocumented`, the `loam gate` partner set (an infra endpoint is dropped from
1369
+ the `partners` payload, exactly as an actor already was), the `loam verify` checklist (a
1370
+ fleet-level store no longer mints a `service.exists` claim no repository can answer), and the
1371
+ scorecard's element denominator, whose `c4` row now counts services rather than services plus
1372
+ their stores. The hook is the kind NAME, with the same limitation `ACTOR_KINDS` lives with: a
1373
+ fleet spelling its kind `postgres` is not covered, and a tag can be added later as a second
1374
+ spelling. A store BOUND to a directory with `metadata { service }`, or titled like one, stays an
1375
+ adopted service on every one of the four surfaces — the census predicate wins over the kind.
1376
+ `landscape.service-undocumented`'s message gained a THIRD remedy beside "bind it" and "tag it
1377
+ `#external`", spelled from the same set: neither of the old two was true of a store the fleet
1378
+ owns, and that message is the only place an author learns which kind spellings loam reads.
1379
+ - `c4.uncovered` was left alone, and the measurement is the reason: a delta drawing
1380
+ `splitLedger = database 'split-ledger'` earns it, and adding `Covers: splitLedger` to the
1381
+ feature's `arch.spec.md` delta silences it exactly, with no `covers.unknown`. The remedy the
1382
+ message names is true, so this is not a residual restriction — and the code fires only on elements
1383
+ a feature DELTA adds, never from `validate --all` over a living tree.
1384
+ - The acceptance criterion, run for real: a `database` drawn at fleet level, owned by the fleet, with
1385
+ one consumer, no `#external` tag and no binding, now earns ZERO findings from `loam validate --all`
1386
+ and from `loam gate`.
1387
+
1388
+ ### Added — `architecture/services.likec4`: one generated overview view per service
1389
+
1390
+ - **New generated path `architecture/services.likec4`**, written by `loam subsystem sync` beside
1391
+ `architecture/subsystems.likec4`, removed by it when the fleet map draws no service, and owned
1392
+ outright by loam: views only, byte-reproducible, one view per service the map resolves, sorted by
1393
+ service directory id. Each view is declared `of` its service's element, which is what makes the
1394
+ fleet map, every subsystem view and every other service view click-through into it with no
1395
+ authored link — measured at likec4 1.59.2. Peers are NAMED one by one rather than wildcarded,
1396
+ because on a map that groups its services the wildcard form collapses every peer to its group box
1397
+ and yields no drill-down at all (measured on a 120-service fleet: 0 of 10 nodes navigable). Median
1398
+ at 58 services in 11 groups: 10 nodes, 8 edges per view, about 7 ms of layout each.
1399
+ - **Titles are folders.** A per-service view is titled `<subsystem path> / <service directory id>`,
1400
+ built from the same path helper the subsystem views use, so both generated files fold into one
1401
+ browser folder tree mirroring `services/`. LikeC4 has no view-folder syntax — `views { group … }`
1402
+ is a parse error at the pin — and splitting the title on `/` is the whole mechanism.
1403
+ - **An authored view of the same element keeps precedence.** Two views `of` one element are legal;
1404
+ the first document alphabetically wins the default navigation target and both stay reachable,
1405
+ because the subject's own node cycles to the other. `architecture/landscape.likec4` sorts before
1406
+ `architecture/services.likec4`. loam does not detect the authored view and never will:
1407
+ docs/DESIGN.md rule 26 forbids reading what a view SHOWS.
1408
+ - **Subsystem views gained two things** in the same generator: `include <member> -> *, * -> <member>`
1409
+ per member, so a subsystem view answers "what does this group touch" and not only "who is in it",
1410
+ and `view subsystem_<name> of <ancestor>` when the map holds an element owning exactly that
1411
+ subsystem's members. Measured at 58/11: ~14 nodes, ~38 edges, 12 of 14 nodes navigable. Every
1412
+ existing `architecture/subsystems.likec4` therefore re-renders differently and is stale once.
1413
+ - **`subsystem.views-stale` now grades two files**, one finding per file, each naming its own path in
1414
+ `subject`, `locations[0]` and the message. No new code was minted: both files are written by one
1415
+ command in one commit and repaired by one run, which is what makes one code honest.
1416
+ - **`subsystem.view-id-collision` reserves `service_` as well as `subsystem_`.** A `service_` id is
1417
+ reserved only for a service the fleet map actually DRAWS, so an unmodelled `services/<id>/`
1418
+ reserves nothing; the message names which generated file and which prefix.
1419
+ - `deployment.doc-reserved` refuses a feature deployment document named `services.likec4` for the
1420
+ same reason it already refused `subsystems.likec4`: both names are generated, overwritten
1421
+ wholesale, and parsed by nothing.
1422
+ - **No writer overwrites a generated views file it did not write, and refusing is now an exit 1.**
1423
+ Both generated paths are filenames loam RESERVES inside `architecture/`, a directory the team
1424
+ owns, and every writer of them rewrites the file whole — so a repository that already kept a
1425
+ hand-authored diagram at one of those paths would have lost it, silently, on the first run after
1426
+ the release that reserved the name. Reproduced before the guard existed: with a hand-authored
1427
+ `architecture/services.likec4` in the tree, `loam subsystem new platform` printed
1428
+ `architecture/services.likec4 updated` and destroyed it, first line and all.
1429
+ - **What makes a file loam's is its FIRST LINE**, `// generated by loam`, with a leading byte-order
1430
+ mark stripped before the test — a Windows editor adds one without changing a fact, and without the
1431
+ strip loam would call its own output authored and refuse to regenerate it for good. A hand EDIT of
1432
+ a generated file keeps that line, so it is still loam's to repair and one `loam subsystem sync`
1433
+ puts it back: that has always been what `subsystem.views-stale` says. Only a file that never
1434
+ carried the mark is somebody else's. **A 0-byte file at either path counts as somebody else's** —
1435
+ it carries no mark — so an empty `architecture/subsystems.likec4` or
1436
+ `architecture/services.likec4` is no longer filled in; delete it and re-run.
1437
+ - **`loam subsystem sync` declines the one file and finishes the rest.** `--json` reports the new
1438
+ `action` value **`refused`** — on `action` for `architecture/subsystems.likec4`, on
1439
+ `serviceViews.action` for `architecture/services.likec4`, one predicate behind both — while the
1440
+ other generated file, the per-service project files and the root `exclude` are still done and
1441
+ still reported, and the **exit code is 1**. That is `loam validate`'s shape rather than a refusal
1442
+ envelope, and the non-zero exit is the point: `loam validate --all` goes on reporting
1443
+ `subsystem.views-stale` for that path with `loam subsystem sync` named as the repair, so a run in
1444
+ a hook or a CI step must not report success over a file it can never fix.
1445
+ - **`loam subsystem new` / `rm` / `move` / `rename` and `loam seed` refuse WHOLE**, before a byte is
1446
+ staged: `error.code` `already-exists`, exit 1, nothing written and — for `move` and `rename` —
1447
+ nothing renamed. No new code was minted, because that is what `already-exists` has always meant on
1448
+ these verbs; `loam seed` can answer it for the first time. They refuse whole rather than per-file
1449
+ because their entire contract is that the generated files land in the SAME commit as the change,
1450
+ so completing the structural half would manufacture the very `subsystem.views-stale` state that
1451
+ commit window exists never to leave behind — irreversibly, for a `move` that also stages renames.
1452
+ - **The refusal sentence is one sentence, verbatim from all five writers.** It names the mark loam
1453
+ looked for, says that not one byte changed, and gives the way out: rename the file to a name loam
1454
+ does not reserve — every other `.likec4` under `architecture/` renders in the same project, so the
1455
+ diagram is not lost — or delete it, then re-run.
1456
+ - **Payload keys, all additive.** `loam subsystem sync --json` keeps `path` and `action` meaning the
1457
+ subsystem file and adds `serviceViews: { path, action }`; `loam subsystem new` / `move` / `rm` /
1458
+ `rename --json` keep `views` and add `serviceViews`; `loam archive --json` keeps
1459
+ `subsystemViewsStale` and adds `staleViews: string[]`, the generated files it left behind. Nothing
1460
+ existing changed meaning.
1461
+ - The generated-file SET is now one table (`GENERATED_ARCHITECTURE_FILES`), read by the path
1462
+ builders, the name reservation, both project loads that must EXCLUDE these files (the fleet map's
1463
+ and `c4.fleet-project-invalid`'s), archive's staging and `loam seed`'s. Four readers that must
1464
+ never disagree about which files are generated, and a name in one but not another failed silently
1465
+ both ways while there was only one.
1466
+
1467
+ ### Added — the intra-service use-case slot, published rather than graded
1468
+
1469
+ - **`loam adopt` briefs `services/<…>/<id>/usecases/<name>.likec4` as an OPTIONAL target**, with the
1470
+ tag grammar and a worked example: a `dynamic view` tagged `#req-<Requirement-ID>` as the first
1471
+ thing in the view body, the tag declared once (in the landscape's `specification` for an extending
1472
+ model, in the model's own for a standalone one), every hop backed by a relationship the model
1473
+ declares, and the view id namespaced by the service because an extending model's views share the
1474
+ root project's flat id namespace. Optional-and-absent prints lowercase `missing` and is not a
1475
+ finding — the existing mechanism for an optional artifact, not a new one.
1476
+ - **The generated AGENTS.md names the slot and its title convention**,
1477
+ `'<subsystem path> / <service id> / <use case>'`, so an authored use-case view lands in the same
1478
+ browser folder as its service's generated overview.
1479
+ - **No new finding code, and no check on absence.** docs/DESIGN.md rule 26 — presence is never owed,
1480
+ and no check may grade a view's absence — is enforced by `scripts/arch-check.mjs`, so there is no
1481
+ service.usecases-missing and there never will be. The grader (`c4.valid`'s
1482
+ ` · K tagged flow(s) graded`) has existed since 2026-09-03; all that was missing was anyone being
1483
+ told. A usecase.flow-untagged check, which would grade a view that EXISTS, is recorded on
1484
+ [ROADMAP.md](ROADMAP.md) with its promotion bar rather than built.
1485
+
13
1486
  ## [0.2.0-alpha.6] - 2026-09-04
14
1487
 
15
1488
  A service model may extend the fleet map, and the nine problem reports one fleet filed against the
@@ -988,7 +2461,10 @@ now rewrites the bytes of every view beneath it.
988
2461
  ## [0.2.0-alpha.3] - 2026-09-03
989
2462
 
990
2463
  The generated subsystem views, made readable — and the defect that hunt found underneath them.
991
- Still a prerelease on the `alpha` line: `npm i @ybotok/loam@alpha` installs it. Nothing below is
2464
+ Still a prerelease on the `alpha` line. **Not published (noted 2026-10-10):** the `v0.2.0-alpha.3`
2465
+ tag was cut, but it was withdrawn before its publish step — the `0.2.0-alpha.4` section above says
2466
+ so, and that release carries everything here — so `npm i @ybotok/loam@alpha` never installed this
2467
+ version, and `npm view @ybotok/loam dist-tags` is the record of what does. Nothing below is
992
2468
  breaking, and one entry is a change every repo NOTICES: the generated file's bytes move, so the
993
2469
  first `loam validate --all` after upgrading asks for one `loam subsystem sync`.
994
2470