@ybotok/loam 0.2.0-alpha.4 → 0.2.0-alpha.6

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 (270) hide show
  1. package/CHANGELOG.md +947 -0
  2. package/MIGRATING-from-OpenSpec.md +7 -1
  3. package/QUICKSTART.md +8 -3
  4. package/README.md +20 -13
  5. package/ROADMAP.md +329 -22
  6. package/SCHEMA.md +472 -24
  7. package/WORKFLOW.md +23 -10
  8. package/dist/commands/adopt/render.js +47 -8
  9. package/dist/commands/adopt/render.js.map +1 -1
  10. package/dist/commands/archive/plan/landscape.js +45 -1
  11. package/dist/commands/archive/plan/landscape.js.map +1 -1
  12. package/dist/commands/archive/plan/model/extending.js +85 -0
  13. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  14. package/dist/commands/archive/run.js +5 -2
  15. package/dist/commands/archive/run.js.map +1 -1
  16. package/dist/commands/context/print.js +11 -1
  17. package/dist/commands/context/print.js.map +1 -1
  18. package/dist/commands/doctor.js +46 -0
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/explore.js +12 -2
  21. package/dist/commands/explore.js.map +1 -1
  22. package/dist/commands/list/views.js +5 -1
  23. package/dist/commands/list/views.js.map +1 -1
  24. package/dist/commands/seed/plan.js +97 -3
  25. package/dist/commands/seed/plan.js.map +1 -1
  26. package/dist/commands/seed/seed.js +7 -2
  27. package/dist/commands/seed/seed.js.map +1 -1
  28. package/dist/commands/show/feature.js +2 -1
  29. package/dist/commands/show/feature.js.map +1 -1
  30. package/dist/commands/show/marks.js +3 -5
  31. package/dist/commands/show/marks.js.map +1 -1
  32. package/dist/commands/show/service.js +39 -18
  33. package/dist/commands/show/service.js.map +1 -1
  34. package/dist/commands/subsystem/sync.js +184 -39
  35. package/dist/commands/subsystem/sync.js.map +1 -1
  36. package/dist/commands/subsystem/txn/exclude.js +132 -0
  37. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  38. package/dist/commands/subsystem/txn/report.js +124 -0
  39. package/dist/commands/subsystem/txn/report.js.map +1 -0
  40. package/dist/commands/subsystem/txn/txn.js +29 -3
  41. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  42. package/dist/commands/subsystem/txn/views.js +113 -16
  43. package/dist/commands/subsystem/txn/views.js.map +1 -1
  44. package/dist/commands/validate/arch-coverage.js +16 -9
  45. package/dist/commands/validate/arch-coverage.js.map +1 -1
  46. package/dist/commands/validate/checks/fleet-shape.js +30 -77
  47. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  48. package/dist/commands/validate/checks/vocabulary.js +4 -19
  49. package/dist/commands/validate/checks/vocabulary.js.map +1 -1
  50. package/dist/commands/validate/feature.js +1 -1
  51. package/dist/commands/validate/feature.js.map +1 -1
  52. package/dist/commands/validate/fleet/census.js +11 -10
  53. package/dist/commands/validate/fleet/census.js.map +1 -1
  54. package/dist/commands/validate/fleet/landscape.js +128 -114
  55. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  56. package/dist/commands/validate/fleet/load.js +66 -0
  57. package/dist/commands/validate/fleet/load.js.map +1 -1
  58. package/dist/commands/validate/fleet/map/attest.js +103 -0
  59. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  60. package/dist/commands/validate/fleet/map/bindings.js +96 -0
  61. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  62. package/dist/commands/validate/fleet/map/consumers.js +298 -0
  63. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  64. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  65. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  66. package/dist/commands/validate/fleet/map/usecases.js +56 -0
  67. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  68. package/dist/commands/validate/fleet/usecases/steps.js +49 -17
  69. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  70. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  71. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  72. package/dist/commands/validate/fleet/views/ids.js +13 -9
  73. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  74. package/dist/commands/validate/fleet/views/projects.js +278 -0
  75. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  76. package/dist/commands/validate/fleet/views/root-documents.js +97 -0
  77. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  78. package/dist/commands/validate/fleet/views/stale.js +2 -2
  79. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  80. package/dist/commands/validate/service/model/diverged.js +226 -0
  81. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  82. package/dist/commands/validate/service/model/grade.js +163 -0
  83. package/dist/commands/validate/service/model/grade.js.map +1 -0
  84. package/dist/commands/validate/service/service.js +110 -34
  85. package/dist/commands/validate/service/service.js.map +1 -1
  86. package/dist/commands/validate/service/specs.js +39 -5
  87. package/dist/commands/validate/service/specs.js.map +1 -1
  88. package/dist/commands/validate/service/spine.js +41 -3
  89. package/dist/commands/validate/service/spine.js.map +1 -1
  90. package/dist/commands/validate/service/usecases/tags.js +113 -0
  91. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  92. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  93. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  94. package/dist/commands/validate/validate.js +10 -24
  95. package/dist/commands/validate/validate.js.map +1 -1
  96. package/dist/core/agent/agents-md/artifacts.js +1 -0
  97. package/dist/core/agent/agents-md/artifacts.js.map +1 -1
  98. package/dist/core/agent/agents-md/command-map.js +18 -2
  99. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  100. package/dist/core/agent/agents-md/cycle.js +24 -9
  101. package/dist/core/agent/agents-md/cycle.js.map +1 -1
  102. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  103. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  104. package/dist/core/agent/agents-md/map/subsystem.js +51 -2
  105. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  106. package/dist/core/agent/agents-md/map/usecases.js +45 -3
  107. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  108. package/dist/core/agent/agents-md.js +14 -4
  109. package/dist/core/agent/agents-md.js.map +1 -1
  110. package/dist/core/agent/support/report.js +39 -5
  111. package/dist/core/agent/support/report.js.map +1 -1
  112. package/dist/core/agent/workflows/adopt.js +125 -6
  113. package/dist/core/agent/workflows/adopt.js.map +1 -1
  114. package/dist/core/agent/workflows/check.js +35 -19
  115. package/dist/core/agent/workflows/check.js.map +1 -1
  116. package/dist/core/agent/workflows/closing.js +3 -1
  117. package/dist/core/agent/workflows/closing.js.map +1 -1
  118. package/dist/core/brief/brief.js +63 -11
  119. package/dist/core/brief/brief.js.map +1 -1
  120. package/dist/core/brief/checks.js +138 -3
  121. package/dist/core/brief/checks.js.map +1 -1
  122. package/dist/core/brief/landscape.js +212 -129
  123. package/dist/core/brief/landscape.js.map +1 -1
  124. package/dist/core/brief/map/extend.js +81 -0
  125. package/dist/core/brief/map/extend.js.map +1 -0
  126. package/dist/core/brief/map/owed.js +284 -0
  127. package/dist/core/brief/map/owed.js.map +1 -0
  128. package/dist/core/brief/targets.js +30 -26
  129. package/dist/core/brief/targets.js.map +1 -1
  130. package/dist/core/brief/unchecked.js +12 -2
  131. package/dist/core/brief/unchecked.js.map +1 -1
  132. package/dist/core/c4/likec4.js +23 -1
  133. package/dist/core/c4/likec4.js.map +1 -1
  134. package/dist/core/c4/parsed/deployment.js +5 -3
  135. package/dist/core/c4/parsed/deployment.js.map +1 -1
  136. package/dist/core/c4/parsed/view-ids.js +39 -1
  137. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  138. package/dist/core/c4/prefetch/fleet.js +142 -0
  139. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  140. package/dist/core/c4/project/architecture.js +52 -4
  141. package/dist/core/c4/project/architecture.js.map +1 -1
  142. package/dist/core/c4/project/batch.js +140 -0
  143. package/dist/core/c4/project/batch.js.map +1 -0
  144. package/dist/core/c4/project/documents.js +9 -4
  145. package/dist/core/c4/project/documents.js.map +1 -1
  146. package/dist/core/c4/project/load.js +77 -38
  147. package/dist/core/c4/project/load.js.map +1 -1
  148. package/dist/core/c4/project/staged.js +46 -12
  149. package/dist/core/c4/project/staged.js.map +1 -1
  150. package/dist/core/c4/resolve/attested.js +0 -0
  151. package/dist/core/c4/resolve/attested.js.map +1 -0
  152. package/dist/core/c4/resolve/service.js +8 -17
  153. package/dist/core/c4/resolve/service.js.map +1 -1
  154. package/dist/core/c4/resolve/steps.js +18 -0
  155. package/dist/core/c4/resolve/steps.js.map +1 -1
  156. package/dist/core/c4/root-project/exclude.js +352 -0
  157. package/dist/core/c4/root-project/exclude.js.map +1 -0
  158. package/dist/core/c4/service-model/documents.js +47 -0
  159. package/dist/core/c4/service-model/documents.js.map +1 -0
  160. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  161. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  162. package/dist/core/c4/service-model/load.js +165 -0
  163. package/dist/core/c4/service-model/load.js.map +1 -0
  164. package/dist/core/c4/service-model/renderer.js +128 -0
  165. package/dist/core/c4/service-model/renderer.js.map +1 -0
  166. package/dist/core/c4/service-model/shape.js +126 -0
  167. package/dist/core/c4/service-model/shape.js.map +1 -0
  168. package/dist/core/c4/service-model/slice.js +188 -0
  169. package/dist/core/c4/service-model/slice.js.map +1 -0
  170. package/dist/core/c4/source-scan.js +26 -0
  171. package/dist/core/c4/source-scan.js.map +1 -1
  172. package/dist/core/c4/splice/authored-source.js +60 -13
  173. package/dist/core/c4/splice/authored-source.js.map +1 -1
  174. package/dist/core/c4/splice/contract.js.map +1 -1
  175. package/dist/core/c4/splice/identity/declared.js +87 -0
  176. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  177. package/dist/core/c4/splice/identity/edges.js +17 -0
  178. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  179. package/dist/core/c4/splice/identity/existing.js +80 -0
  180. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  181. package/dist/core/c4/splice/identity/interior.js +80 -0
  182. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  183. package/dist/core/c4/splice/identity/owner.js +50 -0
  184. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  185. package/dist/core/c4/splice/landscape-merge.js +154 -100
  186. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  187. package/dist/core/c4/splice/model/merge.js +140 -0
  188. package/dist/core/c4/splice/model/merge.js.map +1 -0
  189. package/dist/core/c4/splice/placement.js +14 -4
  190. package/dist/core/c4/splice/placement.js.map +1 -1
  191. package/dist/core/c4/workspace.js +2 -1
  192. package/dist/core/c4/workspace.js.map +1 -1
  193. package/dist/core/coherence/coherence.js +7 -0
  194. package/dist/core/coherence/coherence.js.map +1 -1
  195. package/dist/core/delta/delta.js +9 -0
  196. package/dist/core/delta/delta.js.map +1 -1
  197. package/dist/core/deployment/delta.js +4 -0
  198. package/dist/core/deployment/delta.js.map +1 -1
  199. package/dist/core/diff/base-state.js +21 -5
  200. package/dist/core/diff/base-state.js.map +1 -1
  201. package/dist/core/diff/victims.js +10 -1
  202. package/dist/core/diff/victims.js.map +1 -1
  203. package/dist/core/docs.js +31 -22
  204. package/dist/core/docs.js.map +1 -1
  205. package/dist/core/doctor/config.js +7 -3
  206. package/dist/core/doctor/config.js.map +1 -1
  207. package/dist/core/doctor/doctor.js +48 -9
  208. package/dist/core/doctor/doctor.js.map +1 -1
  209. package/dist/core/doctor/report.js.map +1 -1
  210. package/dist/core/doctor/reports/scan.js +242 -0
  211. package/dist/core/doctor/reports/scan.js.map +1 -0
  212. package/dist/core/explain/families.js +1 -1
  213. package/dist/core/explain/families.js.map +1 -1
  214. package/dist/core/explain/refusals.js +1 -1
  215. package/dist/core/explain/refusals.js.map +1 -1
  216. package/dist/core/explore/explore.js +44 -9
  217. package/dist/core/explore/explore.js.map +1 -1
  218. package/dist/core/fleet-context.js +47 -0
  219. package/dist/core/fleet-context.js.map +1 -1
  220. package/dist/core/gate/partners.js +5 -9
  221. package/dist/core/gate/partners.js.map +1 -1
  222. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  223. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  224. package/dist/core/pack/living.js +47 -13
  225. package/dist/core/pack/living.js.map +1 -1
  226. package/dist/core/pack/pack.js +9 -6
  227. package/dist/core/pack/pack.js.map +1 -1
  228. package/dist/core/projection/arch-slice.js +2 -2
  229. package/dist/core/projection/arch-slice.js.map +1 -1
  230. package/dist/core/repo/paths.js +40 -0
  231. package/dist/core/repo/paths.js.map +1 -1
  232. package/dist/core/repo/tree/render/projects.js +182 -0
  233. package/dist/core/repo/tree/render/projects.js.map +1 -0
  234. package/dist/core/repo/tree/render/stale.js +2 -3
  235. package/dist/core/repo/tree/render/stale.js.map +1 -1
  236. package/dist/core/repo/tree/render/views.js +51 -3
  237. package/dist/core/repo/tree/render/views.js.map +1 -1
  238. package/dist/core/repo/tree/walk.js +45 -2
  239. package/dist/core/repo/tree/walk.js.map +1 -1
  240. package/dist/core/scaffold/landscape.js +219 -9
  241. package/dist/core/scaffold/landscape.js.map +1 -1
  242. package/dist/core/staging/writes.js.map +1 -1
  243. package/dist/core/usecases/delta/claims.js +1 -0
  244. package/dist/core/usecases/delta/claims.js.map +1 -1
  245. package/dist/core/usecases/delta/overlay.js +4 -0
  246. package/dist/core/usecases/delta/overlay.js.map +1 -1
  247. package/dist/core/usecases/fleet.js +18 -6
  248. package/dist/core/usecases/fleet.js.map +1 -1
  249. package/dist/core/usecases/operations.js +7 -0
  250. package/dist/core/usecases/operations.js.map +1 -1
  251. package/dist/core/usecases/service/exclude.js +119 -0
  252. package/dist/core/usecases/service/exclude.js.map +1 -0
  253. package/dist/core/usecases/service/flows.js +161 -0
  254. package/dist/core/usecases/service/flows.js.map +1 -0
  255. package/dist/core/vocabulary/maturity.js +10 -0
  256. package/dist/core/vocabulary/maturity.js.map +1 -1
  257. package/examples/README.md +23 -0
  258. package/examples/docs/architecture/landscape.likec4 +41 -0
  259. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  260. package/examples/docs/likec4.config.json +0 -1
  261. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  262. package/examples/docs/services/order-service/arch.spec.md +13 -6
  263. package/examples/docs/services/order-service/model.likec4 +53 -67
  264. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  265. package/examples/docs/services/payment-service/model.likec4 +32 -33
  266. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  267. package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
  268. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  269. package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
  270. package/package.json +1 -1
@@ -62,7 +62,10 @@ and say it.
62
62
  |---|---|---|
63
63
  | \`service.unknown\` | no \`services/<id>/\` at all — the id is a typo until proven otherwise | use one of the ids the message offers, or \`loam list services\`; never \`loam adopt\` a misspelling |
64
64
  | \`service.no-model\` | the directory is real but there is no model.likec4 | run \`loam adopt\` for it — without the C4 center nothing else has anywhere to hang |
65
- | \`c4.invalid\` | model.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |
65
+ | \`c4.invalid\` | model.likec4 does not parse. How it was READ depends on the file's own grammar (SCHEMA.md, "Two shapes of a service model"): a model declaring its own \`specification\` is parsed ALONE, exactly as before; a model with none EXTENDS the fleet map and is parsed together with \`architecture/landscape.likec4\`. In the second case every error belongs to the model even when the message names the map — the map parsed clean by itself, or \`spine.landscape-invalid\` would be standing here instead and this model could not be read at all | fix this first — an unreadable axis makes every other check meaningless. For an extending model the usual causes are a kind the map does not declare (add \`element database\` to the map's one \`specification\` block), an \`extend\` naming a partial id (it takes the FULLY-QUALIFIED id — \`marketplace.orderService\`, never \`orderService\`), or a \`description\` written inside the \`extend\` block, which belongs on the map's declaration. A model that declares an element kind AND writes \`extend\` is the standalone shape with an \`extend\` that can never resolve; the details say so and name the two lines, one of which has to go |
66
+ | \`landscape.service-unmodelled\` | the same code as the fleet row below, raised on the SERVICE target when this service's model.likec4 EXTENDS the fleet map and no element in \`architecture/\` resolves to \`services/<id>/\`. An extending model says what is INSIDE the element the map binds; with no such element there is nothing to be inside, so no count in the model means anything and neither \`c4.valid\` nor \`c4.invalid\` is claimed. A model that STANDS ALONE never reaches it here — it answers for its own elements, and the fleet run owns that state | draw the element, or bind an existing one with \`metadata { service '<id>' }\`; then re-run |
67
+ | \`c4.declaration-diverged\` (warn) | a model that STANDS ALONE declares an element the fleet map (\`architecture/\`) also declares, and the two copies disagree about \`kind\`, \`title\`, the tag SET or the \`metadata { service }\` binding. The two declarations 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 — so a map that GROUPS its services under a parent is compared like any other, and an ambiguous tail is compared with nothing. A tail that is a BARE last segment (\`db\`, \`api\`, \`agent\`) joins only 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 — because one service's private store and another service's store are two elements owned by two services, not one element declared twice, while a box the map draws at fleet level and a model re-declares at top level is one name given two meanings. Two documents are two authorities on one element, and until now nothing read them together: a copy losing \`#platform\` changes what every view filtering on that tag shows, and no check said so. One finding per divergent element, naming both declarations and which fields differ; \`description\` is deliberately NOT compared, because it is prose nothing grades, while the other four each change what a view shows or what loam resolves. Silent while the map does not parse, and impossible for a model that extends the map — there is one declaration | copy the map's declaration verbatim into the model, treating the map as the authority; or migrate the model to the extending shape (the ordered recipe is in SCHEMA.md, "Two shapes of a service model"), after which the element is declared once and cannot diverge. When the message says copying the map's declaration would not clear it, the difference comes off \`specification { element <kind> { … } }\` and no copy can fix it — the message names the document whose specification declares tags on that kind, and the repair is to match the two blocks there, or to migrate to the extending shape so there is one specification and one declaration |
68
+ | \`c4.element-unowned\` (warn) | a model that EXTENDS the fleet map declares an element outside the element that resolves to this service — one finding per element, with its id and kind. An extending model adds elements under its own element and nowhere else: what it adds elsewhere lands in the fleet project as this service's private copy of somebody else's box | a store or component this service OWNS goes inside the \`extend <fqn> { }\` block, where its id becomes \`<fqn>.<name>\` — that is the usual answer and the one the old wording omitted. Otherwise: declare a system this service merely REACHES in \`architecture/landscape.likec4\`, once, and point an edge at it from the container that reaches it; put another service's internals in that service's own model |
66
69
  | \`c4.no-relationships\` (warn) | the model parses, declares elements, and no relationship joins anything — while its own evidence says it should reach something: more than one nested element, or dependencies declared in this service's health.yaml. A model that reaches nothing is almost never true; without this check, a 2-element model with 0 relationships and a 19-element model with 19 relationships could receive the same green verdict | draw the edges the service actually has — what its containers call, what reads and writes its stores, what it depends on at runtime. A bare root-plus-one-container baseline with no other evidence stays silent on purpose: nothing proves it thin |
67
70
  | \`requirements.missing-scenarios\` | a requirement with no scenario | add the acceptance criteria; do not delete the requirement |
68
71
  | \`requirements.stepless-scenario\` | a scenario with a heading and no recognizable steps — it satisfies the coverage rule and tests nothing: cucumber runs it vacuously green and \`loam verify --results\` can never confirm it. Graded on spec.md and arch.spec.md alike, in \`--service\` and \`--feature\` scope both, because a stepless scenario in a delta merges into the living spec and is called covered forever after | write the body as \`- **Given/When/Then**\` bullets |
@@ -96,7 +99,7 @@ and say it.
96
99
  | \`api.ungoverned\` (warn) | operation(s) no requirement's \`Operations:\` line names | write the requirement, or link an existing one |
97
100
  | \`api.ops-unlinked\` (warn) | operations AND requirements exist but zero \`Operations:\` lines join them — the API axis is vacuously green | link each requirement to the operations it governs |
98
101
  | \`api.requirement-deprecated\` (warn) | a requirement's \`Operations:\` list resolves only to operations the OpenAPI marks \`deprecated: true\` — the behaviour it governs is on its way out | migrate the requirement to the replacement operation, or retire it with the ops it governs |
99
- | \`spine.landscape-invalid\` | the living landscape does not parse, so the C4↔API spine cannot be checked | fix architecture/landscape.likec4 first |
102
+ | \`spine.landscape-invalid\` | the \`architecture/\` project does not parse, so the C4↔API spine cannot be checked. The whole project, not the landscape file alone: a \`architecture/usecases/x.likec4\` that does not read reports this on a single-service run exactly as under \`--all\`. For a service whose model EXTENDS the map the message carries one clause more — the model cannot be read either, so neither \`c4.valid\` nor \`c4.invalid\` is emitted for it and every model-dependent check on that service is suspended rather than failing. The message names the broken document(s) repo-relative and \`locations[0]\` points at the first of them | fix the file the message names, under \`architecture/\`, before anything else — one unreadable document there suspends the cross-service layer for the whole fleet |
100
103
  | \`spine.op-undefined\` | a landscape edge calls an operation this service's OpenAPI does not define | a broken contract between services — fix the edge or add the endpoint |
101
104
  | \`spine.op-link-missing\` (warn) | a landscape "Calls" edge into this service with no \`metadata { op }\` | link it to the operationId |
102
105
  | \`spine.op-deprecated\` (warn) | a landscape edge calls an operation this service's OpenAPI marks \`deprecated: true\` — the consumer is standing on a contract being retired | migrate the consumer to the replacement operation; deprecation is the first step of retiring an op, and the op stays defined until a human removes it |
@@ -114,6 +117,12 @@ and say it.
114
117
  | \`event.messages-unlinked\` (warn) | messages AND requirements exist but zero \`Publishes:\`/\`Consumes:\` lines join them — the event axis is vacuously green | link each requirement to the messages it governs |
115
118
  | \`event.ungoverned\` (warn) | message(s) declared in asyncapi.yaml that no requirement's \`Publishes:\`/\`Consumes:\` line names — \`api.ungoverned\`'s event twin, and the per-message half \`event.messages-unlinked\` cannot see: that code stays silent once even one message is linked | write the requirement that governs each named message, or drop the declaration |
116
119
  | \`covers.unknown\` (warn) | a \`Covers:\` entry in arch.spec.md resolves to no element, edge, alert or SLI — living and feature deltas alike | fix the id (the message offers close ones); a mistyped entry silently costs the coverage it was written for |
120
+ | \`usecase.step-unbacked\` | a hop of a \`#req-\`-tagged \`dynamic view\` in this service's own LikeC4 project — any \`.likec4\` beside model.likec4, or a view inside it — that NO relationship in model.likec4 backs. LikeC4 reports no error for it, so the diagram renders and the renderer's project parses; only this convicts it. The message reads \`<svc>: services/<tree>/<file> — dynamic view '<id>' step N\` | draw the edge in \`model { }\` (with \`metadata { op '<operationId>' }\` where it is a call), or, if the hop is a return step, write it as \`<target> <- <source>\` |
121
+ | \`usecase.step-contested\` (warn) | two or more relationships in model.likec4 back one hop of a service-local flow and name DIFFERENT operations — an absent \`metadata { op }\` counts as a value — so nothing can say which one the hop exercises; \`details\` lists every candidate | give the candidates one operation, or draw the hop between the elements that name exactly one |
122
+ | \`usecase.step-unlinked\` (warn) | the same code as the fleet row below, raised on a service-local flow: the relationship backing a hop of a \`#req-\`-tagged \`dynamic view\` beside model.likec4 carries no \`metadata { op }\` and no \`publishes\`/\`consumes\`, while the hop's caller is not a person and its target resolves to a real \`services/<id>/\` — the stand-in a model declares for a sibling service is the case that reaches it here. Never between two containers of THIS service: a service owes no operationId to itself. The \`--all\` row spells out all four mechanical guards | add \`metadata { op '<operationId>' }\` to the edge in \`model { }\`, or \`metadata { publishes '<message>' }\` / \`consumes\` where the hop is an event |
123
+ | \`usecase.requirement-unresolved\` | a \`#req-<slug>\` tag on a flow beside model.likec4 whose slug names no \`Requirement-ID\` of THIS service — scoped by the directory, never by a \`#cap-\` tag: the ids of spec.md and arch.spec.md together. Four arms: neither document exists, neither declares a \`Requirement-ID:\`, none flattens to the slug (close ids offered as the tag to write), or two flatten alike — a Requirement-ID is unique inside ONE document and this service has two | add \`Requirement-ID: <id>\` to the requirement this flow keeps, or fix the slug; for two colliding ids, rename one |
124
+ | \`usecase.capability-unresolved\` | a \`#cap-\` tag on a \`dynamic view\` inside this service's own project, where no capability can be claimed — a capability is a fleet promise realized by a hop sequence across the fleet map, and this flow's hops are this service's containers. Fires whether or not \`architecture/capabilities.yaml\` exists: the reason is placement, not vocabulary. The view stays opted in and its steps are graded | move the flow to \`architecture/usecases/\` if it crosses services, else drop the \`#cap-\` tag and keep the \`#req-\` one |
125
+ | \`usecase.flow-invalid\` | a \`.likec4\` beside model.likec4 that does not parse as part of the service's LikeC4 project — an unresolved element, a syntax error, or a \`specification\` block the project already has (that is a duplicate whether the model declares one or the fleet map does). Every \`.likec4\` under the service directory is read as ONE project the way the renderer reads it — for an extending model that project is the fleet map plus those documents — and the renderer refuses the whole project over one broken file; no flow in it was graded. model.likec4 itself still parses and is graded (\`c4.valid\`, \`Covers:\`, health) — this is never \`c4.invalid\`. A sibling the service's OWN \`likec4.config.json\` \`exclude\` covers is not in that project and is not graded | fix the file the finding names (\`locations[0]\`, and one \`details\` line per error prefixed by its file) — the renderer refuses the same project until it reads. An undeclared reserved tag adds one further \`details\` line naming the \`tag <name>\` declaration to write and the \`specification { }\` block it belongs in |
117
126
  | \`api.response-ungoverned\` (warn) | an operation declares a 4xx/5xx response that no scenario of any requirement governing it reaches. \`api.ungoverned\` grades whole OPERATIONS, so an endpoint with one happy-path requirement and twelve declared failure codes was fully governed by every other check — and the refusals are where the decision layer lives | write the refusal as a scenario, or as a row in an existing one. A scenario reaches a code when the number appears in its body, \`Examples\` rows included |
118
127
  | \`obligation.unknown\` | a \`#obl-<name>\` tag on the LIVING fleet map that \`architecture/obligations.yaml\` does not declare. An ERROR, and the asymmetry with its sibling below is the point: a tag that resolves to nothing reads exactly like a rule the fleet keeps, in review and in every derived view. \`details\` names every object carrying it | fix the tag (the message offers close names), or declare the obligation |
119
128
  | \`obligation.unapplied\` (warn) | a declared obligation that no \`#obl-\` tag applies anywhere on the map — the mirror of \`permissions.unenforced\`, and it earns its place for the same reason: a vocabulary is worth what cites it. Either a decision that was reversed and left its word behind, or one nobody has placed yet | tag the elements and edges it governs, or drop the declaration |
@@ -133,21 +142,13 @@ and say it.
133
142
  | \`capability.realizes-unknown\` | a \`Realizes:\` entry in spec.md or arch.spec.md — living at the service target, or a feature's delta of either, where it gates \`loam archive\` (\`--approve\` overrides) — that names no capability requirement. Five distinct failures, each with its own fix in the message: the entry is not spelled \`<capability-id>#<Requirement-ID>\`; the capability is undeclared; it is declared but has no document; the document declares no requirements yet; or it declares requirements but not this id. Silent under the same ladder as \`capability.unknown\` | follow the message — it names which of the five happened, and offers close names where there are any |
134
143
  | \`capability.realizes-stale\` (warn) | a PINNED \`Realizes:\` entry whose capability requirement has been rewritten since the pin was stamped. The join still resolves — the address is fine and the promise behind it moved, which no existence check can see. Only pinned entries are graded, so a corpus that has never run \`loam rebase --living\` produces none of these, ever | re-read \`capabilities/<id>/spec.md\` and decide which side is now wrong: change this requirement, change the promise, or — when this requirement is still right as it stands — \`loam rebase --living\` to record the re-read |
135
144
  | \`capability.requirement-unrealized\` (warn) | a requirement in a capability document that no living service requirement's \`Realizes:\` line names, one warning PER requirement, subject = the \`<capability>#<id>\` entry an author would type to fix it. The sharper sibling of \`capability.unrealized\`, and the one that survives a healthy-looking row: a capability with four requirements and three realized reports nothing at all through the other code | add the line to each service requirement that carries part of the promise — or leave it, if the promise is genuinely not implemented yet: writing the business document ahead of the fleet is the intended use, which is why this warns and never gates |
136
- | \`capability.uncovered\` (warn, GATES archive) | a requirement a feature's \`features/<FEAT>/capabilities/<id>/spec.md\` ADDS that no ADDED or MODIFIED requirement in the same feature's \`specs/\` deltas names with \`Realizes: <capability-id>#<Requirement-ID>\`, subject = the capability id. A WARNING because the document is legal — writing a promise ahead of the fleet is the intended use — that GATES because the merge is what is unsafe: once it lands, the only thing that will ever mention the promise again is \`capability.requirement-unrealized\`, a fleet-scope warning nobody reads. MODIFIED is exempt (the promise may already be realized by living text), and a requirement with no \`Requirement-ID:\` is left to \`capability.requirement-unidentified\` alone | add the \`Realizes:\` line to the service requirement that carries part of the promise; or, if the promise CROSSES services, write the flow that keeps it as \`features/<FEAT>/usecases/<name>.likec4\` — a views-only document holding one \`dynamic view\` tagged \`#cap-<capability>\` and \`#req-<requirement>\`, which \`loam archive\` merges into \`architecture/usecases/\` and \`loam unarchive\` takes back; or \`loam archive <FEAT> --approve\` to land the promise ahead of the fleet and let \`capability.requirement-unrealized\` carry it |
137
- | \`usecase.flow-invalid\` | a \`features/<FEAT>/usecases/*.likec4\` loam could not read against the map this feature's own merge would leave behind — a parse error, a hop naming an element the merge does not land, or a landscape merge that itself refuses. The flows were NOT graded, and the archive would copy them into \`architecture/\` for the next reader's \`loam validate --all\` to fail on. \`--approve\` does NOT move it (\`overridable: false\`): that flag overrides loam's judgment about coherence, never its ability to read an axis | fix what the message names. A hop into an element the feature adds is legal — the flow is graded against the POST-MERGE map — so the usual cause is a spelling that matches neither the living landscape nor \`delta.likec4\` |
138
- | \`capability.remove-requirement-realized\` | the same join in the removal direction: a feature's capability delta REMOVES a requirement that something the merge leaves behind still names with \`Realizes:\` — a living service requirement this feature does not also change, or one of its own ADDED/MODIFIED requirements. Without it the archive succeeded and the very next \`loam validate --all\` failed with \`capability.realizes-unknown\` against a service document nobody in the feature had touched. An ERROR, exactly as \`openapi.remove-op-consumed\` is for the identical shape one axis over; \`--approve\` overrides | retire the realizing requirement in the same feature, or drop its \`Realizes:\` line (a MODIFIED does that by restating the requirement without it), or \`loam archive <FEAT> --approve\` to break the join deliberately |
139
- | \`glossary.term-exists\` | a \`features/<FEAT>/glossary/<term>.md\` whose term the LIVING glossary already defines. The merge is a whole-file copy, so it would replace an authored definition wholesale — and \`--approve\` does NOT move this one (\`overridable: false\` in the \`--json\` refusal), because approving it is approving a deletion nobody described. A feature-local glossary document INTRODUCES a term; it does not rewrite one | delete the feature's copy and edit \`glossary/<term>.md\` directly in the same change, where git produces an ordinary conflict if somebody else is editing it too |
140
- | \`usecase.flow-exists\` | a \`features/<FEAT>/usecases/<name>.likec4\` whose flow the LIVING \`architecture/usecases/\` already holds. The merge is a whole-file copy, so it would replace an authored hop sequence wholesale — and \`--approve\` does NOT move this one (\`overridable: false\` in the \`--json\` refusal), because a \`dynamic view\`'s ordered steps are written down in no other document the fleet keeps. A feature-local flow INTRODUCES a use case; it does not rewrite one | delete the feature's copy and edit \`architecture/usecases/<name>.likec4\` directly in the same change, where git produces an ordinary conflict if somebody else is editing it too |
141
- | \`deployment.doc-exists\` | a \`features/<FEAT>/deployment/<name>.likec4\` whose file the LIVING \`architecture/\` already holds. The merge is a whole-file copy, so it would replace an authored topology wholesale, and \`--approve\` does NOT move it — the same act, and the same severity, as \`usecase.flow-exists\` one axis over. The collision is on the FILE, never on what is inside it: two features may both \`extend\` one living region from documents of their own and both archive | delete the feature's copy and edit \`architecture/<name>.likec4\` directly in the same change — or, to ADD to what that file declares, rename the feature's document and say \`extend\` in it |
142
- | \`deployment.doc-invalid\` | a \`features/<FEAT>/deployment/*.likec4\` loam could not read against the map this feature's own merge would leave behind — a parse error, an unresolved element, or a landscape merge that itself refuses. The corpus is the feature's \`deployment/\` PLUS the living \`architecture/\` documents it \`extend\`s, so the fault may be in either and every message names its file. \`--approve\` does not move it: the flag overrides loam's judgement about coherence, never its ability to read an axis | fix what the messages name. A document instancing an element only this feature's \`delta.likec4\` adds is LEGAL and resolves — that is what the merge preview is for; one naming an element the merge does not land is the case this refuses |
143
- | \`deployment.doc-reserved\` | a \`features/<FEAT>/deployment/<name>.likec4\` whose name loam owns inside \`architecture/\`: \`subsystems.likec4\` (generated, overwritten wholesale by \`loam subsystem sync\`, and parsed by nothing — so topology landing there is invisible from the moment it archives), \`landscape.likec4\` (spliced into, never replaced), or any path under \`usecases/\` (the flow tree). Refused whether or not the target exists, which is the point: two of the three fail silently when it does not | rename the feature's document — its name is yours to choose, and \`extend\` is how it adds to what a living document already declares |
144
145
  | \`glossary.unlinked\` (warn) | a term defined in \`glossary/<term>.md\` that no document OUTSIDE \`glossary/\` links to — the mirror of \`permissions.unenforced\` and \`capability.unrealized\`, and it earns its place for their reason: a vocabulary is worth what cites it. A term cited only by OTHER TERMS is still unlinked and the citing terms are listed in \`details\` — a glossary is a network of definitions, so \`order-line\` linking to \`order\` says the glossary is consistent, not that the fleet uses the word. When NOTHING outside cites anything it is one finding for the whole glossary rather than one per term, because that is one afternoon's work and not N breaches. Warn and never a gate: writing the vocabulary ahead of the fleet is the intended use | cite the term from the requirement, capability or intent that uses it — \`[Order](../../glossary/order.md)\` — or delete the definition. \`loam list glossary\` shows every term with the documents that cite it |
145
146
  | \`link.unresolved\` | a markdown link in an authored document whose relative target names nothing. ONE finding per document, listing every broken link with its line, because a rotted set of links is one editing session rather than ten separate breaches. The corpus is every authored markdown document: both spec axes living and delta, \`runbook.md\`, every \`adrs/\` file at all three altitudes, \`intent.md\`, and each capability document — the fleet's own ADRs and living capability tree are graded once, on the \`landscape\` target, and never repeated per service. Three things are deliberately NOT graded, each of them a question loam has no answer to: a target outside the docs repo (that tree may not be checked out beside these documents), a bare \`#section\` anchor, and anything inside a fenced block or an inline code span. CASE **is** graded — a link resolves only when each path segment is spelled the way the directory stores it, and when only the case is wrong the finding names the stored spelling, so a link that works on a Windows laptop and 404s on GitHub is caught here rather than in review | fix the path, or write the document it points at. A link here is a join of the same kind as \`Operations:\` and \`Covers:\` — write it as a relative path, which is the only form that can resolve |
146
147
  | \`link.unreadable\` | a document in that corpus could not be decoded — almost always UTF-16, which PowerShell's \`>\` and \`Out-File\` write unasked. Its links were not checked; everything else about the service or feature still was, which is exactly why this is not \`service.unreadable\` | re-save the file as UTF-8 (\`Out-File -Encoding utf8\`) |
147
148
  | \`docs.commit-interrupted\` | a \`.loam-commit\` journal sits in the docs repo: a writer's commit never finished, so the living docs may be half-merged — nothing graded in the same run can be trusted to describe one state, and a green here would certify bytes no run produced | run the command the finding names (it recovers first), or \`loam doctor\` for the file-by-file account; the finding leads every validate mode until the journal is gone |
148
149
  | \`health.invalid\` (warn) | health.yaml exists but does not parse — alert/SLI ids are unreadable, so \`Covers: alert:/sli:\` entries and health coverage are unchecked (a missing health.yaml is legal and silent; an unreadable one used to masquerade as \`covers.unknown\` typos) | fix the YAML — the health axis resumes once it reads |
149
150
  | \`health.uncovered\` (warn) | health.yaml declares an alert or SLI no arch.spec.md requirement covers | write the arch requirement with \`Covers: alert:<id>\` / \`Covers: sli:<id>\` — a signal nothing tests is dashboard decoration |
150
- | \`health.dependency-unmodelled\` (warn) | a \`dependencies:\` id in health.yaml that nothing in this service's OWN model.likec4 answers to — by element id, \`metadata { service }\` binding, or title. Resolved against the service model, never the landscape: the model carries everything the service touches (private stores included, as nested containers), and the on-call file must name the same world. Either a dependency nobody modelled, or one that no longer exists — for example, a disabled optional service registry left as \`critical: startup\` under a green validator | model it (an external system as its own element with the real edge; a private store as a nested container), correct the id to one of the element's three names, or delete the entry |
151
+ | \`health.dependency-unmodelled\` (warn) | a \`dependencies:\` id in health.yaml that nothing in this service's OWN model.likec4 answers to — by element id, \`metadata { service }\` binding, or title, matched exactly and never by suffix. Resolved against the service model, never the fleet map read on its own: the model carries everything the service touches (private stores included, as nested containers), and the on-call file must name the same world. For a model that EXTENDS the map the set is its own SLICE — the elements it adds, plus every endpoint of its own edges and each endpoint's ANCESTORS — so \`kafka\` answers through an edge that names \`kafka.orderEvents\`, and a peer service answers through the binding on the map's element the model points at. Either a dependency nobody modelled, or one that no longer exists — for example, a disabled optional service registry left as \`critical: startup\` under a green validator | model it (a system the service reaches as an element of the FLEET MAP with the real edge drawn from the container that reaches it; a private store as a nested container), correct the id to one of the element's three names — the service id or a top-level system id is the spelling that survives either model shape — or delete the entry |
151
152
  | \`gherkin.missing\` (warn) | a living scenario no generated .feature scenario carries a digest for — the suite has no test for these words. Fires only in the service's repo, once \`<gherkinDir>/loam/\` exists | \`loam gherkin --service <id>\` regenerates the suite |
152
153
  | \`gherkin.stale\` (warn) | a generated scenario's digest matches no living scenario while its requirement still exists — the spec moved under the file (a reworded scenario reports stale + missing together) | regenerate; never edit a generated file to catch it up |
153
154
  | \`gherkin.orphaned\` (warn) | a generated file whose requirement no longer exists in the living spec (a file for a feature still in flight is exempt — it answers to its feature until it archives) | regenerate; the orphaned file is deleted and reported |
@@ -208,6 +209,14 @@ what lets a feature ship:
208
209
  | \`intent.empty\` (warn, gates archive) | intent.md is missing, or its Why/Scope hold nothing outside the scaffold's own comments | write why the feature exists — it is the first thing a reviewer reads. \`--approve\` overrides |
209
210
  | \`archedge.uncovered\` (warn) | no scenario names a tagged edge (a heuristic) | write the scenario, or say why the edge needs none |
210
211
  | \`c4.uncovered\` (warn) | a NEW tagged element or edge in delta.likec4 that no arch requirement covers via \`Covers:\` — the mechanical check, not the heuristic above | add the requirement to \`specs/<svc>/arch.spec.md\` (outbox? retries? alerts?), or the architectural obligations ship unchecked |
212
+ | \`capability.uncovered\` (warn, GATES archive) | a requirement a feature's \`features/<FEAT>/capabilities/<id>/spec.md\` ADDS that no ADDED or MODIFIED requirement in the same feature's \`specs/\` deltas names with \`Realizes: <capability-id>#<Requirement-ID>\`, subject = the capability id. A WARNING because the document is legal — writing a promise ahead of the fleet is the intended use — that GATES because the merge is what is unsafe: once it lands, the only thing that will ever mention the promise again is \`capability.requirement-unrealized\`, a fleet-scope warning nobody reads. MODIFIED is exempt (the promise may already be realized by living text), and a requirement with no \`Requirement-ID:\` is left to \`capability.requirement-unidentified\` alone | add the \`Realizes:\` line to the service requirement that carries part of the promise; or, if the promise CROSSES services, write the flow that keeps it as \`features/<FEAT>/usecases/<name>.likec4\` — a views-only document holding one \`dynamic view\` tagged \`#cap-<capability>\` and \`#req-<requirement>\`, which \`loam archive\` merges into \`architecture/usecases/\` and \`loam unarchive\` takes back; or \`loam archive <FEAT> --approve\` to land the promise ahead of the fleet and let \`capability.requirement-unrealized\` carry it |
213
+ | \`usecase.flow-invalid\` | a \`features/<FEAT>/usecases/*.likec4\` loam could not read against the map this feature's own merge would leave behind — a parse error, a hop naming an element the merge does not land, or a landscape merge that itself refuses. The flows were NOT graded, and the archive would copy them into \`architecture/\` for the next reader's \`loam validate --all\` to fail on. \`--approve\` does NOT move it (\`overridable: false\`): that flag overrides loam's judgment about coherence, never its ability to read an axis. Also fires on the SERVICE target, under \`--service\` and per service under \`--all\`, for a \`.likec4\` beside model.likec4 that does not read as part of that service's own project — there the flows beside the model were not graded while model.likec4 itself still was | fix what the message names. A hop into an element the feature adds is legal — the flow is graded against the POST-MERGE map — so the usual cause is a spelling that matches neither the living landscape nor \`delta.likec4\` |
214
+ | \`capability.remove-requirement-realized\` | the same join in the removal direction: a feature's capability delta REMOVES a requirement that something the merge leaves behind still names with \`Realizes:\` — a living service requirement this feature does not also change, or one of its own ADDED/MODIFIED requirements. Without it the archive succeeded and the very next \`loam validate --all\` failed with \`capability.realizes-unknown\` against a service document nobody in the feature had touched. An ERROR, exactly as \`openapi.remove-op-consumed\` is for the identical shape one axis over; \`--approve\` overrides | retire the realizing requirement in the same feature, or drop its \`Realizes:\` line (a MODIFIED does that by restating the requirement without it), or \`loam archive <FEAT> --approve\` to break the join deliberately |
215
+ | \`glossary.term-exists\` | a \`features/<FEAT>/glossary/<term>.md\` whose term the LIVING glossary already defines. The merge is a whole-file copy, so it would replace an authored definition wholesale — and \`--approve\` does NOT move this one (\`overridable: false\` in the \`--json\` refusal), because approving it is approving a deletion nobody described. A feature-local glossary document INTRODUCES a term; it does not rewrite one | delete the feature's copy and edit \`glossary/<term>.md\` directly in the same change, where git produces an ordinary conflict if somebody else is editing it too |
216
+ | \`usecase.flow-exists\` | a \`features/<FEAT>/usecases/<name>.likec4\` whose flow the LIVING \`architecture/usecases/\` already holds. The merge is a whole-file copy, so it would replace an authored hop sequence wholesale — and \`--approve\` does NOT move this one (\`overridable: false\` in the \`--json\` refusal), because a \`dynamic view\`'s ordered steps are written down in no other document the fleet keeps. A feature-local flow INTRODUCES a use case; it does not rewrite one | delete the feature's copy and edit \`architecture/usecases/<name>.likec4\` directly in the same change, where git produces an ordinary conflict if somebody else is editing it too |
217
+ | \`deployment.doc-exists\` | a \`features/<FEAT>/deployment/<name>.likec4\` whose file the LIVING \`architecture/\` already holds. The merge is a whole-file copy, so it would replace an authored topology wholesale, and \`--approve\` does NOT move it — the same act, and the same severity, as \`usecase.flow-exists\` one axis over. The collision is on the FILE, never on what is inside it: two features may both \`extend\` one living region from documents of their own and both archive | delete the feature's copy and edit \`architecture/<name>.likec4\` directly in the same change — or, to ADD to what that file declares, rename the feature's document and say \`extend\` in it |
218
+ | \`deployment.doc-invalid\` | a \`features/<FEAT>/deployment/*.likec4\` loam could not read against the map this feature's own merge would leave behind — a parse error, an unresolved element, or a landscape merge that itself refuses. The corpus is the feature's \`deployment/\` PLUS the living \`architecture/\` documents it \`extend\`s, so the fault may be in either and every message names its file. \`--approve\` does not move it: the flag overrides loam's judgement about coherence, never its ability to read an axis | fix what the messages name. A document instancing an element only this feature's \`delta.likec4\` adds is LEGAL and resolves — that is what the merge preview is for; one naming an element the merge does not land is the case this refuses |
219
+ | \`deployment.doc-reserved\` | a \`features/<FEAT>/deployment/<name>.likec4\` whose name loam owns inside \`architecture/\`: \`subsystems.likec4\` (generated, overwritten wholesale by \`loam subsystem sync\`, and parsed by nothing — so topology landing there is invisible from the moment it archives), \`landscape.likec4\` (spliced into, never replaced), or any path under \`usecases/\` (the flow tree). Refused whether or not the target exists, which is the point: two of the three fail silently when it does not | rename the feature's document — its name is yours to choose, and \`extend\` is how it adds to what a living document already declares |
211
220
  | \`delta.unknown-section\` | a heading that nearly matches the delta grammar | fix it — everything under it merges as NOTHING today, silently |
212
221
  | \`delta.no-delta-sections\` | requirements, but no \`## ADDED/MODIFIED/REMOVED Requirements\` section anywhere — the whole delta would merge nothing | put every changed requirement under its delta section |
213
222
  | \`delta.requirement-not-merged\` (warn, gates archive) | a requirement under a prose heading (\`## Behavior\`) instead of a delta section | move it under \`## ADDED\`/\`## MODIFIED\`/\`## REMOVED Requirements\` — as written, archive drops it. If it really is documentation, quote it under \`## Requirements\`, which is exempt |
@@ -232,29 +241,36 @@ what lets a feature ship:
232
241
  | code | what it means | what to do |
233
242
  |---|---|---|
234
243
  | \`landscape.missing\` | \`architecture/landscape.likec4\` does not exist — an ERROR as soon as \`services/\` holds one service, a warning only in an empty docs repo | create it: a \`specification { ... }\` block declaring the kinds, then a \`model { ... }\` block with one bound element per service (\`metadata { service '<id>' }\`) and one edge per cross-service call carrying \`metadata { op '<operationId>' }\`. With no fleet map every cross-service check is blind, not passing |
235
- | \`landscape.invalid\` | the living landscape does not parse | fix it first — the fleet cross-check cannot run against a document nobody can read |
244
+ | \`landscape.invalid\` | the living landscape does not parse. The fleet map is graded as the \`architecture/\` project and NOTHING ELSE — the landscape plus every \`architecture/**/*.likec4\` except the generated \`subsystems.likec4\` — whatever shape the service models take. That is deliberate: the renderer's root project is wider (it also holds every extending model), so a hop in \`architecture/usecases/\` naming a CONTAINER an extending model declares renders perfectly and is \`landscape.invalid\` here. A cross-service flow names service elements; a container-level flow lives beside the model that declares the container. One broken service model must never blank the fleet's own map | fix it first — the fleet cross-check cannot run against a document nobody can read. If the unresolved name is a container an extending model declares, the reference belongs one altitude down whatever carries it: a use-case hop moves beside that service's model.likec4 and is tagged \`#req-\`; a model edge names the SERVICE on the map and the container in the service's own model |
236
245
  | \`service.id-invalid\` | a \`services/<id>/\` directory whose name is not a legal service id — every authoring command refuses it, so nothing in that directory can be changed through loam. Fleet scope only: the SET of service directories is what makes the id a question, and a \`--service\` run is already refused by its own guard before it gets this far (the guard refuses only a name that neither exists as a directory nor is a legal id — a badly-named directory that DOES exist resolves through the enumeration and grades as an ordinary target, still without this finding). Graded before the map is opened, so it stands even when the landscape is missing or unreadable | rename the directory, then update its \`service:\` frontmatter, its \`metadata { service }\` binding in the landscape, and any \`features/<FEAT>/specs/<id>/\` naming it |
237
246
  | \`subsystem.unmarked\` | a directory under \`services/\` that is neither a subsystem (no \`subsystem.yaml\`) nor a service (no service artifact), with directories beneath it — the shape an ordinary merge produces when one branch deletes an emptied subsystem's marker while another moves a service in. The services beneath STAY enumerated and the finding names every one of them: the fleet is never reported smaller than it is | restore \`subsystem.yaml\` in that directory (an empty file is a valid marker), or move the directories out |
238
247
  | \`subsystem.marker-misplaced\` | \`subsystem.yaml\` sitting BESIDE service artifacts (spec.md, model.likec4, …) — a directory is a subsystem or a service, never both; it stays classified as a service | delete the marker, or move the artifacts into a service directory of their own inside the group |
239
248
  | \`subsystem.name-collision\` | one name claimed by two or more directories in the \`services/\` tree — service ids and subsystem names share one flat namespace, unique at any depth, because a service's identity is its leaf directory name alone and placement never disambiguates it. The message names every claimant | rename all but one, then update whichever frontmatter, landscape bindings and \`features/<FEAT>/specs/<id>/\` name the renamed service |
240
249
  | \`subsystem.name-invalid\` | a subsystem directory name that breaks the id grammar (a space, a \`..\`, a Windows-reserved stem, a trailing dot) — subsystem names take the service-id grammar because both become directory names in one shared tree. Its services stay enumerated meanwhile | rename the directory to a legal name |
241
250
  | \`subsystem.invalid\` | a \`subsystem.yaml\` that exists but cannot be read as subsystem metadata — presence still classifies the directory as a subsystem (content is metadata, never identity), and exactly one finding is emitted per file | fix the file: a YAML mapping with optional \`title\`, \`description\` and \`owner\`; never \`members\` — the directory itself is the membership record. An empty file is also a valid marker |
242
- | \`subsystem.views-stale\` | \`architecture/subsystems.likec4\` — the GENERATED file holding one LikeC4 view per subsystem — does not match the tree: it drifted after a move, it is absent while subsystems exist, or it lingers after the last subsystem was removed. One error on one file, graded by byte comparison against the deterministic re-render (no check ever reads the file's content) | run \`loam subsystem sync\` — the single regenerator. Never edit the file by hand; authored views belong in the landscape or a file of your own |
243
- | \`subsystem.view-id-collision\` | \`architecture/landscape.likec4\` declares a view whose id is one loam GENERATES into \`architecture/subsystems.likec4\`. LikeC4 merges every file in \`architecture/\` into one project and view ids are a flat namespace there, so the renderer refuses the WHOLE project and draws nothing — while every other check here stays green. Only a real collision is reported: a \`subsystem_\`-looking id matching no current subsystem breaks nothing yet, and fires on the run after somebody creates that subsystem | rename the view in \`architecture/landscape.likec4\`. Ids beginning \`subsystem_\` are loam's to mint; \`architecture/subsystems.likec4\` is generated (\`loam subsystem sync\`) and is never hand-edited |
251
+ | \`subsystem.views-stale\` | \`architecture/subsystems.likec4\` — the GENERATED file holding one LikeC4 view per subsystem — does not match the tree: it drifted after a move, it is absent while subsystems exist, it lingers after the last subsystem was removed, or the map's global style groups changed. Styling restales it twice over: declaring or removing a group named \`subsystems\` changes whether the generated views carry the \`global style subsystems\` line, and declaring a group under ANY other name changes the disclosure comment that names what is available — either way the deterministic re-render differs from the bytes on disk. The census reads every \`.likec4\` under \`architecture/\` the root \`likec4.config.json\` does not exclude, so a palette in a sibling counts. One error on one file, graded by byte comparison against that re-render (no check ever reads the file's content) | run \`loam subsystem sync\` — the single regenerator. Never edit the file by hand; authored views belong in the landscape or a file of your own. \`sync\` refuses to rewrite this file while \`architecture/\` does not parse (\`action: "blocked"\`) — fix the map first; \`validate\` already skips the staleness question in that state |
252
+ | \`service.likec4-config-missing\` (warn) | a model that STANDS ALONE — it declares its own \`specification\` block, so it is parsed alone and cannot share a project with the map — with no \`likec4.config.json\` beside it. It renders only as a project of its own and this one has none: a box on the fleet map with nothing renderable inside it. One warning per service, on the fleet target, only when the root \`likec4.config.json\` exists (\`loam doctor\` prints that one); never gating, because no loam check reads the model any differently. A model that EXTENDS the map never earns this — it belongs to the root project | run \`loam subsystem sync\` — it creates the file (create-only: a file that already exists is yours and is never touched). Never hand-write it; the bytes are a function of the service id, and the writer spells them once |
253
+ | \`service.likec4-config-stray\` (warn) | the mirror: a \`likec4.config.json\` beside a model that EXTENDS the fleet map. The renderer registers that file as a project of its own rooted at that directory, so \`likec4 validate .\` at the docs root 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 — it happened on a seeded fleet and not on \`examples/docs\`). Fleet target, root-gated like its siblings, never gating | run \`loam subsystem sync\` — it deletes the file (or delete it yourself). loam never writes one beside an extending model, so this one is a leftover from before the model migrated, or was hand-written |
254
+ | \`service.model-excluded\` (warn) | a model that EXTENDS the fleet map whose directory the root \`likec4.config.json\`'s \`exclude\` covers — or whose \`model.likec4\` a FILE-shaped entry (\`services/**/model.likec4\`) names; the message says which shape it found. An entry is read the way the renderer reads it: a bare entry is a raw path PREFIX from a \`/\` boundary, so \`services\`, \`services/\`, \`services/*\`, \`services/**\`, \`services/**/*\`, a wildcard segment in the middle (\`services/*/legacy\`) and the bare \`services/pay\` all cover \`services/payment-service\` — while \`services/pay/**\` wants the literal separator and covers nothing there. The renderer never loads the model, so the service is a box with nothing inside it from the docs root, while every loam check still grades it perfectly: the gap is invisible except here. The finding quotes the entry verbatim, because the repair is to edit that line | for a DIRECTORY entry, run \`loam subsystem sync\` — it rewrites the root project's \`exclude\` to cover exactly the models that stand alone, keeping every entry that is not about \`services/\` in the order the team wrote it. For a FILE-shaped entry sync repairs nothing: remove or narrow that line by hand, then re-run |
255
+ | \`service.model-unexcluded\` (warn) | the mirror of that one: a model that STANDS ALONE whose directory the root \`exclude\` does NOT cover. The renderer merges it into the root project and reports every kind and element it declares as a duplicate of the map's — which blanks the whole root project, not just this service. The most damaging of the four, and the one a fresh scaffold walks into the moment somebody writes a model in the old shape. An entry already covering the directory in any of its spellings satisfies it — no second glob is wanted, and none is appended | run \`loam subsystem sync\` — it adds \`services/<tree>/**\` to the root project's \`exclude\`. Or migrate the model to extend the map (SCHEMA.md, "Two shapes of a service model"), after which no exclusion is wanted |
256
+ | \`landscape.excluded\` (warn) | the same root \`exclude\` list, one level up: an entry covering \`architecture/landscape.likec4\` itself. The renderer then loads NO fleet map — it draws nothing, and every model that EXTENDS the map resolves against nothing in the root project, which is also what WOULD turn \`c4.fleet-project-invalid\` into a page of unresolved references — measured on the packaged example fleet, one authored line came back as 161 of them. So that check is not graded at all while this entry stands, and this finding is the only line in the run that names the cause; remove the entry and the fleet project is graded again. loam is otherwise unaffected: the architecture loader keeps the map whatever the list says, so every grade in the run is right, and that is exactly why the state was invisible until this code existed. An entry is matched as a FILE path here, not as a directory, so \`architecture/*.likec4\` — a line somebody wrote for a palette — covers it while \`services/**\` and \`features/**\` do not. Quoted verbatim, because the repair is to edit that line | remove the entry, or narrow it to the paths it was written for. NOT \`loam subsystem sync\`: that command maintains only the \`services/\` entries in the list and leaves every other line exactly as the team wrote it, so it would report success and change nothing |
257
+ | \`c4.fleet-project-invalid\` (warn) | every document reads clean where loam grades it, and the ONE project the renderer builds out of them does not parse. \`--all\` only, only when at least one extending model exists, the map itself parses, and the root \`exclude\` does not cover the map (\`landscape.excluded\` then names that one entry and this check stands down behind it, because this load reads what the renderer reads and has no floor under the map): loam walks the docs root and loads what the renderer's root project loads — everything the root \`exclude\` leaves, minus the generated \`subsystems.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 that is NOT already present in the map alone, in some per-service project, or in a service-flow project the same run made. A \`.likec4\` in a SUBSYSTEM directory, above every service, is in that set. That difference is exactly the class no per-service grade can see: two services each declaring \`tag req-AUTH\` locally, two models adding the same child id under one element, a broken untagged sibling nobody opted in. All of them are renderer-fatal and all of them were silent. \`locations[0]\` is the file the error is IN, \`details\` carry \`<path>: L<n>: <error>\` 1-based — path, colon, space, then the line, exactly as the message's own head spells it, so an agent splitting on the separator gets the same two halves from either — and the message tail names the failure it actually is — a double declaration, an unresolved reference, or neither. \`subject\` is the service whose directory holds the file, where there is one | declare it once — in \`architecture/landscape.likec4\`, or in the single service that owns it — at the file and line the message names. Never by excluding the directory: that hides the model from the renderer instead (\`service.model-excluded\`) |
258
+ | \`subsystem.view-id-collision\` | a document the root project reads declares a view whose id is one loam GENERATES into \`architecture/subsystems.likec4\`. LikeC4 merges every file of a project into one model and view ids are a flat namespace there, so the renderer refuses the WHOLE project and draws nothing — while every other check here stays green. Under \`--all\` the census covers every EXTENDING model's authored view ids as well as the landscape's, because those documents are in the same root project now; a standalone model's are not, since its views live in a project of its own. Only a real collision is reported: a \`subsystem_\`-looking id matching no current subsystem breaks nothing yet, and fires on the run after somebody creates that subsystem | rename the view in the file the finding names. Ids beginning \`subsystem_\` are loam's to mint; \`architecture/subsystems.likec4\` is generated (\`loam subsystem sync\`) and is never hand-edited |
244
259
  | \`landscape.merge-conflict\` | the fleet map still holds conflict markers — it is two halves of two different maps, and every cross-service check reads it. Checked BEFORE the parse and returns like \`landscape.invalid\`: nothing is concluded from the file, so no \`landscape.service-unmodelled\` fires behind it | resolve it keeping BOTH sides' elements and edges; ten people adopting ten services into one landscape.likec4 in the same week is how it happens. \`loam archive\` refuses on it too (\`merge-failed\`, not overridable with \`--approve\`) |
245
260
  | \`landscape.service-unmodelled\` | a \`services/<svc>/\` no element in the landscape resolves to | draw it, or bind an existing element with \`metadata { service '<svc>' }\` — the fleet map is incomplete until you do |
246
261
  | \`landscape.service-undocumented\` (warn) | a landscape element with no \`services/<id>/\` | document the service, bind the element to the directory it means, or tag it \`#external\` if it is not ours |
247
262
  | \`landscape.binding-unknown\` | an element's \`metadata { service }\` names a directory that does not exist | fix the id or create the service — a binding is a claim, and this one is false |
248
263
  | \`landscape.binding-duplicate\` (warn) | two elements resolve to the same \`services/<id>/\` | every element→service join picks one of them arbitrarily, so the other's edges are filed under a service that does not own them — bind each element to the directory it actually is, or tag the stray one \`#external\` |
264
+ | \`landscape.service-isolated\` (warn) | an element resolves to a \`services/<id>/\` and no edge anywhere in \`architecture/\` touches it, while that service's own \`model.likec4\` declares a call across its boundary. The map draws the service and nothing reaches it, so every cross-service check is vacuous over it — and \`loam validate --service\` cannot see it. Evidence-gated: a service whose model declares no cross-boundary call is silent here, and the adopt brief's \`landscape.touched\` is the only place that state is named. Binary, too: ONE edge closes it, so a map drawing one of five attested calls is silent — the check is touched/untouched, not a set difference, and the calls left undrawn are named nowhere. An \`#external\` element is never the subject, whatever it is bound to; the touched side applies no tag filter — an edge on an \`#external\` element the service owns (a kind-tagged topic, say) still touches it, exactly as the adopt brief's \`landscape.touched\` counts it | draw each attested call as one edge on the element, collapsed to the service, carrying \`metadata { op '<operationId>' }\` for a call or \`publishes\`/\`consumes\` for an event, naming the other party as the map already spells it — never a second element |
249
265
  | \`landscape.kind-tag-graded\` | the \`specification\` block declares \`#external\` or \`#platform\` on an element KIND, and at least one element of that kind stands for a real \`services/<id>/\` — bound with \`metadata { service }\`, or titled like one. Since LikeC4 1.59.0 a kind's tags land on every element of it, and loam reads both tags as "not ours, stop grading", so the exemption is silencing the checks for a service of yours; the run would otherwise end on \`landscape.matched\` over a map nothing checked | remove the tag from the kind and write \`#external\` / \`#platform\` on the individual elements that really are foreign — or stop drawing your own services as that kind. A graded tag on a kind whose elements stand for no directory of yours (the \`element topic { #external }\` idiom for the topics inside a broker) is correct and stays silent, and any OTHER tag is safe on any kind — loam does not read it |
250
266
  | \`landscape.platform-candidate\` (warn) | an \`#external\` element consumed by three or more services and not tagged \`#platform\` — a hub on its way to making the fleet view unreadable | declare \`tag platform\` in the \`specification\` block and tag the element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then excludes it, and a platform view over \`include * -> element.tag = #platform\` keeps "who depends on it" answerable |
251
- | \`landscape.datastore-private\` (warn) | a datastore element with exactly one consumer service at fleet level — drawn as a peer, it reads as a system in its own right | it is that service's internals: move it into \`services/<id>/model.likec4\` as a nested container and delete it from the landscape, or add the second consumer's edge if another service really reaches the same data |
252
- | \`landscape.datastore-shared\` (warn) | a datastore element consumed by two or more services — the strongest coupling in the fleet, stated rather than inferred | shared means the same DATA: if they read the same tables or keys, keep it drawn and let the warning state the coupling; if they only share a host or cluster (two schemas, two lock paths), model one private store per service and put the blast radius in the runbook |
253
- | \`usecase.capability-unresolved\` | a \`dynamic view\` tagged \`#cap-<slug>\` whose slug matches no declared capability id, or more than one. The tag spells the id with every character outside \`[A-Za-z0-9_-]\` flattened to \`-\` (that is all a LikeC4 tag name accepts) and that flattening is not injective, so \`payments/refunds\` and \`payments-refunds\` both answer to one tag. Silent while \`architecture/capabilities.yaml\` is absent or invalid, like the rest of that family — the FILE is the capability axis's opt-in | zero matches: fix the tag, or declare the capability in the fleet vocabulary (the message offers close ids, each already spelled as the tag you would write). More than one: rename one of the colliding ids — nothing in the tag can say which the flow realizes, and a guess files every hop of it under an owner who never claimed it |
267
+ | \`landscape.datastore-private\` (warn) | a datastore element with exactly one consumer service at fleet level — drawn as a peer, it reads as a system in its own right. Consumers are counted from the map's edges AND from every EXTENDING model's attested outbound calls, so a store whose consuming edges all live in models is graded rather than silent | it is that service's internals: write it inside the element that resolves to that service — the \`extend <fqn> { }\` block of \`services/<id>/model.likec4\` when that model EXTENDS the map, or inside \`<fqn>\` on the map when the map draws that service's containers — and delete the peer. The message names whichever the consumer's model shape allows: a model that declares its own \`specification\` is parsed alone, so \`extend <fqn>\` resolves nothing there and the map is the only placement (migrate the model to the extending shape, or write one when there is none). Either way the two placements give the store the same id, \`<fqn>.<store>\`, and with one consumer either move is free — out of the \`extend\` block onto the map always is, and back into it while no other service's model or map edge names the store, an \`extend\` block being visible to its own service's project alone. Nesting closes no door: loam counts a consumer an EXTENDING model attests, so a second service reaching the same data is \`landscape.datastore-shared\` rather than silence — a consumer whose model stands alone is not counted, its ids being its own file's, so its edge belongs on the map. That second edge resolves only against a store the MAP declares (a per-service project is \`architecture/\` plus one model), so nest it on the map when a second consumer is foreseeable. If another service reaches the same data today, draw its edge and this becomes that warning now |
268
+ | \`landscape.datastore-shared\` (warn) | a datastore consumed by two or more services — the strongest coupling in the fleet, stated rather than inferred. Two shapes: a store drawn as a fleet-level peer, and a store NESTED inside its owner's element that another service reaches — the message adds \` — <owner>'s own store, written as '<store-fqn>'\` for the second, \`<store-fqn>\` being the store's own id, and then says the move is ONE-WAY at this point: the other consumers' edges resolve only against a declaration the MAP holds, so out of the owner's \`extend\` block onto the map is always safe while moving it back INTO the \`extend\` block breaks them (\`c4.invalid\` on that service's model, or \`landscape.invalid\` when the edge is drawn on the map). Consumers are counted from the map's edges AND from every extending model's attested outbound calls | shared means the same DATA: if they read the same tables or keys, keep it drawn and let the warning state the coupling; if they only share a host or cluster (two schemas, two lock paths), model one private store per service and put the blast radius in the runbook |
269
+ | \`usecase.capability-unresolved\` | a \`dynamic view\` tagged \`#cap-<slug>\` whose slug matches no declared capability id, or more than one. The tag spells the id with every character outside \`[A-Za-z0-9_-]\` flattened to \`-\` (that is all a LikeC4 tag name accepts) and that flattening is not injective, so \`payments/refunds\` and \`payments-refunds\` both answer to one tag. Silent while \`architecture/capabilities.yaml\` is absent or invalid, like the rest of that family — the FILE is the capability axis's opt-in, or carried by a flow inside a service's own project (a \`dynamic view\` beside model.likec4), where no capability can be claimed — that arm fires whether or not the vocabulary exists, and the fix is to drop the tag or move the flow to \`architecture/usecases/\` | zero matches: fix the tag, or declare the capability in the fleet vocabulary (the message offers close ids, each already spelled as the tag you would write). More than one: rename one of the colliding ids — nothing in the tag can say which the flow realizes, and a guess files every hop of it under an owner who never claimed it |
254
270
  | \`usecase.requirement-unresolved\` | a \`dynamic view\` tagged \`#req-<slug>\` whose slug names no requirement of the capability that view is about. Six failures with six fixes, and the message says which: the view resolves no \`#cap-\` tag (a \`Requirement-ID\` is unique only inside its own document, so the id has no scope), it resolves two (nothing says which document), the capability has no \`capabilities/<id>/spec.md\`, that document declares no requirements yet, it declares none flattening to this slug, or two of its ids flatten to one slug. It suppresses NOTHING: only a RESOLVED claim counts as a promise kept, so a broken tag leaves \`capability.requirement-unrealized\` firing beside it | follow the message. Several \`#req-\` tags on one view are legal and normal — a flow commonly keeps two of a capability's promises |
255
271
  | \`usecase.step-unbacked\` | a hop of a capability-tagged \`dynamic view\` that NO relationship in the model backs, on either tier. LikeC4 reports no error for it — a dynamic view is allowed to show a call the model never declared — so the diagram renders, the project parses, and every other check stays green. The finding names the file the view was written in, the step's ordinal and title, and the oriented pair | draw the edge in \`model { }\` (with \`metadata { op '<operationId>' }\` where it is a call), or, if the hop is a return step, write it as \`<target> <- <source>\`: a reply is attributed to the CALL it answers, so the \`<-\` spelling looks the hop up under the reversed pair instead |
256
272
  | \`usecase.step-contested\` (warn) | two or more relationships back one hop and they name DIFFERENT operations — an absent \`metadata { op }\` counts as a value there, not as a gap — so nothing can say which one the hop exercises. \`details\` lists every candidate as \`<source> -> <target> "<title>" (op: <id>)\` | give the candidates one operation, or draw the hop between the elements that name exactly one. loam names them rather than picking, because a guessed operation reads exactly like a right one in every rollup built over the flow. It suppresses \`usecase.step-unlinked\` on the same hop: one breach, one finding |
257
- | \`usecase.step-unlinked\` (warn) | the relationship backing a hop carries no \`metadata { op }\` and no \`publishes\`/\`consumes\`, while the hop's CALLER is not a person and its target resolves to a real \`services/<id>/\` — the flow reaches the fleet map and stops short of the contract it drives. \`spine.op-link-missing\` at use-case altitude. A hop FROM a person never fires it — the first hop of almost every sequence diagram is somebody using the app, and an app owes no operationId for a click — and neither does a hop INTO an \`#external\` system, an actor, a database, a cache or a queue: none of those owns a contract to name. A \`publishes\`/\`consumes\` edge already reaches an API — an AsyncAPI one | add \`metadata { op '<operationId>' }\` to the edge in \`model { }\`, or \`metadata { publishes '<message>' }\` / \`consumes\` where the hop is an event |
273
+ | \`usecase.step-unlinked\` (warn) | the relationship backing a hop carries no \`metadata { op }\` and no \`publishes\`/\`consumes\`, while the hop's CALLER is not a person and its target resolves to a real \`services/<id>/\` — the flow reaches the fleet map and stops short of the contract it drives. \`spine.op-link-missing\` at use-case altitude. A hop FROM a person never fires it — the first hop of almost every sequence diagram is somebody using the app, and an app owes no operationId for a click — and neither does a hop INTO an \`#external\` system, an actor, a database, a cache or a queue: none of those owns a contract to name. A \`publishes\`/\`consumes\` edge already reaches an API — an AsyncAPI one. Never on a hop whose caller and provider resolve to the SAME service — two containers of one service, inside its own project or drawn on the landscape — because a service owes no operationId to itself; that is the fourth mechanical guard, and its whole scope. A hop from a service-local flow into ANOTHER service's element (a stand-in model.likec4 declares for a sibling, with no \`metadata { op }\`) still warns, on the service target, exactly as it does on the fleet map | add \`metadata { op '<operationId>' }\` to the edge in \`model { }\`, or \`metadata { publishes '<message>' }\` / \`consumes\` where the hop is an event |
258
274
  | \`feature.unreadable\` | an artifact of a feature in flight could not be read — nothing about that feature was graded, and the rest of the fleet still was | fix the file the finding names; a feature silently skipped would have read as a clean one |
259
275
  | \`sources.unverifiable-from-here\` (ok) | one finding PER SERVICE whose \`sources\` only its own repo can check — a confirmation, not work, so it never trips \`--strict\`, and it is present in \`--service\` runs from the docs repo too | run \`loam validate --service <id>\` from inside that service's repo to actually grade its provenance; the fleet rollup line is derived from these findings |
260
276
  | \`docs.binary-behind\` (warn) | the docs repo's AGENTS.md stamp names a loam NEWER than the one running. This is the one finding that changes what a PASS means: a binary predating a grammar addition does not fail on the newer directive, it reads it as prose, has no join to fail, and reports green — so the corpus was checked less than the run implies. Warn rather than error because a mixed-version fleet is ordinary; \`--strict\` is the lever if it should gate | upgrade loam and re-run. Do NOT edit AGENTS.md — its stamp is correct and this binary is the older half |
@@ -319,7 +335,7 @@ always carries them):
319
335
 
320
336
  | code | what it means | what to do |
321
337
  |---|---|---|
322
- | \`c4.valid\` (ok) | the service's model.likec4 parses and the C4 axis was actually graded | nothing — it is the confirmation |
338
+ | \`c4.valid\` (ok) | the service's model.likec4 parses and the C4 axis was actually graded. Its message says HOW the file was read: "extends the fleet map — N elements · M relationships" for a model with no \`specification\` block of its own, parsed together with \`architecture/landscape.likec4\`; the plain wording for one that stands alone. A trailing \` · K tagged flow(s) graded\` says the service's own project declared use cases and they were graded — the one surface a HEALTHY intra-service flow appears on | nothing — it is the confirmation |
323
339
  | \`delta.valid\` (ok) | the feature's delta.likec4 parses and its tagged change is well-formed | nothing — it is the confirmation |
324
340
  | \`coherence.ok\` (ok) | the feature's axes agree with each other and with the fleet in flight — nothing gates its archive | nothing — it is the confirmation |
325
341
  | \`requirements.covered\` (ok) | every requirement carries at least one scenario | nothing — it is the confirmation |