@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.13

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 (901) hide show
  1. package/CHANGELOG.md +3448 -0
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +47 -23
  5. package/QUICKSTART.md +189 -12
  6. package/README.md +274 -65
  7. package/ROADMAP.md +572 -31
  8. package/SCHEMA.md +1709 -155
  9. package/WORKFLOW.md +298 -72
  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/adopt/render.js +50 -9
  15. package/dist/commands/adopt/render.js.map +1 -1
  16. package/dist/commands/archive/archive.js +16 -19
  17. package/dist/commands/archive/archive.js.map +1 -1
  18. package/dist/commands/archive/closing.js +42 -0
  19. package/dist/commands/archive/closing.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/asyncapi.js +36 -42
  21. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  23. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  24. package/dist/commands/archive/plan/contracts/openapi.js +77 -43
  25. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  26. package/dist/commands/archive/plan/contracts/verdict-lines.js +103 -0
  27. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  28. package/dist/commands/archive/plan/document/head.js +151 -0
  29. package/dist/commands/archive/plan/document/head.js.map +1 -0
  30. package/dist/commands/archive/plan/document/links.js +284 -0
  31. package/dist/commands/archive/plan/document/links.js.map +1 -0
  32. package/dist/commands/archive/plan/document/run.js +136 -0
  33. package/dist/commands/archive/plan/document/run.js.map +1 -0
  34. package/dist/commands/archive/plan/gate.js +21 -19
  35. package/dist/commands/archive/plan/gate.js.map +1 -1
  36. package/dist/commands/archive/plan/landscape.js +167 -30
  37. package/dist/commands/archive/plan/landscape.js.map +1 -1
  38. package/dist/commands/archive/plan/model/extending.js +85 -0
  39. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  40. package/dist/commands/archive/plan/refusal.js +14 -19
  41. package/dist/commands/archive/plan/refusal.js.map +1 -1
  42. package/dist/commands/archive/plan/specs.js +137 -56
  43. package/dist/commands/archive/plan/specs.js.map +1 -1
  44. package/dist/commands/archive/plan/state.js +3 -0
  45. package/dist/commands/archive/plan/state.js.map +1 -1
  46. package/dist/commands/archive/run.js +134 -30
  47. package/dist/commands/archive/run.js.map +1 -1
  48. package/dist/commands/archive/shipped.js +49 -0
  49. package/dist/commands/archive/shipped.js.map +1 -0
  50. package/dist/commands/context/context.js +85 -18
  51. package/dist/commands/context/context.js.map +1 -1
  52. package/dist/commands/context/print.js +29 -7
  53. package/dist/commands/context/print.js.map +1 -1
  54. package/dist/commands/delta/delta.js +102 -14
  55. package/dist/commands/delta/delta.js.map +1 -1
  56. package/dist/commands/delta/print.js +17 -1
  57. package/dist/commands/delta/print.js.map +1 -1
  58. package/dist/commands/dependencies.js +3 -7
  59. package/dist/commands/dependencies.js.map +1 -1
  60. package/dist/commands/diff/diff.js +20 -27
  61. package/dist/commands/diff/diff.js.map +1 -1
  62. package/dist/commands/doctor.js +60 -1
  63. package/dist/commands/doctor.js.map +1 -1
  64. package/dist/commands/explain/explain.js +89 -13
  65. package/dist/commands/explain/explain.js.map +1 -1
  66. package/dist/commands/explore.js +16 -10
  67. package/dist/commands/explore.js.map +1 -1
  68. package/dist/commands/gate/gate.js +45 -8
  69. package/dist/commands/gate/gate.js.map +1 -1
  70. package/dist/commands/gate/print.js +15 -3
  71. package/dist/commands/gate/print.js.map +1 -1
  72. package/dist/commands/gherkin/commit.js +13 -54
  73. package/dist/commands/gherkin/commit.js.map +1 -1
  74. package/dist/commands/gherkin/gherkin.js +36 -20
  75. package/dist/commands/gherkin/gherkin.js.map +1 -1
  76. package/dist/commands/gherkin/reconcile.js +10 -13
  77. package/dist/commands/gherkin/reconcile.js.map +1 -1
  78. package/dist/commands/init/first-hour.js +44 -0
  79. package/dist/commands/init/first-hour.js.map +1 -1
  80. package/dist/commands/init/init.js +18 -22
  81. package/dist/commands/init/init.js.map +1 -1
  82. package/dist/commands/list/campaign/campaign.js +4 -6
  83. package/dist/commands/list/campaign/campaign.js.map +1 -1
  84. package/dist/commands/list/json.js +5 -7
  85. package/dist/commands/list/json.js.map +1 -1
  86. package/dist/commands/list/list.js +44 -29
  87. package/dist/commands/list/list.js.map +1 -1
  88. package/dist/commands/list/print.js +52 -9
  89. package/dist/commands/list/print.js.map +1 -1
  90. package/dist/commands/list/suites/suites.js +140 -0
  91. package/dist/commands/list/suites/suites.js.map +1 -0
  92. package/dist/commands/list/views.js +37 -34
  93. package/dist/commands/list/views.js.map +1 -1
  94. package/dist/commands/mcp/dispatch.js +91 -36
  95. package/dist/commands/mcp/dispatch.js.map +1 -1
  96. package/dist/commands/mcp/mcp.js +56 -15
  97. package/dist/commands/mcp/mcp.js.map +1 -1
  98. package/dist/commands/mcp/serve.js.map +1 -1
  99. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  100. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  101. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  102. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  103. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  104. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  105. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  106. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  107. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  108. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  109. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  110. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  111. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  112. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  113. package/dist/commands/new/capability-template.js +118 -0
  114. package/dist/commands/new/capability-template.js.map +1 -0
  115. package/dist/commands/new/commit.js +8 -22
  116. package/dist/commands/new/commit.js.map +1 -1
  117. package/dist/commands/new/modifies.js +206 -0
  118. package/dist/commands/new/modifies.js.map +1 -0
  119. package/dist/commands/new/new.js +59 -14
  120. package/dist/commands/new/new.js.map +1 -1
  121. package/dist/commands/new/templates.js +48 -124
  122. package/dist/commands/new/templates.js.map +1 -1
  123. package/dist/commands/open.js +3 -7
  124. package/dist/commands/open.js.map +1 -1
  125. package/dist/commands/policy/gate.js +27 -0
  126. package/dist/commands/policy/gate.js.map +1 -1
  127. package/dist/commands/rebase/plan.js +2 -2
  128. package/dist/commands/rebase/plan.js.map +1 -1
  129. package/dist/commands/rebase/rebase.js +60 -35
  130. package/dist/commands/rebase/rebase.js.map +1 -1
  131. package/dist/commands/seed/commit.js +55 -26
  132. package/dist/commands/seed/commit.js.map +1 -1
  133. package/dist/commands/seed/plan.js +158 -13
  134. package/dist/commands/seed/plan.js.map +1 -1
  135. package/dist/commands/seed/seed.js +13 -11
  136. package/dist/commands/seed/seed.js.map +1 -1
  137. package/dist/commands/show/feature.js +9 -5
  138. package/dist/commands/show/feature.js.map +1 -1
  139. package/dist/commands/show/introduced.js +157 -0
  140. package/dist/commands/show/introduced.js.map +1 -0
  141. package/dist/commands/show/marks.js +35 -6
  142. package/dist/commands/show/marks.js.map +1 -1
  143. package/dist/commands/show/service.js +41 -33
  144. package/dist/commands/show/service.js.map +1 -1
  145. package/dist/commands/show/show.js +17 -7
  146. package/dist/commands/show/show.js.map +1 -1
  147. package/dist/commands/status/fleet-next.js +90 -0
  148. package/dist/commands/status/fleet-next.js.map +1 -0
  149. package/dist/commands/status/json.js +2 -0
  150. package/dist/commands/status/json.js.map +1 -1
  151. package/dist/commands/status/print.js +89 -37
  152. package/dist/commands/status/print.js.map +1 -1
  153. package/dist/commands/status/status.js +3 -7
  154. package/dist/commands/status/status.js.map +1 -1
  155. package/dist/commands/steps/steps.js +103 -23
  156. package/dist/commands/steps/steps.js.map +1 -1
  157. package/dist/commands/subsystem/edit.js +7 -7
  158. package/dist/commands/subsystem/edit.js.map +1 -1
  159. package/dist/commands/subsystem/move.js +106 -46
  160. package/dist/commands/subsystem/move.js.map +1 -1
  161. package/dist/commands/subsystem/query.js +2 -4
  162. package/dist/commands/subsystem/query.js.map +1 -1
  163. package/dist/commands/subsystem/subsystem.js +4 -7
  164. package/dist/commands/subsystem/subsystem.js.map +1 -1
  165. package/dist/commands/subsystem/sync.js +289 -74
  166. package/dist/commands/subsystem/sync.js.map +1 -1
  167. package/dist/commands/subsystem/txn/exclude.js +132 -0
  168. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  169. package/dist/commands/subsystem/txn/links.js +128 -0
  170. package/dist/commands/subsystem/txn/links.js.map +1 -0
  171. package/dist/commands/subsystem/txn/report.js +174 -0
  172. package/dist/commands/subsystem/txn/report.js.map +1 -0
  173. package/dist/commands/subsystem/txn/txn.js +147 -45
  174. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  175. package/dist/commands/subsystem/txn/views.js +200 -21
  176. package/dist/commands/subsystem/txn/views.js.map +1 -1
  177. package/dist/commands/tasks/actions.js +107 -0
  178. package/dist/commands/tasks/actions.js.map +1 -0
  179. package/dist/commands/tasks/render.js +253 -0
  180. package/dist/commands/tasks/render.js.map +1 -0
  181. package/dist/commands/tasks/tasks.js +119 -0
  182. package/dist/commands/tasks/tasks.js.map +1 -0
  183. package/dist/commands/unarchive/manifest.js +42 -26
  184. package/dist/commands/unarchive/manifest.js.map +1 -1
  185. package/dist/commands/unarchive/restore.js +83 -33
  186. package/dist/commands/unarchive/restore.js.map +1 -1
  187. package/dist/commands/unarchive/unarchive.js +22 -30
  188. package/dist/commands/unarchive/unarchive.js.map +1 -1
  189. package/dist/commands/validate/arch-coverage.js +78 -27
  190. package/dist/commands/validate/arch-coverage.js.map +1 -1
  191. package/dist/commands/validate/checks/fleet-shape.js +34 -126
  192. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  193. package/dist/commands/validate/checks/health-deps.js +9 -6
  194. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  195. package/dist/commands/validate/checks/introduced.js +102 -0
  196. package/dist/commands/validate/checks/introduced.js.map +1 -0
  197. package/dist/commands/validate/checks/requirements.js +18 -8
  198. package/dist/commands/validate/checks/requirements.js.map +1 -1
  199. package/dist/commands/validate/checks/sources.js +11 -2
  200. package/dist/commands/validate/checks/sources.js.map +1 -1
  201. package/dist/commands/validate/coverage/covers-after.js +105 -0
  202. package/dist/commands/validate/coverage/covers-after.js.map +1 -0
  203. package/dist/commands/validate/feature.js +26 -20
  204. package/dist/commands/validate/feature.js.map +1 -1
  205. package/dist/commands/validate/fleet/census.js +37 -13
  206. package/dist/commands/validate/fleet/census.js.map +1 -1
  207. package/dist/commands/validate/fleet/landscape.js +144 -126
  208. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  209. package/dist/commands/validate/fleet/load.js +66 -0
  210. package/dist/commands/validate/fleet/load.js.map +1 -1
  211. package/dist/commands/validate/fleet/map/attest.js +98 -0
  212. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  213. package/dist/commands/validate/fleet/map/bindings.js +107 -0
  214. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  215. package/dist/commands/validate/fleet/map/consumers.js +244 -0
  216. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  217. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  218. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  219. package/dist/commands/validate/fleet/map/usecases.js +54 -0
  220. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  221. package/dist/commands/validate/fleet/obligations.js +44 -14
  222. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  223. package/dist/commands/validate/fleet/scorecard/print.js +10 -2
  224. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  225. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  226. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  227. package/dist/commands/validate/fleet/usecases/place.js.map +1 -1
  228. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  229. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  230. package/dist/commands/validate/fleet/views/ids.js +50 -15
  231. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  232. package/dist/commands/validate/fleet/views/projects.js +278 -0
  233. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  234. package/dist/commands/validate/fleet/views/root-documents.js +83 -0
  235. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  236. package/dist/commands/validate/fleet/views/stale.js +40 -47
  237. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  238. package/dist/commands/validate/links/corpus.js +1 -4
  239. package/dist/commands/validate/links/corpus.js.map +1 -1
  240. package/dist/commands/validate/links/glossary.js.map +1 -1
  241. package/dist/commands/validate/render/repeats.js +63 -0
  242. package/dist/commands/validate/render/repeats.js.map +1 -0
  243. package/dist/commands/validate/render/rollups.js +115 -0
  244. package/dist/commands/validate/render/rollups.js.map +1 -0
  245. package/dist/commands/validate/render/text.js +199 -0
  246. package/dist/commands/validate/render/text.js.map +1 -0
  247. package/dist/commands/validate/report.js +36 -160
  248. package/dist/commands/validate/report.js.map +1 -1
  249. package/dist/commands/validate/scope.js +113 -0
  250. package/dist/commands/validate/scope.js.map +1 -0
  251. package/dist/commands/validate/service/api.js +21 -107
  252. package/dist/commands/validate/service/api.js.map +1 -1
  253. package/dist/commands/validate/service/contracts/generated.js +134 -0
  254. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  255. package/dist/commands/validate/service/events/events.js +36 -14
  256. package/dist/commands/validate/service/events/events.js.map +1 -1
  257. package/dist/commands/validate/service/events/message-producers.js +1 -1
  258. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  259. package/dist/commands/validate/service/evidence-pins.js +56 -10
  260. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  261. package/dist/commands/validate/service/model/diverged.js +226 -0
  262. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  263. package/dist/commands/validate/service/model/grade.js +163 -0
  264. package/dist/commands/validate/service/model/grade.js.map +1 -0
  265. package/dist/commands/validate/service/service.js +121 -61
  266. package/dist/commands/validate/service/service.js.map +1 -1
  267. package/dist/commands/validate/service/specs.js +88 -28
  268. package/dist/commands/validate/service/specs.js.map +1 -1
  269. package/dist/commands/validate/service/spine.js +38 -3
  270. package/dist/commands/validate/service/spine.js.map +1 -1
  271. package/dist/commands/validate/service/usecases/tags.js +113 -0
  272. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  273. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  274. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  275. package/dist/commands/validate/validate.js +85 -60
  276. package/dist/commands/validate/validate.js.map +1 -1
  277. package/dist/commands/verify/cross/diff.js +2 -6
  278. package/dist/commands/verify/cross/diff.js.map +1 -1
  279. package/dist/commands/verify/evidence/contract.js +1 -1
  280. package/dist/commands/verify/evidence/contract.js.map +1 -1
  281. package/dist/commands/verify/evidence/read.js +12 -3
  282. package/dist/commands/verify/evidence/read.js.map +1 -1
  283. package/dist/commands/verify/frozen.js +45 -23
  284. package/dist/commands/verify/frozen.js.map +1 -1
  285. package/dist/commands/verify/record.js +69 -17
  286. package/dist/commands/verify/record.js.map +1 -1
  287. package/dist/commands/verify/report.js +27 -44
  288. package/dist/commands/verify/report.js.map +1 -1
  289. package/dist/commands/verify/results.js +112 -30
  290. package/dist/commands/verify/results.js.map +1 -1
  291. package/dist/commands/verify/verify.js +55 -52
  292. package/dist/commands/verify/verify.js.map +1 -1
  293. package/dist/commands/vouch/pack/pack.js +2 -2
  294. package/dist/commands/vouch/pack/pack.js.map +1 -1
  295. package/dist/commands/vouch/pack/print.js +1 -6
  296. package/dist/commands/vouch/pack/print.js.map +1 -1
  297. package/dist/commands/vouch/pack/sections.js +1 -2
  298. package/dist/commands/vouch/pack/sections.js.map +1 -1
  299. package/dist/commands/vouch/run.js +26 -81
  300. package/dist/commands/vouch/run.js.map +1 -1
  301. package/dist/commands/vouch/sample/plan.js +20 -34
  302. package/dist/commands/vouch/sample/plan.js.map +1 -1
  303. package/dist/commands/vouch/vet/verify.js +39 -7
  304. package/dist/commands/vouch/vet/verify.js.map +1 -1
  305. package/dist/commands/vouch/vouch.js +10 -17
  306. package/dist/commands/vouch/vouch.js.map +1 -1
  307. package/dist/core/agent/agents-md/command-map.js +81 -106
  308. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  309. package/dist/core/agent/agents-md/map/explain.js +21 -9
  310. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  311. package/dist/core/agent/agents-md/map/lenses/context.js +17 -8
  312. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  313. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  314. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  315. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  316. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  317. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  318. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  319. package/dist/core/agent/agents-md/map/status.js +24 -8
  320. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  321. package/dist/core/agent/agents-md/map/subsystem.js +80 -8
  322. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  323. package/dist/core/agent/agents-md/map/usecases.js +88 -10
  324. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  325. package/dist/core/agent/agents-md/refusals.js +49 -22
  326. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  327. package/dist/core/agent/agents-md.js +39 -6
  328. package/dist/core/agent/agents-md.js.map +1 -1
  329. package/dist/core/agent/agents-stamp.js +10 -0
  330. package/dist/core/agent/agents-stamp.js.map +1 -1
  331. package/dist/core/agent/protocol.js +1 -1
  332. package/dist/core/agent/protocol.js.map +1 -1
  333. package/dist/core/agent/scaffold.js +10 -15
  334. package/dist/core/agent/scaffold.js.map +1 -1
  335. package/dist/core/agent/support/report.js +46 -5
  336. package/dist/core/agent/support/report.js.map +1 -1
  337. package/dist/core/agent/tools/dialects.js +1 -0
  338. package/dist/core/agent/tools/dialects.js.map +1 -1
  339. package/dist/core/agent/tools/registry.js +41 -76
  340. package/dist/core/agent/tools/registry.js.map +1 -1
  341. package/dist/core/agent/workflows/adopt.js +158 -11
  342. package/dist/core/agent/workflows/adopt.js.map +1 -1
  343. package/dist/core/agent/workflows/check.js +78 -39
  344. package/dist/core/agent/workflows/check.js.map +1 -1
  345. package/dist/core/agent/workflows/closing.js +88 -27
  346. package/dist/core/agent/workflows/closing.js.map +1 -1
  347. package/dist/core/agent/workflows/feature.js +128 -13
  348. package/dist/core/agent/workflows/feature.js.map +1 -1
  349. package/dist/core/agent/workflows/implement.js +96 -13
  350. package/dist/core/agent/workflows/implement.js.map +1 -1
  351. package/dist/core/agent/workflows/reference/authoring.js +106 -4
  352. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  353. package/dist/core/agent/workflows/reference/codes.js +8 -6
  354. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  355. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  356. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  357. package/dist/core/agent/workflows/reference/drawing/drawing.js +253 -0
  358. package/dist/core/agent/workflows/reference/drawing/drawing.js.map +1 -0
  359. package/dist/core/agent/workflows/reference/reference.js +6 -3
  360. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  361. package/dist/core/agent/workflows/reference/spine.js +68 -27
  362. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  363. package/dist/core/asyncapi/baseline/plan.js +14 -81
  364. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  365. package/dist/core/asyncapi/digest.js +30 -25
  366. package/dist/core/asyncapi/digest.js.map +1 -1
  367. package/dist/core/asyncapi/merge/components.js +5 -4
  368. package/dist/core/asyncapi/merge/components.js.map +1 -1
  369. package/dist/core/asyncapi/merge/error.js +7 -2
  370. package/dist/core/asyncapi/merge/error.js.map +1 -1
  371. package/dist/core/asyncapi/merge/markers.js +32 -50
  372. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  373. package/dist/core/asyncapi/merge/merge.js +33 -53
  374. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  375. package/dist/core/asyncapi/merge/pin.js +12 -36
  376. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  377. package/dist/core/asyncapi/model.js.map +1 -1
  378. package/dist/core/asyncapi/read.js +36 -12
  379. package/dist/core/asyncapi/read.js.map +1 -1
  380. package/dist/core/brief/brief.js +96 -36
  381. package/dist/core/brief/brief.js.map +1 -1
  382. package/dist/core/brief/checks.js +138 -3
  383. package/dist/core/brief/checks.js.map +1 -1
  384. package/dist/core/brief/landscape.js +209 -130
  385. package/dist/core/brief/landscape.js.map +1 -1
  386. package/dist/core/brief/map/extend.js +84 -0
  387. package/dist/core/brief/map/extend.js.map +1 -0
  388. package/dist/core/brief/map/owed.js +266 -0
  389. package/dist/core/brief/map/owed.js.map +1 -0
  390. package/dist/core/brief/targets.js +109 -31
  391. package/dist/core/brief/targets.js.map +1 -1
  392. package/dist/core/brief/unchecked.js +14 -3
  393. package/dist/core/brief/unchecked.js.map +1 -1
  394. package/dist/core/c4/arch.js +90 -182
  395. package/dist/core/c4/arch.js.map +1 -1
  396. package/dist/core/c4/direction/direction.js +114 -0
  397. package/dist/core/c4/direction/direction.js.map +1 -0
  398. package/dist/core/c4/likec4.js +169 -14
  399. package/dist/core/c4/likec4.js.map +1 -1
  400. package/dist/core/c4/locate/unlocated.js +150 -0
  401. package/dist/core/c4/locate/unlocated.js.map +1 -0
  402. package/dist/core/c4/parsed/deployment.js +160 -0
  403. package/dist/core/c4/parsed/deployment.js.map +1 -0
  404. package/dist/core/c4/parsed/dynamic-views.js.map +1 -1
  405. package/dist/core/c4/parsed/values.js +54 -1
  406. package/dist/core/c4/parsed/values.js.map +1 -1
  407. package/dist/core/c4/parsed/view-ids.js +39 -1
  408. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  409. package/dist/core/c4/prefetch/fleet.js +134 -0
  410. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  411. package/dist/core/c4/project/architecture.js +123 -7
  412. package/dist/core/c4/project/architecture.js.map +1 -1
  413. package/dist/core/c4/project/batch.js +140 -0
  414. package/dist/core/c4/project/batch.js.map +1 -0
  415. package/dist/core/c4/project/documents.js +43 -12
  416. package/dist/core/c4/project/documents.js.map +1 -1
  417. package/dist/core/c4/project/load.js +79 -43
  418. package/dist/core/c4/project/load.js.map +1 -1
  419. package/dist/core/c4/project/staged.js +277 -0
  420. package/dist/core/c4/project/staged.js.map +1 -0
  421. package/dist/core/c4/resolve/attested.js +0 -0
  422. package/dist/core/c4/resolve/attested.js.map +1 -0
  423. package/dist/core/c4/resolve/external.js +51 -0
  424. package/dist/core/c4/resolve/external.js.map +1 -0
  425. package/dist/core/c4/resolve/service.js +78 -17
  426. package/dist/core/c4/resolve/service.js.map +1 -1
  427. package/dist/core/c4/resolve/steps.js +290 -0
  428. package/dist/core/c4/resolve/steps.js.map +1 -0
  429. package/dist/core/c4/resolve/systems.js +154 -0
  430. package/dist/core/c4/resolve/systems.js.map +1 -0
  431. package/dist/core/c4/root-project/exclude.js +352 -0
  432. package/dist/core/c4/root-project/exclude.js.map +1 -0
  433. package/dist/core/c4/seed/template.js +65 -1
  434. package/dist/core/c4/seed/template.js.map +1 -1
  435. package/dist/core/c4/service-model/created/models.js +96 -0
  436. package/dist/core/c4/service-model/created/models.js.map +1 -0
  437. package/dist/core/c4/service-model/documents.js +47 -0
  438. package/dist/core/c4/service-model/documents.js.map +1 -0
  439. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  440. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  441. package/dist/core/c4/service-model/load.js +165 -0
  442. package/dist/core/c4/service-model/load.js.map +1 -0
  443. package/dist/core/c4/service-model/renderer.js +128 -0
  444. package/dist/core/c4/service-model/renderer.js.map +1 -0
  445. package/dist/core/c4/service-model/shape.js +106 -0
  446. package/dist/core/c4/service-model/shape.js.map +1 -0
  447. package/dist/core/c4/service-model/slice.js +188 -0
  448. package/dist/core/c4/service-model/slice.js.map +1 -0
  449. package/dist/core/c4/source-mask.js +297 -10
  450. package/dist/core/c4/source-mask.js.map +1 -1
  451. package/dist/core/c4/source-scan.js +222 -141
  452. package/dist/core/c4/source-scan.js.map +1 -1
  453. package/dist/core/c4/splice/authored-source.js +79 -13
  454. package/dist/core/c4/splice/authored-source.js.map +1 -1
  455. package/dist/core/c4/splice/contract.js +20 -0
  456. package/dist/core/c4/splice/contract.js.map +1 -1
  457. package/dist/core/c4/splice/delta-blocks.js +20 -20
  458. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  459. package/dist/core/c4/splice/identity/declared.js +87 -0
  460. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  461. package/dist/core/c4/splice/identity/edges.js +37 -0
  462. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  463. package/dist/core/c4/splice/identity/existing.js +80 -0
  464. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  465. package/dist/core/c4/splice/identity/interior.js +80 -0
  466. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  467. package/dist/core/c4/splice/identity/owner.js +50 -0
  468. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  469. package/dist/core/c4/splice/landscape-merge.js +182 -114
  470. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  471. package/dist/core/c4/splice/model/merge.js +151 -0
  472. package/dist/core/c4/splice/model/merge.js.map +1 -0
  473. package/dist/core/c4/splice/placement.js +160 -56
  474. package/dist/core/c4/splice/placement.js.map +1 -1
  475. package/dist/core/c4/splice/text/spot.js +131 -0
  476. package/dist/core/c4/splice/text/spot.js.map +1 -0
  477. package/dist/core/c4/workspace.js +13 -19
  478. package/dist/core/c4/workspace.js.map +1 -1
  479. package/dist/core/capabilities/capabilities.js +6 -6
  480. package/dist/core/capabilities/capabilities.js.map +1 -1
  481. package/dist/core/capabilities/delta/doc.js +7 -10
  482. package/dist/core/capabilities/delta/doc.js.map +1 -1
  483. package/dist/core/capabilities/delta/uncovered.js +2 -2
  484. package/dist/core/capabilities/delta/uncovered.js.map +1 -1
  485. package/dist/core/capabilities/findings.js +18 -22
  486. package/dist/core/capabilities/findings.js.map +1 -1
  487. package/dist/core/capabilities/realizes/findings.js +12 -19
  488. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  489. package/dist/core/capabilities/realizes/join.js +0 -20
  490. package/dist/core/capabilities/realizes/join.js.map +1 -1
  491. package/dist/core/capabilities/rollup.js +6 -0
  492. package/dist/core/capabilities/rollup.js.map +1 -1
  493. package/dist/core/capabilities/tree.js +6 -28
  494. package/dist/core/capabilities/tree.js.map +1 -1
  495. package/dist/core/capabilities/usecase-join.js +3 -13
  496. package/dist/core/capabilities/usecase-join.js.map +1 -1
  497. package/dist/core/coherence/authoring/scaffold.js +11 -3
  498. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  499. package/dist/core/coherence/authoring/sentinels.js +2 -1
  500. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  501. package/dist/core/coherence/calls/called.js +52 -0
  502. package/dist/core/coherence/calls/called.js.map +1 -0
  503. package/dist/core/coherence/coherence.js +103 -22
  504. package/dist/core/coherence/coherence.js.map +1 -1
  505. package/dist/core/coherence/consumed/operations.js +70 -0
  506. package/dist/core/coherence/consumed/operations.js.map +1 -0
  507. package/dist/core/coherence/consumed/refusal.js +38 -0
  508. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  509. package/dist/core/coherence/covers/health.js +85 -0
  510. package/dist/core/coherence/covers/health.js.map +1 -0
  511. package/dist/core/coherence/declared.js +60 -34
  512. package/dist/core/coherence/declared.js.map +1 -1
  513. package/dist/core/coherence/events/declared.js +44 -59
  514. package/dist/core/coherence/events/declared.js.map +1 -1
  515. package/dist/core/coherence/events/events.js +147 -16
  516. package/dist/core/coherence/events/events.js.map +1 -1
  517. package/dist/core/coherence/events/grades.js +31 -8
  518. package/dist/core/coherence/events/grades.js.map +1 -1
  519. package/dist/core/coherence/events/lookups.js +28 -87
  520. package/dist/core/coherence/events/lookups.js.map +1 -1
  521. package/dist/core/coherence/events/produced.js +100 -0
  522. package/dist/core/coherence/events/produced.js.map +1 -0
  523. package/dist/core/coherence/lookups.js +18 -78
  524. package/dist/core/coherence/lookups.js.map +1 -1
  525. package/dist/core/coherence/retired/archived.js +285 -0
  526. package/dist/core/coherence/retired/archived.js.map +1 -0
  527. package/dist/core/coherence/retired/event-pair.js +62 -0
  528. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  529. package/dist/core/coherence/retired/merged.js +79 -0
  530. package/dist/core/coherence/retired/merged.js.map +1 -0
  531. package/dist/core/coherence/retired/retirement.js +49 -0
  532. package/dist/core/coherence/retired/retirement.js.map +1 -0
  533. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  534. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  535. package/dist/core/coherence/retired/revival/revived.js +297 -0
  536. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  537. package/dist/core/consumers/after.js +113 -0
  538. package/dist/core/consumers/after.js.map +1 -0
  539. package/dist/core/consumers/consumers.js +296 -0
  540. package/dist/core/consumers/consumers.js.map +1 -0
  541. package/dist/core/consumers/current.js +211 -0
  542. package/dist/core/consumers/current.js.map +1 -0
  543. package/dist/core/consumers/scan.js +46 -0
  544. package/dist/core/consumers/scan.js.map +1 -0
  545. package/dist/core/consumers/state.js +27 -0
  546. package/dist/core/consumers/state.js.map +1 -0
  547. package/dist/core/delta/delta.js +51 -6
  548. package/dist/core/delta/delta.js.map +1 -1
  549. package/dist/core/dependencies/fanin.js +2 -2
  550. package/dist/core/dependencies/fanin.js.map +1 -1
  551. package/dist/core/deployment/delta.js +191 -0
  552. package/dist/core/deployment/delta.js.map +1 -0
  553. package/dist/core/deployment/objects.js +145 -0
  554. package/dist/core/deployment/objects.js.map +1 -0
  555. package/dist/core/diff/base-git.js +94 -12
  556. package/dist/core/diff/base-git.js.map +1 -1
  557. package/dist/core/diff/base-state.js +49 -21
  558. package/dist/core/diff/base-state.js.map +1 -1
  559. package/dist/core/diff/joins.js +136 -0
  560. package/dist/core/diff/joins.js.map +1 -0
  561. package/dist/core/diff/scope/changed-paths.js +22 -6
  562. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  563. package/dist/core/diff/scope/edges.js +82 -0
  564. package/dist/core/diff/scope/edges.js.map +1 -0
  565. package/dist/core/diff/semantic.js +100 -31
  566. package/dist/core/diff/semantic.js.map +1 -1
  567. package/dist/core/docs.js +31 -22
  568. package/dist/core/docs.js.map +1 -1
  569. package/dist/core/doctor/agents.js +34 -18
  570. package/dist/core/doctor/agents.js.map +1 -1
  571. package/dist/core/doctor/config.js +7 -3
  572. package/dist/core/doctor/config.js.map +1 -1
  573. package/dist/core/doctor/doctor.js +91 -11
  574. package/dist/core/doctor/doctor.js.map +1 -1
  575. package/dist/core/doctor/report.js.map +1 -1
  576. package/dist/core/doctor/reports/scan.js +242 -0
  577. package/dist/core/doctor/reports/scan.js.map +1 -0
  578. package/dist/core/document/apply.js +158 -11
  579. package/dist/core/document/apply.js.map +1 -1
  580. package/dist/core/document/frontmatter.js +55 -0
  581. package/dist/core/document/frontmatter.js.map +1 -1
  582. package/dist/core/document/parse.js +29 -6
  583. package/dist/core/document/parse.js.map +1 -1
  584. package/dist/core/document/scenarios.js +17 -0
  585. package/dist/core/document/scenarios.js.map +1 -1
  586. package/dist/core/document/spec.js +4 -11
  587. package/dist/core/document/spec.js.map +1 -1
  588. package/dist/core/envelope/config.js +10 -0
  589. package/dist/core/envelope/config.js.map +1 -1
  590. package/dist/core/envelope/json.js +34 -0
  591. package/dist/core/envelope/json.js.map +1 -1
  592. package/dist/core/explain/families.js +26 -21
  593. package/dist/core/explain/families.js.map +1 -1
  594. package/dist/core/explain/lookup.js +14 -1
  595. package/dist/core/explain/lookup.js.map +1 -1
  596. package/dist/core/explain/refusals.js +5 -5
  597. package/dist/core/explain/refusals.js.map +1 -1
  598. package/dist/core/explain/terms/documents.js +227 -0
  599. package/dist/core/explain/terms/documents.js.map +1 -0
  600. package/dist/core/explain/terms/done-check.js +144 -0
  601. package/dist/core/explain/terms/done-check.js.map +1 -0
  602. package/dist/core/explain/terms/entry.js +14 -0
  603. package/dist/core/explain/terms/entry.js.map +1 -0
  604. package/dist/core/explain/terms/stages.js +71 -0
  605. package/dist/core/explain/terms/stages.js.map +1 -0
  606. package/dist/core/explain/terms/tasks.js +97 -0
  607. package/dist/core/explain/terms/tasks.js.map +1 -0
  608. package/dist/core/explain/terms.js +68 -109
  609. package/dist/core/explain/terms.js.map +1 -1
  610. package/dist/core/explore/explore.js +40 -10
  611. package/dist/core/explore/explore.js.map +1 -1
  612. package/dist/core/fleet-context.js +81 -95
  613. package/dist/core/fleet-context.js.map +1 -1
  614. package/dist/core/gate/checks.js +11 -4
  615. package/dist/core/gate/checks.js.map +1 -1
  616. package/dist/core/gate/partners.js +69 -28
  617. package/dist/core/gate/partners.js.map +1 -1
  618. package/dist/core/gate/report.js.map +1 -1
  619. package/dist/core/gate/verification.js +75 -12
  620. package/dist/core/gate/verification.js.map +1 -1
  621. package/dist/core/gherkin/stale.js +13 -37
  622. package/dist/core/gherkin/stale.js.map +1 -1
  623. package/dist/core/glossary/delta.js +10 -7
  624. package/dist/core/glossary/delta.js.map +1 -1
  625. package/dist/core/glossary/tree.js +6 -19
  626. package/dist/core/glossary/tree.js.map +1 -1
  627. package/dist/core/kernel/concurrency.js +19 -0
  628. package/dist/core/kernel/concurrency.js.map +1 -1
  629. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  630. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  631. package/dist/core/kernel/path-safety.js +38 -0
  632. package/dist/core/kernel/path-safety.js.map +1 -1
  633. package/dist/core/kernel/records.js +11 -0
  634. package/dist/core/kernel/records.js.map +1 -1
  635. package/dist/core/links/backlinks.js +1 -3
  636. package/dist/core/links/backlinks.js.map +1 -1
  637. package/dist/core/links/corpus.js +51 -37
  638. package/dist/core/links/corpus.js.map +1 -1
  639. package/dist/core/links/findings.js.map +1 -1
  640. package/dist/core/links/parse.js +38 -9
  641. package/dist/core/links/parse.js.map +1 -1
  642. package/dist/core/mcp/argv.js +101 -0
  643. package/dist/core/mcp/argv.js.map +1 -1
  644. package/dist/core/mcp/author-tools.js +2 -1
  645. package/dist/core/mcp/author-tools.js.map +1 -1
  646. package/dist/core/mcp/protocol.js +2 -2
  647. package/dist/core/mcp/protocol.js.map +1 -1
  648. package/dist/core/mcp/tools.js +43 -33
  649. package/dist/core/mcp/tools.js.map +1 -1
  650. package/dist/core/openapi/baseline/gate.js +53 -12
  651. package/dist/core/openapi/baseline/gate.js.map +1 -1
  652. package/dist/core/openapi/baseline/plan.js +40 -10
  653. package/dist/core/openapi/baseline/plan.js.map +1 -1
  654. package/dist/core/openapi/baseline/record.js +0 -1
  655. package/dist/core/openapi/baseline/record.js.map +1 -1
  656. package/dist/core/openapi/contract-text.js +33 -0
  657. package/dist/core/openapi/contract-text.js.map +1 -0
  658. package/dist/core/openapi/contract-tree.js +301 -0
  659. package/dist/core/openapi/contract-tree.js.map +1 -0
  660. package/dist/core/openapi/depth.js +38 -24
  661. package/dist/core/openapi/depth.js.map +1 -1
  662. package/dist/core/openapi/digest.js +52 -2
  663. package/dist/core/openapi/digest.js.map +1 -1
  664. package/dist/core/openapi/doc.js +12 -4
  665. package/dist/core/openapi/doc.js.map +1 -1
  666. package/dist/core/openapi/merge/components.js +25 -60
  667. package/dist/core/openapi/merge/components.js.map +1 -1
  668. package/dist/core/openapi/merge/error.js +3 -13
  669. package/dist/core/openapi/merge/error.js.map +1 -1
  670. package/dist/core/openapi/merge/markers.js +26 -30
  671. package/dist/core/openapi/merge/markers.js.map +1 -1
  672. package/dist/core/openapi/merge/merge.js +28 -52
  673. package/dist/core/openapi/merge/merge.js.map +1 -1
  674. package/dist/core/openapi/merge/pin.js +17 -35
  675. package/dist/core/openapi/merge/pin.js.map +1 -1
  676. package/dist/core/openspec/decide/verdict.js.map +1 -1
  677. package/dist/core/openspec/inventory.js +4 -5
  678. package/dist/core/openspec/inventory.js.map +1 -1
  679. package/dist/core/openspec/model/mapping.js +30 -28
  680. package/dist/core/openspec/model/mapping.js.map +1 -1
  681. package/dist/core/openspec/model/model.js +12 -7
  682. package/dist/core/openspec/model/model.js.map +1 -1
  683. package/dist/core/openspec/scan/changes.js +10 -4
  684. package/dist/core/openspec/scan/changes.js.map +1 -1
  685. package/dist/core/openspec/scan/config.js +7 -16
  686. package/dist/core/openspec/scan/config.js.map +1 -1
  687. package/dist/core/openspec/scan/shape.js +63 -19
  688. package/dist/core/openspec/scan/shape.js.map +1 -1
  689. package/dist/core/openspec/scan/walk.js +21 -38
  690. package/dist/core/openspec/scan/walk.js.map +1 -1
  691. package/dist/core/pack/living.js +43 -4
  692. package/dist/core/pack/living.js.map +1 -1
  693. package/dist/core/pack/pack.js +50 -6
  694. package/dist/core/pack/pack.js.map +1 -1
  695. package/dist/core/projection/arch-slice.js +2 -2
  696. package/dist/core/projection/arch-slice.js.map +1 -1
  697. package/dist/core/projection/introduced.js +14 -0
  698. package/dist/core/projection/introduced.js.map +1 -0
  699. package/dist/core/provenance/findings.js +48 -12
  700. package/dist/core/provenance/findings.js.map +1 -1
  701. package/dist/core/provenance/git.js +33 -57
  702. package/dist/core/provenance/git.js.map +1 -1
  703. package/dist/core/provenance/gitq/moves.js +71 -114
  704. package/dist/core/provenance/gitq/moves.js.map +1 -1
  705. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  706. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  707. package/dist/core/provenance/sources.js +23 -6
  708. package/dist/core/provenance/sources.js.map +1 -1
  709. package/dist/core/provenance/stamp.js +5 -17
  710. package/dist/core/provenance/stamp.js.map +1 -1
  711. package/dist/core/repo/authored/paths.js +18 -0
  712. package/dist/core/repo/authored/paths.js.map +1 -1
  713. package/dist/core/repo/entries.js.map +1 -1
  714. package/dist/core/repo/paths.js +92 -6
  715. package/dist/core/repo/paths.js.map +1 -1
  716. package/dist/core/repo/repo.js +74 -14
  717. package/dist/core/repo/repo.js.map +1 -1
  718. package/dist/core/repo/tree/find.js +12 -3
  719. package/dist/core/repo/tree/find.js.map +1 -1
  720. package/dist/core/repo/tree/fs.js +27 -1
  721. package/dist/core/repo/tree/fs.js.map +1 -1
  722. package/dist/core/repo/tree/render/body.js +258 -0
  723. package/dist/core/repo/tree/render/body.js.map +1 -0
  724. package/dist/core/repo/tree/render/projects.js +182 -0
  725. package/dist/core/repo/tree/render/projects.js.map +1 -0
  726. package/dist/core/repo/tree/render/service/header.js +125 -0
  727. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  728. package/dist/core/repo/tree/render/service/peers.js +28 -0
  729. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  730. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  731. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  732. package/dist/core/repo/tree/render/service/views.js +78 -0
  733. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  734. package/dist/core/repo/tree/render/stale.js +42 -0
  735. package/dist/core/repo/tree/render/stale.js.map +1 -0
  736. package/dist/core/repo/tree/render/text.js +84 -0
  737. package/dist/core/repo/tree/render/text.js.map +1 -0
  738. package/dist/core/repo/tree/render/views.js +293 -0
  739. package/dist/core/repo/tree/render/views.js.map +1 -0
  740. package/dist/core/repo/tree/walk.js +53 -32
  741. package/dist/core/repo/tree/walk.js.map +1 -1
  742. package/dist/core/results.js +47 -12
  743. package/dist/core/results.js.map +1 -1
  744. package/dist/core/scaffold/landscape.js +121 -13
  745. package/dist/core/scaffold/landscape.js.map +1 -1
  746. package/dist/core/scaffold/legacy.js +271 -0
  747. package/dist/core/scaffold/legacy.js.map +1 -0
  748. package/dist/core/staging/commit.js +26 -15
  749. package/dist/core/staging/commit.js.map +1 -1
  750. package/dist/core/staging/lock.js +74 -12
  751. package/dist/core/staging/lock.js.map +1 -1
  752. package/dist/core/staging/recovery/intent.js +2 -5
  753. package/dist/core/staging/recovery/intent.js.map +1 -1
  754. package/dist/core/staging/recovery/recover.js +84 -6
  755. package/dist/core/staging/recovery/recover.js.map +1 -1
  756. package/dist/core/staging/snapshot/manifest.js +188 -0
  757. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  758. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +39 -45
  759. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  760. package/dist/core/staging/txn/forward.js +24 -14
  761. package/dist/core/staging/txn/forward.js.map +1 -1
  762. package/dist/core/staging/writes.js.map +1 -1
  763. package/dist/core/status/actions/execution.js +11 -0
  764. package/dist/core/status/actions/execution.js.map +1 -1
  765. package/dist/core/status/contracts.js +39 -15
  766. package/dist/core/status/contracts.js.map +1 -1
  767. package/dist/core/status/feature/artifacts.js +33 -8
  768. package/dist/core/status/feature/artifacts.js.map +1 -1
  769. package/dist/core/status/feature/feature.js +24 -17
  770. package/dist/core/status/feature/feature.js.map +1 -1
  771. package/dist/core/status/feature/next.js +20 -7
  772. package/dist/core/status/feature/next.js.map +1 -1
  773. package/dist/core/status/feature/words.js +96 -0
  774. package/dist/core/status/feature/words.js.map +1 -0
  775. package/dist/core/status/fleet/fleet.js +43 -14
  776. package/dist/core/status/fleet/fleet.js.map +1 -1
  777. package/dist/core/status/fleet/next.js +153 -58
  778. package/dist/core/status/fleet/next.js.map +1 -1
  779. package/dist/core/status/report.js.map +1 -1
  780. package/dist/core/status/scan.js.map +1 -1
  781. package/dist/core/status/verification.js +30 -15
  782. package/dist/core/status/verification.js.map +1 -1
  783. package/dist/core/tasks/packet.js +199 -0
  784. package/dist/core/tasks/packet.js.map +1 -0
  785. package/dist/core/tasks/record.js +43 -0
  786. package/dist/core/tasks/record.js.map +1 -0
  787. package/dist/core/tasks/sources.js +181 -0
  788. package/dist/core/tasks/sources.js.map +1 -0
  789. package/dist/core/tasks/stamps.js +76 -0
  790. package/dist/core/tasks/stamps.js.map +1 -0
  791. package/dist/core/tasks/unit.js +0 -0
  792. package/dist/core/tasks/unit.js.map +1 -0
  793. package/dist/core/usecases/capability.js +11 -14
  794. package/dist/core/usecases/capability.js.map +1 -1
  795. package/dist/core/usecases/delta/claims.js +31 -7
  796. package/dist/core/usecases/delta/claims.js.map +1 -1
  797. package/dist/core/usecases/delta/flows.js +5 -7
  798. package/dist/core/usecases/delta/flows.js.map +1 -1
  799. package/dist/core/usecases/delta/modified.js +315 -0
  800. package/dist/core/usecases/delta/modified.js.map +1 -0
  801. package/dist/core/usecases/delta/overlay.js +32 -191
  802. package/dist/core/usecases/delta/overlay.js.map +1 -1
  803. package/dist/core/usecases/fleet.js +195 -36
  804. package/dist/core/usecases/fleet.js.map +1 -1
  805. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/capability-tag.js +8 -9
  806. package/dist/core/usecases/grade/capability-tag.js.map +1 -0
  807. package/dist/core/usecases/grade/feature.js +88 -0
  808. package/dist/core/usecases/grade/feature.js.map +1 -0
  809. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/requirement-tag.js +3 -4
  810. package/dist/core/usecases/grade/requirement-tag.js.map +1 -0
  811. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/steps.js +102 -33
  812. package/dist/core/usecases/grade/steps.js.map +1 -0
  813. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/usecases.js +23 -10
  814. package/dist/core/usecases/grade/usecases.js.map +1 -0
  815. package/dist/core/usecases/operations.js +26 -14
  816. package/dist/core/usecases/operations.js.map +1 -1
  817. package/dist/core/usecases/place.js.map +1 -1
  818. package/dist/core/usecases/service/exclude.js +119 -0
  819. package/dist/core/usecases/service/exclude.js.map +1 -0
  820. package/dist/core/usecases/service/flows.js +164 -0
  821. package/dist/core/usecases/service/flows.js.map +1 -0
  822. package/dist/core/usecases/suites/participants.js +83 -0
  823. package/dist/core/usecases/suites/participants.js.map +1 -0
  824. package/dist/core/usecases/suites/suites.js +45 -0
  825. package/dist/core/usecases/suites/suites.js.map +1 -0
  826. package/dist/core/verify/answers.js +12 -5
  827. package/dist/core/verify/answers.js.map +1 -1
  828. package/dist/core/verify/build.js +102 -82
  829. package/dist/core/verify/build.js.map +1 -1
  830. package/dist/core/verify/checklist.js +70 -12
  831. package/dist/core/verify/checklist.js.map +1 -1
  832. package/dist/core/verify/location.js +67 -0
  833. package/dist/core/verify/location.js.map +1 -0
  834. package/dist/core/verify/pins/lint.js +3 -2
  835. package/dist/core/verify/pins/lint.js.map +1 -1
  836. package/dist/core/verify/pins/pin.js +1 -1
  837. package/dist/core/verify/pins/pin.js.map +1 -1
  838. package/dist/core/verify/read/file.js +224 -0
  839. package/dist/core/verify/read/file.js.map +1 -0
  840. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  841. package/dist/core/verify/read/shape.js.map +1 -0
  842. package/dist/core/verify/read/union.js +105 -0
  843. package/dist/core/verify/read/union.js.map +1 -0
  844. package/dist/core/verify/record.js +35 -12
  845. package/dist/core/verify/record.js.map +1 -1
  846. package/dist/core/verify/store/commit.js +40 -30
  847. package/dist/core/verify/store/commit.js.map +1 -1
  848. package/dist/core/verify/store/render.js +8 -4
  849. package/dist/core/verify/store/render.js.map +1 -1
  850. package/dist/core/vocabulary/issue.js +4 -0
  851. package/dist/core/vocabulary/issue.js.map +1 -1
  852. package/dist/core/vocabulary/maturity.js +75 -4
  853. package/dist/core/vocabulary/maturity.js.map +1 -1
  854. package/dist/core/vocabulary/report.js +16 -1
  855. package/dist/core/vocabulary/report.js.map +1 -1
  856. package/examples/README.md +54 -11
  857. package/examples/docs/architecture/landscape.likec4 +62 -6
  858. package/examples/docs/architecture/services.likec4 +70 -0
  859. package/examples/docs/architecture/subsystems.likec4 +7 -0
  860. package/examples/docs/architecture/usecases/order-notification.likec4 +16 -7
  861. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  862. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  863. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +4 -1
  864. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  865. package/examples/docs/likec4.config.json +0 -1
  866. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  867. package/examples/docs/services/order-service/arch.spec.md +13 -6
  868. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  869. package/examples/docs/services/order-service/model.likec4 +58 -70
  870. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  871. package/examples/docs/services/payment-service/model.likec4 +38 -37
  872. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  873. package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
  874. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  875. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  876. package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
  877. package/package.json +1 -1
  878. package/dist/commands/validate/checks/vocabulary.js +0 -31
  879. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  880. package/dist/commands/validate/fleet/usecases/capability-tag.js.map +0 -1
  881. package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +0 -1
  882. package/dist/commands/validate/fleet/usecases/steps.js.map +0 -1
  883. package/dist/commands/validate/fleet/usecases/usecases.js.map +0 -1
  884. package/dist/core/agent/agents-md/artifacts.js +0 -220
  885. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  886. package/dist/core/agent/agents-md/cycle.js +0 -165
  887. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  888. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  889. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  890. package/dist/core/agent/agents-md/spine.js +0 -40
  891. package/dist/core/agent/agents-md/spine.js.map +0 -1
  892. package/dist/core/coherence/events/merged.js +0 -40
  893. package/dist/core/coherence/events/merged.js.map +0 -1
  894. package/dist/core/diff/current-state.js +0 -96
  895. package/dist/core/diff/current-state.js.map +0 -1
  896. package/dist/core/diff/victims.js +0 -247
  897. package/dist/core/diff/victims.js.map +0 -1
  898. package/dist/core/repo/tree/views.js +0 -172
  899. package/dist/core/repo/tree/views.js.map +0 -1
  900. package/dist/core/staging/snapshot.js.map +0 -1
  901. package/dist/core/verify/file.js.map +0 -1
package/SCHEMA.md CHANGED
@@ -62,6 +62,7 @@ docs/
62
62
  architecture/
63
63
  landscape.likec4 global C4 (system landscape)
64
64
  subsystems.likec4 one view per subsystem, views only [GENERATED: loam subsystem sync]
65
+ services.likec4 one overview view per service, views only [GENERATED: loam subsystem sync]
65
66
  permissions.yaml system-wide authorization vocabulary [authored, opt-in]
66
67
  capabilities.yaml system-wide capability vocabulary [authored, opt-in]
67
68
  obligations.yaml system-wide architectural obligations [authored, opt-in]
@@ -71,11 +72,13 @@ docs/
71
72
  spec.md narrative, then `## Requirements` — `Requirement-ID:` REQUIRED
72
73
  glossary/<term>.md the system's domain vocabulary, one file per term; nesting allowed [authored, opt-in]
73
74
  services/<svc>/ at the root (unfiled), or inside subsystem directories at any depth
74
- model.likec4 boundary C4 (containers/components) [adopt]
75
+ model.likec4 boundary C4 (containers/components); extends the map, or stands alone [adopt]
76
+ likec4.config.json one LikeC4 project per STANDALONE service model, create-only [GENERATED: loam subsystem sync]
75
77
  spec.md living requirements (Requirement/Scenario) [adopt]
76
78
  arch.spec.md living ARCHITECTURE requirements (outbox, retries, alerts; Covers:) [authored]
77
79
  openapi.yaml API contract [adopt / authored]
78
80
  asyncapi.yaml async contract (AsyncAPI 3) [adopt / authored]
81
+ usecases/<name>.likec4 optional flows INSIDE this boundary — dynamic views over its containers, graded when tagged #req- (any .likec4 beside model.likec4 is read the same way) [authored, opt-in]
79
82
  adrs/NNNN-*.md MADR decisions [adopt-seed / authored]
80
83
  runbook.md operational runbook [adopt-draft / authored]
81
84
  health.yaml SLI/SLO, checks, critical deps [authored]
@@ -86,24 +89,57 @@ docs/
86
89
  delta.likec4 C4 delta (architecture) [authored]
87
90
  specs/<svc>/spec.md requirement delta (ADDED/MODIFIED/REMOVED + scenarios) [authored]
88
91
  specs/<svc>/arch.spec.md architectural requirement delta, same algebra [authored]
89
- specs/<svc>/openapi.yaml the endpoints this feature adds [authored]
90
- specs/<svc>/asyncapi.yaml the event-contract delta, slot-pinned [authored]
92
+ specs/<svc>/openapi.yaml only the endpoints it adds/changes/removes [authored]
93
+ specs/<svc>/asyncapi.yaml only the event slots it adds/changes/removes [authored]
91
94
  capabilities/<cap>/spec.md capability delta, same requirement grammar [authored]
92
95
  glossary/<term>.md a word this feature brings with it; create-only [authored]
93
96
  usecases/<name>.likec4 a FLOW this feature brings; views-only, create-only [authored]
94
- adrs/NNNN-*.md feature-level decisions [authored]
95
- verification.yaml the done-check: claims + verdicts + evidence, one attestation per service [loam verify --service <id> --record]
97
+ adrs/NNNN-*.md feature-level decisions; they stay with the feature when it archives [authored]
98
+ verification/<svc>.yaml the done-check, one file per attesting service: claims + verdicts + evidence [loam verify --service <id> --record]
99
+ verification.yaml the same answered all at once, and every record from before the directory — read forever [loam verify --record]
96
100
  features/archive/<FEAT>/ a shipped change — the same files, plus:
97
- .loam-before/ the bytes the merge overwrote [written by loam]
101
+ .loam-before/ the bytes the merge overwrote, each as files/<path>.before, and the checklist it shipped with [written by loam]
98
102
  .loam-lock transient: one writer at a time [archive/unarchive only]
99
103
  .loam-commit transient: the commit intent journal [archive/unarchive only]
100
104
  ```
101
105
 
102
106
  There is no `flows/` tree, and there will not be one. A cross-service **interaction flow** is a
103
107
  `dynamic view` under `architecture/` — the one place that already holds every cross-service edge —
104
- tagged with the capability it realizes. A flow that spans services does not belong under one
105
- service's directory, and the view's steps join to the landscape's own relationships, which is what
106
- makes the flow checkable rather than merely drawn.
108
+ tagged with the capability it realizes, or with the suites it runs in (below). A flow that spans
109
+ services does not belong under one service's directory, and the view's steps join to the
110
+ landscape's own relationships, which is what makes the flow checkable rather than merely drawn.
111
+
112
+ A flow that stays INSIDE one service — the hop sequence behind an arch.spec.md requirement whose
113
+ `Covers:` names containers — cannot live under `architecture/`: loam parses that directory as its
114
+ OWN document set — the map plus its siblings, and nothing under `services/` — so a step naming a
115
+ container does not resolve there, and one such file takes the whole fleet map down
116
+ (`landscape.invalid` plus one `spine.landscape-invalid` per service). That holds however the models
117
+ are shaped: the renderer's root project may also hold every extending model and draw such a step
118
+ perfectly, and loam still grades the map alone, because the map is the document every cross-service
119
+ check reads. It lives beside
120
+ the model instead: every `.likec4` under `services/<…>/<svc>/` other than model.likec4, plus any
121
+ tagged `dynamic view` inside model.likec4 itself, is that service's own LikeC4 project — for an
122
+ extending model, the map plus those documents; for a standalone one, the set
123
+ `services/<svc>/likec4.config.json` registers for the renderer — and `loam validate --service <id>`
124
+ (and `--all`, per service) reads exactly that set, whether or not the project file exists.
125
+ `usecases/<name>.likec4` is the convention; `views.likec4` reads the same. Tag the view
126
+ `#req-<Requirement-ID>` of a requirement in that service's spec.md or arch.spec.md. The tag has to
127
+ be DECLARED somewhere the project can see: for a standalone model that is model.likec4's own
128
+ `specification` — the one block that project has; for an extending model it is
129
+ `architecture/landscape.likec4`'s block, or a tags-only `specification { tag req-… }` beside the
130
+ `extend` (legal, and it lands the tag in the project's specification). Declaring it in BOTH is a
131
+ duplicate, and the code depends on which document carries the second declaration — the finding
132
+ lands where the file is, not at fleet altitude: `c4.invalid` (an ERROR, and the model's grading is
133
+ suspended behind it) when the tags-only block is in model.likec4, `usecase.flow-invalid` when it is
134
+ in a sibling. `c4.fleet-project-invalid` is the fleet-scope class one step out: TWO SERVICES each
135
+ declaring the same tag locally, where no per-service project sees the collision.
136
+ model.likec4 stays THE model:
137
+ `c4.valid`, `Covers:` and every other model check read it alone, and a sibling that does not parse
138
+ is one `usecase.flow-invalid` while the model keeps grading. Files loam does not read: a `.likec4`
139
+ in a subsystem directory, in a feature directory outside `delta.likec4`/`usecases/`/`deployment/`,
140
+ the generated `subsystems.likec4` and `services.likec4`, a sibling the service's own `likec4.config.json` `exclude`
141
+ covers (it is in no project the renderer loads either), and every UNTAGGED `dynamic view`
142
+ anywhere — a diagram, never a use case.
107
143
 
108
144
  A feature **brings** one the way it brings every other axis: `features/<FEAT>/usecases/<name>.likec4`
109
145
  is a views-only document that `loam archive` copies into `architecture/usecases/` and
@@ -112,6 +148,276 @@ would leave behind — so a flow may draw a hop into a service the feature's own
112
148
  and a hop into an element the merge does NOT land is refused at plan time rather than archived into
113
149
  a map the next reader's `loam validate --all` fails on.
114
150
 
151
+ **A fleet flow can also say which SUITE it runs with: `#suite-<name>`.** A suite is a set of
152
+ journeys a test run exercises together — a smoke suite, a payments suite — and a journey joins one
153
+ by carrying the tag on its `dynamic view`, declared like every LikeC4 tag in the landscape's one
154
+ `specification` block (`tag suite-smoke`), so a mistyped suite is a parse error rather than a second,
155
+ empty suite. It is a SEPARATE prefix, not `#cap-`: a capability says what a flow is FOR, a suite says
156
+ what it is RUN WITH, and membership is **many-to-many** — one view may carry several `#suite-` tags,
157
+ and a suite holds as many views as carry its tag, which is why it is a tag and not a directory.
158
+
159
+ **Tagging is the opt-in, and it grades.** A view carrying only `#suite-` tags — no `#cap-`, no
160
+ `#req-` — IS a use case: every reader takes it (the predicate and the byte gate below now read three
161
+ prefixes), and `loam validate --all` grades its hops like any other's (`usecase.step-unbacked`,
162
+ `usecase.step-contested`, `usecase.step-unlinked`). A journey a test stand is provisioned from must
163
+ be held to the model it is drawn over. The suite tag claims no capability and no requirement, so it
164
+ earns nothing from the capability axis. An UNTAGGED view stays ungraded, exactly as before.
165
+
166
+ `loam list suites [<name>] [--json]` is the answer the tag exists for: **which services must be up
167
+ for suite X.** For each suite it lists the views carrying the tag, the enumerated services their
168
+ steps resolve to — through the same element→service resolver the step grades use, so a step into
169
+ one of order-service's containers counts order-service — and every participant that resolves to NO
170
+ service, by element id, with whether it (or anything it is drawn inside) carries `#external`: the
171
+ customer who drives the flow, a topic, a partner's API. The walk up stops at the first element
172
+ bound to or titled for a service, as `loam gate`'s partner reading does, so a service drawn inside
173
+ an `#external` grouping is still ours. Those are named and never dropped, because an environment
174
+ list that is silently short comes up, and the journey then fails against something nobody was
175
+ told to start. The payload is `suites[]` (`name`, `views[]` of `{id, file}`, `services[]`, `unresolved[]`
176
+ of `{element, external}`), `ungrouped[]` — the use cases carrying no `#suite-` tag, which every
177
+ per-suite line would otherwise leave out — and `useCases` (`state`: `absent` when the fleet
178
+ declares no use case, `ok`, or `unreadable` with `error`, beside the `unreadable` boolean
179
+ `list capabilities` spells). Over an unreadable `architecture/` the section degrades rather than
180
+ refusing, and `suites` and `ungrouped` are ABSENT rather than `[]`. A `<name>` narrows `suites[]`
181
+ only; an unknown one refuses `unknown-target` with the close names. The listing reads the FLEET
182
+ map's use cases; a `#suite-` tag on a flow inside one service's own project is graded there like any
183
+ other use-case tag, and is not listed — that flow runs inside one service, whose environment is the
184
+ service itself.
185
+
186
+ ### Two shapes of a service model
187
+
188
+ `services/<…>/<svc>/model.likec4` comes in two shapes, and **the file's own grammar decides which**
189
+ — never a config key, never a flag. loam scans the bytes (comments and strings masked) for a
190
+ top-level `specification { }` block containing an `element` declaration:
191
+
192
+ - an **extending model** declares no element kind. It is parsed INSIDE the fleet's `architecture/`
193
+ project — the landscape plus every other `architecture/**/*.likec4` except the generated views
194
+ files (`subsystems.likec4`, `services.likec4`) — and says what is inside the service with `extend <fqn> { … }` on the element
195
+ the map already binds to the directory. `<fqn>` is the FULLY-QUALIFIED id: `extend orderService`
196
+ resolves to nothing when the element is `marketplace.orderService`. No `specification` is needed,
197
+ no partner is re-declared, and `model {}` or no model at all are both fine. `extend` accepts
198
+ `#tags` and a `metadata { }` block on the element it opens (merged into it — the map's `service`
199
+ binding survives) and REFUSES `description`, which belongs on the map's declaration. This is the
200
+ shape `loam adopt` briefs, and the shape `loam init --create` scaffolds a repository for.
201
+ - a **standalone model** declares at least one `element <kind>` inside a `specification { }` block.
202
+ It is parsed ALONE, exactly as every model was before this shape existed; it may re-declare the
203
+ partners it names; and it renders only as a LikeC4 project of its own. Legal forever — a service
204
+ whose docs are reviewed from its own repository, a model adopted before the fleet had a map — and
205
+ not what a new model is written as.
206
+
207
+ **Who declares kinds and tags.** In the extending shape, `architecture/landscape.likec4` is the one
208
+ `specification` block: a kind a model needs and the map does not declare (`element database`,
209
+ `element queue`) is added THERE, once, and a missing one is a single parse error on the model. A
210
+ tags-only `specification { tag req-CANCEL }` beside an `extend` is legal and lands the tag in the
211
+ project's specification — but a tag the map already declares, declared again there, is a duplicate
212
+ blamed on both files. In the standalone shape the model declares everything it uses, which is
213
+ exactly why its copies of the map's elements can drift.
214
+
215
+ **Seven grades come with the pair**, and one existing grade is narrowed:
216
+
217
+ | code | shape | what it says |
218
+ |---|---|---|
219
+ | `c4.declaration-diverged` (warn, service) | standalone | an element this model and the fleet map (`architecture/`) both declare, whose `kind`, `title`, tag SET or `metadata { service }` binding differ. The two are joined by id, then by the `metadata { service }` binding both wrote, then by the map element whose id ends with the model's as a dotted tail (`paymentService` ↔ `marketplace.paymentService`) — a tail two map elements answer to joins nothing, and a tail that is a BARE last segment (`db`, `api`) is a key only for a FLEET-LEVEL map element: one no ancestor of which stands for a service, so a top-level element or a box the map draws inside a plain grouping element, never an element drawn inside a service's own. `description` is deliberately not compared — it is prose, nothing grades it, and the other four each change what a view shows or what loam resolves. When the tag sets are all that differ and copying the map's declaration would not clear them — because the difference comes off `specification { element <kind> { … } }` on one side or the other — the message says so and names the document whose specification declares them, rather than asking for a copy. One finding per divergent element; silent while the map does not parse |
220
+ | `c4.element-unowned` (warn, service) | extending | the model declares an element OUTSIDE the element that resolves to this service. Three remedies, first first: a store or component this service owns goes INSIDE the `extend <fqn> { }` block, where its id becomes `<fqn>.<name>`; a system the service reaches belongs in the map, declared once; another service's internals belong in that service's model |
221
+ | `c4.fleet-project-invalid` (warn, landscape) | extending | `--all` only, and not while `landscape.excluded` holds — this load reads what the renderer reads and so has no floor under the map, and one `exclude` entry covering the map turns every extending model's `extend` into an unresolved reference (measured on `examples/docs`: 161 warnings from one line). loam loads the fleet project the way the renderer does — a walk of the docs root, minus everything the root `exclude` covers, minus the generated views files (`subsystems.likec4`, `services.likec4`), minus every STANDALONE service directory (`service.model-unexcluded` owns that state), minus `features/` (a delta carries its own `specification` and the directory is transient) — and reports each error NOT already present in the map alone or in any per-service project the same run made. A `.likec4` in a SUBSYSTEM directory, above every service, is in that set. This is the class no per-service grade can see: two services declaring the same tag locally, two models adding the same child id, a broken untagged sibling nobody opted in. All of them are renderer-fatal. `locations[0]` is the file the error is in and the message tail names the failure it actually is |
222
+ | `service.likec4-config-stray` (warn, landscape) | extending | a `likec4.config.json` beside an extending model. The renderer registers it as a project of its own rooted at that directory, so the docs root stops being a one-project workspace (`likec4 validate .` then refuses without `--project`), and wherever that nested project claims the model it parses alone, out of the root project where the map's kinds are: the model does not resolve and `export json --project fleet` loses the service's containers (measured, likec4 1.59.2 — which of the two happens depends on the workspace; on `examples/docs` the fleet export was byte-identical with and without the file, on a seeded fleet the containers went). Run `loam subsystem sync`: it deletes the file. Deleting it by hand works too. loam never writes one beside an extending model |
223
+ | `service.model-excluded` (warn, landscape) | extending | the root `likec4.config.json`'s `exclude` covers this model's directory, so the renderer never loads it and the service is a box with nothing inside it. An entry is read the way the renderer reads it (measured, likec4 1.59.2): a bare entry is a raw path PREFIX starting on a `/` boundary, so `services`, `services/`, `services/*`, `services/**`, `services/**/*`, a wildcard segment in the middle and the bare `services/pay` all cover `services/payment-service` — while the glob form `services/pay/**` wants the literal separator and covers nothing there. An entry shaped like a FILE rather than a directory (`services/**/model.likec4`, `**/model.likec4`) is reported too, with a different repair: `loam subsystem sync` maintains only the `services/` directory entries, so that shape has to be narrowed or deleted by hand. The finding quotes the entry verbatim, because the repair is to edit that line; `loam subsystem sync` is the repair for the directory shape |
224
+ | `service.model-unexcluded` (warn, landscape) | standalone | the mirror, and the one that costs most: the root `exclude` does NOT cover a standalone model's directory, so the renderer merges it into the root project and reports every kind and element it declares as a duplicate — blanking the whole project, not just this service. An entry already covering the directory in any of the spellings above satisfies it, and `sync` appends no second glob for one. Same repair |
225
+ | `service.likec4-config-missing` (warn, landscape) | standalone **only** | a standalone model with no project file beside it. It renders nowhere from the docs root until `loam subsystem sync` writes one. An extending model never earns this |
226
+ | `landscape.excluded` (warn, landscape) | either | `--all` only. The root `likec4.config.json`'s `exclude` covers `architecture/landscape.likec4` itself — an `architecture/*.likec4` written for a palette is the spelling that does it. The renderer then loads NO fleet map: it draws nothing, and every extending model in the root project resolves against nothing. loam is unaffected — the architecture loader keeps the map whatever the list says — which is exactly why the state was invisible. `c4.fleet-project-invalid` is suspended behind it, on the cascade rule `permissions.invalid` states: that check reads the renderer's project, so the same entry would come back once per unresolved reference instead of once as the cause, and the message says the fleet project is not graded until the entry goes. The finding quotes the entry verbatim, and `loam subsystem sync` is NOT the repair: it maintains only the `services/` entries and leaves every other line as the team wrote it |
227
+
228
+ `c4.invalid` and `c4.valid` are unchanged codes with adjusted scope: for an extending model every
229
+ error the per-service project raises is THIS model's, including one the parser blames on the
230
+ landscape, because the map parsed clean on its own; and `c4.valid`'s message reads
231
+ `extends the fleet map — N elements · M relationships`, followed by ` · K tagged flow(s) graded`
232
+ when the service's own project declared any (the one surface a healthy intra-service flow appears
233
+ on). When the map itself does not parse, an
234
+ extending model cannot be read at all: neither code is emitted, and `spine.landscape-invalid` says
235
+ so with the clause ` — and model.likec4 extends it, so the model cannot be read either`.
236
+
237
+ Every other model-dependent check reads the same slice it always read — the service's OWN elements
238
+ and relationships. For an extending model that is what the project holds minus what the map alone
239
+ holds, plus the **partners** its own edges point at and their ancestors, so `health.yaml`'s
240
+ `kafka` still resolves when the only edge names `kafka.orderEvents`. Nothing reads the map's edges
241
+ as the model's.
242
+
243
+ **What loam grades is still the `architecture/` project alone.** The renderer's root project is
244
+ WIDER than the fleet map: it also holds every extending model. loam's fleet checks — the spine, the
245
+ bindings, the fleet-shape advisories, and the steps of every `architecture/usecases/` flow — read
246
+ the landscape plus its `architecture/` siblings and nothing else, whatever shape the models take.
247
+ So a cross-service flow names SERVICE elements: a hop naming a container an extending model
248
+ declares renders perfectly and is `landscape.invalid` for loam. That is deliberate, and the reason
249
+ is blast radius — a service's model is graded on its own merits, and one broken model must never
250
+ blank the fleet's own map. A container-level flow has its own home beside the model that declares
251
+ the container. The same holds for a feature's `features/<FEAT>/usecases/` flow, and the gate says
252
+ so before the merge rather than after it: the preview stages no service model, and it ROUTES a
253
+ nested addition into the model that owns that service's interior exactly as `loam archive` would.
254
+ So a hop naming `<service-fqn>.<container>` resolves in neither the preview nor the tree the archive
255
+ leaves, and `usecase.flow-invalid` refuses — where the un-routed preview resolved it, passed the
256
+ gate `--approve` cannot override, and landed a flow the next `loam validate --all` failed on.
257
+
258
+ **Which relationship a hop exercises.** A hop is backed by the relationships declared between its
259
+ two endpoints, or — only when there are none — by those whose endpoints resolve to the same two
260
+ services, so a hop between two services matches an edge drawn into one of their containers. A reply
261
+ written `a <- b` is looked up under the call it answers. Nothing behind the hop is
262
+ `usecase.step-unbacked` (error). Candidates that all name one operation attribute the hop to it.
263
+ Candidates that name different operations — an edge with no `metadata { op }` counts as naming
264
+ none, which differs from naming one — are `usecase.step-contested` (warn), unless **the step's
265
+ title opens with a name one candidate declares**: its `op`, or the message it `publishes` or
266
+ `consumes`, followed by the end of the title or by punctuation. Then the hop is attributed to that
267
+ candidate:
268
+
269
+ ```
270
+ model {
271
+ web -> payments 'Calls authorizePayment' { metadata { op 'authorizePayment' } }
272
+ web -> payments 'Calls refundPayment' { metadata { op 'refundPayment' } }
273
+ }
274
+ views {
275
+ dynamic view uc_refund {
276
+ #cap-payments
277
+ web -> payments 'authorizePayment — authorizes the payment'
278
+ web -> payments 'refundPayment(orderId)'
279
+ web -> payments 'takes the payment back'
280
+ }
281
+ }
282
+ ```
283
+
284
+ The first two hops are attributed and the third is contested. The rule is deliberately narrow,
285
+ because a guessed operation reads exactly like a right one in every rollup built over a flow. The
286
+ name must OPEN the title, matched exactly with case included. It must end there or at a character
287
+ that cannot continue a name — anything but a letter, digit, `_`, `.` or `-` — so
288
+ `authorizePaymentAsync` picks nothing. Any words after it must follow punctuation (`:`, `(`, `,`, a
289
+ dash): as the first word of a sentence it is prose, so against a `read` edge `read: the order`
290
+ picks and `read the order` does not, and neither does `reads the order before release` against a
291
+ `release` edge. A relationship's own title is prose and picks nothing. The pick runs only among the
292
+ candidates the endpoints found, and only when they disagree: a title never backs an unbacked hop,
293
+ never reaches the service tier when the exact one answered, and never overrules candidates that
294
+ agree. A pick that still disagrees — one name declared as one edge's `op` and another's `publishes`
295
+ — leaves the hop contested over every candidate. A hop picked by a message is an event hop, which
296
+ `usecase.step-unlinked` exempts. `loam diff --base` and the contract-removal gate read the same
297
+ attribution, so a picked hop is a victim of the removal of its own operation and of no other.
298
+
299
+ It is the title because nothing else on a step can carry the choice, as measured at the LikeC4
300
+ 1.59.2 pin. `metadata` and tags on a step are parse errors. A step's kind exists only as
301
+ `-[kind]->`; it needs a declared `relationship` kind, and it names a transport, so a caller's
302
+ reserve, read and release calls to one provider are one kind. And drawing the hop between elements
303
+ that each carry exactly one candidate works only where the graded project declares those elements —
304
+ a service's own flow over its containers, or a map that draws a service's containers itself. It is
305
+ closed on a fleet flow whose two services are single elements on the map, because a container an
306
+ extending model declares does not resolve there (above). A sequence diagram labels a message with
307
+ the operation it invokes anyway.
308
+
309
+ **Private stores.** **Where a store is drawn is the team's choice.** A store one service owns may be
310
+ nested UNDER that service's element — written in the service's `extend <fqn> { store = database … }`
311
+ block, or inside the service's element on the map when the map draws that service's containers — or
312
+ drawn beside the services as an element of its own. Both are first-class and loam grades neither as
313
+ wrong. Until 2026-09-07 the second earned `landscape.datastore-private` at one consumer, a warning
314
+ whose whole message prescribed deleting the peer while the identical store nested was silent: it
315
+ graded the drawing rather than the architecture, and it is retired. Infrastructure is not a service
316
+ either — an element whose kind is `database`, `queue`, `topic`, `cache` or `bucket` is out of the
317
+ service census, so an owned store drawn at fleet level is not `landscape.service-undocumented`, is
318
+ not a `loam gate` partner, and mints no `service.exists` verification claim. The `extend` placement
319
+ needs an EXTENDING model: a model that declares its own `specification` is parsed alone, so
320
+ `extend <fqn>` resolves nothing there, and for such a service the map is the only nesting site (or
321
+ migrate the model — see "Two shapes of a service model" above).
322
+
323
+ What the choice DOES decide is one thing, and it is a fact about LikeC4 rather than a preference: **a
324
+ second consumer's edge resolves only against a declaration the MAP holds**, because a per-service
325
+ project is `architecture/` plus one model. Draw a container-level edge to `<owner-fqn>.<store>` in
326
+ another service's model while the store lives in the owner's `extend` block and that model is
327
+ `c4.invalid` ("Could not resolve reference to Referenceable named '<store>'"). So a store a second
328
+ consumer will name has to be on the map — nested inside the owner's element there, or beside the
329
+ services. That was always the load-bearing half; the prescription around it was not.
330
+
331
+ Nesting closes no door in the other direction: loam counts a
332
+ consumer a service's EXTENDING model attests exactly as it counts one the map draws, so a nested
333
+ store two services reach is `landscape.datastore-shared` — the store's owner plus every service
334
+ whose model draws a call at it or below it, outbound or, since a store never initiates, inbound
335
+ from it (*Which way an edge points*). The second consumer draws that call as a
336
+ container-level edge to `<owner-fqn>.<store>` in its OWN model, and the service-level twin on the
337
+ map as usual. The two placements give the store the same id,
338
+ `<owner-fqn>.<store>`, so moving the declaration OUT of the owner's `extend` block into the owner's
339
+ element on the map rewrites no edge and no `Covers:` line — measured on `examples/docs`, where
340
+ moving `db` out of order-service's `extend` block into `orderService` on the map left the model at
341
+ 12 elements · 8 relationships, left `Covers: marketplace.orderService.db` in its `arch.spec.md`
342
+ resolving, and left checkout-web's container-level edge to it resolving. The move BACK is free only
343
+ while no other service names the store: with checkout-web's edge drawn, putting `db` back into
344
+ order-service's `extend` block is `c4.invalid` on checkout-web's model ("Could not resolve
345
+ reference to Referenceable named 'db'"), or `landscape.invalid` plus one `spine.landscape-invalid`
346
+ per service when that second edge is drawn on the map. `landscape.datastore-shared` needs a second
347
+ consumer to fire, so it always prints in the state where map→extend breaks, and its message says
348
+ so. It is the one datastore grade left, it fires at two consumers for both placements alike, and it
349
+ states a coupling rather than prescribing a move: shared means the same DATA, and if two services
350
+ only share a host or a cluster the honest model is one private store each. The consumer census is
351
+ fleet-scope: it reads
352
+ EXTENDING models only, because their ids are the map's fully-qualified ids and the join is exact,
353
+ while a standalone model's ids are its own file's and are never matched to a landscape element.
354
+
355
+ **Renderer consequences.** The root project holds the map **and** every extending model, so one
356
+ `npx likec4 start` at the docs root draws the fleet and the inside of every adopted service, and
357
+ the project picker holds one entry. A standalone model is the exception, and it costs two files:
358
+ ONE entry in the root `likec4.config.json`'s `exclude` covering its directory (its kinds would be
359
+ duplicates of the map's inside one project), plus its own `likec4.config.json`, which is what makes
360
+ it a project of its own.
361
+ `loam subsystem sync` maintains both halves — it writes the per-service file for standalone models
362
+ only, and rewrites the root `exclude` so that it holds `services/<tree>/**` for exactly the models
363
+ that stand alone. The rewrite removes the wholesale `services/**` and every `services/<tree>/**`
364
+ naming a directory that IS a service, keeps every other entry in the order the team wrote it (a
365
+ `services/legacy/**` for a directory that is no service survives untouched), and appends the
366
+ standalone entries sorted. It runs when at least one extending model exists, or when a standalone
367
+ model's directory is not covered — so a repository whose models all stand alone behind `services/**`
368
+ is never touched, and a fresh scaffold that gains its first standalone model earns the entry on the
369
+ next sync. `--json` reports it as `projects.exclude`, with what was added and removed; the text view
370
+ prints one line when it rewrote. An entry covers a directory when it names that directory —
371
+ `<dir>`, `<dir>/`, `<dir>/*`, `<dir>/**` and `<dir>/**/*` all do, and a wildcard segment in the
372
+ middle (`services/*/legacy`) matches one level — or when it is a raw path prefix of it from a `/`
373
+ boundary, which is what the renderer measures: `services/pay` covers `services/payment-service`,
374
+ while `services/pay/**` covers nothing there. An entry is normalised the way the renderer
375
+ normalises it before anything is matched: a leading `./`, an inner `/./` and an inner `//` are
376
+ resolved away, a TRAILING `/` is not (bare `architecture` also hides `architecture-old/`,
377
+ `architecture/` does not), `.//` leaves a literal `.` directory and hides nothing, and a BACKSLASH
378
+ is a literal character to the matcher, never a separator — `services\order-service\**` hides
379
+ nothing at all, so loam neither grades on it nor rewrites it. Measured at the 1.59.2 pin against
380
+ `npx likec4 validate .` file counts; the table is in `src/core/c4/root-project/exclude.ts` and
381
+ pinned in `test/root-exclude.test.ts`. A directory the list already covers in
382
+ any of those spellings earns no second entry. `projects.exclude` carries `updated`, `entries`,
383
+ `added`, `removed` and `unreadable` — the last is `true` when the root file exists and loam could
384
+ not read an `exclude` list out of it, which is never the same answer as `entries: []`, and the list
385
+ is then reported untouched. Beside it, `--json` carries
386
+ `projects.created` (the per-service project files written) and `projects.removed` (POSIX paths
387
+ relative to the DOCS ROOT, deleted this run, sorted by path; `[]` otherwise) — and
388
+ `projects.created` is spelled the same way. Both are relative to the docs root, not to the
389
+ repository root, which differ whenever `docsDir` is a subdirectory. A fresh repository is
390
+ scaffolded with
391
+ `exclude: ["**/node_modules/**", "features/**"]`.
392
+
393
+ **Migrating a standalone model to the extending shape** is a documented recipe an agent performs;
394
+ loam does not rewrite a model, because the comments in a real one are somebody's and not loam's to
395
+ reflow. In order:
396
+
397
+ 1. Write into `architecture/landscape.likec4` any element kind the model uses that the map does not
398
+ declare (`element database`, `element queue`), and any element the model reaches that the map
399
+ does not draw — an external system, a broker's topic — as the fleet's own declaration.
400
+ 2. Delete the model's `specification` block.
401
+ 3. Delete every element the map declares: the service's own element, and every re-declared partner.
402
+ 4. Wrap what is left in `model { extend <fqn> { … } }`, `<fqn>` being the fully-qualified id of the
403
+ map's element for this service. Child ids inside the block stay relative; internal edges may be
404
+ written inside it.
405
+ 5. Move the cross-boundary edges outside the `extend` block and spell both endpoints in full —
406
+ `marketplace.orderService.outbox -> kafka.orderEvents`.
407
+ 6. Requalify every id that names one of those elements from outside the file: the `Covers:` lines in
408
+ `arch.spec.md`, the `of <element>` and `include` lines of any `views` block, and the hops of any
409
+ `#req-`-tagged flow beside the model. There is no suffix matching anywhere — `orderService.db`
410
+ does not answer for `marketplace.orderService.db` — so every model-local nested id has to be
411
+ rewritten. `health.yaml`'s `dependencies:` are the one place that usually needs no edit, because
412
+ a dependency resolves when an element of the own slice answers to it **by id, `metadata { service }`
413
+ binding or title**, and the slice holds the service's own elements plus every endpoint of the
414
+ model's own edges and each endpoint's ANCESTORS: `kafka` still answers through an edge naming
415
+ `kafka.orderEvents`, and a peer service still answers through the binding on the map's element.
416
+ Prefer the service id or a top-level system id there — those two spellings resolve under either
417
+ model shape, and a model-local nested id does not.
418
+ 7. Delete `services/<…>/<svc>/likec4.config.json`.
419
+ 8. Run `loam subsystem sync` (it rewrites the root `exclude`), then `loam validate --all`.
420
+
115
421
  ## Subsystems — a navigable tree under `services/`
116
422
 
117
423
  `services/` may be one flat level (the compatibility case, and forever legal) or an unbounded tree
@@ -134,6 +440,13 @@ A directory under `services/` is classified **three ways, never two**:
134
440
  subsystem's marker while another moves a service in, and the group directory would otherwise be
135
441
  read as one empty service. The fleet is never reported smaller than it is.
136
442
 
443
+ A directory named for one of the service's own subdirectories (`adrs/`, `usecases/`, `ui/`) is
444
+ service interior wherever it sits: the walk never reads one as a service or a subsystem, and never
445
+ counts one as the subdirectory that makes its parent a group. Writing the documented
446
+ `services/<id>/usecases/<name>.likec4` slot into a directory that holds no artifact FILE yet used
447
+ to make the walk read `usecases` as a service and its parent as an unmarked group — three errors
448
+ earned by following the instruction `loam init` had just scaffolded.
449
+
137
450
  `subsystem.yaml` is a **marker, not a manifest**:
138
451
 
139
452
  ```yaml
@@ -154,7 +467,9 @@ claimant). Subsystem names take the service-id grammar (`subsystem.name-invalid`
154
467
  is unbounded and is the author's problem, not the tool's. A service directly under `services/` is
155
468
  **unfiled** — permanent, normal, and never a finding; `loam list` carries the count
156
469
  (`unfiledServices`, plus `services[].subsystem` and `subsystems[]`), and an empty subsystem is
157
- legal, since `loam subsystem new` must be usable before anything moves in.
470
+ legal, since `loam subsystem new` must be usable before anything moves in. A service `loam archive`
471
+ creates lands unfiled too: the archive takes no placement input but the tree itself, never the map's
472
+ nesting ("How archive writes, and how unarchive undoes it" has the decision).
158
473
 
159
474
  The tree is mirrored into **one generated file, never into the authored landscape**:
160
475
  `architecture/subsystems.likec4` holds LikeC4 **views only** — no model, no tags, no `specification`
@@ -164,21 +479,263 @@ check uses; a member nothing models is omitted, which `landscape.service-unmodel
164
479
  reports). The output is deterministic and line-oriented — subsystems sorted by path, members by id,
165
480
  one `include` per line, no timestamps, no absolute paths — so it is byte-reproducible on any machine
166
481
  and two concurrent moves into different groups merge in git without intervention.
482
+
483
+ **Styling.** Each generated view carries `global style subsystems` — after its `title` and
484
+ `description`, before its `include`/`group` — exactly when the `architecture/` project declares a
485
+ global style with that id: `global { styleGroup subsystems { style element.tag = #external { color gray } } }`
486
+ (or the single-rule `global { style subsystems … }`), in the landscape or in ANY `.likec4` under
487
+ `architecture/` the root `likec4.config.json`'s `exclude` does not cover — a `palette.likec4` beside
488
+ the map works, and one under an excluded path is loaded by neither the renderer nor loam.
489
+ `architecture/landscape.likec4` is the one exception: loam keeps the map in that set however the
490
+ list is written, because an architecture project with no documents in it reads as a fleet that
491
+ models nothing. With the targeted tag declared in the `specification` block.
492
+ Declaring it is the whole opt-in. loam reads only that the id is declared, never what the group
493
+ says, and writes nothing when it is not — a reference to an undeclared style is a parse error that
494
+ blanks the whole `architecture/` project in the renderer. When the project declares style groups
495
+ under other names only, the file opens with one comment line naming them and saying none is
496
+ `subsystems`, and the views carry the renderer's defaults; there is no single-group fallback.
497
+ Both halves restale the file once (`subsystem.views-stale`) until the next `loam subsystem sync`:
498
+ declaring or removing the group named `subsystems` changes whether the views carry the
499
+ `global style subsystems` line, and declaring or removing a group under ANY OTHER name changes the
500
+ disclosure comment that lists what is available. Whether a service's own views can borrow that group depends on the shape of
501
+ its model: a model that EXTENDS the map is in the root project, so a `global styleGroup` declared
502
+ under `architecture/` reaches the views beside it and referencing the id there is legal; a model
503
+ that STANDS ALONE is a project of its own, and a group declared under `architecture/` does not reach
504
+ it — those views need a declaration of their own, or the reference is the parse error that blanks
505
+ that project.
506
+
507
+ Each view carries the **label a human reads**: `title`, and `description` when the marker authors
508
+ one. The title is the subsystem's **path** — every marked ancestor's label and its own, joined with
509
+ ` / `, so `services/platform/order-flow` reads `Platform / Order flow` — and each label is the
510
+ marker's `title`, falling back to the directory name (an unlabelled view is shown by its id, which
511
+ is hex-escaped for injectivity and reads as `subsystem_platform__order_2dflow`). LikeC4
512
+ reads that path as folders: the view list mirrors the directory tree, showing one folder per
513
+ ancestor and the leaf's own label as the title. A `/` **inside** a label becomes `∕` (U+2215), so an
514
+ authored slash can neither hide half of what somebody wrote nor invent a folder level the tree does
515
+ not have. Both strings are somebody's prose written into syntax, so both are normalized to one line
516
+ and escaped. Titles are not unique — only ids are — and renaming a parent's title rewrites every
517
+ view beneath it.
518
+
519
+ The **boundary** each view draws is a fact about the landscape, never a setting:
520
+
521
+ 1. an element that holds *exactly* the subsystem's members and nothing else is included beside the
522
+ leaves, so the renderer nests them in the box the author drew, under the author's title;
523
+ 2. a landscape that nests nothing puts the services in a `group '<title>'` of loam's own, mirroring
524
+ the directory subtree, so a parent subsystem shows its children as boxes rather than one pile;
525
+ 3. members that ARE nested with no element owning exactly them get no box and one comment line —
526
+ `// model: no boundary — <id> also holds services outside this subsystem.` — because a synthetic
527
+ box would look identical whether or not the tree and the map agree. That comment is the whole of
528
+ what loam says about the two disagreeing: nothing grades it, nothing fails on it, and it lands in
529
+ the diff of the commit that caused it. **The tree and any `group` nesting in the landscape stay
530
+ independent**; `loam subsystem move` never rewrites the map, and no finding compares them.
531
+
532
+ Because the boundary depends on which *other* services sit under an element, an unrelated adoption
533
+ or archive can flip a view from boxed to flat — a new reason to see `subsystem.views-stale`, and why
534
+ `loam archive` withholds its "complete + current" line and names `loam subsystem sync` when it left
535
+ the file behind.
536
+
167
537
  `loam subsystem sync` is the one regenerator (it also *removes* the file when the tree has no
168
538
  subsystems), every `loam subsystem` write verb regenerates it inside the same transaction, and
169
- `validate --all` grades staleness by **byte comparison** — exactly one error,
170
- `subsystem.views-stale`, on exactly that file; nothing in loam ever parses it: a views-only document
171
- does not parse standalone (its `include` lines reference elements the landscape defines), and it
172
- holds **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
173
- which merges the whole `architecture/` project, resolves its includes.
539
+ `validate --all` grades staleness by **content comparison** (line endings normalized; a CRLF
540
+ checkout changes no fact) — one error,
541
+ `subsystem.views-stale`, per stale generated file, each naming its own path (there are two of them;
542
+ `architecture/services.likec4` is below); nothing in loam ever parses either: a views-only document
543
+ does not parse standalone (its `include` lines reference elements the landscape defines), and both
544
+ hold **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
545
+ which merges the whole `architecture/` project, resolves their includes.
546
+
547
+ `sync` refuses to rewrite this file while the `architecture/` project cannot be read. The render
548
+ needs the map's elements and its global-style census, and a tolerant "no elements" answer wrote a
549
+ file holding title and description alone — the `global style subsystems` line, every `include` and
550
+ the boundary comment gone — while reporting `updated … 1 subsystem view(s)` and answering `current`
551
+ ever after. So: a load error falls back to the landscape FILE when that parses on its own (a
552
+ duplicate `subsystems` group declared in two `architecture/` documents costs nothing), and when
553
+ that fails too the facts are UNKNOWN. With an existing views file, `sync` then writes nothing about
554
+ it, prints why, and reports the `--json` action `blocked`, with `blocked.reason` reading
555
+ `landscape-invalid` — exit 0, because `loam validate --all`'s `landscape.invalid` is the gating
556
+ surface. With no file yet, and inside a tree-changing verb's commit window (`new`/`move`/`rm`/
557
+ `rename`, whose contract is that the file lands with the change), the tolerant shape is written as
558
+ before and the same note is printed.
559
+
560
+ ### `architecture/services.likec4` — one overview view per service
561
+
562
+ The **second generated views file**, written by the same `loam subsystem sync`, in the same commit,
563
+ removed by it when the map draws no service, and graded by the same `subsystem.views-stale` on its
564
+ own path. Views only, one per service the fleet map draws, sorted by service directory id:
565
+
566
+ ```
567
+ view service_payment_2dservice of marketplace.paymentService {
568
+ title 'Payments / payment-service'
569
+ global style services
570
+ include *
571
+ include kafka.orderEvents
572
+ include marketplace.checkoutWeb
573
+ include stripe
574
+ exclude * -> *
575
+ include -> marketplace.paymentService
576
+ include marketplace.paymentService ->
577
+ include marketplace.paymentService.* -> marketplace.paymentService.*
578
+ }
579
+ ```
580
+
581
+ **Why the peers are named one by one, measured at the 1.59.2 pin on a 120-service and a 58-service
582
+ fleet.** The obvious recipe — `include <fqn> -> *` and `include * -> <fqn>` — is wrong at fleet
583
+ scale and looks perfect on a small flat fixture, which is how it would have shipped. LikeC4 draws a
584
+ counterpart at the highest ancestor that is not also an ancestor of the subject, so on a map that
585
+ GROUPS its services (which is exactly what a 100-service fleet needs at fleet level) every peer
586
+ collapses to its group box: the naive view of one service in the 120-service fleet drew six group
587
+ boxes and **0 of its 10 nodes had drill-down**. loam already reads elements and relationships, so it
588
+ names each peer itself — the far endpoint of every edge touching the service or anything inside it,
589
+ spelled as the map declares it, so an edge at a topic names the topic and not the broker, which is
590
+ the truer picture and the one the map already draws. `exclude * -> *` then drops the peer-to-peer
591
+ cross traffic (23 edges down to 7 on the same fixture), and the three lines after it put back
592
+ exactly the subject's own: the UNARY `include -> <fqn>` / `include <fqn> ->`, never the binary
593
+ wildcard, which re-introduces the parent group boxes as extra nodes (16 instead of 10). `include
594
+ <fqn>.* -> <fqn>.*` is the interior wiring, and it is legal on a childless element — it simply
595
+ matches nothing — which is what makes one line work for both model shapes: an extending model's
596
+ containers are in the root project and appear, a standalone model's are not and the line is silent.
597
+
598
+ Measured at 58 services in 11 groups: a median of **10 nodes and 8 edges** per generated view, every
599
+ peer that is a service carrying drill-down into its own generated view, and `npx likec4 export json`
600
+ — which layouts every view — costing about **7 ms per generated view** (1.3 s with 120 of them
601
+ against 451 ms without). `likec4 start` layouts on demand, so a reader pays for the one view they
602
+ open, and the generated views are all small BY CONSTRUCTION; the expensive view is the hand-authored
603
+ fleet one, and it is unaffected.
604
+
605
+ **Drill-down is the `of`, and it costs one keyword.** `view <id> of <element-fqn>` names the view's
606
+ subject, and LikeC4 then computes `navigateTo` on that element's node in **every other view that
607
+ draws it** — no authored links anywhere. So one generated view per service makes the fleet map,
608
+ every subsystem view and every other service view click-through into the service. The subsystem
609
+ views take the same keyword when the map holds an element owning exactly that subsystem's members,
610
+ which closes the mesh — `fleet → payments → payments/service-22 → identity/service-45 → identity`,
611
+ every hop generated, none authored.
612
+
613
+ **Two views of one element are legal, and the FIRST DOCUMENT ALPHABETICALLY wins** the default
614
+ navigation target — measured by renaming the files, LikeC4's load order being alphabetical by path.
615
+ Both stay reachable: inside a view of the subject, the subject's own node navigates to the OTHER
616
+ view of the same element, so the renderer cycles between siblings and nothing is orphaned.
617
+ `architecture/landscape.likec4` sorts before `architecture/services.likec4`, so a team's authored
618
+ `view of <element>` written in the map keeps precedence over the generated one, and neither is lost.
619
+ loam does not detect this and never will: **docs/DESIGN.md rule 26 forbids loam reading what a view
620
+ SHOWS**, in any document, so it may not look at an authored `view of` to decide whether to emit. It
621
+ emits for every service the map draws, and the alphabet settles the rest.
622
+
623
+ **The first line is the deed, and every writer checks it.** Both generated views files open with a
624
+ `// generated by loam` mark, and that mark is what makes the path loam's rather than the team's.
625
+ These are the only two filenames loam RESERVES inside `architecture/`, a directory that is otherwise
626
+ entirely authored, so a repository that already had a file at one of those paths must not lose it to
627
+ an upgrade — the archive's promise that authored bytes stay authored holds here too. Every writer of
628
+ these paths reads the first line first: `loam subsystem sync` reports that one file `refused`,
629
+ writes nothing for it, still does the rest of its work, and exits 1; `loam subsystem new`, `rm`,
630
+ `move` and `rename` and `loam seed` refuse the WHOLE operation with `already-exists`, writing,
631
+ renaming and staging nothing, because their commit window exists precisely to land the generated
632
+ file in the same commit as the change to the tree — completing half of that would manufacture the
633
+ `subsystem.views-stale` state the window exists never to leave behind. The consequence to know: a
634
+ file with no mark is never regenerated, and an EMPTY file carries no mark either, so a truncated one
635
+ must be deleted by hand before sync will write it again. loam's own staging is atomic and cannot
636
+ produce one.
637
+
638
+ **Titles are folders, because there is nothing else.** LikeC4 has **no view-folder syntax** —
639
+ `views { group 'X' { … } }` is a parse error at the pinned version and the AST carries no view-group
640
+ type at all. What it has is the title: a view title is split on `/` into browser folders and only
641
+ the last segment is shown. So a per-service view is titled `<subsystem path> / <service directory
642
+ id>`, built from the same path helper the subsystem views use, and the two generated files fold into
643
+ ONE folder tree mirroring `services/`. An unfiled service is its own id at the root. The service
644
+ **directory id** is used rather than the map element's title: it is the name every loam command
645
+ addresses the service by, it cannot drift from the map, and a directory id cannot contain `/`. A
646
+ `dynamic view` you author under `services/<…>/<id>/usecases/` lands in the same folder when you
647
+ title it `'<subsystem path> / <service id> / <use case>'` — a convention loam states and never
648
+ grades.
649
+
650
+ **Which services appear.** Exactly those the element→service join resolves, the same join the
651
+ subsystem views use. A service the map does not draw is omitted — its absence is already
652
+ `landscape.service-unmodelled`, and rendering only what the committed map resolves is what keeps the
653
+ output a function of committed bytes. **View ids** are `service_` plus the same hex escaping the
654
+ subsystem view ids use, so the two generated files cannot collide with each other, and an authored
655
+ view claiming one is `subsystem.view-id-collision` — which names the file and the reserved prefix. A
656
+ `service_` id is reserved only for a service the map actually draws. **Style**: `global style
657
+ services`, presence-detected off the parsed map's declared style ids exactly as `subsystems` is, and
658
+ written only when a group of that name is declared — a reference to an undeclared group is a parse
659
+ error that blanks the whole project.
660
+
661
+ ### The per-service LikeC4 project file
662
+
663
+ A different artifact from the two generated views files above, and the tree's other renderer
664
+ concern: `services/<…>/<svc>/likec4.config.json`,
665
+ which `loam subsystem sync` also writes, beside every **standalone** `model.likec4` that has none.
666
+ An extending model needs no such file and never gets one: it belongs to the root project, which is
667
+ where it parses, and a project file beside it is `service.likec4-config-stray` — which `sync`
668
+ DELETES, in the same journaled commit as everything else it writes. A standalone model
669
+ needs one because it cannot share a project with the map — it declares its own kinds, and inside
670
+ one project every declaration is a duplicate blamed on the map too — so `sync` also keeps the root
671
+ `exclude` covering exactly the directories of the models that stand alone. Before extending models
672
+ existed, `services/**` was excluded wholesale and every model needed the file; what that exclusion
673
+ cost went unstated for a release: an
674
+ adopted service was a box on the fleet map with nothing renderable inside it. Measured at the
675
+ pinned likec4, a project file INSIDE a service directory is registered as a project of its own
676
+ regardless of the exclusion, so a docs root holding standalone models loads `fleet` plus one
677
+ project per such service, each carrying LikeC4's own `index` view of the containers beside the
678
+ authored one, and a model that does not parse breaks only its own project. (An extending model
679
+ is inside `fleet`, so a model that does not parse breaks the root project instead — the trade the
680
+ shape makes, and `c4.fleet-project-invalid` is what names it before a reader meets it in the
681
+ renderer.) The file is **create-only for a standalone model** —
682
+ never compared, never rewritten — under the root config's ownership rule, not the
683
+ views file's: there is no `-stale` grade because nothing can make it stale. It is removed in
684
+ exactly one state, and only there: beside a model that EXTENDS the map. That nested project claims
685
+ the model out of the root project — the workspace loads two projects, the model is Invalid inside
686
+ its own one (the map's kinds are not in there with it) and `export json --project fleet` then loses
687
+ the service's containers. Which of the two the workspace does was measured both ways at the 1.59.2
688
+ pin: on a seeded fleet whose model extends a top-level id the containers went; on `examples/docs`
689
+ the root project kept the model and the export was byte-identical. What is true in every state is
690
+ the second project itself — `likec4 validate .` at the docs root refuses without `--project` from
691
+ the moment that file exists. So it is not a harmless leftover, and `loam subsystem sync` deletes
692
+ it. One beside a model that is GONE stays: that one really is the team's — though the same second
693
+ project it registers is there too, so `likec4 validate .` and `likec4 build` at the docs root need
694
+ `--project` while it stands (ROADMAP's "Known and carried, 2026-09-04"). Its bytes are a
695
+ function of the id alone (two keys, `name` and `title`), so placement never changes them and a
696
+ `subsystem move` carries the file with no write of its own — it rides inside the rename the move's
697
+ transaction already journals (in a git-backed repo, commit the files `sync` wrote first: `move`
698
+ refuses over untracked paths, `move-uncommitted`); a compare would only fight the keys LikeC4 lets a
699
+ team add (`title`, styles). `name` is the id with every
700
+ `.` folded to `-` (LikeC4's name grammar refuses the byte; loam's id grammar admits it), prefixed
701
+ `service-` when the result would equal the root project's `fleet` (a nested project of that name
702
+ is silently registered as `fleet-1`) or LikeC4's reserved `default`; `title` is the id verbatim,
703
+ and the title is what the renderer's project picker shows. The name is **not injective**, and
704
+ that is disclosed rather than escaped away: an id spelled billing.core beside a service literally
705
+ named billing-core, or `service-fleet` beside `fleet`, share a name and LikeC4 suffixes the later
706
+ one `-1`; no loam
707
+ finding grades it, because the picker still reads the id. Order is a gate — the root file first
708
+ (`loam init --create` writes it, `loam doctor` prints it), then `sync`: without the root file
709
+ nothing is written and the text output says so. No model, no file: a spec-only service is owed
710
+ nothing and gets its file on the first `sync` after its model lands. No legal id, no file either: a
711
+ directory whose name breaks the service-id grammar (`service.id-invalid`) would get a `name` the
712
+ renderer silently drops, and a create-only file the renderer ignores would silence the grade for
713
+ good, so `sync` skips it and the rename that clears `service.id-invalid` earns the file.
714
+ `validate --all` grades presence only — one warning per service with
715
+ a STANDALONE model and no file, `service.likec4-config-missing`, on the fleet `landscape` target,
716
+ never an error, never in `--service`, `status` or `doctor`; the fix is `loam subsystem sync`, never
717
+ a hand-written file. Its two siblings grade the extending shape's version of the same question —
718
+ `service.likec4-config-stray` for a project file that should not be there, `service.model-excluded`
719
+ for a root `exclude` entry that hides the model from the renderer — and `sync` is the repair for
720
+ both: it removes the stray and rewrites the entry. A third reads the same list and is nobody's to
721
+ rewrite: `landscape.excluded` says an entry covers `architecture/landscape.likec4` itself, so the
722
+ renderer loads no map at all — `sync` maintains only the `services/` entries and will not touch that
723
+ line, and the finding quotes it so its author can. It also suspends
724
+ `c4.fleet-project-invalid` for that run, because the renderer's project is exactly what the entry
725
+ broke and grading it would report the one line once per extending model. A
726
+ feature's `delta.likec4` is deliberately out of scope: the directory is transient and
727
+ `loam archive` moves it where a project file would still be registered, so every shipped feature
728
+ would keep a dead project in the list forever.
174
729
 
175
730
  A move is **one journaled transaction over N directory renames plus the regenerated views file**
176
731
  (`loam subsystem move <name>... --into <sub|.>`; `rename` is a one-rename move; whole subtrees move
177
- as one rename). It refuses only when git reports uncommitted changes under a directory being moved
178
- (`move-uncommitted` — where git will not say, it proceeds), and after landing it stages the renames
179
- and the views file in git without committing. Archive snapshots survive any of this: a version-3
180
- snapshot re-keys `services/` entries by `(service, artifact)` and restores wherever the service
181
- lives *today*.
732
+ as one rename), **plus every relative markdown link the renames would break** — out of the moved
733
+ documents and into them, rewritten in the same commit (Conventions, above, has the rule). It
734
+ refuses only when git reports uncommitted changes under a directory being moved, or in a document
735
+ outside it that the move would relink (`move-uncommitted` — where git will not say, it proceeds),
736
+ and after landing it stages the renames, the relinked documents and the views file in git without
737
+ committing. Archive snapshots survive any of this: a keyed (version 3 or 4) snapshot re-keys
738
+ `services/` entries by `(service, artifact)` and restores wherever the service lives *today*.
182
739
 
183
740
  ## Canonical joins
184
741
 
@@ -193,6 +750,7 @@ lives *today*.
193
750
  | Capability | requirement `Capability:` (list; also `Capabilities:`) | `architecture/capabilities.yaml` OR `capabilities/<cap>/spec.md` | declared capability id, `/` allowed for nesting |
194
751
  | Capability requirement | requirement `Realizes:` (list) | a requirement in `capabilities/<cap>/spec.md` | `<capability-id>#<Requirement-ID>`, split at the LAST `#`, with an optional `@<digest>` pin |
195
752
  | Capability requirement (flow) | a `dynamic view` tag `#req-<slug>` | a requirement of the capability the view's `#cap-` tag names | the `Requirement-ID` with every character outside `[A-Za-z0-9_-]` flattened to `-` |
753
+ | Service requirement (flow) | a `dynamic view` tag `#req-<slug>` in a `.likec4` beside model.likec4, or inside it | a requirement of the service whose directory holds the file, from either spec axis | the `Requirement-ID` flattened as above |
196
754
 
197
755
  These keys are independent: an operation id is not a permission and a service id is not inferred
198
756
  from a feature directory. A file can therefore be structurally valid while a join is broken; the
@@ -202,6 +760,71 @@ The last two exist only inside an `archive`/`unarchive` commit window and are re
202
760
  belong in the docs repo's `.gitignore`. Finding one at rest means a write did not finish:
203
761
  `loam doctor` grades it, and the next `archive`/`unarchive` repairs or refuses.
204
762
 
763
+ ### Which way an edge points
764
+
765
+ LikeC4 accepts an arrow in either direction, and some of loam's readers draw meaning from it. This
766
+ section lists every reader and the drawing each one expects. A reader not listed here ignores
767
+ direction.
768
+
769
+ | Reader | Expects | Drawn the other way round |
770
+ |---|---|---|
771
+ | The HTTP spine, `metadata { op }` | caller `->` provider: the `op` resolves against the TARGET's `openapi.yaml` | graded against the wrong service: `spine.op-undefined` there |
772
+ | The event spine, `metadata { publishes }` | producer `->` topic: `publishes` binds the edge's SOURCE | when the source is infrastructure and the target is a service, the key binds nothing: `spine.message-unbound` (warn) on the target |
773
+ | The event spine, `metadata { consumes }` | topic `->` consumer: `consumes` binds the edge's TARGET | when the target is infrastructure and the source is a service, the key binds nothing: `spine.message-unbound` (warn) on the source |
774
+ | The consumer census behind `landscape.datastore-shared` and `landscape.platform-candidate` | a service at the SOURCE of an edge into the element, or an outbound call in the service's extending model | for infrastructure, either direction counts; for any other element the arrow is read, so a system calling INTO the fleet is not consumed by the services it calls |
775
+ | Review order (`loam list --review-order`) | a plain edge is a call, so its source depends on its target; a `consumes` edge is a delivery, so its target depends on its source | the dependency is counted backwards |
776
+ | Use-case hops (`dynamic view`) | each hop backed by a relationship drawn the same way | `usecase.step-unbacked`; a reply hop is written `<target> <- <source>` and is looked up under the call it answers |
777
+ | `Covers:` edge entries | `a -> b` names the edges from `a` to `b` | `covers.unknown` |
778
+
779
+ **Infrastructure** here is an element whose kind, or an ancestor's, is one of `database`, `queue`,
780
+ `topic`, `cache` or `bucket`, and which stands for no service: a table nested in a `database` is
781
+ part of it. These are the kinds that exempt an owned store from `landscape.service-undocumented`,
782
+ and the limitation is the same: the hook is a kind NAME, so a kind spelled `postgres`, or a broker
783
+ drawn as a plain `softwareSystem`, is not infrastructure. Infrastructure never initiates and never
784
+ decides: a store, a topic or a bucket neither calls nor publishes nor consumes. Both rules below
785
+ follow from that one fact.
786
+
787
+ **THE ARROW FOLLOWS THE MESSAGE on the event spine**, producer to topic and topic to consumer. A
788
+ key bound to a topic is therefore on the wrong end of its edge, because a topic is what sits
789
+ between a producer and a consumer and is never either of them. Through 0.2.0-alpha.10 such a key
790
+ was skipped without a word: the document validated, the metadata parsed, and the spine graded one
791
+ link fewer. `spine.message-unbound` now says so, at service scope (the living map) and at feature
792
+ scope (a delta's tagged edges), and it spells the edge turned round. When the edge carries both
793
+ keys, turning it round would only move the warning to the other key, so the message says to put
794
+ the key at fault on an edge of its own.
795
+
796
+ loam reports the reversed edge and does not quietly bind it to the other end. Review order and
797
+ use-case hops read the same arrow, and re-binding the spine alone would make it agree with a
798
+ drawing those readers still take the opposite way. An `#external` system at the bound end is not
799
+ this case. `crm -> billingService` carrying `publishes` says that an outside party produces the
800
+ message, which `spine.message-unproduced` accepts and `spine.message-external` grades, and a
801
+ `consumes` on an outside system says that an outside party consumes it. Both drawings are correct
802
+ and stay silent. So does an edge with no fleet service at either end, such as an `#external`
803
+ producer publishing to the broker. A key bound to a service is graded against that service's
804
+ contract, so a wrong drawing there already surfaces as `spine.message-undefined`.
805
+
806
+ **For the census, infrastructure's direction carries no meaning.** Fleets draw a read as a call
807
+ (`service -> store`) or as the data's flow (`store -> service`), and both are common. The census
808
+ counted only the first, so a second consumer drawn as data flow vanished, and so did the
809
+ `landscape.datastore-shared` it should have caused. The event doctrine above draws every consumer
810
+ as `topic -> service`, so no broker's consumers were ever counted toward
811
+ `landscape.platform-candidate`. Both directions now count, from the map's edges and from an
812
+ extending model's attested calls alike.
813
+
814
+ **A relationship's KIND is drawing, never a join.** No reader in the table above reads a kind:
815
+ `metadata { op }`, `publishes` and `consumes` are what every grade joins on, and `-[calls]->`
816
+ changes what the renderer draws and nothing loam decides. The convention loam teaches is in
817
+ `loam instructions loam-drawing`: one kind per kind of traffic — `publishes` from producer to topic,
818
+ `delivers` from topic to consumer, `reads`, `writes` and `readsWrites` with a store edge always
819
+ written service `->` store and the kind's style placing the arrowhead, `calls` for a request, and
820
+ `connects` wherever no evidence gives a direction yet. The fleet map `loam init --create`
821
+ scaffolds and the one `loam seed` templates both declare those seven kinds, and declare them only:
822
+ seed's edges stay plain, because `fleet.yaml` says who calls whom and nothing about what an edge
823
+ carries. One consequence is mechanical rather than a convention. `loam archive` splices a delta's
824
+ edge into the map verbatim and never copies a `specification` declaration, so a kind the living
825
+ map does not declare leaves the merged map unparseable and the archive refuses with
826
+ `merge-failed`. Declare a kind in the map before a feature that draws one archives.
827
+
205
828
  ## `loam.json` — the wiring, one file per repository
206
829
 
207
830
  The docs repo is one repository; each governed implementation boundary is represented by a code
@@ -242,7 +865,10 @@ so running loam from a subdirectory works; `loam init` writes the file where it
242
865
  this repo, digests that document and compares it with the committed
243
866
  `services/<id>/openapi.yaml`: `openapi.generated-stale` (warn) when they differ,
244
867
  `contracts.source-missing` (error) when nothing is there, `contracts.source-invalid` (error) when
245
- the path escapes the repository or the file does not parse.
868
+ the path escapes the repository or the file does not parse. The stale warning's `details` name
869
+ the operationIds that differ (served but not in the committed copy, committed but no longer
870
+ served), and its message says when the two sets agree, so a reworded description no longer reads
871
+ the same as twenty vanished endpoints.
246
872
 
247
873
  **loam reads it and never writes it.** The copy into the docs repo stays a human `cp` reviewed in
248
874
  a pull request — that is what keeps the committed contract a document somebody agreed to rather
@@ -404,10 +1030,119 @@ the target is unchanged, only the route to it — which is what lets an author w
404
1030
  resolves where they wrote it, in the pull request that reviews it, and keeps it resolving after it
405
1031
  ships. This applies to the service and capability spec merges alike, and predates the glossary: a
406
1032
  delta requirement citing an ADR always had it. A link inside a fenced block or a code span, an
407
- absolute URL, a site-root path and a bare `#section` are left exactly as written.
1033
+ absolute URL, a site-root path and a bare `#section` are left exactly as written, and so is a link
1034
+ whose text still reaches its target from where it lands.
1035
+
1036
+ **A link into the feature itself follows the file to where the archive puts it.** The same archive
1037
+ that moves a requirement two directories up moves the rest of the feature too, so the rule for
1038
+ writing a link is one sentence — write the link that resolves where you write it — and archive owns
1039
+ the rest:
1040
+
1041
+ - a file the archive COPIES into the living tree is cited at its living copy: a term the feature
1042
+ introduces (`features/<FEAT>/glossary/<term>.md` → `glossary/<term>.md`), a flow
1043
+ (`architecture/usecases/`), a topology document (`architecture/`);
1044
+ - every other file under the feature — its ADRs above all, its intent, its deltas — is cited where
1045
+ the archived feature keeps it, `features/archive/<FEAT>/…`.
1046
+
1047
+ So `[Order](../../glossary/order.md)` written in `features/<FEAT>/specs/<svc>/spec.md` (the
1048
+ feature's own term) and `[Order](../../../../glossary/order.md)` (the term's future living path,
1049
+ which `link.unresolved` already treats as resolved while the feature is in flight) land as the same
1050
+ link. **A feature ADR is never copied into the living tree**: it records a decision taken for that
1051
+ change, it is part of the change's history exactly as the deltas are, and the living documents that
1052
+ cite it point into `features/archive/<FEAT>/adrs/`. A decision meant to govern beyond the change
1053
+ belongs in `architecture/adrs/` or the service's own `adrs/`, written there in the same pull
1054
+ request. A term the archive copies gets the same treatment as the requirements: its own relative
1055
+ links are re-expressed for `glossary/`, two directories up from where it was written — the one
1056
+ change to an otherwise byte-for-byte copy.
1057
+
1058
+ **The documents that stay in the feature keep links that resolve from `features/archive/<FEAT>/`.**
1059
+ The archive is a directory rename, and every markdown document it carries — the intent, the
1060
+ feature's ADRs, its deltas, the archived copy of a term, any other `.md` under the feature — was
1061
+ written one directory shallower than where it now sits: `../../services/x/spec.md` in
1062
+ `features/<FEAT>/intent.md` would name `features/services/x/spec.md` from the archive. So the archive
1063
+ re-expresses their links for the move, in the same commit, under the rename's own rule: a target
1064
+ inside the feature moves with it, so a link from the intent to its own ADR or its own term is left
1065
+ exactly as written (the archived record keeps citing the archived copy it was reviewed against, not a
1066
+ living one that goes on changing), and every other target stays put, so only the routes the rename
1067
+ broke change. The plan lists each one as an `update` before the `move`, and the text output says
1068
+ `feature documents: N relinked for features/archive/<FEAT>/`; the snapshot parks each pre-image like
1069
+ any other write, so `loam unarchive` puts the authored bytes back and archive-then-unarchive stays a
1070
+ byte-for-byte round trip — and, like any file the archive wrote, one edited under
1071
+ `features/archive/<FEAT>/` since is a later change `unarchive` will not discard without `--force`
1072
+ (`snapshot-stale`). A document that is not UTF-8 is left as written, and the text output says so. The
1073
+ links are true **as of the archive**, and that is the whole promise: archived features stay out of
1074
+ `link.unresolved` and out of `subsystem move`'s rewrite, because a later rename is a later commit and
1075
+ the archive is history — what the archive owes is not to break them itself.
1076
+
1077
+ **A document outside the feature that links into it keeps a link that resolves.** The same rename
1078
+ takes `features/<FEAT>/` away from under every document that cites something in it — another change
1079
+ in flight that builds on this one's decision (`../<FEAT>/adrs/0001-x.md` from its intent), a living
1080
+ ADR citing the decision taken for a change still open — and no edit made in advance could have
1081
+ helped, because the archived path does not exist until the archive does. So the archive rewrites the
1082
+ links INTO the feature from every other authored document — the fleet's own documents, every
1083
+ service's, every other in-flight feature's: the corpus `subsystem move` rewrites for its renames —
1084
+ in the same commit, under the rule for a link into the feature above: a file the archive copies into
1085
+ the living tree is cited at its living copy, everything else where the archived feature keeps it.
1086
+ These documents are the living corpus and the other changes in flight, not the record of this one, so
1087
+ they follow a term to its living copy where the feature's own documents keep citing the archived one.
1088
+ Every other link keeps its spelling, and a living document the merge also rewrites is relinked from
1089
+ its merged text, as one write. The plan lists each as an `update` before the `move`, and the text
1090
+ output says `links into the feature: N document(s) relinked for features/archive/<FEAT>/`. The costs
1091
+ are the ones `subsystem move` names, said the archive's way: a `verified` `spec.md` or `arch.spec.md`
1092
+ the relink moves off its `content_digest` joins `revouchOwed`, exactly like one the merge moves, and
1093
+ a living requirement whose route it rewrote moves the digest a feature in flight may pin with
1094
+ `Based-On:` (`delta.baseline-stale` until `loam rebase` — a `note:` line in the text output). There
1095
+ is no `move-uncommitted` refusal: archive does not stage in git, and its undo is the snapshot, where
1096
+ each relinked document is an ordinary row. `loam unarchive` puts the authored bytes back, and a
1097
+ document edited since — another feature's intent included — is a later change it will not discard
1098
+ without `--force` (`snapshot-stale`). A document whose own feature has been archived or removed since
1099
+ is neither: `unarchive` leaves it as it is and says so (`left:` in the text output; `--json` has no
1100
+ key for it), because putting it back would recreate a one-file `features/<other>/` that every reader
1101
+ lists as a change in flight — beside the archived one, or in place of the one somebody abandoned.
1102
+
1103
+ **`loam subsystem move` and `rename` keep the same promise.** A move is a directory rename and a
1104
+ relative link is a route between directories, so the move rewrites every relative link it would
1105
+ break, in the same transaction as the renames: the links out of every markdown document under a
1106
+ moved directory, and the links into a moved directory from every other authored document (the
1107
+ fleet's own documents, every service's, every in-flight feature's — archived features are history
1108
+ and are left as they are). A link whose text still reaches its target from the new place — a
1109
+ sibling moving with its document — keeps its exact spelling. Each rewritten document is listed in
1110
+ the text output (`relinked <path>`) and in `--json` as `relinked`, staged in git with the renames,
1111
+ and asked the same uncommitted-work question: a document OUTSIDE the moved directories that the
1112
+ move would relink, and that git reports as uncommitted or untracked, refuses the move with
1113
+ `move-uncommitted` exactly as a dirty file under a moved directory does. A rewritten route is
1114
+ still a changed byte, and the move says what that costs rather than leaving it to the next gate: a
1115
+ `verified` document it relinks no longer hashes to its `content_digest` (`content.stale` until
1116
+ its owner re-vouches — the archive merge's `revouchOwed` in another form), and a living
1117
+ requirement it relinks moves the digest a feature in flight may pin with `Based-On:`
1118
+ (`delta.baseline-stale` until `loam rebase`). Both arrive as `warnings` (`note:` lines in
1119
+ text); neither is softened, because the alternative was a link that resolves to nothing.
1120
+
1121
+ **When archive CREATES a living document, it carries what the delta marked as the document's own —
1122
+ and nothing else.** "loam merges requirements, never prose" is not an aversion to prose: a delta's
1123
+ prose is about the CHANGE — the scaffold's own title is `<svc> — requirement delta for <FEAT>` — and
1124
+ a living document that kept it would describe a pull request that merged months ago. A machine
1125
+ cannot tell narrative about the service from narrative about the change, so a new
1126
+ `services/<svc>/spec.md`, `arch.spec.md` or `capabilities/<cap>/spec.md` gets:
1127
+
1128
+ - the delta's frontmatter **`owner`** (service specs; a capability document carries no frontmatter)
1129
+ — the one field that describes the service rather than the change or a vouch;
1130
+ - the delta's **H1**, unless it is about the change — it names the feature, or calls itself a
1131
+ delta — in which case the title is the id (`<svc> — architecture` for an `arch.spec.md`);
1132
+ - the delta's **`## Purpose`** section, verbatim, with its links re-expressed like the
1133
+ requirements' — the explicit marker, and the one OpenSpec's living specs already use;
1134
+ - one sentence linking back to the archived delta — "Created when FEAT-1 was archived; its delta
1135
+ keeps the prose written for that change", with "its delta" the link — so nothing the merge left
1136
+ behind is unreachable.
1137
+
1138
+ The plan prints one note per created document saying which of these it carried and where the rest
1139
+ stays. Only on creation: a living document that already exists keeps its own narrative, edited in
1140
+ place, and a `## Purpose` in a delta against it merges as nothing — the plan says so on that file's
1141
+ line rather than letting it vanish.
408
1142
 
409
1143
  **Frontmatter**: `status`, `owner`, `service` or `feature`, `last_verified`, `vouched_by`, `sources`
410
- (literal files and directories), `sources_digest`, `content_digest`, `sources_files`, `vouch_scope`.
1144
+ (literal files and directories), `sources_digest`, `content_digest`, `sources_files`, `vouch_scope`,
1145
+ `adoption`.
411
1146
  `loam validate` reads it in exactly three files — a service's living `spec.md`, its living
412
1147
  `arch.spec.md` when present (same conventions, same checks, only the label differs), and a feature's
413
1148
  `intent.md`: the documents whose identity and status everything else joins on. `adrs/` and
@@ -456,7 +1191,12 @@ two checked files:
456
1191
  `--all` alike, from the docs repo included. A `verified` spec whose current body hash mismatches
457
1192
  gets `content.stale` (**warning**, the `sources.stale` grading: the doc changed since it was
458
1193
  vouched, and only a person can say whether verified still holds). A verified doc with no
459
- `content_digest` predates the stamp and stays quiet — the fix is re-vouching.
1194
+ `content_digest` predates the stamp and stays quiet — the fix is re-vouching. Every fleet readout
1195
+ carries the same fact, because it is computable from the docs repo: `loam list` shows
1196
+ `vouched (stale)` and owes the row `re-vouch` in `missing` (so `--needs-work` lists it), `loam
1197
+ status` counts it under `services.contentStale` and emits `next.revouch` instead of
1198
+ `next.fleet-clean`, and `validate --all`'s scorecard counts it under `provenance.contentStale`.
1199
+ The rung stays `vouched` — a person did stamp the document — exactly as for `vouch_scope` below.
460
1200
  - `vouch_scope` — written by `loam vouch --sample <n>`, never by hand, and present only when the
461
1201
  person who vouched read a recorded SAMPLE of the document rather than all of it. One flat string,
462
1202
  `sampled <k>/<n> seed=<16 hex>`: k sections read of n, and the seed that chose them. `status`
@@ -472,6 +1212,23 @@ two checked files:
472
1212
  `loam list`, `loam show` and `loam status`, since the sample is per file. A later FULL
473
1213
  `loam vouch` deletes the field, which is the only thing that clears the warning; deleting it by
474
1214
  hand forges a full vouch out of a partial one, and the generated `AGENTS.md` says so.
1215
+ - `adoption: partial` — written by `loam archive`, never by hand, on a service's living `spec.md`
1216
+ only, and only when the archive CREATES it for a service that existed before the feature (its
1217
+ directory is enumerated) and that the feature's own `delta.likec4` does not introduce. What lands
1218
+ then is that feature's slice — the requirements it added — and nothing about what the service
1219
+ already did, so the document is not an adoption, and the decision is to say so where every
1220
+ fleet readout looks rather than to refuse the archive: the plan prints why the file is marked,
1221
+ the maturity ladder keeps the service at `partial` (a spec.md carrying the marker does not count
1222
+ toward `documented`; `loam list`'s `missing` names `loam adopt --service <id>`, so the scorecard
1223
+ and `--needs-work` see it too, and `loam gate --service <id>` refuses the deploy with
1224
+ `gate.service-undocumented` exactly as it did before the archive), and `loam status` keeps
1225
+ `next.adopt` for it — in place of `next.complete-service` — with a sentence that does not claim
1226
+ the file is missing. A service the feature introduces is never marked — its slice
1227
+ is all of it — and neither is a spec.md that already existed. `loam adopt` flags the file `PARTIAL`
1228
+ and counts it as owed (`targets[]` carries `partial: true`), while its action stays `diff` — never
1229
+ replace it: write the baseline from the code around the slice, then **delete the line**, which is
1230
+ the only thing that clears it. Any value other than `partial` reads
1231
+ as absent.
475
1232
  - Missing fields are warnings, and `loam list` reports the fleet's draft/verified/unmarked split.
476
1233
 
477
1234
  **Vouching** (`loam vouch --service <id>`, run in the service's own repo) is the human promotion
@@ -615,7 +1372,21 @@ There is no manifest of services: the directories under `services/` **are** the
615
1372
  why the two are graded differently;
616
1373
  - two elements resolving to the **same** `services/<svc>/` → `landscape.binding-duplicate`
617
1374
  (**warning**): every element→service join picks one of them arbitrarily, so the other's edges are
618
- quietly filed under a service that does not own them.
1375
+ quietly filed under a service that does not own them;
1376
+ - an element resolving to a `services/<svc>/` that no edge in the map touches, while that service's
1377
+ `model.likec4` parses and declares at least one call across its own boundary →
1378
+ `landscape.service-isolated` (**warning**, never gating): the service is drawn and invisible to
1379
+ every cross-service check, and the finding names the attested calls to draw as edges on the
1380
+ existing element, collapsed to the service. Evidence-gated on purpose — a service with no model,
1381
+ an unparseable one (`c4.invalid`), or a model reaching nothing is silent here, because an edgeless
1382
+ element that is TRUE is a fact and an invented edge is a dependency the fleet then plans against;
1383
+ the adopt brief's `landscape.touched` is the only place that silent state is named. And BINARY:
1384
+ one edge closes it, so a map drawing one of five attested calls is silent too — the check is
1385
+ touched/untouched, never a set difference over the attested calls, because loam cannot tell a
1386
+ call the map forgot from one the fleet deliberately does not draw at its own altitude. "Edge in
1387
+ the map" means an edge in the `architecture/` PROJECT — the landscape plus every other `.likec4`
1388
+ under it, minus the generated views files (`subsystems.likec4`, `services.likec4`) — and `loam adopt` reads the same set. An
1389
+ `#external` element is never the subject, whatever directory it is bound to.
619
1390
 
620
1391
  **`architecture/landscape.likec4` is required, not optional.** Its absence is a finding, never a
621
1392
  skipped check: an **error** as soon as `services/` holds at least one service directory, and a
@@ -626,7 +1397,7 @@ it cannot be silence: with no map, the whole cross-service layer — the C4↔AP
626
1397
  `landscape.service-unmodelled`, every derived view — computes nothing and *reports* nothing, which
627
1398
  reads exactly like a clean fleet. `loam init` scaffolds the file (empty, commented, parsing clean)
628
1399
  so a fresh docs repo starts from a real one rather than from that ambiguity, and `loam adopt` briefs
629
- the block each service still owes it.
1400
+ the write each service still owes it — the block, or the edges on an element no edge touches.
630
1401
 
631
1402
  Absences inside a service directory are graded too. A directory with no `spec.md` is a **warning**
632
1403
  (`service.no-spec`) — partial adoption is a supported state, but the checks that file feeds are
@@ -649,7 +1420,11 @@ A missing `openapi.yaml` is graded on what already points into it, under one cod
649
1420
  And `loam validate --service <id>` for a directory that does not exist at all is an **error**
650
1421
  (`service.unknown`) that suggests close ids — never `loam adopt`, which would steer an agent into
651
1422
  adopting the typo. Only a legal id earns that grading: a name that is neither an existing directory
652
- nor a legal service id is refused as `invalid-option` before any path is built from it. A
1423
+ nor a legal service id is refused as `invalid-option` before any path is built from it. A service
1424
+ an active feature introduces is not a typo: its only documents are that feature's `specs/<svc>/`
1425
+ until it archives, so the same error names the feature and `loam validate --feature <FEAT>` instead
1426
+ of a near-miss, and a bare `loam validate` in a repository bound to that service validates the
1427
+ introducing features in its place (`--json` says so in `boundService`). A
653
1428
  `features/<FEAT>/specs/<svc>/` directory addressing a service that neither exists as
654
1429
  `services/<svc>/` nor is introduced by that feature's own tagged `delta.likec4` is the same class of
655
1430
  mistake and gets its own **error**, `delta.service-unknown`, naming close ids: archiving would
@@ -717,9 +1492,64 @@ under any flag — `x-loam-remove` and `x-loam-based-on` are stripped at path le
717
1492
  level alike, on every merge branch. A path item consisting of nothing but a marker publishes no
718
1493
  phantom path. The same feature must carry a `REMOVED` requirement whose `Operations:` line names
719
1494
  that id. Coherence gates an absent path+method, an id mismatch, an unjustified marker, a removed
720
- requirement with no marker, and a new tagged C4 edge consuming the operation being removed.
1495
+ requirement with no marker, a new tagged C4 edge consuming the operation being removed, and a
1496
+ removal anything would still consume after the merge (`openapi.remove-op-consumed`, error,
1497
+ `--approve` overrides — **Who still consumes it** below says what counts). A `REMOVED`
1498
+ requirement owes a marker only while there is a living operation for it to retire: not for an id
1499
+ the service's living contract does not define (a consumer's `REMOVED` requirement naming another
1500
+ service's operation), and not for one this delta already replaces at its address (the overwrite
1501
+ below — no marker could be written there, since the replacement holds the slot). A living
1502
+ contract that does not parse proves neither exemption, so the marker stays owed.
721
1503
  `serviceOperationIds` evaluates the feature as living upserts minus removals, so other same-feature
722
- checks see the post-archive contract. Archive deletes only the matching method, reports removals in
1504
+ checks see the post-archive contract.
1505
+
1506
+ **An overwrite can remove an operation too, and is graded as one.** The merge writes per method and
1507
+ path, so a delta defining ANOTHER operationId where a living operation lives replaces it there — and
1508
+ when the contract the merge would write then defines the living one nowhere, it has left the
1509
+ contract with no marker for any of the checks above to read. Coherence asks the removal's own
1510
+ consumer question of it, on the merged contract: `openapi.remove-op-consumed` (error, `--approve`
1511
+ overrides, exactly as for a marker) while anything would still consume it after the merge. With no
1512
+ marker there is no `REMOVED` requirement either, so the provider's own requirement naming the old id
1513
+ stays unless the feature `MODIFIES` it to name the new operation or `REMOVES` it — and a `REMOVED`
1514
+ one owes no marker here. A new tagged edge onto the replaced operation is `c4-api.op-removing`. The
1515
+ fix is the delta's choice: define the replaced operation at another address in the same delta if it
1516
+ is to stay; for a rename, redraw the living edge and restate the governing requirement. An
1517
+ overwrite of an operation nothing names stays the advisory `api.op-unconsumed` it always was.
1518
+
1519
+ **Who still consumes it — one answer for the gate, `loam diff` and `loam archive`.** The consumers
1520
+ of an operation are read once, the same way for every reader (`core/consumers/`):
1521
+
1522
+ - every edge of the `architecture/` **project** whose `metadata { op }` names it and whose target
1523
+ resolves to the provider — any document the spine grades, never `landscape.likec4` alone (when
1524
+ only a sibling document fails to parse, the map file's own edges still count);
1525
+ - another service's requirement — in `spec.md` **or** `arch.spec.md` — whose `Operations:` line
1526
+ names it, **unless that service's own contract defines the same id** — then the line names that
1527
+ service's own operation: `validate` resolves a spec.md `Operations:` line against its own
1528
+ service's contract, and `loam dependencies` reads a line in either file naming an operation its
1529
+ own service provides as no dependency on anybody (an unreadable or absent contract proves no
1530
+ ownership, and the line counts);
1531
+ - the use-case hops the model attributes to it (a contested hop is a doubt, never a consumer);
1532
+ - the provider's OWN documents still naming it — its `spec.md` requirements, and edges it draws to
1533
+ itself. Not its `arch.spec.md` lines: `validate` grades an `Operations:` line in spec.md only, so
1534
+ one in arch.spec.md strands nothing (another service's does count — it is a dependency on this
1535
+ contract, as `loam dependencies` reads it). The gate counts these, because the merge strands them as surely; `loam diff` and the archive's
1536
+ `openapi.op-modified` warnings answer who ELSE is affected, and leave them out (a service owes no
1537
+ operationId to itself).
1538
+
1539
+ The gate reads all of it **after the feature's merge**: a requirement the same feature `MODIFIES`
1540
+ to drop the id, or `REMOVES`, no longer counts — another service's too, so retiring the consumer in
1541
+ the same feature is a fix the gate sees — and one of its own `ADDED`/`MODIFIED` requirements naming
1542
+ the id does. A delta the merge would refuse to apply fails closed: the living requirements count,
1543
+ plus every one the delta adds or modifies. Edges and hops are read as they stand: the landscape
1544
+ merge only ADDS edges and a feature's flows are create-only, so no feature document can take one
1545
+ away — drop or redraw a living edge by editing the living map in the same change. The refusal lists
1546
+ the consumers in its `details[]`, in the order `loam diff` lists the victims of the same removal.
1547
+ A consumer document nobody could read refuses too, and is named there: a spec file that does not
1548
+ decode (another service's, or the provider's own), or a service id two directories claim — the gate
1549
+ cannot prove such a document does not name what is leaving, so it says it could not check, never
1550
+ that the fleet still consumes it. An edge drawn with no `metadata { op }` and a contested use-case
1551
+ hop are doubts about a drawing: `loam diff` lists them, and they never refuse, since `loam seed`
1552
+ draws every edge without an op. Archive deletes only the matching method, reports removals in
723
1553
  text and `openapiRemovals` JSON, never copies the marker into living OpenAPI, and deliberately does
724
1554
  not garbage-collect components. A removal marker in a living `openapi.yaml` is
725
1555
  `openapi.remove-marker-living` (error). The usual staged-write rollback and `unarchive` snapshot
@@ -731,9 +1561,11 @@ A `paymentService -> kafka 'Publishes PaymentAuthorized'` edge used to read as f
731
1561
  while naming no contract anybody could check. The event axis is the API axis's mirror, in **AsyncAPI
732
1562
  3.0** — only 3.0, because its operations are named top-level objects carrying
733
1563
  `action: send|receive`, the exact analog of an `operationId`, where 2.x spells the same thing as
734
- `publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A 2.x
735
- document declares no `operations` and reads as a contract with no messages rather than as a
736
- mis-parsed one. Like OpenAPI it is read with a shallow structural walk over the `yaml` parse, never
1564
+ `publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A document
1565
+ whose `asyncapi:` field says 1.x or 2.x is refused as one `asyncapi.invalid` naming the version, and
1566
+ nothing on the event axis is graded against it: reading it as a 3.0 contract with no messages used
1567
+ to turn every edge and requirement into a misleading `spine.message-undefined`. The AsyncAPI CLI's
1568
+ `asyncapi convert --target-version=3.0.0` upgrades it. Like OpenAPI it is read with a shallow structural walk over the `yaml` parse, never
737
1569
  a real AsyncAPI parser: the slice loam joins on is small enough that the parser's model would be
738
1570
  dead weight, and adding it would be a fourth runtime dependency.
739
1571
 
@@ -796,9 +1628,14 @@ the edges point at channels it is the channels that collect three consumers each
796
1628
  broker leaves a row of untagged hubs behind it.
797
1629
 
798
1630
  **The feature lifecycle, mirrored from the OpenAPI axis.** A feature changes this contract through
799
- `features/<FEAT>/specs/<svc>/asyncapi.yaml` — a **complete AsyncAPI 3.0 document**, exactly as an
800
- openapi delta is a complete document: the author restates the living contract around the part they
801
- are changing, and the machinery tells restatement from edit. Identity is the **slot** — the pair
1631
+ `features/<FEAT>/specs/<svc>/asyncapi.yaml` — a self-contained AsyncAPI 3.0 document holding **only
1632
+ the slots the feature adds, changes or removes**, exactly as an openapi delta holds only its own
1633
+ operations: the merge walks only the slots the delta declares, so a slot it leaves out stays as the
1634
+ living contract has it, and an edited slot is written out whole. A slot restated without being
1635
+ changed is legal and the machinery tells it from an edit, but it buys nothing and costs a pin,
1636
+ which a parallel feature's removal turns into `asyncapi.baseline-invalid` and `asyncapi.slot-revived`
1637
+ — or, unpinned, into `asyncapi.slot-revived` alone, the contract axis's revival check (under *Living
1638
+ spec vs delta* below) on this axis's identity. Identity is the **slot** — the pair
802
1639
  (section, key) over the three named sections `channels.<key>`, `operations.<key>` and
803
1640
  `components.messages.<key>` — and the join token stays the message name. Three feature-only keys,
804
1641
  spelled and shaped exactly as on the OpenAPI axis: `x-loam-based-on` inside a slot's value (written
@@ -850,11 +1687,50 @@ decision that reverses that stance: the removal family exists, a message-slot re
850
1687
  justified by a `REMOVED` requirement's `Publishes:`/`Consumes:` line (channel and operation slots
851
1688
  need exactness only — requirements join on message names and never on channel or operation keys, so
852
1689
  demanding a justifying requirement there would invent a join the corpus cannot express), and
853
- `asyncapi.remove-message-consumed` gates a removal the living fleet still consumes. The lag argument
1690
+ `asyncapi.remove-message-consumed` gates a removal the fleet would still consume after the merge —
1691
+ a `consumes` edge of the `architecture/` project, another service's `Consumes:` line in either spec
1692
+ file (no ownership rule here: a consumer declares the message in its own contract as `receive` by
1693
+ construction), a use-case hop backed by such an edge — or whose provider's own `Publishes:` line the
1694
+ merge would leave behind, or its own `publishes` edge (the spine grades that edge on its source,
1695
+ `spine.message-undefined`; drop or redraw it in the living map), which covers a message slot re-keyed to another name or a send operation
1696
+ turned around as surely as a marker: read after the merge, as on the HTTP axis, and a `REMOVED`
1697
+ requirement owes no marker for a message the merge already takes out. **A consumer counts only
1698
+ while nothing would still send the message after the merge** — no service, and no `#external`
1699
+ element whose edge publishes it: the join is fleet-global, and a consumer with another producer is
1700
+ not stranded — `spine.message-unproduced` grades a consumer by exactly that, an outside producer
1701
+ included (*The direction is not decoration* above), and `loam diff` reports such a removal as
1702
+ `diff.message-removed`, "still sent by …", naming an outside producer as `<title> (#external)`.
1703
+ The senders are read after the merge too: a service this feature carries an event delta for sends
1704
+ what the merged contract would (the delta itself, where it creates the contract), and vouches for
1705
+ nothing where the merge cannot be simulated, so a feature retiring the message from every sender
1706
+ is still refused; an unreadable contract never vouches. The outside producer is read off the
1707
+ living `architecture/` project with validate's own rule, and only while that project parses. The
1708
+ landscape merge only adds to the map, so no feature takes its edge or tag away — but the rule also
1709
+ asks that no `services/<id>/` directory stand for the element, and the merge creates one from each
1710
+ `specs/<id>/` the feature carries. So the rule is asked over the directories as the merge leaves
1711
+ them: a feature that onboards the outside system as a service (`specs/legacy-gateway/`, its element
1712
+ titled or bound `legacy-gateway`, or a topic nested inside it) takes the producer away, and a
1713
+ retirement in it is refused unless something else still sends the message — the new service's own
1714
+ contract, in its `specs/legacy-gateway/` delta, for one. A `publishes` edge
1715
+ from an `#external` element that only the feature's own C4 delta draws does not count either —
1716
+ draw it in the living map, or archive with `--approve`. Another sender does not excuse the
1717
+ provider's own `Publishes:` lines and `publishes` edges — those are graded against its own
1718
+ contract, whoever else sends the message — nor its own unreadable spec files; another service's
1719
+ unreadable spec file, or an id two directories claim, refuses only while nothing would send. The lag argument
854
1720
  survives as an **operational warning**, not a veto: passing the gate proves the *documents* agree
855
1721
  nobody consumes the message — it does not prove no consumer is still reading the topic with lag, so
856
1722
  sequence the runtime retirement accordingly.
857
1723
 
1724
+ **The same senders answer the consumer this feature ADDS.** A delta `Consumes:` line or tagged
1725
+ `consumes` edge naming a message nothing would send after the merge is `spine.message-unproduced`
1726
+ at feature scope — the error the next `validate --all` would raise against the consumer, raised
1727
+ before the archive lands it. Here an `#external` publisher the feature's own delta draws DOES count:
1728
+ it is an addition, the one thing the landscape merge makes, and the map `validate --all` then reads
1729
+ carries it. The suspension is service scope's: any contract in the fleet that does not read leaves
1730
+ the question unanswered. When only another in-flight feature's delta sends the message, the finding
1731
+ softens to `spec-event.message-pending` (a requirement line) or `c4-event.message-pending` (an
1732
+ edge) — archive that feature first; archived ahead of it, the consumer is unproduced until it lands.
1733
+
858
1734
  **Payload-schema correctness stays out of scope, stated so the boundary reads as a decision rather
859
1735
  than a gap.** loam never validates that a payload schema is correct JSON Schema, correct Avro, or
860
1736
  correct anything: digests are content identity and every check joins on names. A fleet that wants
@@ -914,7 +1790,9 @@ the source for tests.
914
1790
  diff, tagged to the feature.
915
1791
  - **`loam archive <FEAT>`** merges the delta into the living state on three axes — **requirements**
916
1792
  (`spec.md`, and `arch.spec.md` beside it: the two files ride one merge code path, parameterized by
917
- filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4`) — then archives the
1793
+ filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4`, plus the
1794
+ `services/<…>/<svc>/model.likec4` of each service whose EXTENDING model owns an addition nested
1795
+ inside it) — then archives the
918
1796
  feature, so the living state stays complete. Archived deltas are the evolution history (like
919
1797
  `git log`). `--dry-run` prints the whole plan and writes nothing.
920
1798
  - **Coherence gate:** `loam validate --feature` checks the three axes agree (C4 edge `op` ↔ OpenAPI
@@ -933,7 +1811,11 @@ the source for tests.
933
1811
  (`spec-api.op-pending` / `c4-api.op-pending`) naming that feature — archive it first — because the
934
1812
  fix is ordering, not authoring.
935
1813
  - **`loam unarchive <FEAT>`** takes an archive back: it restores the living docs and re-opens the
936
- feature.
1814
+ feature, and names any generated views file the restore left behind the tree (`staleViews`).
1815
+ - **A document the merge CREATES** — a new service's `spec.md` or `arch.spec.md`, a capability's
1816
+ `spec.md` — carries what the delta marked as its own (frontmatter `owner`, a title that is not
1817
+ the change's, the `## Purpose` section) and one line linking back to the archived delta;
1818
+ Conventions, above, has the rule.
937
1819
  - **The requirement baseline.** A MODIFIED requirement carries its full new text rather than a diff,
938
1820
  so the merge REPLACES the living requirement instead of folding into it — and two features
939
1821
  rewriting one requirement destroy the loser's text entirely. `loam validate` names the other
@@ -962,19 +1844,35 @@ the source for tests.
962
1844
  never the resolution itself — a pin says "I read this version", so `loam rebase` reports every
963
1845
  requirement that moved and says plainly that restamping without re-reading merges your text over
964
1846
  theirs. There is deliberately no three-way merge: requirement prose does not merge.
965
- - **The same baseline on the contract axis, where it matters more.** A feature's `openapi.yaml` is a
966
- **complete document, not a patch** — authors restate the living contract around the slot they
967
- change — and the merge upserts every operation the document spells. The collision therefore needs
968
- no overlap between the two features at all: two deltas over one service, editing *different*
969
- operations, destroy each other, because whichever archives second pushes its authoring-time copy
970
- of the operation it never meant to touch back over what landed in between. On a service with
971
- thirty operations a delta quotes twenty-nine of them, and each is a candidate. So every operation
972
- in a feature contract carries **`x-loam-based-on`** — the same 16-hex digest, in the
1847
+ - **The same baseline on the contract axis.** A feature's `openapi.yaml` is a **self-contained
1848
+ document holding only what the feature adds, changes or removes** — not a copy of the living
1849
+ contract. The merge walks only the paths and methods the delta spells: an operation it leaves out
1850
+ stays exactly as the living contract has it, a local `$ref` (`#/components/schemas/Order`)
1851
+ resolves into the living document, and a brand-new operation, path-level key or component has no
1852
+ living version to pin, so an add-only delta never trips `openapi.baseline-missing` and needs no
1853
+ `loam rebase` at all. What the merge does upsert is every operation the document *does* spell,
1854
+ so an operation quoted without being changed — copied for context, or a whole contract pasted in
1855
+ — is a collision waiting for somebody else's edit: two deltas over one service, each quoting the
1856
+ operation the other edits, destroy each other, because whichever archives second pushes its
1857
+ authoring-time copy back over what landed in between. A quote also costs something even pinned:
1858
+ it is one more pin, and a pinned quote whose living operation a parallel feature removes is
1859
+ `openapi.baseline-invalid` — the living slot is gone, so the pin is founded on nothing — an
1860
+ error that blocks the archive until the quote is deleted. Unpinned, the same quote has no living
1861
+ operation behind it, so the baseline axis cannot tell it from an addition — and it used to
1862
+ archive as one, at exit 0, putting the retired operation back. The **revival check** below is
1863
+ what refuses it now. So a delta quotes nothing it does not change, and every operation in it that
1864
+ the living contract already holds carries
1865
+ **`x-loam-based-on`** — the same 16-hex digest, in the
973
1866
  vendor-extension shape this axis already uses for `x-loam-remove`, written by `loam rebase` and
974
1867
  stripped on merge exactly like `Based-On:`. The digest is taken over the operation's *canonical*
975
1868
  form (object keys sorted), which is the same comparison `isDeepStrictEqual` makes when the merge
976
1869
  decides an operation "differs" — so a pin goes stale exactly when the merge would say something
977
- moved, never over a reordered key. The pin is always the digest of the **living** operation, and
1870
+ moved, never over a reordered key. The pin is keyed by the slot, method and path, on both sides,
1871
+ so an operation written with no `operationId` — living or in the delta — is pinned, graded and
1872
+ quoted exactly as a named one: `loam rebase` always pinned against whatever stood at the slot,
1873
+ and the gate once read named operations alone, so it refused the pin rebase had just written as
1874
+ one over "no operation at that slot" and never asked an unnamed quote for a pin. The pin is
1875
+ always the digest of the **living** operation, and
978
1876
  that one rule yields both merge verdicts by itself: an operation the author only quoted is
979
1877
  byte-equal to living, so its pin equals its own content and **the merge skips it entirely**
980
1878
  (reported as `· quotes …` in the plan, so the plan never silently writes less than the delta
@@ -984,7 +1882,8 @@ the source for tests.
984
1882
  decision, reverting an operation nobody edited is a bug. `openapi.baseline-missing` is a warning
985
1883
  that gates the archive, exactly like its requirement-axis sibling — every unpinned restatement is
986
1884
  a candidate rollback — and it is reported **once per service** rather than per operation: the fix
987
- is one command, and twenty-nine identical findings teach people to filter the code out. **Covered
1885
+ is one command, and the twenty-nine identical findings a contract pasted whole from a
1886
+ thirty-operation service would raise teach people to filter the code out. **Covered
988
1887
  since the path-item/component baselines landed:** the same discipline now reaches the two surfaces
989
1888
  the operation pin cannot — path-level keys (`parameters`, `servers`, `summary`, `x-*`) and every
990
1889
  `components/<kind>/<name>` — through a single feature-only root record **`x-loam-baselines`** (`{
@@ -999,6 +1898,93 @@ the source for tests.
999
1898
  component copying is verdict-driven, with genuinely new components copied only when reachable from
1000
1899
  content actually written. Removal markers still carry no pin, since
1001
1900
  `openapi.remove-target-mismatch` already checks that exact slot.
1901
+ - **A retired operation does not come back by accident.** An operation the living contract does
1902
+ not hold is either new or *retired*, and the living contract alone cannot say which: both are
1903
+ simply absent. So the feature gate also reads what every **archived** feature took out — the
1904
+ `x-loam-remove: true` markers left in `features/archive/<FEAT>/specs/<svc>/openapi.yaml` and
1905
+ `asyncapi.yaml`, through the same relocation rule the gate applies to an in-flight delta (a
1906
+ marker on an id the same delta redefines elsewhere moved it, and retired nothing). A delta entry
1907
+ that is not itself a removal, that its service's living contract does not hold, and that an
1908
+ archived feature retired is **`openapi.op-revived`** (by operationId — a
1909
+ retired operation quoted at a new path is the same operation back) or **`asyncapi.slot-revived`**
1910
+ (a `channels` or `operations` slot by its key; a message by its *name*, wherever it is declared).
1911
+ A relocation retires nothing, but it VACATES an address — and so does a retirement, once a later
1912
+ feature re-adds the id somewhere else — so the index records, per id, every address an archived
1913
+ feature's executed marker vacated, which feature, and for a move where its delta put the id. The
1914
+ finding is judged on the contract the archive would WRITE: the check runs the archive's own
1915
+ merge of the delta over the living contract and compares where each operationId stands there with
1916
+ where it stands now. An (operationId, address) the merged contract has and the living one lacks
1917
+ is `openapi.op-revived` when the living contract holds the id nowhere and an archived feature
1918
+ retired it (anywhere — by identity) or moved it away from that very address, or when the merged
1919
+ contract answers the id at two or more addresses and an archived feature retired it at, or moved
1920
+ it away from, that very address: archived, it would answer the id at both. An id that the living
1921
+ contract still holds and the merged contract answers at exactly one address is never a revival —
1922
+ a delta moving it back with its own removal marker where it lives now, or swapping another
1923
+ operation in there (named or not: the merge writes per method and path), leaves it at one
1924
+ address — while a marker standing where the id does not live deletes nothing, and excuses
1925
+ nothing. One address is no exemption for an id the living contract holds nowhere: that is the
1926
+ first case, so a quote of an address a move vacated is the finding even after a hand edit took
1927
+ the id out of where the move put it. Reading the merged contract replaced a
1928
+ case-by-case re-derivation of the merge that each review found a new hole in.
1929
+ On the event axis the same rule runs on the message name, over the event merge: the addresses a
1930
+ feature vacates are `components.messages` keys (a marker there) and the inline declarations a
1931
+ channel removal took. A delta that restates the channel an inline declaration stands in (with
1932
+ the message as a `$ref`) or retires that channel takes the name out, since a channel upsert
1933
+ replaces the channel's interior; a marker nested on the inline message does not
1934
+ (`asyncapi.remove-marker-inline`), so the finding names the channel as the remedy. A channel or
1935
+ operation slot's identity is its key, so moving one is a retirement plus an addition, graded as
1936
+ such. A message declared inline under a channel the delta brings back is that channel's
1937
+ interior, and the channel's finding stands for it — unless the merged contract declares the
1938
+ name somewhere else too (an archived feature re-declared it under `components` when it retired
1939
+ the channel): then the inline declaration is a second place of its own, and gets its own
1940
+ finding, since the channel's says nothing of the duplicate `--approve` would archive.
1941
+ Both are warnings that **gate** the archive, `openapi.baseline-missing`'s doctrine: the document
1942
+ is legal, and re-adding a retired operation can be exactly what a feature means — a retirement
1943
+ rolled back — so `--approve` carries the call, and the archive output then names what came back
1944
+ and whose retirement it reverses. The message offers both remedies: delete the quote if it only
1945
+ restated the operation; archive with `--approve` if the feature brings it back on purpose (a pin
1946
+ over nothing is `openapi.baseline-invalid`, so a deliberate re-add drops it first). At a vacated
1947
+ address beside a living one it names the address to drop instead: delete the quote, or, to move
1948
+ the id back on purpose, take it out of where it lives now in the same delta (a removal marker
1949
+ there; for a message declared inline, restate or retire its channel; for a `components.messages`
1950
+ key, also restate every channel or operation of the merged contract still `$ref`-ing it, with
1951
+ the `$ref` pointed at the quoted key — the message names them, since a marker alone would leave
1952
+ them pointing at nothing and their send would stop sending the message) — and where the delta
1953
+ already spells that address, so no marker can stand there, archive with `--approve`. A slot
1954
+ holds one entry, so where the living contract has ANOTHER operation (or message) at the address
1955
+ the delta spells the id at, the merge replaces it there: the message names it ("now holds
1956
+ 'requestCancellation', and archiving this delta replaces it there"), and the move-back remedy
1957
+ says to define it at another address (another key, for a message) in the same delta if it is
1958
+ to stay — never an address
1959
+ "the living contract no longer has", a remedy that once led to a clean gate and an archive that
1960
+ deleted the other entry unnamed. `loam rebase`
1961
+ never pins such an entry as new: it prints the same finding (`revived[]` in `--json`) and leaves
1962
+ the decision to the author. It asks the question of the delta as it will WRITE it, pins
1963
+ included: a quote pinned to its own content is skipped by the merge, and the same quote
1964
+ repinned to whatever now lives at its address is an edit the merge writes — so a revival
1965
+ rebase's own repin creates is in that list, `--dry-run` included, and the next validate agrees. A channel-slot removal retires the messages its living channel
1966
+ declared inline, which the delta never spells, so those names are read from the archived
1967
+ feature's undo snapshot (`.loam-before/`, the pre-image of the contract the merge ran against) —
1968
+ only while that pre-image still hashes to what the manifest recorded. A marker is a request, not a
1969
+ record of what happened: the OpenAPI merge deletes a living operation only when the one at the
1970
+ marker's own method and path carries the marker's `operationId`, and the event merge deletes a
1971
+ `channels` or `operations` slot only when the living contract has that key, and a
1972
+ `components.messages` slot only when the message declared there carries the marker's name. A
1973
+ marker that missed — archived past `remove-target-missing` or `remove-target-mismatch` under
1974
+ `--approve` — deleted nothing, so each marker counts only if the same pre-image held its target
1975
+ (a contract the archive created, or never wrote, lost nothing to it); a feature with no intact
1976
+ snapshot cannot be checked, and its markers count as written. Two marker
1977
+ shapes are never sources, because neither can match anything: a marker with no `operationId`
1978
+ (`openapi.remove-marker-anonymous`) and one written at path level
1979
+ (`openapi.remove-marker-path-level`) — a feature archived past either under `--approve` left the
1980
+ operation living.
1981
+ **The honest limit:** the check knows only what the files under `features/archive/` record. A
1982
+ removal made by hand-editing a living contract leaves no marker anywhere, and an archived feature
1983
+ deleted from disk takes its markers with it; both read as "never retired", and a quote of such an
1984
+ operation grades as an addition exactly as it did before the check existed. A channel's inline
1985
+ messages drop out the same way when its feature carries no intact pre-image (archived before the
1986
+ snapshot existed, or the snapshot edited or removed since) — the channel slot itself is still
1987
+ caught. No git history is read: the archive directory is the record.
1002
1988
 
1003
1989
  A requirement may carry a stable identity in its body: `Requirement-ID: <id>`, where the ID matches
1004
1990
  `[A-Za-z][A-Za-z0-9._-]{0,127}`. It is optional for backward compatibility. When present in a delta,
@@ -1044,14 +2030,26 @@ scenarios exercise. Comma-separated, same keep-last-line quirk (and the same rep
1044
2030
 
1045
2031
  - a **C4 element** — its id (`paymentService.db`), or the service a bound/titled element stands for;
1046
2032
  resolved against the service's own model plus the landscape (for a feature delta: the feature's
1047
- `delta.likec4`, the landscape, and the service's model);
2033
+ `delta.likec4`, the landscape, the service's model, and the map the merge would leave — and an
2034
+ entry that resolves ONLY through the delta's own ids is `covers.unknown` at feature scope, because
2035
+ the merge lands a stub's children under the living element it binds to, so the delta's spelling
2036
+ is not the map's: write the service name, or the id the merged map gives the element);
1048
2037
  - an **edge** — `paymentService -> kafka`, each side resolved the same way against the declared
1049
- relationships;
2038
+ relationships. Direction counts: `kafka -> paymentService` names a different edge (*Which way an
2039
+ edge points*). The entry may carry the relationship's kind exactly as LikeC4 writes the edge,
2040
+ `paymentService -[async]-> kafka` or `paymentService .async kafka`. loam identifies an edge by
2041
+ its two endpoints and reads no relationship kind, so the kind is read past rather than compared:
2042
+ either kinded spelling covers exactly what `a -> b` covers, every kind of edge from `a` to `b`.
2043
+ (Through 0.2.0-alpha.10 both kinded spellings were always `covers.unknown`: the bracket form
2044
+ split at the `->` after the `]`, and the dotted form was read as one element id.);
1050
2045
  - a **health signal** — `alert:<id>` / `sli:<id>`, ids the service's `health.yaml` declares.
1051
2046
 
1052
2047
  An entry that resolves to nothing is `covers.unknown` (**warning**, with close ids offered where
1053
2048
  they exist): the typo guard, because a mistyped entry silently costs exactly the coverage it was
1054
- written for.
2049
+ written for. It is suspended — every form at once — for a service whose `model.likec4` could not be
2050
+ read, whether the model itself does not parse (`c4.invalid`) or it extends a map that does not
2051
+ (`spine.landscape-invalid`): an entry naming a container of an unread model is not a typo, and the
2052
+ one repair brings the whole grade back on the next run.
1055
2053
 
1056
2054
  **Coverage obligations** — both warnings, deliberately: they never gate `archive`, and `--strict` is
1057
2055
  the CI escalation.
@@ -1059,7 +2057,10 @@ the CI escalation.
1059
2057
  - `c4.uncovered` (feature scope): a NEW tagged element or tagged edge in the feature's
1060
2058
  `delta.likec4` that no requirement across the feature's `arch.spec.md` deltas covers.
1061
2059
  Grouping-only elements follow the landscape checks' exemptions (`person`-kind elements,
1062
- `#external` tags).
2060
+ `#external` tags, and a GROUPING — an element that stands for no service and contains one, such
2061
+ as a new domain `group` drawn around a new service: there is no obligation a group can carry, and
2062
+ the service inside it is graded on its own). The `Covers:` line it offers is the spelling that survives the merge — an edge
2063
+ by its endpoints' service names, an element by its service or the id the merge gives it.
1063
2064
  - `health.uncovered` (service scope): an alert or SLI declared in `services/<svc>/health.yaml` that
1064
2065
  no requirement in the LIVING `arch.spec.md` covers. This is the moment `health.yaml` stops being
1065
2066
  inert — and all loam reads out of it is ids: the recognized keys are top-level `slis:` and
@@ -1072,12 +2073,21 @@ the CI escalation.
1072
2073
  **warning** (`health.invalid`): its ids are unknown, not empty, so `health.uncovered` stays quiet
1073
2074
  AND the `Covers: alert:/sli:` entries of that service go ungraded — the old silence turned every
1074
2075
  such entry into a false `covers.unknown` "typo" pointing at the spec, when the truth was the file.
2076
+ At FEATURE scope the same code names what the merge takes away: a signal the living
2077
+ `arch.spec.md` covers that this feature's arch delta would leave covered by nothing — a MODIFIED
2078
+ requirement restates its whole block, so a `Covers:` entry not carried over is one it deletes.
1075
2079
  - `health.dependency-unmodelled` (service scope): a `dependencies:` id that nothing in the service's
1076
2080
  OWN `model.likec4` answers to — by element id, `metadata { service }` binding, or title; exact
1077
- match, with a did-you-mean hint. Deliberately the service model and never the landscape: the
2081
+ match, with a did-you-mean hint. Deliberately the service model and never the landscape read on
2082
+ its own: the
1078
2083
  landscape carries only what crosses the service boundary, the model carries everything the service
1079
- touches (a private store as a nested container — `landscape.datastore-private` is the check
1080
- pushing it there), and that is the set the on-call file must agree with.
2084
+ touches (a private store nested inside the service's own element — one of the two placements
2085
+ "Private stores" above leaves to the team, and `landscape.datastore-shared` is what a second
2086
+ consumer of that nested store earns), and that is the set the on-call file must agree with. For an extending model
2087
+ the set is its own SLICE: the elements it adds, plus the partners its own edges point at and their
2088
+ ancestors — so a dependency named `kafka` resolves through an edge that names `kafka.orderEvents`,
2089
+ and a peer service resolves through the `metadata { service }` binding on the map's element the
2090
+ model points at. A partner the model never reaches still does not answer, which is the check.
1081
2091
 
1082
2092
  The rest of the machinery treats the axis as what it is — requirements. `loam archive` merges an
1083
2093
  `arch.spec.md` delta into the living one through the same code path as `spec.md` (same delta
@@ -1161,12 +2171,15 @@ digest:
1161
2171
  - stamps: line 1 is `# generated by loam v<version> — …` (the AGENTS.md stamp pattern — the file is
1162
2172
  never to be hand-edited), and each scenario carries the tag `@loam-digest-<16hex>` on the line
1163
2173
  above its `Scenario:` keyword: the first 16 hex characters of `loam verify`'s scenario body hash —
1164
- sha256 of the body lines joined and edge-trimmed, salted with the file name on the arch axis, the
1165
- exact recipe `scenario.tested` claim ids fold in — so the stamp, the claim and the spec can never
1166
- disagree about what a scenario says, and identically-worded scenarios across `spec.md` and
1167
- `arch.spec.md` can never share a digest (a business-axis test run cannot answer for an arch
1168
- scenario). A tag rather than a comment because cucumber's JSON report carries tags per scenario
1169
- (`elements[].tags`): the stamp rides through the runner untouched, which is what
2174
+ sha256 of the body lines joined and edge-trimmed, salted with the owning SERVICE and, on the arch
2175
+ axis, with that axis's file name — the exact recipe `scenario.tested` claim ids fold in — so the
2176
+ stamp, the claim and the spec can never disagree about what a scenario says. Both salts are there
2177
+ for one reason: a digest that spanned two namespaces would let a green run answer a question
2178
+ nobody ran a test for. Identically-worded scenarios across `spec.md` and `arch.spec.md` can never
2179
+ share a digest (a business-axis test run cannot answer for an arch scenario), and neither can two
2180
+ services wording a scenario the same way (one repository's green run cannot answer another
2181
+ repository's claim). A tag rather than a comment because cucumber's JSON report carries tags per
2182
+ scenario (`elements[].tags`): the stamp rides through the runner untouched, which is what
1170
2183
  `loam verify --results` matches on.
1171
2184
 
1172
2185
  **Staleness** (`loam validate --service <id>`, service-repo-scoped like the `sources.*` chain):
@@ -1239,6 +2252,12 @@ requirement also needs a current `Based-On:` pin before archive; `loam rebase <F
1239
2252
  Omitting or leaving the intent as scaffold text raises the gating `intent.empty` /
1240
2253
  `scaffold.placeholder` warning, so the old one-file shape is not archive-ready.
1241
2254
 
2255
+ Below that floor is no feature at all. A change that touches no requirement, no operation and
2256
+ no fleet edge — a refactor, a dependency bump, a fix that brings the code back to behaviour the
2257
+ living spec already documents — leaves the docs repo untouched: it is a code PR, then
2258
+ `loam validate --service <id>` in the service repo, where `sources.stale` names the moved paths, and
2259
+ the re-vouch they owe (`loam vouch --pack` is the reading list; the vouch is a person's).
2260
+
1242
2261
  Each axis may be legitimately empty, and the checks grade absence differently from
1243
2262
  emptiness-by-accident:
1244
2263
 
@@ -1249,11 +2268,17 @@ emptiness-by-accident:
1249
2268
  declared-but-untagged is almost always a forgotten tag, while declaring nothing is a legible
1250
2269
  statement that the architecture is unchanged.
1251
2270
  - **openapi delta** — absent whenever the feature adds no operations; a requirement change that
1252
- stays inside the existing contract has no API axis to speak on.
2271
+ stays inside the existing contract has no API axis to speak on. Present, it holds only the
2272
+ operations the feature adds, changes or removes.
1253
2273
  - **requirement delta** — the axis that is nearly always present, because a feature that changes no
1254
2274
  requirement changes no promised behaviour. The exception is architecture-only work: a new service
1255
2275
  in the C4 delta with no `specs/<svc>/spec.md` gets `service.no-requirement-delta` as a warning,
1256
- not an error.
2276
+ not an error. "New service" is the fleet census's reading of the map the merge leaves: a tagged
2277
+ `softwareSystem` of ours — not `#external`, not a grouping, drawn inside nothing but groupings
2278
+ the delta declares — that no `services/<id>/` answers for and the living map does not already draw. A
2279
+ delta's `#external` producer owes no requirement delta, and neither does the stub of a living
2280
+ element (whose living tags the merge keeps, so a re-declared `#external` system stays external
2281
+ whatever the stub says).
1257
2282
 
1258
2283
  `intent.md` is where the "why" lives — the delta says what changed, and nothing else says what for —
1259
2284
  and the archive gate holds it to that: `intent.empty` (a warning that gates, `--approve`
@@ -1262,6 +2287,16 @@ a feature cannot fold into the living docs with its reason unwritten. Its siblin
1262
2287
  `scaffold.placeholder` refuses the rest of an unedited `loam new` scaffold the same way — the exact
1263
2288
  template strings, a requirement or description nobody authored, must not become living truth.
1264
2289
 
2290
+ The scaffolded `intent.md` also carries an optional `## Open questions` heading over a comment.
2291
+ It is where `loam-feature`'s analyze step — between authoring and `loam rebase` — writes what the
2292
+ documents leave open: a SHALL two engineers could build two ways, a value or window two services
2293
+ state differently and which side refuses, a `Realizes:` that does not deliver its capability, an
2294
+ edge with no stated timeout, retry, partial-failure or compensation answer, a neighbour left out of
2295
+ `--touches`. A person answers by editing the file. Nothing grades the section: `intent.empty` reads
2296
+ past it, so questions written there are never mistaken for the Why, and they never gate the archive —
2297
+ every check loam has is a join between exact tokens, and whether two services MEAN the same thing by
2298
+ one is the part no join reads.
2299
+
1265
2300
  ### Where a capability lives (a declared name, and the document behind it)
1266
2301
 
1267
2302
  A business capability — "payment splitting" — is spread across the living specs of every service
@@ -1428,13 +2463,19 @@ refusing it, which is why the flattening is a whitelist and not a list of substi
1428
2463
  document declares no requirements yet, it declares none flattening to this slug (close ids
1429
2464
  offered, each already spelled as the tag to write), or two of its ids flatten to one slug.
1430
2465
 
1431
- **Either reserved prefix opts a view in.** A view carrying `#req-` and no `#cap-` is an author who
2466
+ **Any reserved prefix opts a view in.** A view carrying `#req-` and no `#cap-` is an author who
1432
2467
  asked to be graded and forgot half the claim; it earns the `unscoped` message rather than silence.
1433
- That opt-in is **one predicate**, and every reader takes it: `loam validate`, `loam diff`,
1434
- `loam delta`, `loam status`, the context and explore packs and `loam list capabilities` all decide
1435
- "is this a use case" the same way, so a flow one of them can see is a flow all of them can see. The
1436
- byte scan that lets a fleet with no use cases skip the LikeC4 load looks for both prefixes for the
1437
- same reason — a gate narrower than the predicate is silence, not a smaller answer.
2468
+ A view carrying only `#suite-<name>` is a use case too (see the layout section above). That opt-in
2469
+ is **one predicate**, and every reader takes it: `loam validate`, `loam diff`, `loam delta`,
2470
+ `loam status`, the context and explore packs, `loam list capabilities` and `loam list suites` all
2471
+ decide "is this a use case" the same way, so a flow one of them can see is a flow all of them can
2472
+ see. The byte scan that lets a fleet with no use cases skip the LikeC4 load looks for every prefix
2473
+ — `cap-`, `req-` and `suite-` — for the same reason: a gate narrower than the predicate is silence,
2474
+ not a smaller answer. It looks for each written as a tag REFERENCE: a `#`, then only what LikeC4
2475
+ skips between the `#` and a tag's name (whitespace, line breaks, comments), then the prefix — the
2476
+ only way a view carries a tag. A prefix in prose, a title or a `tag cap-…` declaration does not open
2477
+ it, so a hand-drawn `.likec4` beside a service's model that names `suite-` in its title, carries no
2478
+ tagged view and does not parse is not loaded and earns no `usecase.flow-invalid`.
1438
2479
 
1439
2480
  Being SEEN is not the same as claiming something, and the difference is worth stating because it
1440
2481
  looks like a gap and is not. A view with no resolved `#cap-` tag claims no capability, so it seeds
@@ -1465,6 +2506,21 @@ turn a mistake into a green fleet. And a run that could not READ the flows (a si
1465
2506
  an `architecture/` that did not parse) suspends `capability.requirement-unrealized` entirely: loam
1466
2507
  did not look, which is never the same answer as "there is nothing there".
1467
2508
 
2509
+ **A `#req-` tag inside a service's own project is scoped by the SERVICE, not by a capability.** A
2510
+ `dynamic view` beside model.likec4 carrying `#req-<slug>` resolves against the `Requirement-ID`s of
2511
+ that service's spec.md and arch.spec.md together — flattened by the same rule, an id both declare
2512
+ resolving once, and `many` when the two documents declare ids that flatten alike, because a
2513
+ Requirement-ID is unique inside ONE document and a flow beside the model has two.
2514
+ `usecase.requirement-unresolved` there has four arms: no document at all, no identified requirement,
2515
+ none flattening to the slug (close ids offered), or two that do. No `#cap-` tag belongs there
2516
+ (`usecase.capability-unresolved` says so, vocabulary or not); `usecase.step-unlinked` never fires
2517
+ on a hop whose caller and provider resolve to the same service — a service owes no operationId to
2518
+ itself — and still fires on a hop from such a flow into another service's element (a stand-in the
2519
+ model declares for a sibling, with no `metadata { op }`), exactly as on the fleet map; and a
2520
+ resolved service-local claim is the join being checked and nothing more today: it
2521
+ enters no capability `keptBy`, counts toward no `Covers:` or `c4.uncovered`, appears in no pack,
2522
+ `loam list capabilities`, `loam diff` or `loam delta`, and no feature carries one yet.
2523
+
1468
2524
  **Both corpora are reported together, and neither is derived from the other.**
1469
2525
  `loam list capabilities --json` carries `keptBy` (the flows keeping a promise) beside `realizedBy`
1470
2526
  (the service requirements realizing it) on every `requirements[]` row, with the same three-state
@@ -1516,9 +2572,11 @@ requirement grammar and delta algebra every service spec already uses —
1516
2572
  nesting spelled by the tree exactly as the living corpus spells it
1517
2573
  (`features/FEAT-9/capabilities/payments/refunds/spec.md`). `loam archive` merges it through the same
1518
2574
  transactional path as every other document and **creates `capabilities/<cap>/spec.md`, and the
1519
- `capabilities/` tree itself, when the feature is the first to name that capability**;
1520
- `loam unarchive` takes it back and removes what the archive created; `loam rebase` pins its
1521
- `MODIFIED`/`REMOVED` requirements.
2575
+ `capabilities/` tree itself, when the feature is the first to name that capability** — titled by
2576
+ the delta's own H1 unless that title is about the change, with the delta's `## Purpose` as its
2577
+ narrative and a line linking back to the archived delta (Conventions, "When archive CREATES a
2578
+ living document"); `loam unarchive` takes it back and removes what the archive created;
2579
+ `loam rebase` pins its `MODIFIED`/`REMOVED` requirements.
1522
2580
 
1523
2581
  **Both lifecycles coexist and neither is deprecated.** A capability document may still be edited
1524
2582
  directly under PR review, like `capabilities.yaml` and the use cases. What changes for a document a
@@ -1533,6 +2591,41 @@ they gate archive. Without that the feature-local delta becomes a second way to
1533
2591
  requirements: a capability requirement carrying `Operations:` would merge into the living tree and
1534
2592
  only then earn its error, against a document whoever reads the finding did not write.
1535
2593
 
2594
+ **A promise a living flow keeps, rewritten, is named.** A MODIFIED keeps the `Requirement-ID` and
2595
+ replaces the text, so a living flow tagged `#req-<id>` still resolves after the archive and every
2596
+ grade of it stays green — while it may draw exactly what the new wording forbids. Nothing in loam
2597
+ reads a flow's meaning, so `usecase.requirement-modified` (**warning**, never gates) names each such
2598
+ flow under `validate --feature` and in the archive's warnings: an `architecture/` flow whose `#cap-`
2599
+ tag scopes its `#req-` tag to a capability requirement this feature modifies, and — at the service
2600
+ altitude, where the directory is the scope — a flow beside a service's model whose `#req-` tag names
2601
+ one of that service's spec.md / arch.spec.md requirements the feature's `specs/` delta modifies. Read
2602
+ the flow against the replacement, and redraw it (or drop the tag) in the same change if it no longer
2603
+ keeps the promise.
2604
+
2605
+ **A promise a living flow keeps, removed, is the fleet's error one archive early.** A REMOVED
2606
+ requirement leaves the flow's `#req-` tag resolving to nothing, so the very next `validate --all`
2607
+ reports `usecase.requirement-unresolved` against the living flow. The same walk, over the same two
2608
+ altitudes and scopes, now raises that code at feature scope — **error**, gating the archive, with the
2609
+ view as its subject exactly as the fleet names it, and a message naming the living flow file and the
2610
+ requirement the feature removes. It is graded against what the merge leaves, not against the REMOVED
2611
+ list: a removal the same delta re-spells under an id that flattens to the same tag keeps the tag
2612
+ resolving and earns nothing; and a service-local tag reads BOTH of that service's documents, skipping
2613
+ a requirement under a `## REMOVED Requirements` heading as `validate --all` does, so an id the
2614
+ document the feature leaves alone still declares keeps it resolving too. The same verdict is asked of
2615
+ a MODIFIED: it keeps its id, but an id the feature adds beside it can flatten to the same tag, and
2616
+ then the flow earns this error (naming the colliding ids) instead of `usecase.requirement-modified`,
2617
+ whose "the tag will still resolve" would be false. Only a tag that resolves today is asked about —
2618
+ one already broken is `validate --all`'s finding now. `--approve`
2619
+ may override it, as it overrides `capability.remove-requirement-realized` for the same retirement
2620
+ seen through a `Realizes:` line: whether to retire a promise something still claims is the author's
2621
+ call, and the living docs then carry exactly the error named. The fix inside the feature is to drop
2622
+ the tag from the living flow (or point it at the promise that replaces this one) in the same change,
2623
+ or to MODIFY the requirement instead of removing it.
2624
+
2625
+ The cost of both is a byte scan for the tag's spelling, the living document of a touched service's
2626
+ other axis, and a LikeC4 load only for a project a document carrying the tag is in; a feature that
2627
+ modifies and removes nothing pays nothing.
2628
+
1536
2629
  **The archive refuses a promise nothing keeps, in both directions.**
1537
2630
 
1538
2631
  - `capability.uncovered` (**warning that gates**, `--approve`-overridable) — the feature ADDS a
@@ -1552,6 +2645,13 @@ only then earn its error, against a document whoever reads the finding did not w
1552
2645
  does not land, or a landscape merge that itself refuses. The flows were not graded, so the archive
1553
2646
  would copy them into `architecture/` for somebody else's `validate --all` to fail on. `--approve`
1554
2647
  overrides loam's judgement about coherence, never its ability to read an axis.
2648
+ - `usecase.step-unbacked`, `usecase.requirement-unresolved`, `usecase.capability-unresolved`
2649
+ (**errors**) and `usecase.step-contested`, `usecase.step-unlinked` (warnings) — the flows that DO
2650
+ read, graded by the very grader `validate --all` runs over a living flow, against the post-merge
2651
+ map and the capability vocabulary and requirement ids the merge would leave (an id this feature's
2652
+ capability delta adds resolves; one it removes does not). Each message opens with the feature
2653
+ file's path. Before these ran here, a hop nothing backs and a `#req-` tag nothing scopes archived
2654
+ clean and turned the next `validate --all` red.
1555
2655
  - `usecase.flow-exists` (**error**, never `--approve`-overridable) — a flow this feature introduces
1556
2656
  that `architecture/usecases/` already holds. The merge is a whole-file copy, so it would replace
1557
2657
  an authored hop sequence wholesale; the same shape and the same severity as `glossary.term-exists`,
@@ -1604,7 +2704,10 @@ ready. Several useful product shapes remain outside that boundary on purpose:
1604
2704
  a third thing to keep truthful.
1605
2705
  - **Authored `tasks.md` as authoritative state** — loam derives its actionable checklist (`loam
1606
2706
  delta`, `loam verify`), so it cannot drift from the delta. Imported task lists are preserved as
1607
- non-authoritative migration evidence until a human chooses their disposition.
2707
+ non-authoritative migration evidence until a human chooses their disposition. The refusal stands,
2708
+ and the derived projection now exists as `loam tasks`, which answers the same question the file
2709
+ would have — recomputed from the delta, the generated suite and the verification record on every
2710
+ run, so there is nothing to tick and nothing to go stale.
1608
2711
  - **Per-tool workflow forks** — `AGENTS.md` is the tool-agnostic contract. Generated adapters wrap
1609
2712
  shared bodies and are never independent process definitions.
1610
2713
 
@@ -1675,7 +2778,7 @@ the merge wrote, and `before`, of the pre-image sitting beside the manifest (nul
1675
2778
  created the file). That directory travels with the feature into `features/archive/`, and
1676
2779
  `loam unarchive` puts it back.
1677
2780
 
1678
- The manifest is **version 3**. Version 2 added `before`: a version-1 snapshot described only what
2781
+ The manifest is **version 4**. Version 2 added `before`: a version-1 snapshot described only what
1679
2782
  archive *wrote*; nothing described the bytes it would *restore*, so a pre-image altered inside the
1680
2783
  archived feature directory — a bad merge, a rebase, a stray editor — was written back verbatim and
1681
2784
  every loam surface certified the result as "the living docs are back to what they said before the
@@ -1688,12 +2791,42 @@ naming the pre-image under `.loam-before/files/`. At restore time the write targ
1688
2791
  the **current services enumeration**: the bytes land wherever the service lives *today*, not
1689
2792
  wherever it lived on the day of the archive, and an id the enumeration no longer answers falls back
1690
2793
  to the literal path (an absent service restores as a create, exactly as before). Entries under
1691
- `architecture/` and `features/` stay literal-path-only — they do not move. Version-2 manifests are
1692
- still read, forever, under their own literal-path rules: they already sit inside archived features,
1693
- which carry them across loam upgrades. Correspondingly, `archive` refuses to replace a
1694
- `.loam-before/` whose pre-images no longer match the living bytes — version 2 or 3 alike, with a
1695
- version-3 claim graded at its service's current directory: the previous archive's writes are still
1696
- in the docs and those files are the only way back.
2794
+ `architecture/` stay literal-path-only — they do not move. Entries for the feature's own documents
2795
+ (the ones the archive relinks for its move, Conventions above) are literal too, recorded where the
2796
+ move leaves them — `features/archive/<FEAT>/…`, which is where `unarchive` restores them before it
2797
+ moves the directory back — so they needed no layout version of their own; the two readers that meet
2798
+ a snapshot before its feature moved, the clobber guard below and the crash repair, look for them
2799
+ across the move. The documents outside the feature whose links into it the archive rewrites are
2800
+ ordinary rows: a service's keyed by `(service, artifact)` like every `services/` entry, the rest — a
2801
+ fleet ADR, a glossary term, another in-flight feature's intent — by their literal path. Version 4 stores each
2802
+ pre-image as **`files/<path>.before`** rather than under its living name. A pre-image kept as
2803
+ `files/architecture/landscape.likec4` was a second copy of the map inside the archived feature, and
2804
+ every tool that reads by extension took it for a document: a feature with its own
2805
+ `likec4.config.json`, or one rendered alone (`likec4 start features/archive/<FEAT>`), loaded the map
2806
+ twice beside the delta and failed on duplicate elements, as did a fleet project that did not
2807
+ exclude `features/`. With the suffix no pre-image is a `.likec4`, `.md` or `.yaml` to anything.
2808
+ Version-2 and version-3 manifests are still read, forever, each under its own rules — version 2 by
2809
+ literal path, version 3 keyed, both with pre-images under their living names: they already sit
2810
+ inside archived features, which carry them across loam upgrades, and a snapshot is restored from
2811
+ where it was written, never migrated. An older loam reads a version-4 snapshot as one it cannot
2812
+ honour (`snapshot-missing` from `unarchive`), which is the refusal it owes a layout it would have to
2813
+ guess at. Correspondingly, `archive` refuses to replace a `.loam-before/` whose pre-images no longer
2814
+ match the living bytes — version 2, 3 or 4 alike, with a keyed claim graded at its service's
2815
+ current directory: the previous archive's writes are still in the docs and those files are the only
2816
+ way back.
2817
+
2818
+ The manifest also carries one key that is not about bytes, added without a version bump because
2819
+ nothing that restores reads it: **`checklist`**, the digest of the checklist a fresh `loam verify`
2820
+ derived for the feature just before the merge — written when the feature carried a readable
2821
+ verification record then, absent otherwise and in every manifest an older loam wrote. It is the
2822
+ question the feature asked as it shipped. Once archived, a record is frozen history and the checklist
2823
+ cannot be re-derived (the merge moved the feature's operations into the living contract, so a
2824
+ derivation after it can only mismatch), so this is the one thing an archived record can still be
2825
+ held to: `loam gate --since` grades the record against it, and a record that answered an older
2826
+ checklist reads `stale`. `unarchive` takes the snapshot away with the rest of it, so a feature
2827
+ archived again gets a fresh capture. `list`, `status` and `verify` keep reading an archived record as
2828
+ frozen history; a manifest with no `checklist` leaves the gate reading it that way too, and saying
2829
+ so.
1697
2830
 
1698
2831
  Some checks run at plan time rather than in the gate, because only the computed merge can see them.
1699
2832
  A **living** requirement sitting outside `## Requirements` refuses the whole archive
@@ -1702,15 +2835,63 @@ requirement would land in the file twice — and `--approve` does not override i
1702
2835
  duplication is mechanical, not a judgment call; re-home the requirement first. A delta that
1703
2836
  redefines a path+method the living OpenAPI already has (`openapi.op-modified`, warning) merges
1704
2837
  anyway, but the plan says so — the overwrite is wholesale, and the living definition it replaces
1705
- deserved an eye. An unparseable `delta.likec4` fails the archive at plan time regardless of
2838
+ deserved an eye. One that defines a different operationId there, leaving a consumed operation
2839
+ defined nowhere, never reaches the plan without `--approve`: the gate refuses it first
2840
+ (`openapi.remove-op-consumed`, the overwrite shape). An unparseable `delta.likec4` fails the archive at plan time regardless of
1706
2841
  `--approve`: the alternative was archiving with the architecture axis silently dropped, which is the
1707
2842
  data loss this tool exists to prevent.
1708
2843
 
2844
+ **What a replacement takes away is named, not just counted.** A MODIFIED requirement carries its
2845
+ full new text and replaces the living one whole, so every living scenario and every join-line entry
2846
+ (`Operations:`, `Covers:`, `Requires:`, `Capability:`, `Realizes:`, `Publishes:`, `Consumes:`) it
2847
+ does not restate is gone after the merge. Under each `← +A ~M -R` line the plan now prints one line
2848
+ per replaced or removed requirement saying what goes — the scenarios by name, the entries verbatim —
2849
+ and `archive --json` carries the same rows as `dropped`
2850
+ (`[{ path, kind, requirement, id?, scenarios, directives: { covers?: […], … } }]`: `path` the living
2851
+ document; `kind` `MODIFIED`, `REMOVED`, or `ADDED` for an ADDED that lands on a living requirement
2852
+ of the same id or name; `requirement` and `id` the LIVING requirement's heading and id, `id` absent
2853
+ when it has none; `directives` keyed by field — `operations`, `covers`, `requires`,
2854
+ `capabilities`, `realizes`, `publishes`, `consumes` — each present only for a line that loses an
2855
+ entry, and a `Realizes:` entry compared by its target so a `@<digest>` pin is never a drop). `loam delta --json` carries the identical rows as its own `dropped` key, and its human view
2856
+ prints them under *Taken out of the living docs by the merge*, so the brief says before the archive
2857
+ what the plan says during it. Read verbatim and by name, deliberately: a renamed scenario or a
2858
+ `Covers:` entry respelled from an element id to the service name is listed, because what is listed
2859
+ is what the living text says and the merged text does not — the author decides whether it meant the
2860
+ same. Never a warning and never a gate: rewriting a requirement smaller is legitimate; what was
2861
+ missing was being told. The one drop that IS graded is the health signal's, as before
2862
+ (`health.uncovered` at feature scope).
2863
+
2864
+ **The contract merges write over the living text, not out of a printer.** Both the OpenAPI and the
2865
+ AsyncAPI merge used to hand the living contract back through the `yaml` library's printer, which
2866
+ re-folds every plain scalar at 80 columns, re-pads every flow sequence (`[a, b]` → `[ a, b ]`) and
2867
+ turns CRLF into LF — so a delta adding one channel rewrote a long `description` three sections away.
2868
+ `lineWidth: 0` is not the fix (measured: it joins every scalar the author folded by hand onto one
2869
+ line). The merge now SPLICES: the living and merged trees are walked block map by block map, every
2870
+ entry whose resolved value is unchanged keeps its bytes, a changed entry is descended into or
2871
+ replaced, a new one is printed after the entry the merge placed it behind, a removed one is cut with
2872
+ the comment lines directly above it, and new lines take the file's own line ending. Only those
2873
+ entries are printed by the library. The spliced text is parsed back and must equal the merged tree
2874
+ value for value; a shape the walk does not understand (a non-scalar key, a key spelled twice, a
2875
+ reorder, an alias it cannot print alone) or a mismatch falls back to the whole-document write, so
2876
+ the splice can narrow a diff and can never change what a contract says. `loam rebase` writes its
2877
+ `x-loam-based-on` pins and `x-loam-baselines` record into a FEATURE's contract the same way, so a
2878
+ rebase changes those lines and no others in the author's file.
2879
+
2880
+ **The delta's `info` is not merged, and the plan says so.** Neither merge carries `info` (nor any
2881
+ root key outside the sections it merges) — a delta is a diff of operations and slots, and its `info`
2882
+ is most often a copy. But a delta that DID move it lost the change at exit 0 with nothing in the
2883
+ plan. Now every `info` key the delta declares with a value different from the living contract's is
2884
+ reported — `openapi.info-not-merged` / `asyncapi.info-not-merged`, a warning naming the keys
2885
+ (`info.version`, `info.description`) — including for a delta whose only change is its `info`, which
2886
+ merges nothing at all. Keys the delta leaves out are not compared. If the `info` should move, edit
2887
+ the living contract in the same change.
2888
+
1709
2889
  The landscape merge **splices authored source, it does not re-serialize**. Each new element and
1710
2890
  relationship is copied into the living landscape byte-for-byte from `delta.likec4` — technology,
1711
2891
  style, icons, links, metadata, nested children, every tag — with exactly one edit: the feature's own
1712
2892
  tag is stripped (the additions are baseline now — the drop above is why unarchive restores bytes),
1713
- and a construct the strip empties goes with it (`x = kind 'y' { #FEAT-1 }` lands as `x = kind 'y'`).
2893
+ together with the `,` or `;` that set it apart in a tag list (`#FEAT-1, #x` lands as `#x`), and a
2894
+ construct the strip empties goes with it (`x = kind 'y' { #FEAT-1 }` lands as `x = kind 'y'`).
1714
2895
  Placement is deterministic and **service-grouped**, and never moves an authored byte — it only
1715
2896
  chooses where the new blocks go. A top-level element whose service the model already draws (the
1716
2897
  `metadata { service }` binding first, the title as fallback) lands immediately after the last
@@ -1725,18 +2906,129 @@ its leading statement (an anchored join sits where its anchor is, wherever its o
1725
2906
  sort, so comparing per statement would make placement depend on which archive ran first). That is
1726
2907
  what makes the scheme **order-independent**: archiving feature A then feature B (touching different
1727
2908
  services) yields the byte-identical landscape to B then A, so concurrent archive PRs usually splice
1728
- different lines and git merges them without help. The insertions carry no marker comment — which
2909
+ different lines and git merges them without help. The relationship walk sorts **only within the
2910
+ region's ascending tail** — the run of cluster heads that already reads in key order from the end,
2911
+ which loam's own insertions keep that way — and stops where the existing order descends. A
2912
+ hand-written relationship region is sorted by nobody, and walking into it made the landing spot a
2913
+ function of unrelated edges: turning one hand-drawn edge round (`svc -> broker` to
2914
+ `broker -[delivers]-> svc`) changed the head key the walk compared and moved an addition sixty lines
2915
+ away. On a region loam wrote the bound changes nothing, and it keeps the scheme order-independent:
2916
+ the statements before the tail never change, so every archive inserts into the same sorted run. (The
2917
+ element walk keeps its plain insertion sort over the few top-level elements, as before.) Wherever a
2918
+ walk stops **before** a statement, or at the closing brace, the addition lands **above the comment
2919
+ written directly over it** — a comment is about what follows it, or heads the section it opens, and
2920
+ an addition spliced at the statement's own line sat between a six-line explanation and the edge it
2921
+ explained. "Directly" is literal: a blank line ends the comment run, so a comment separated from
2922
+ the statement below it by a blank line stands alone and stays where it is, and the tail of a block
2923
+ comment opened on an earlier line is never split. The same rule at both stopping points is what
2924
+ keeps two additions landing in one gap order-free. The insertions carry no marker comment — which
1729
2925
  archive added what is the git history's to say. A **nested** element lands inside its parent's block
1730
2926
  — inside the living parent when the landscape already has it, riding verbatim inside the spliced
1731
- parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level. LikeC4
2927
+ parent when the parent is new — never as a flat dotted id, which LikeC4 rejects at top level. A
2928
+ landscape may be written in **several top-level `model { }` blocks**, which LikeC4 reads as one
2929
+ model, and the merge reads all of them: an ANCHORED addition — nested in a parent, joining its
2930
+ service's neighborhood, or joining its source's edges — lands in its anchor's block, and an
2931
+ unanchored one lands in the trailing region of the **first** block, the one every addition went to
2932
+ before a second block was readable. The walks never cross a block boundary, so the scheme stays
2933
+ order-independent, and a one-block map places exactly as before. LikeC4
1732
2934
  reference resolution is not order-dependent, so a relationship placed before an element it names
1733
2935
  still parses — and because splicing is text surgery, the computed landscape is **parsed in memory
1734
2936
  before anything is written**: a result LikeC4 rejects — a splice bug, or legal inputs the living
1735
2937
  document cannot absorb, like an element kind or tag its `specification` block never declares —
1736
2938
  refuses the archive at plan time (`merge-failed`, nothing written), the same discipline as the
1737
- unparseable delta. A living declaration sharing its line with another statement (legal LikeC4 the
1738
- splice map cannot see around — its statement scan runs to the end of the line) refuses the same way:
1739
- give each top-level declaration its own line, then re-run.
2939
+ unparseable delta. A living declaration the splice map cannot locate refuses the same way rather
2940
+ than be spliced around blind — most often one sharing its line with another statement, which is
2941
+ legal LikeC4 the splice map cannot see around, since it reads one statement per line. The refusal
2942
+ names the line LikeC4 parsed the declaration on and what the splice map read there instead, and
2943
+ says "shares that line" only when the line shows it: give each top-level declaration its own line,
2944
+ then re-run.
2945
+
2946
+ **Which document an addition lands in.** The C4 merge asks it per declaration, not per feature. An
2947
+ element whose PARENT resolves to a service whose `model.likec4` extends the map is spliced into that
2948
+ model — and so is the interior a delta writes inside a service element the same archive introduces,
2949
+ since those containers carry no feature tag and would otherwise ride onto the map inside their
2950
+ parent's bytes. The parent chain has to be spelled in the map's own ids for either to happen. The
2951
+ splice goes inside the `extend <fqn> { … }` block for the parent — in whichever `model { }` block
2952
+ holds it, and appended to the first one when the model has none, as
2953
+ the `model { }` block itself is appended to a model that has none (an empty file, a `views { }`-only
2954
+ file and a tags-only `specification { }` are all extending models, and all three are legal to
2955
+ archive into). A
2956
+ relationship goes to the model of the service owning its SOURCE when the source is interior to such
2957
+ a service, else to the model owning its TARGET, else to the map: a service-level edge stays where
2958
+ every fleet check reads it. A service with a STANDALONE model, or none yet, owns nothing — the map
2959
+ draws its containers, which is legal and is what every repository did before the extending shape.
2960
+ The merged model is proved the way the grader reads it (the `architecture/` documents with this
2961
+ archive's own merged landscape in place, plus the model) before anything is written, and it appears
2962
+ in the `--json` plan as an ordinary write, so `loam unarchive` restores it from the same
2963
+ `.loam-before` snapshot as any other overwritten file. Two limits are deliberate and each refuses at
2964
+ plan time with nothing written: a merged model that does not parse beside the map (`merge-failed`),
2965
+ and a container-level edge whose other end lives in a THIRD document — another service's interior
2966
+ resolves in neither this model nor the map, so that call is drawn service to service on the map. One
2967
+ more is silent: a delta that spells the parent under its own name rather than the map's (the legacy
2968
+ title-join) keeps landing on the map, because splicing its bytes into a document that shares the
2969
+ map's id namespace would carry ids nothing there resolves. It is the WHOLE chain that decides, not
2970
+ the outermost box — `marketplace.orderSvc.cache` under a map that spells the service
2971
+ `marketplace.orderService` is a title join and stays on the map, even though the group
2972
+ `marketplace` is the map's own. A block a delta writes for a NEW service is split — the box to the
2973
+ map, its containers to the model — unless the block also draws a relationship between those
2974
+ containers. Such an edge names its endpoints by their local names inside the block, so splitting it
2975
+ would leave the map naming children it no longer declares; the block rides whole instead, as it did
2976
+ before routing existed. The block decides once for everything written inside it: a container or an
2977
+ edge the delta tags with the feature follows its block rather than its own tag, so it is placed
2978
+ exactly once whichever way the block goes. Re-archiving is idempotent in the models
2979
+ too: an addition whose id, or whose title inside the same bound service, an extending model already
2980
+ declares is skipped exactly as one the map declares is. The title half is service-scoped in a model
2981
+ and global on the map — the map's global join is the fallback for documents written before
2982
+ `metadata { service }` existed, and a service's interior is where two boxes called `api` would
2983
+ collide.
2984
+
2985
+ **A service the archive creates gets its model, empty.** A service the feature carries
2986
+ `specs/<svc>/` for and the fleet does not enumerate is one whose directory the archive creates; when
2987
+ the map the merge leaves also DRAWS it — the living map, or an element the delta adds, bound to it or
2988
+ titled with its name; a context element the delta only quotes is merged nowhere — the archive writes its
2989
+ `model.likec4` beside its first documents: a header comment and an empty
2990
+ `model { }`, extending the map, with whatever the delta draws inside the service routed into it
2991
+ like any extending model's interior (FEAT-101's `api` container lands there, not on the map). It is
2992
+ a `create` in the plan, so `loam unarchive` deletes it. Two alternatives were rejected. Leaving the
2993
+ file out was the old behaviour: `service.no-model` on the next `validate --all` after an archive
2994
+ that exited 0, and the new service's containers riding onto the map because no model existed to own
2995
+ them. Writing a container model would be inventing one. An EMPTY extending model invents nothing —
2996
+ LikeC4 reads it as the map's box with nothing drawn inside, and an empty file is already a legal
2997
+ extending model — while it lets the service's first `validate --all` grade it rather than stop at a
2998
+ missing file. A team that pre-placed an empty extending model got exactly this result; nobody has to
2999
+ know to now. The feature gate's merge preview routes the same way, so a feature flow hopping into
3000
+ the new service's container is `usecase.flow-invalid` before the archive, as it would be
3001
+ `landscape.invalid` after it — and a feature deployment document naming it
3002
+ `deployment.doc-invalid`. A service whose directory already exists is enumerated and left alone.
3003
+ `service.no-model` is still owed — at error severity, never gating — in two shapes: a service the
3004
+ map gains with no `specs/<svc>/` behind it (there is no directory, `landscape.binding-unknown`
3005
+ follows, and a model would be the one document in a directory the archive invented), and a
3006
+ `specs/<svc>/` the merged map never draws, which the gate already calls an unknown service and only
3007
+ `--approve` brings to the merge — a model there would fabricate a service out of what is usually a
3008
+ typo, which is also why the spec merge creates no empty living spec.
3009
+
3010
+ **Where it lands is the tree's to say, never the map's.** The directory is always the unfiled
3011
+ `services/<svc>/`. The archive does not read a subsystem off the delta's group nesting — the map's
3012
+ grouping and the tree are independent (Subsystems, above), and a map edit must never decide a
3013
+ directory — and it takes no `metadata { subsystem }` hint either: that key would be spliced into the
3014
+ authored landscape, writing tree placement into the one document the tree is never mirrored into.
3015
+ The tree is placement's only input. To file a new service at archive time, create its directory
3016
+ first — an empty `model.likec4` at `services/<subsystem>/<svc>/` does it, and the archive then writes
3017
+ the specs there and routes the interior into that model — or move it afterwards with
3018
+ `loam subsystem move <svc> --into <subsystem>`, which the archive prints when the tree has a
3019
+ subsystem.
3020
+
3021
+ **What the splice reads off kinds.** An extending model declares no kinds, so the archive scans it
3022
+ with the map's: without them its `kind name` declarations and its `a .kind b` edges read as property
3023
+ lines, an edge routed into the model landed above a dotted edge, a delta restating a dotted edge
3024
+ added a second copy, and a child of a kind-first container was refused as nesting under nothing.
3025
+ Kinds declared in an `architecture/` document other than the landscape are not borrowed — the
3026
+ landscape is the extending shape's one `specification`. And a relationship kind may carry tags
3027
+ (`relationship publishes { #async }`, LikeC4 1.59.0+): the parse hands every edge of the kind those
3028
+ tags, the delta's statement shows only what is written in it, so the splice matches a parsed addition
3029
+ to its statement with the kind's tags added to the written ones. Matched as written, every edge of a
3030
+ tagged kind found no statement — `merge-failed` from an archive `validate --feature` had called ready,
3031
+ or `usecase.flow-invalid` when the feature had flows.
1740
3032
 
1741
3033
  The OpenAPI merge carries components along with operations, and since the path-item/component
1742
3034
  baselines it decides each one **by verdict rather than by reachability**: a component the feature
@@ -1753,6 +3045,14 @@ the written content and copied components that resolves in **neither** document
1753
3045
  overrides it — unlike the mechanical refusals, this one is a judgment call. External references —
1754
3046
  URLs, file paths, anything not starting `#/` — are out of scope: left untouched, never gated.
1755
3047
 
3048
+ A service with no living contract gets the feature's `openapi.yaml`, stripped of removal markers —
3049
+ unless that document describes nothing: no path, no webhook and no component in any section. Then no
3050
+ living `openapi.yaml` is created and the plan says `nothing to merge`, the spec axis's rule for a
3051
+ delta that leaves no requirements. A `paths: {}` stub published as a living contract says "this
3052
+ service exposes nothing" to every consumer that joins against it; with no file the service reads as
3053
+ one with no HTTP surface, which `service.no-openapi` already grades on evidence — silent while
3054
+ nothing calls it, an error once something does.
3055
+
1756
3056
  `loam archive --json` speaks the same machine contract as everything else: on success (or
1757
3057
  `--dry-run`) the plan (`{path, action, to?}` per file), the non-blocking warnings, and whatever
1758
3058
  `--approve` overrode; on refusal a stable `error.code` — `not-coherent` and
@@ -1779,6 +3079,16 @@ lists the files that need a hand.
1779
3079
  - `commit-interrupted` — a previous `archive`/`unarchive` was killed mid-commit and this run cannot
1780
3080
  repair it on its own.
1781
3081
 
3082
+ What `unarchive` restores is what the archive WROTE, and the two generated views files were never
3083
+ among it: `loam subsystem sync` owns them, and a sync run between the archive and the undo rewrote
3084
+ them against the merged map. So after the restore `unarchive` asks the question archive asks after
3085
+ its merge — through `validate`'s own staleness grade — and when `architecture/subsystems.likec4` or
3086
+ `architecture/services.likec4` no longer matches the restored tree and map it names each one
3087
+ (`⚠ <path> no longer matches the tree and this map — run \`loam subsystem sync\``) instead of
3088
+ closing on "the living docs are back to what they said before the archive", and `--json` carries
3089
+ them as `staleViews` — the same key, with the same meaning, as `archive --json`'s; absent when
3090
+ nothing is stale.
3091
+
1782
3092
  Rules (`loam validate`): every requirement has ≥1 scenario; every C4 edge `op` resolves to an
1783
3093
  OpenAPI operation governed by a requirement; and **the diff applies** — a `MODIFIED`/`REMOVED`
1784
3094
  requirement exists in the living spec, an `ADDED` one does not, and a section heading matches
@@ -1808,12 +3118,14 @@ design. The heading must be reachable, too — a leading UTF-8 BOM used to hide
1808
3118
 
1809
3119
  ## The verification record
1810
3120
 
1811
- `features/<FEAT>/verification.yaml` is the done-check written down. `loam verify <FEAT>` derives a
3121
+ The done-check is written down inside the feature: one `features/<FEAT>/verification/<svc>.yaml` per
3122
+ attesting service, or — for the all-at-once form, and every record written before the directory —
3123
+ the one file `features/<FEAT>/verification.yaml`. `loam verify <FEAT>` derives a
1812
3124
  **checklist** from the feature's own artifacts — the same files `validate` reads, never the code:
1813
3125
 
1814
3126
  | kind | one claim per | reads |
1815
3127
  |---|---|---|
1816
- | `service.exists` | tagged top-level element the delta introduces | `delta.likec4` |
3128
+ | `service.exists` | tagged element the delta draws as a service of ours: at top level or only inside groupings the delta declares (elements that stand for no service and contain one — any other level owns what is drawn inside it), and not a person, `#external`, unbound infrastructure or a grouping itself. The stub of a living element is read with the living element's tags, and the stub of a service that already has a `services/<id>/` is asked about only when tagged at top level | `delta.likec4` |
1817
3129
  | `api.exposes` | operationId the feature's openapi delta adds that the living one lacks | `specs/<svc>/openapi.yaml` |
1818
3130
  | `event.declares` | (direction, message) the feature's asyncapi delta declares that the living contract's send/receive sets lack — quotes and removals are never questions | `specs/<svc>/asyncapi.yaml` |
1819
3131
  | `c4.calls` | tagged edge carrying `metadata { op }` | `delta.likec4` |
@@ -1832,18 +3144,26 @@ nobody wrote must not carry over as if it still applied.
1832
3144
  anything that does not answer the *current* checklist, each under its own `--json` `error.code`:
1833
3145
  `answers-unreadable` (not JSON, or a verdict outside `confirmed` / `unconfirmed`),
1834
3146
  `answers-mismatch` (an id nobody asked about, a claim with no answer, or one answered twice),
1835
- `answers-unevidenced` (a `confirmed` with no `file:line` behind it). An unchecked claim must never
1836
- be able to masquerade as checked.
3147
+ `answers-unevidenced` (a `confirmed` with no `file:line` behind it, or one citing a file or a line
3148
+ that does not exist in the repository the record is written from — the all-at-once form included,
3149
+ which binds to no commit but no longer takes a citation on trust). The all-at-once form checks what
3150
+ is shaped like a citation and only that: an evidence entry that is one token ending in `:<digits>`
3151
+ and is not a URL must name a regular file inside the repository with that line, while prose ("manual
3152
+ QA by Alice"), a URL or a path with no line is recorded as it always was. The federated form keeps
3153
+ its stricter contract — every agent entry a `file:line` bound to the attested commit. An unchecked
3154
+ claim must never be able to masquerade as checked.
1837
3155
 
1838
3156
  **`--results <report.json>` answers the `scenario.tested` claims mechanically, from a cucumber JSON
1839
- test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave and SpecFlow
1840
- speak too. The contract is exactly this and nothing more: a top-level array of features, each
3157
+ test report** — the format `cucumber-js --format json` emits, and cucumber-jvm, behave,
3158
+ SpecFlow/Reqnroll, pytest-bdd (`--cucumberjson`) and godog's cucumber formatter speak too. The contract is exactly this and nothing more: a top-level array of features, each
1841
3159
  `elements[]` (scenarios) with `name`, `tags[] {name}` and `steps[] {result {status}}` — plus the two
1842
3160
  places those dialects put a failure the steps never see: cucumber-jvm's per-element
1843
3161
  `before[]`/`after[]` hook results and behave's element-level `status`. Every other field is ignored,
1844
3162
  tagless elements (backgrounds, hand-written scenarios) are invisible, and a file that is not that
1845
3163
  array at all refuses under `answers-unreadable` rather than quietly answering every claim "not
1846
- found".
3164
+ found". A file that is not JSON at all — a JUnit XML report is the usual one — refuses the same way,
3165
+ and the refusal names both shapes `--results` reads, because the second one exists for exactly that
3166
+ runner.
1847
3167
 
1848
3168
  Matching is by digest and nothing else: a claim is answered by the report scenarios carrying its
1849
3169
  `@loam-digest-<16hex>` tag — the tag `loam gherkin` stamped, the same 16 hex of the same body hash
@@ -1852,12 +3172,21 @@ name), so a business run can never answer an arch claim however identically the
1852
3172
  worded. Names never match anything: a reworded spec scenario matches nothing until the suite is
1853
3173
  regenerated and re-run — an agent must not be able to SAY a scenario is tested; only a green run
1854
3174
  may. The verdicts: every matching occurrence (a digest the report holds twice is a re-run, and all
1855
- occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`, and
1856
- the element-level status (when the dialect carries one) is `passed` → `confirmed`, with evidence
3175
+ occurrences count) ran at least one step, every step `passed`, every before/after hook `passed`, the
3176
+ element-level status (when the dialect carries one) is `passed`, and **it ran exactly as many steps
3177
+ as the stamped scenario has** — the count `loam gherkin` emits for it, `And`/`But` included →
3178
+ `confirmed`, with evidence
1857
3179
  `<report-path>: <feature uri or name> › <scenario name>` per occurrence; a failed / undefined /
1858
3180
  pending / ambiguous step (`failed at step N`), a failing hook (`failed in a before/after hook`), a
1859
3181
  failed element status, a skipped-only run (`skipped`), or no match at all (`not found in report`) →
1860
- `unconfirmed` with the reason as the note.
3182
+ `unconfirmed` with the reason as the note — and a run whose step count differs from the stamped
3183
+ scenario's is `unconfirmed` too (`ran N step(s) where the stamped scenario has M`). The digest is
3184
+ only a tag, and a tag is text: a one-step stub carrying the right digest used to confirm five-step
3185
+ scenarios, because nothing compared what ran with what was stamped. The count is what every dialect
3186
+ can be held to; the step TEXT is not compared, because an outline's steps reach the report with
3187
+ their placeholders filled in and dialects disagree about keyword spacing and localisation. The hook
3188
+ entries cucumber-js lists among `steps[]` (marked `hidden`, keyword `Before`/`After`) are read as
3189
+ hooks, never counted as steps — and a failing one still sinks the run.
1861
3190
 
1862
3191
  `--results` OWNS every `scenario.tested` claim **in the same run**: an answers-file entry for one is
1863
3192
  refused (`answers-mismatch` — the runner owns it), `--record` alongside must answer exactly the
@@ -1889,6 +3218,8 @@ own runner-neutral shape, chosen by its marker key:
1889
3218
  ```
1890
3219
 
1891
3220
  `digest` is the 16 lowercase hex of the `@loam-digest-…` tag `loam gherkin` stamped on the scenario.
3221
+ An entry carries no steps, so it is held to its `status` alone — the step-count check above is a
3222
+ property of the cucumber shape.
1892
3223
  `status` is `passed` or `failed` — nothing else. `test` is optional free text: the runner's own name
1893
3224
  for the run, used as the evidence string a reader follows. `loamScenarioReport: 1` is the format
1894
3225
  version this loam reads; any other value refuses rather than being partly understood.
@@ -1914,12 +3245,18 @@ carrying the marker is always graded as this shape, even when malformed: falling
1914
3245
  cucumber parser would tell an author their report is not a cucumber array, which is true, useless,
1915
3246
  and hides the real mistake.
1916
3247
 
1917
- **Two services wording a scenario identically share one digest**, and a single report then cannot
1918
- say whose suite ran it. Those claims are left `unconfirmed` with `verify.digest-contested` (warn)
1919
- naming both services and the affected claim ids, rather than confirming both from one run. Record
1920
- each service's claims from its own repository with `--service`, which narrows the checklist before
1921
- matching. Scenario digests are salted by *axis* (business versus arch) but **not** by service, so a
1922
- digest identifies scenario words; `--service` is what gives a report an owner.
3248
+ **Scenario digests are salted by both the owning service and the axis** (business versus arch), so
3249
+ two services wording a scenario identically do not share one: the hash folds in the service the
3250
+ claim is filed under. The service salt is what makes ordinary repeated wording CORRECT rather than
3251
+ merely refused — "the service returns 404 for an unknown id" is a sentence a fleet writes many
3252
+ times, and before the salt those scenarios shared a digest, so one repository's green run could
3253
+ appear to confirm claims from suites that never ran each other's tests, and the only defence was to
3254
+ decline to answer. `verify.digest-contested` (warn) survives as the guard on that invariant rather
3255
+ than as a diagnosis of ordinary authoring: if a digest is ever claimed by two services it is a
3256
+ truncated-hash collision, a single report still cannot say whose suite ran it, and those claims are
3257
+ left `unconfirmed` naming both services and the affected claim ids rather than confirmed from one
3258
+ run. Record each service's claims from its own repository with `--service`, which narrows the
3259
+ checklist before matching.
1923
3260
 
1924
3261
  ### Contract-test reports answer the `api.exposes` claims
1925
3262
 
@@ -2013,7 +3350,7 @@ service's as a pure lens — no repository binding, usable from the docs repo, r
2013
3350
  `unknown-service` when the checklist has no claims owned by that id — and both agents must have
2014
3351
  answered the same scope. Combining the flag with `--record`, `--results` or `--contract-results`
2015
3352
  refuses `invalid-option`, as does any arity other than exactly two files and an archived feature
2016
- (frozen history has no current checklist to answer). A `verification.yaml` too damaged to read does
3353
+ (frozen history has no current checklist to answer). A verification record too damaged to read does
2017
3354
  **not** block the diff — the lens never reads the record, and the moment a cross-examination is most
2018
3355
  needed is before re-recording over a broken one.
2019
3356
 
@@ -2033,7 +3370,7 @@ block pins each file's path and the sha256 of the exact bytes read.
2033
3370
  confirmed/unconfirmed/unanswered. The payload also deliberately carries **no `verified`, `verdict`
2034
3371
  or `attested` key at all**: two answer sets agreeing — usually two contexts of the same model, so
2035
3372
  correlated — ranks where a reviewer reads first; it is not verification, it upgrades nothing, and it
2036
- leaves `verification.yaml` byte-identical (no docs lock is taken, and the diff runs while a writer
3373
+ leaves the verification record byte-identical (no docs lock is taken, and the diff runs while a writer
2037
3374
  holds one).
2038
3375
 
2039
3376
  ### Attested is not verified
@@ -2068,28 +3405,79 @@ the decision:
2068
3405
  So "a green run is the only thing that confirms a scenario" is a claim about the **record**, not
2069
3406
  about the CLI's flags: the short path still works, and it is legible afterwards as the short path.
2070
3407
 
2071
- ### The record is federated: one file, one attestation per service
3408
+ ### The record is federated: one attestation per service, one file each
2072
3409
 
2073
3410
  A cross-service feature's claims belong to several services, and each service's code lives in its
2074
3411
  own repository — so no single place can honestly answer all of them.
2075
3412
  `loam verify <FEAT> --service <id> --record answers.json`, run **in that service's own repo**,
2076
3413
  narrows the checklist to that service's claims and binds the answers to this repository's commit.
2077
- The result is one `schema: 2` `verification.yaml` holding a list of **`attestations`**, one per
2078
- service: the service id, the commit the answers were bound to, and that service's answered claims.
2079
-
2080
- The merge semantics are additive and non-destructive. Recording for service B keeps service A's
2081
- attestation exactly as it stood — the new record is the old attestations minus B's previous one,
2082
- plus B's new one. Answers the new record does *not* carry are reported in `discarded`, tagged
2083
- `unattested` (a schema-1 answer, or one whose attestation is gone) or `off-checklist` (a claim the
2084
- current checklist no longer asks). Nothing is dropped silently.
2085
-
2086
- The write behind that merge holds the same advisory lock archive does, and holds it from before the
2087
- feature is resolved to after the record lands — two `--record` runs for different services serialize
2088
- (the later one waits briefly, then merges over what the first left, so **both** attestations
2089
- survive), and a record can no longer be written into a path an `archive` is renaming out from under
2090
- it. The commit itself is staged beside the target and swapped in by one rename over the exact bytes
2091
- the merge was computed from, so a kill mid-record leaves the previous record whole — never truncated
2092
- YAML — and an edit that landed in between refuses instead of being buried:
3414
+ The result is that service's **attestation**, written to a file of its own:
3415
+ `features/<FEAT>/verification/<id>.yaml` — a `schema: 2` record holding exactly that service's
3416
+ attestation (the service id, the commit the answers were bound to, the checklist digest it answered,
3417
+ and its answered claims) and nothing of anybody else's. **The directory is the record**, the rule
3418
+ `glossary/` and `capabilities/` already follow: loam reads every file in it and adds them up, and
3419
+ the verdict and the counts are computed from the union at read time — exactly as they always were,
3420
+ since nothing ever trusted a stored `summary`. Each file's own `summary` counts the whole checklist,
3421
+ with the claims other services own honestly `unanswered` in it.
3422
+
3423
+ **Why one file per service.** The record used to be ONE `verification.yaml` for every service, and
3424
+ every attestation rewrote its `summary`, its `recorded` date and its checklist-ordered `claims`. Two
3425
+ teams recording two different services on two docs-repo branches therefore always conflicted when
3426
+ the PRs merged — add/add from an empty start, content conflicts on the summary, the claims and the
3427
+ attestations once one had landed — and sorting the attestations did not help, because two
3428
+ insertions at one anchor are a conflict whatever the order. The advisory lock below serializes
3429
+ writers on one checkout only; a fleet records from many. With a file per service, two teams' records
3430
+ touch two different paths and merge cleanly. Recording a service again replaces that service's file
3431
+ and nothing else; another service's file is never rewritten, and its pins never touched.
3432
+
3433
+ The record answers **one version of the checklist**: the one the feature asks now — `loam verify`
3434
+ (read and record alike), `tasks`, and `status`, `list` and `gate` for an in-flight feature all
3435
+ derive it, and the files that answered it are the ones that count. Only where no reader knows the
3436
+ current version (the frozen view of an archived feature) or no file answered it does the newest
3437
+ file decide (by `recorded`, a later service id breaking a same-day tie). Newest-by-date used to
3438
+ decide always, and `recorded` is a date: a service that recorded the current checklist lost a
3439
+ same-day tie to a service with a later id that had recorded an older one, so the current answers
3440
+ were dropped and every reader called the record stale. A file that answered a different version
3441
+ keeps its attestation in the list — `verify.checklist-forked` names it, on the recording run's
3442
+ output as well as the read view — but its answers do not count until that service records again.
3443
+ The one shared file used to settle this by rewriting: the last writer pruned everybody's answers to
3444
+ its own checklist. Per-service files are never rewritten by anyone else, so nothing can prune them,
3445
+ and counting an answer filed against another question set would let an id the feature no longer
3446
+ asks fill the slot of the question that replaced it.
3447
+
3448
+ **`features/<FEAT>/verification/` is loam's.** Every non-dot `*.yaml` in it is read as one
3449
+ service's attestation file — the file name is the claim about whose word it holds — so a file there
3450
+ that is not one (a hand-made `k6.yaml`, say) makes the record `record-unreadable`, exactly like a
3451
+ damaged attestation. Keep anything else beside the feature, not inside that directory.
3452
+
3453
+ **The one-file record is read forever.** A `verification.yaml` written before the directory existed
3454
+ — a federated record with several attestations in it, active or archived — reads exactly as it
3455
+ always did while no service has a file of its own. Once services do, it is read beside them: a
3456
+ service that records again supersedes its share of the one-file record (that attestation and its
3457
+ answers drop out of the union; the file itself is not rewritten), and the write that leaves nothing
3458
+ in it counting removes it. An all-at-once record (no `schema`, no attestations) never counts once a
3459
+ service has a file — its answers carry no commit attestation — so the first `--service` write
3460
+ removes it in the same commit and lists its answers in `discarded`. Answers a write takes off the
3461
+ disk that its new file does not carry are reported in `discarded`, tagged `unattested` (an
3462
+ all-at-once answer) or `off-checklist` (a claim the current checklist no longer asks). Nothing is
3463
+ dropped silently.
3464
+
3465
+ **The all-at-once form keeps the one file, by decision.** `--record` without `--service` answers the
3466
+ whole checklist from one place on one word, with no commit and no service — so there is no second
3467
+ writer for it to collide with and no service to name a file after, and every record it ever wrote
3468
+ (the example fleet's included) stays byte-for-byte what it is. There is one reader either way:
3469
+ `loam verify`, `status`, `list`, `tasks`, `gate` and `validate` all read the record through the same
3470
+ union, whichever files it is made of.
3471
+
3472
+ The write holds the same advisory lock archive does, and holds it from before the feature is
3473
+ resolved to after the record lands — two `--record` runs for different services serialize (the
3474
+ later one waits briefly, then writes its own file, so **both** attestations survive), and a record
3475
+ can no longer be written into a path an `archive` is renaming out from under it. The commit itself
3476
+ is staged beside the target and swapped in by one rename over the exact bytes the read found, so a
3477
+ kill mid-record leaves the previous record whole — never truncated YAML — and an edit that landed in
3478
+ between refuses instead of being buried. A write that also removes a superseded one-file record
3479
+ holds that removal to the same compare, and lands the service's own file first, so a kill between
3480
+ the two leaves a record that still reads whole:
2093
3481
 
2094
3482
  - `docs-busy` — another writer held the lock longer than the record was willing to wait (5 s).
2095
3483
  Nothing was read or written; re-run once it finishes. The contention runs both ways: `archive`,
@@ -2110,9 +3498,10 @@ Recording refuses rather than guesses, each under its own `--json` `error.code`:
2110
3498
  - `repository-unavailable` — `--service` from a repo that declares none, or one whose commit cannot
2111
3499
  be read. An attestation with nothing to bind to is not an attestation.
2112
3500
  - **`record-federated`** — the legacy all-at-once form (`--record` with **no** `--service`) over a
2113
- record that already carries attestations. That form writes the whole file from one place, so it
2114
- would erase every other repository's evidence; loam refuses outright and names the attestors, and
2115
- never silently migrates or merges. Re-run with `--service`.
3501
+ record that already carries attestations, in per-service files or in a one-file record. That form
3502
+ writes the whole record from one place, so it would put one unattested answer set over every
3503
+ other repository's evidence; loam refuses outright and names the attestors, and never silently
3504
+ migrates or merges. Re-run with `--service`.
2116
3505
  - `record-unreadable` — see below.
2117
3506
 
2118
3507
  Reading is unrestricted: `loam verify <FEAT> --service <id>` **without** `--record` is a pure lens
@@ -2121,12 +3510,26 @@ in `services`, which is how you find out which repositories still owe an attesta
2121
3510
 
2122
3511
  The record is YAML, like the other data artifacts, and holds the claim text next to each verdict so
2123
3512
  it reads without loam. Because it has to survive without loam, human edits are expected — and read
2124
- back defensively. A file that exists but does not read as a record (broken YAML, or a shape missing
3513
+ back defensively. A record file that exists but does not read as a record (git conflict markers a
3514
+ merge left in it — named before the YAML parser sees them, since the parser either fails on a line
3515
+ that is not the problem or reads both sides as one plausible document; broken YAML; a shape missing
2125
3516
  what the readers use — `summary` with its counts, `recorded`, `checklist`, claims each with an `id`
2126
- and a known `verdict`) is **`record-unreadable`**: reported with the YAML line, and **never
3517
+ and a known `verdict`; or a `verification/<svc>.yaml` holding anything but that one service's
3518
+ attestation and answers) is **`record-unreadable`**: reported with the file and the line, and **never
2127
3519
  overwritten**. It used to count as *no record at all*, which was the wrong direction on the one
2128
3520
  artifact loam cannot reconstruct — "no record" invites writing a fresh one over evidence somebody's
2129
- edit merely broke. Only a genuinely absent file is "no verification record". Extra keys a human adds
3521
+ edit merely broke. Only a genuinely absent file is "no verification record".
3522
+
3523
+ The refusal names the ONE file that failed and the repair that is safe for it, because the two kinds
3524
+ of file call for opposite advice. A service's own `verification/<svc>.yaml` holds that service's
3525
+ word and nobody else's — after a merge, both sides of a conflict in it are that service's own
3526
+ attestations — so repairing it by hand, or deleting it and recording that service again from its
3527
+ repository, loses no other team's evidence. The one-file `verification.yaml` may hold every other
3528
+ service's attestation, so its advice is to repair it or **restore it from version control** (after a
3529
+ merge, the copy on the branch you merged into) and then record again — **never to delete it**. The
3530
+ old one-size advice, "delete it and record again", erased other teams' attestations when followed on
3531
+ a shared record, and the miscount advice, "record the answers again", sent the reader straight back
3532
+ into this refusal: `--record` will not write over a file it could not read. Both now end in a record. Extra keys a human adds
2130
3533
  still pass through untouched. Every verdict names who answered it — `answered_by: runner` (a
2131
3534
  cucumber report's green run, mechanical), `answered_by: external-runner` (a contract-test report's
2132
3535
  operationId-matched green entry, mechanical too), or `answered_by: agent` (somebody's word about the
@@ -2196,7 +3599,7 @@ next `--record` makes it drift-checkable), warn `evidence.unresolved` (the cited
2196
3599
  unsafe, not a regular file, or the cited line is past its end), `evidence.moved` (the file's digest
2197
3600
  differs while the cited line still reads as pinned), `evidence.line-changed` (the cited line no
2198
3601
  longer says what was recorded), `evidence.token-missing` (the file contained the token at the
2199
- attested commit and no longer does) and `evidence.record-unreadable` (a verification.yaml that
3602
+ attested commit and no longer does) and `evidence.record-unreadable` (a verification record file that
2200
3603
  exists but cannot be read — none of its evidence was checked, and a silent skip would read as clean,
2201
3604
  on the very file a hand edit to the pins most plausibly breaks). Archived records are deliberately
2202
3605
  not linted — frozen history, and post-ship code drift is normal evolution. From the docs repo the
@@ -2206,13 +3609,18 @@ verdict in either direction, `attested` never silently becomes `verified`, and t
2206
3609
  fail `validate`; `--strict` remains the CI lever it always was.
2207
3610
 
2208
3611
  It lives inside the feature, so `archive` carries it into `features/archive/<FEAT>/` with everything
2209
- else. `loam verify` re-run later compares `checklist` against the current digest and reports the
3612
+ else — the `verification/` directory moves with the feature it sits in, and `unarchive` brings it
3613
+ back the same way; the frozen view reads it through the same union. `loam verify` re-run later compares `checklist` against the current digest and reports the
2210
3614
  record as **stale** if the feature moved under it — while the feature is active. Once archived there
2211
3615
  is no current checklist to be stale against (the merge itself moved the feature's operations into
2212
3616
  the living OpenAPI, so a re-derived checklist could only disagree): `loam verify` on an archived
2213
3617
  feature renders the record verbatim as **frozen history** (`frozen: true` under `--json`, no
2214
3618
  staleness judgment), and `--record` refuses (`invalid-option` — there is no current checklist for
2215
- the answers to answer): the record is about the words that shipped, and it stays that way.
3619
+ the answers to answer): the record is about the words that shipped, and it stays that way. One
3620
+ reader holds it to something all the same: `loam gate --since <ref>` grades an archived record
3621
+ against the checklist digest `archive` captured as the feature merged (the snapshot manifest's
3622
+ `checklist`, above), because the deploy question is whether what shipped was what was checked — a
3623
+ record that had gone stale before the archive is `stale` there, never `verified` again.
2216
3624
 
2217
3625
  **Verification does not gate `archive`.** Coherence gates because loam computed it from the
2218
3626
  documents; a verdict is an agent's word about code loam never read, and a gate in front of shipping
@@ -2221,6 +3629,76 @@ comes later, and an `unconfirmed` claim with a note is worth more there than a `
2221
3629
  can back up. `loam verify` exits 0 either way; branch on `verified` in the `--json` payload, or on
2222
3630
  `verdict` when you need to tell `attested` from `unverified`.
2223
3631
 
3632
+ ### The same checklist read forwards (`loam tasks`)
3633
+
3634
+ `loam verify` answers *answered, and by whom*. `loam tasks <FEAT> [--service <id>] [--next]
3635
+ [--json]` answers the other half of the same question — *still open, at which step, and what fact
3636
+ closes it* — and it is the identical checklist read in the other direction. It is derived on every
3637
+ run from the feature's deltas, the generated suite and the verification record; it writes nothing,
3638
+ anywhere, ever, and it adds no artifact for a later run to disagree with.
3639
+
3640
+ **One unit per promise, and a unit's id is the claim's id.** Every claim the checklist derives is
3641
+ one work unit, under the same identity on both commands, so the thing to do and the answer that
3642
+ closes it can never be two different objects. One kind of unit has no claim behind it: an ADDED or
3643
+ MODIFIED requirement with no scenario under it asks the checklist nothing — there is no scenario to
3644
+ test — so `loam tasks` mints a unit of its own for it, of the kind requirement.described. That is
3645
+ the promise nobody has written acceptance criteria for yet, and it is where the loop starts.
3646
+
3647
+ **Where a unit stands** is its `at` field, and the four values are the steps of the loop this
3648
+ document already describes rather than a percentage: `describe` (the requirement carries no scenario
3649
+ at all, so there is nothing to generate a test from), `generate` (the scenario exists and this
3650
+ repository's generated suite does not carry its stamp yet), `implement` (everything else still
3651
+ open), `attested` (an agent's word confirmed the claim and no run has). It is spelled `at`
3652
+ deliberately, and not `stage` or `verdict`: both of those already mean something else on payloads an
3653
+ agent reads beside this one, and a third meaning for either word is how a consumer comes to branch
3654
+ on the wrong field.
3655
+
3656
+ **Done is a fact, not a tick.** A closed unit simply stops appearing, and exactly one thing closes
3657
+ one: a claim recorded `confirmed` in a verification record that is **not stale** against the current
3658
+ checklist. A stale record closes nothing — the words moved under the answers, which is the state
3659
+ `loam verify` already reports. An agent-attested scenario never vanishes either: it stays visible at
3660
+ `attested`, because attested is not verified, and a work list that hid the difference would be
3661
+ exactly the gate that teaches everyone the cheapest way past it is to say yes.
3662
+
3663
+ **The frontier** is the first ten open units in derived order, providers before consumers — ordered
3664
+ on the exact token join `c4.calls.token == api.exposes.operation`, never on a reading of prose.
3665
+ **The order is advice and never a lock**: nothing is assigned, nothing is reserved, and working out
3666
+ of order is legal and invisible to every other command.
3667
+
3668
+ **`--next` is the leaf**: the same packet with `units` narrowed to the frontier head, which is what
3669
+ an orchestrator hands a coding agent. Everything else — the counts, the frontier, the review
3670
+ notes — is left whole on purpose, so a cheap model handed one unit cannot read it as the whole job.
3671
+ The text view prints that leaf in full: the scenario's body verbatim and never reflowed (the bytes
3672
+ the digest was taken over), the digest itself — the same `@loam-digest-<16hex>` join key
3673
+ `loam gherkin` stamps and `loam verify --results` matches on — the `done` block whose `check` is the
3674
+ fact that closes the unit, and the literal follow-up command to run next, spelled out rather than
3675
+ left as a shape to fill in.
3676
+
3677
+ **`--service <id>` is a pure lens.** It narrows the units to one service's without binding a
3678
+ repository, and adds `waitingOn[]`: the open units in OTHER services that this service's units
3679
+ depend on. A team learns what it is blocked by from the same run that tells it what to do. In a
3680
+ service repository (`loam.json` names a `service`) the lens defaults to that service when the
3681
+ feature touches it, because the generated-suite stamps that tell `generate` from `implement` are
3682
+ read from that repository alone; the docs repo, and a feature that does not touch the bound
3683
+ service, get the whole feature.
3684
+
3685
+ **What the payload says about what it could not see.** `stamps` is `read` or `unknown`, and
3686
+ `unknown` means the bound service repo's generated suite could not be read — could-not-look, never
3687
+ nothing-wrong, because a question loam was unable to ask must not read as one that came back clean.
3688
+ Beside it the payload carries `checklist` (the digest of the question set these units derive from),
3689
+ `record` (`absent`, `recorded`, `stale` or `unreadable`), `intent`, `services[]`,
3690
+ `size{requirements,scenarios,operations,messages,calls}` — the size of the DELTA and never of the
3691
+ open work, so that "this feature is small" and "this feature is nearly done" cannot become one
3692
+ sentence — `contracts{expose[],call[]}`, `counts{open,done,attested}` and `review[]`, the four
3693
+ instructions for re-deriving this answer and reviewing it, because a projection that says what to do
3694
+ must also say how to check it was read correctly. A broken `delta.likec4` or contract axis names
3695
+ what it could not read in `unreadable` and exits 1 with `ok: true` — `loam delta`'s own rule rather
3696
+ than a new one: a feeder nothing could read empties the packet, an empty packet reads as "nothing to
3697
+ do", so the envelope stays true while the exit code stops a pipeline building on it.
3698
+
3699
+ The command mints no finding or refusal code of its own, and `loam_tasks` serves the same projection
3700
+ read-only over MCP.
3701
+
2224
3702
  ## Operating at fleet scale
2225
3703
 
2226
3704
  One docs repo, a hundred services, many teams. The repo scales the way any shared codebase does —
@@ -2263,14 +3741,28 @@ through the forge, not through loam features:
2263
3741
  **parsed** model and never computes a view — it reads what a view **declares** (a `dynamic view`'s
2264
3742
  tags and ordered steps, already materialized by the parse loam pays for) and never what a view
2265
3743
  **shows**. It renders nothing, and view *computation* is what is superlinear in edges. (If you
2266
- want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start` in
2267
- the docs repo for the fleet map, `npx likec4 start services/<id>` for one service model — and
2268
- scope the views, because an `include *` over a fleet-sized landscape takes minutes. `loam init`
3744
+ want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start`
3745
+ at the docs root, which renders the fleet map AND every adopted service's model: an extending
3746
+ model is part of the root `fleet` project, beside the map, and a standalone one is the project its
3747
+ own `services/<svc>/likec4.config.json` declares (written by `loam subsystem sync`; the Layout
3748
+ section, "Two shapes of a service model", has the rule), and `npx likec4 start features/<FEAT>`
3749
+ for a delta, which is
3750
+ transient and gets no project — and scope the views, because an `include *` over a fleet-sized
3751
+ landscape takes minutes. Two costs arrive with the first STANDALONE service project, and only
3752
+ with it. Measured at the
3753
+ pinned likec4: with more than one project, `likec4 validate` at the docs root refuses without
3754
+ `--project <name>`, so a CI step that ran it bare changes (`likec4 build` and `likec4 export` take
3755
+ every project and need no flag). And the renderer's project list grows by one per such service
3756
+ beside `fleet` — LikeC4 projects are separate models, so a box on the fleet map is not a link into
3757
+ that service's project. A fleet whose models all extend the map pays neither: one project, one
3758
+ picker entry, and the containers of every adopted service reachable from the map's own boxes.
3759
+ `loam init`
2269
3760
  scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it
2270
3761
  never draws **— and never asks for one: a fleet that draws nothing owes no views block.** It also
2271
- scaffolds a `likec4.config.json` that scopes the root LikeC4 project to `architecture/`: loam
2272
- parses every `.likec4` file **alone**, so each one declares its own `specification` block, and a
2273
- renderer given the whole tree at once merges them and reports every declaration as a duplicate.)
3762
+ scaffolds a `likec4.config.json` whose `exclude` is `["**/node_modules/**", "features/**"]`: a
3763
+ feature's `delta.likec4` re-declares the landscape's own identifiers and carries a `specification`
3764
+ block of its own, so it is read alone, and `services/` is deliberately NOT excluded — the models a
3765
+ fresh repository writes extend the map and the root project is the only place they parse.)
2274
3766
  `loam validate --all --json` over the same 120 services used to be the outlier at ~13–14 s — one
2275
3767
  fresh LikeC4 workspace per service; since the batched loader it parses the run's C4 documents in
2276
3768
  one shared workspace and answers in **~0.7 s median** on the committed 120-service benchmark
@@ -2308,8 +3800,15 @@ repairing anything:
2308
3800
  `needs` and (for authored files) the grading command in `after`. It is a **projection over the gates, never
2309
3801
  a second opinion**: `stage` is taken from the union of what `validate --feature` errors on and
2310
3802
  what `archive` refuses to merge (severity *or* `gates`), so status may be more pessimistic than
2311
- either and is never greener than both — an invariant with a test behind it. The fleet form is
2312
- capped at ten substantive steps plus a `next.elided` notice saying how many were left out, and
3803
+ either and is never greener than both — an invariant with a test behind it. The stage values are
3804
+ for branching, not progress words: `ready` means the documents are written and clean, nothing
3805
+ about code or evidence, and an archived feature is `done` whatever its record says — so the human
3806
+ view prints the stage as a sentence with the claims counted, and the payload carries
3807
+ `feature.title` (intent.md's title) and `checklist.claims` (what the checklist asks now; null once
3808
+ archived). The fleet form lists every feature in flight before any service's adoption rung — and
3809
+ carries every rung uncapped in `serviceSteps[]` (`{code, services, command}` per code), which the
3810
+ human view folds into one counted line per code. It is
3811
+ capped at ten substantive steps plus a `next.elided` notice saying how many of which codes were left out, and
2313
3812
  whenever anything is outstanding its **last** entry is `next.fleet-gate` →
2314
3813
  `loam validate --all --json` (a repository with work in it must never get a list that omits the
2315
3814
  command CI runs; a fleet whose services exist and owe nothing says so instead, with
@@ -2358,11 +3857,14 @@ repairing anything:
2358
3857
 
2359
3858
  And it reports the **agent surface**: which tool and profile the repo is wired for, how many command and skill
2360
3859
  files it holds, how many a newer loam would lay down (`doctor.agent-files-missing`), and how many
2361
- have fallen behind (`doctor.agent-files-stale`). The staleness check reads exactly one thing — the
2362
- `<!-- generated by loam vX.Y.Z -->` line every generated body opens with — and nothing else.
3860
+ have fallen behind (`doctor.agent-files-stale`). A file has fallen behind when BOTH hold: its
3861
+ bytes, the `<!-- generated by loam vX.Y.Z -->` line every generated body opens with set aside, are
3862
+ not what this binary writes; and that stamp is missing or older than this binary. A stamp-only
3863
+ difference is not staleness — the stamp moves in every file at every release, and a warning that
3864
+ fires on every upgrade hides the release where a pointer's body really changed. A customized body
3865
+ under a current stamp is not staleness either: the stamp is the record that somebody reviewed it.
2363
3866
  The digest manifest is what authorizes `init` to refresh an unchanged pointer; a different body is
2364
- treated as human-owned and never overwritten. A stamp is a separate claim and can be stale while
2365
- every edit around it is legitimate. Which deliveries a repo holds is asked of the files rather
3867
+ treated as human-owned and never overwritten. Which deliveries a repo holds is asked of the files rather
2366
3868
  than of a record: `loam.json` stores tools and profile, not deliveries, and a repo initialized
2367
3869
  `--no-skills` is not reported as missing the selected skills.
2368
3870
 
@@ -2376,7 +3878,28 @@ repairing anything:
2376
3878
 
2377
3879
  `loam-report` is a separate support protocol, generated as a command and skill in every profile
2378
3880
  but never appended to the lifecycle. It writes
2379
- `loam-reports/YYYY-MM-DD-<short-slug>.md` in the current repository with expected/actual behavior,
3881
+ `loam-reports/NNN-YYYY-MM-DD-<short-slug>.md` — `NNN` an ORDINAL of at least three digits,
3882
+ zero-padded, one more
3883
+ than the highest already in the directory (`001` when it is empty or absent, and widening past
3884
+ three digits rather than reusing a number once a corpus gets there), so a report has a
3885
+ short handle a commit message, an issue or the next report can cite. The document carries a
3886
+ `- Status: open` line beside its classification, whose vocabulary is `open` / `sent` (handed to
3887
+ the author, set by whoever forwards it) / `fixed in <version>` / `superseded by <NNN>`, and
3888
+ `loam doctor` reads the directory: how many reports it holds, how many stand at each status, and
3889
+ which ordinal comes next. `doctor --json` carries that as the `reports` object — `reports.dir`
3890
+ (the ABSOLUTE path of the directory, `loam-reports/` included, so an agent writes inside it rather
3891
+ than joining the name on a second time), resolved beside the `loam.json` that parsed, or beside
3892
+ the one loam FOUND when it did not parse, and only beside the run directory when there is no
3893
+ config file at or above it — `doctor` is the preflight that runs precisely when the config may not
3894
+ resolve, so it counts the corpus that is there rather than handing out an ordinal five existing
3895
+ reports already used — `reports.present`, `reports.total`, `reports.next`, and
3896
+ `reports.entries[]` with a `file`, an `ordinal` (`null` for a name written before the numbering)
3897
+ and a `status`, in ordinal order, numerically, with unnumbered names last (byte order stopped
3898
+ being report order at exactly the width the ordinal widens to: `1000-…` sorts before `998-…`). The status is read from the report's HEADER FIELD BLOCK alone — the lines above
3899
+ the first `##` heading, quoted code skipped in both markdown spellings (a fence, closed only by a
3900
+ run of the same character at least as long as the one that opened it, and an indented block,
3901
+ which is how the protocol prints its own template) — so a report quoting this protocol's own
3902
+ template still counts as whatever its own header says. The body carries expected/actual behavior,
2380
3903
  version, stable envelope and finding codes, relevant locations, write state, a minimal safe
2381
3904
  reproduction, classification and missing evidence. Before writing, the agent redacts secrets and
2382
3905
  home paths and reduces command output to the excerpt that proves the symptom. It never retries a
@@ -2444,6 +3967,7 @@ and publishes it wherever the fleet view reads:
2444
3967
  "errors": 0,
2445
3968
  "warnings": 2,
2446
3969
  "sources": { "current": 1, "stale": 0, "unvouched": 0, "absent": 0 },
3970
+ "contentStale": 0,
2447
3971
  "generatedAt": "2026-08-03T12:00:00Z"
2448
3972
  }
2449
3973
  ```
@@ -2462,10 +3986,16 @@ loam validate --service "$SVC" --json | jq '{
2462
3986
  unvouched: [.targets[0].findings[] | select(.code == "sources.unvouched")] | length,
2463
3987
  absent: [.targets[0].findings[] | select(.code == "sources.absent")] | length
2464
3988
  },
3989
+ contentStale: [.targets[0].findings[] | select(.code == "content.stale")] | length,
2465
3990
  generatedAt: (now | todate)
2466
3991
  }'
2467
3992
  ```
2468
3993
 
3994
+ `contentStale` counts the spec documents whose words moved after their vouch — the doc-side half of
3995
+ staleness, beside the code-side `sources.stale`. Unlike that half it is not owed to the service repo
3996
+ alone (the docs repo's own `validate --all` scorecard counts it too), but a summary that left it out
3997
+ would publish "0 stale" over a document nobody re-read.
3998
+
2469
3999
  `generatedAt` is the CI's clock, never loam's — loam output carries no timestamps, by design, so the
2470
4000
  same repo state always yields the same bytes. That is also what makes the timestamp the liveness
2471
4001
  signal: the rule is that every service repo's CI publishes this summary on every run, so a summary
@@ -2485,20 +4015,44 @@ hand-merges the block. That resolution has a documented cost, not a surprise: a
2485
4015
  landscape moves the file away from the bytes the archive's `.loam-before/` snapshot recorded, so a
2486
4016
  later `loam unarchive` of that feature refuses with `snapshot-stale` and needs `--force` — the
2487
4017
  resolver is trading undo precision for the merge, and should know it. The trigger for revisiting is
2488
- written down so nobody relitigates it early: when landscape merge conflicts become routine — weekly,
2489
- not monthly — move per-service internals fully into `services/<svc>/model.likec4` and thin the
2490
- landscape down to top-level elements plus cross-service edges. Until then, one file keeps the fleet
2491
- map one diffable document.
4018
+ written down so nobody relitigates it early — and half of what it once asked for has already
4019
+ arrived. Per-service internals now live in `services/<svc>/model.likec4` **by default**, and that is
4020
+ where a second team adopting a service writes, so it adds nothing to this file. It is a default and
4021
+ not a prohibition: the map MAY hold a service's interior, and sometimes must. A store a second
4022
+ service will reach has to be declared where that consumer's project can see it — a per-service
4023
+ project is `architecture/` plus one model — so it is drawn ON THE MAP, nested inside the owner's
4024
+ element or beside the services, whichever the team prefers. Which of the two is nobody's business
4025
+ but the team's: "Private stores" above leaves the placement open and loam grades neither, so nothing
4026
+ here says a store must move. A delta spelling a parent in the legacy
4027
+ title-join lands its container on the map too. What an extending model owns is what it DECLARES,
4028
+ not everything below the service's element. `loam archive` routes rather than forbids — a delta's
4029
+ additions nested under a service whose model extends the map, with the whole parent chain spelled
4030
+ in the map's own ids, are spliced into that model's `extend <fqn> { … }` block instead of this
4031
+ file, and the merged model is proved to parse beside the merged map before anything is written. What is left of the
4032
+ trigger is the CROSS-SERVICE EDGE, which still exists twice — container-level in the model, service
4033
+ level here — because the spine, the platform census and every other fleet check read this file
4034
+ alone. So: when landscape merge conflicts become routine — weekly, not monthly — thin the map to
4035
+ elements plus cross-service edges and teach the fleet checks to read the edges out of the models
4036
+ that attest them. Until then, one file keeps the fleet map one diffable document, and
4037
+ `landscape.service-isolated` remains the nudge that carries an attested call up into it.
2492
4038
 
2493
4039
  ## Two flows
2494
4040
 
2495
4041
  - **Bootstrap (reverse):** `loam adopt --service <id>` emits a **brief**, not an extraction — the
2496
4042
  target paths (and which already exist, to be diffed and never overwritten), the grammar of each
2497
- artifact, what the living landscape already says about the service, the frontmatter to write, the
4043
+ artifact, what the living fleet map — the `architecture/` project, not the landscape file alone —
4044
+ already says about the service, the frontmatter to write, the
2498
4045
  checks `loam validate --service <id>` will then run, and the ones that do not exist. One target is
2499
4046
  not under `services/<id>/`: `architecture/landscape.likec4`, carried with `action: "edit"` and a
2500
- `landscape.instruction` naming the block this service owes the fleet map, and present only while
2501
- nothing in the map resolves to the service. An agent reads the code and writes draft
4047
+ `landscape.instruction` naming the write this service owes the fleet map — the element while
4048
+ nothing in the map resolves to the service, the EDGES while an element does and no edge touches it
4049
+ (`landscape.touched: false`, with `landscape.attested` listing the calls the service's own
4050
+ model.likec4 declares across its boundary); absent, and the instruction `null`, only once an
4051
+ element resolves and an edge touches it. `landscape.callersKnown` says whether loam could read WHO
4052
+ CALLS this service — true when the `architecture/` project parses, and true when it does not but
4053
+ `architecture/landscape.likec4` does. It is what an empty `landscape.expects` has to be read
4054
+ against: only a readable landscape makes "nothing calls this service" evidence, and it is the one
4055
+ fact `openapi.yaml`'s `required` is derived from. An agent reads the code and writes draft
2502
4056
  `model.likec4` + `spec.md` + `openapi.yaml` + `adrs/`, `runbook.md`, `health.yaml`, and makes that
2503
4057
  one addition to the shared map. A human promotes `draft` -> `verified` with `loam vouch`.
2504
4058
  - **Forward (generative):** author `features/<FEAT>/delta.likec4` -> `loam delta <FEAT>` projects it
@@ -2511,7 +4065,7 @@ says what its architecture means, and two generated models of the same code disa
2511
4065
  every run — a done-check built on diffing them would flap until somebody turned it off. What is
2512
4066
  deterministic is the **question**: which files, in which grammar, bound to which existing elements;
2513
4067
  which operations, which edges, which scenarios. loam owns the questions and the checking; an agent
2514
- owns the reading. `sources` and `verification.yaml` are where the answers are written down.
4068
+ owns the reading. `sources` and the verification record are where the answers are written down.
2515
4069
 
2516
4070
  ## Status
2517
4071
 
@@ -2524,9 +4078,9 @@ component-surface pins), `verify`,
2524
4078
  transactional `archive` / `unarchive`, `vouch`, and the OpenSpec audit/migration on-ramp are
2525
4079
  implemented, each with a `--json` contract.
2526
4080
 
2527
- Known and unclosed, so nobody has to discover them: the complete gate still needs repeatable CI and
2528
- installed-package evidence observed from an actual push, even though behavior, coverage and the
2529
- local stress runner pass. Both contract axes now merge a components-only delta: a feature
4081
+ Known and unclosed, so nobody has to discover them: the scheduled CI `stability` job has still to be
4082
+ observed green, even though behavior, coverage, the installed-package smoke and the local stress
4083
+ runner all pass. Both contract axes now merge a components-only delta: a feature
2530
4084
  asyncapi.yaml declaring no channel/operation/message slot (`components.schemas`-only, say) merges
2531
4085
  the component surfaces it declares, pinned by the root `x-loam-baselines` record, and they enter the
2532
4086
  ref sweep like anything else the merge wrote — so a `$ref` whose target the delta itself defines