@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
package/SCHEMA.md CHANGED
@@ -71,11 +71,13 @@ docs/
71
71
  spec.md narrative, then `## Requirements` — `Requirement-ID:` REQUIRED
72
72
  glossary/<term>.md the system's domain vocabulary, one file per term; nesting allowed [authored, opt-in]
73
73
  services/<svc>/ at the root (unfiled), or inside subsystem directories at any depth
74
- model.likec4 boundary C4 (containers/components) [adopt]
74
+ model.likec4 boundary C4 (containers/components); extends the map, or stands alone [adopt]
75
+ likec4.config.json one LikeC4 project per STANDALONE service model, create-only [GENERATED: loam subsystem sync]
75
76
  spec.md living requirements (Requirement/Scenario) [adopt]
76
77
  arch.spec.md living ARCHITECTURE requirements (outbox, retries, alerts; Covers:) [authored]
77
78
  openapi.yaml API contract [adopt / authored]
78
79
  asyncapi.yaml async contract (AsyncAPI 3) [adopt / authored]
80
+ 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
81
  adrs/NNNN-*.md MADR decisions [adopt-seed / authored]
80
82
  runbook.md operational runbook [adopt-draft / authored]
81
83
  health.yaml SLI/SLO, checks, critical deps [authored]
@@ -105,6 +107,38 @@ tagged with the capability it realizes. A flow that spans services does not belo
105
107
  service's directory, and the view's steps join to the landscape's own relationships, which is what
106
108
  makes the flow checkable rather than merely drawn.
107
109
 
110
+ A flow that stays INSIDE one service — the hop sequence behind an arch.spec.md requirement whose
111
+ `Covers:` names containers — cannot live under `architecture/`: loam parses that directory as its
112
+ OWN document set — the map plus its siblings, and nothing under `services/` — so a step naming a
113
+ container does not resolve there, and one such file takes the whole fleet map down
114
+ (`landscape.invalid` plus one `spine.landscape-invalid` per service). That holds however the models
115
+ are shaped: the renderer's root project may also hold every extending model and draw such a step
116
+ perfectly, and loam still grades the map alone, because the map is the document every cross-service
117
+ check reads. It lives beside
118
+ the model instead: every `.likec4` under `services/<…>/<svc>/` other than model.likec4, plus any
119
+ tagged `dynamic view` inside model.likec4 itself, is that service's own LikeC4 project — for an
120
+ extending model, the map plus those documents; for a standalone one, the set
121
+ `services/<svc>/likec4.config.json` registers for the renderer — and `loam validate --service <id>`
122
+ (and `--all`, per service) reads exactly that set, whether or not the project file exists.
123
+ `usecases/<name>.likec4` is the convention; `views.likec4` reads the same. Tag the view
124
+ `#req-<Requirement-ID>` of a requirement in that service's spec.md or arch.spec.md. The tag has to
125
+ be DECLARED somewhere the project can see: for a standalone model that is model.likec4's own
126
+ `specification` — the one block that project has; for an extending model it is
127
+ `architecture/landscape.likec4`'s block, or a tags-only `specification { tag req-… }` beside the
128
+ `extend` (legal, and it lands the tag in the project's specification). Declaring it in BOTH is a
129
+ duplicate, and the code depends on which document carries the second declaration — the finding
130
+ lands where the file is, not at fleet altitude: `c4.invalid` (an ERROR, and the model's grading is
131
+ suspended behind it) when the tags-only block is in model.likec4, `usecase.flow-invalid` when it is
132
+ in a sibling. `c4.fleet-project-invalid` is the fleet-scope class one step out: TWO SERVICES each
133
+ declaring the same tag locally, where no per-service project sees the collision.
134
+ model.likec4 stays THE model:
135
+ `c4.valid`, `Covers:` and every other model check read it alone, and a sibling that does not parse
136
+ is one `usecase.flow-invalid` while the model keeps grading. Files loam does not read: a `.likec4`
137
+ in a subsystem directory, in a feature directory outside `delta.likec4`/`usecases/`/`deployment/`,
138
+ the generated `subsystems.likec4`, a sibling the service's own `likec4.config.json` `exclude`
139
+ covers (it is in no project the renderer loads either), and every UNTAGGED `dynamic view`
140
+ anywhere — a diagram, never a use case.
141
+
108
142
  A feature **brings** one the way it brings every other axis: `features/<FEAT>/usecases/<name>.likec4`
109
143
  is a views-only document that `loam archive` copies into `architecture/usecases/` and
110
144
  `loam unarchive` takes back. Create-only, and graded before the copy against the map the same merge
@@ -112,6 +146,176 @@ would leave behind — so a flow may draw a hop into a service the feature's own
112
146
  and a hop into an element the merge does NOT land is refused at plan time rather than archived into
113
147
  a map the next reader's `loam validate --all` fails on.
114
148
 
149
+ ### Two shapes of a service model
150
+
151
+ `services/<…>/<svc>/model.likec4` comes in two shapes, and **the file's own grammar decides which**
152
+ — never a config key, never a flag. loam scans the bytes (comments and strings masked) for a
153
+ top-level `specification { }` block containing an `element` declaration:
154
+
155
+ - an **extending model** declares no element kind. It is parsed INSIDE the fleet's `architecture/`
156
+ project — the landscape plus every other `architecture/**/*.likec4` except the generated
157
+ `subsystems.likec4` — and says what is inside the service with `extend <fqn> { … }` on the element
158
+ the map already binds to the directory. `<fqn>` is the FULLY-QUALIFIED id: `extend orderService`
159
+ resolves to nothing when the element is `marketplace.orderService`. No `specification` is needed,
160
+ no partner is re-declared, and `model {}` or no model at all are both fine. `extend` accepts
161
+ `#tags` and a `metadata { }` block on the element it opens (merged into it — the map's `service`
162
+ binding survives) and REFUSES `description`, which belongs on the map's declaration. This is the
163
+ shape `loam adopt` briefs, and the shape `loam init --create` scaffolds a repository for.
164
+ - a **standalone model** declares at least one `element <kind>` inside a `specification { }` block.
165
+ It is parsed ALONE, exactly as every model was before this shape existed; it may re-declare the
166
+ partners it names; and it renders only as a LikeC4 project of its own. Legal forever — a service
167
+ whose docs are reviewed from its own repository, a model adopted before the fleet had a map — and
168
+ not what a new model is written as.
169
+
170
+ **Who declares kinds and tags.** In the extending shape, `architecture/landscape.likec4` is the one
171
+ `specification` block: a kind a model needs and the map does not declare (`element database`,
172
+ `element queue`) is added THERE, once, and a missing one is a single parse error on the model. A
173
+ tags-only `specification { tag req-CANCEL }` beside an `extend` is legal and lands the tag in the
174
+ project's specification — but a tag the map already declares, declared again there, is a duplicate
175
+ blamed on both files. In the standalone shape the model declares everything it uses, which is
176
+ exactly why its copies of the map's elements can drift.
177
+
178
+ **Seven grades come with the pair**, and one existing grade is narrowed:
179
+
180
+ | code | shape | what it says |
181
+ |---|---|---|
182
+ | `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 |
183
+ | `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 |
184
+ | `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 `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 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 |
185
+ | `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 |
186
+ | `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 |
187
+ | `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 |
188
+ | `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 |
189
+ | `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 |
190
+
191
+ `c4.invalid` and `c4.valid` are unchanged codes with adjusted scope: for an extending model every
192
+ error the per-service project raises is THIS model's, including one the parser blames on the
193
+ landscape, because the map parsed clean on its own; and `c4.valid`'s message reads
194
+ `extends the fleet map — N elements · M relationships`, followed by ` · K tagged flow(s) graded`
195
+ when the service's own project declared any (the one surface a healthy intra-service flow appears
196
+ on). When the map itself does not parse, an
197
+ extending model cannot be read at all: neither code is emitted, and `spine.landscape-invalid` says
198
+ so with the clause ` — and model.likec4 extends it, so the model cannot be read either`.
199
+
200
+ Every other model-dependent check reads the same slice it always read — the service's OWN elements
201
+ and relationships. For an extending model that is what the project holds minus what the map alone
202
+ holds, plus the **partners** its own edges point at and their ancestors, so `health.yaml`'s
203
+ `kafka` still resolves when the only edge names `kafka.orderEvents`. Nothing reads the map's edges
204
+ as the model's.
205
+
206
+ **What loam grades is still the `architecture/` project alone.** The renderer's root project is
207
+ WIDER than the fleet map: it also holds every extending model. loam's fleet checks — the spine, the
208
+ bindings, the fleet-shape advisories, and the steps of every `architecture/usecases/` flow — read
209
+ the landscape plus its `architecture/` siblings and nothing else, whatever shape the models take.
210
+ So a cross-service flow names SERVICE elements: a hop naming a container an extending model
211
+ declares renders perfectly and is `landscape.invalid` for loam. That is deliberate, and the reason
212
+ is blast radius — a service's model is graded on its own merits, and one broken model must never
213
+ blank the fleet's own map. A container-level flow has its own home beside the model that declares
214
+ the container. The same holds for a feature's `features/<FEAT>/usecases/` flow, and the gate says
215
+ so before the merge rather than after it: the preview stages no service model, and it ROUTES a
216
+ nested addition into the model that owns that service's interior exactly as `loam archive` would.
217
+ So a hop naming `<service-fqn>.<container>` resolves in neither the preview nor the tree the archive
218
+ leaves, and `usecase.flow-invalid` refuses — where the un-routed preview resolved it, passed the
219
+ gate `--approve` cannot override, and landed a flow the next `loam validate --all` failed on.
220
+
221
+ **Private stores.** A store one service owns is nested UNDER that service's element — written in
222
+ the service's `extend <fqn> { store = database … }` block, or inside the service's element on the
223
+ map when the map draws that service's containers. The `extend` placement needs an EXTENDING model:
224
+ a model that declares its own `specification` is parsed alone, so `extend <fqn>` resolves nothing
225
+ there, and for such a service the map is the only placement (or migrate the model — see "Two
226
+ shapes of a service model" above). `landscape.datastore-private` names whichever of the two the
227
+ consumer's model shape allows. Drawn beside the services instead it reads as a
228
+ system in its own right (`landscape.datastore-private`). Nesting closes no door: loam counts a
229
+ consumer a service's EXTENDING model attests exactly as it counts one the map draws, so a nested
230
+ store two services reach is `landscape.datastore-shared` — the store's owner plus every service
231
+ whose model draws an outbound call at it or below it. The second consumer draws that call as a
232
+ container-level edge to `<owner-fqn>.<store>` in its OWN model, and the service-level twin on the
233
+ map as usual. Which of the two placements to choose is decided by that edge: a per-service project
234
+ is `architecture/` plus one model, so the edge resolves only when the MAP declares the store —
235
+ written in the owner's `extend` block, the same edge is `c4.invalid` on the consumer ("Could not
236
+ resolve reference to Referenceable named '<store>'"). Nest it on the map when a second consumer is
237
+ foreseeable, and in the model when it is not. The choice is cheap in ONE direction, and the
238
+ direction is the whole of the rule. The two placements give the store the same id,
239
+ `<owner-fqn>.<store>`, so moving the declaration OUT of the owner's `extend` block into the owner's
240
+ element on the map rewrites no edge and no `Covers:` line — measured on `examples/docs`, where
241
+ moving `db` out of order-service's `extend` block into `orderService` on the map left the model at
242
+ 12 elements · 8 relationships, left `Covers: marketplace.orderService.db` in its `arch.spec.md`
243
+ resolving, and left checkout-web's container-level edge to it resolving. The move BACK is free only
244
+ while no other service names the store: with checkout-web's edge drawn, putting `db` back into
245
+ order-service's `extend` block is `c4.invalid` on checkout-web's model ("Could not resolve
246
+ reference to Referenceable named 'db'"), or `landscape.invalid` plus one `spine.landscape-invalid`
247
+ per service when that second edge is drawn on the map. `landscape.datastore-shared` needs a second
248
+ consumer to fire, so it always prints in the state where map→extend breaks, and its message says
249
+ so. The consumer census is fleet-scope: it reads
250
+ EXTENDING models only, because their ids are the map's fully-qualified ids and the join is exact,
251
+ while a standalone model's ids are its own file's and are never matched to a landscape element.
252
+
253
+ **Renderer consequences.** The root project holds the map **and** every extending model, so one
254
+ `npx likec4 start` at the docs root draws the fleet and the inside of every adopted service, and
255
+ the project picker holds one entry. A standalone model is the exception, and it costs two files:
256
+ ONE entry in the root `likec4.config.json`'s `exclude` covering its directory (its kinds would be
257
+ duplicates of the map's inside one project), plus its own `likec4.config.json`, which is what makes
258
+ it a project of its own.
259
+ `loam subsystem sync` maintains both halves — it writes the per-service file for standalone models
260
+ only, and rewrites the root `exclude` so that it holds `services/<tree>/**` for exactly the models
261
+ that stand alone. The rewrite removes the wholesale `services/**` and every `services/<tree>/**`
262
+ naming a directory that IS a service, keeps every other entry in the order the team wrote it (a
263
+ `services/legacy/**` for a directory that is no service survives untouched), and appends the
264
+ standalone entries sorted. It runs when at least one extending model exists, or when a standalone
265
+ model's directory is not covered — so a repository whose models all stand alone behind `services/**`
266
+ is never touched, and a fresh scaffold that gains its first standalone model earns the entry on the
267
+ next sync. `--json` reports it as `projects.exclude`, with what was added and removed; the text view
268
+ prints one line when it rewrote. An entry covers a directory when it names that directory —
269
+ `<dir>`, `<dir>/`, `<dir>/*`, `<dir>/**` and `<dir>/**/*` all do, and a wildcard segment in the
270
+ middle (`services/*/legacy`) matches one level — or when it is a raw path prefix of it from a `/`
271
+ boundary, which is what the renderer measures: `services/pay` covers `services/payment-service`,
272
+ while `services/pay/**` covers nothing there. An entry is normalised the way the renderer
273
+ normalises it before anything is matched: a leading `./`, an inner `/./` and an inner `//` are
274
+ resolved away, a TRAILING `/` is not (bare `architecture` also hides `architecture-old/`,
275
+ `architecture/` does not), `.//` leaves a literal `.` directory and hides nothing, and a BACKSLASH
276
+ is a literal character to the matcher, never a separator — `services\order-service\**` hides
277
+ nothing at all, so loam neither grades on it nor rewrites it. Measured at the 1.59.2 pin against
278
+ `npx likec4 validate .` file counts; the table is in `src/core/c4/root-project/exclude.ts` and
279
+ pinned in `test/root-exclude.test.ts`. A directory the list already covers in
280
+ any of those spellings earns no second entry. `projects.exclude` carries `updated`, `entries`,
281
+ `added`, `removed` and `unreadable` — the last is `true` when the root file exists and loam could
282
+ not read an `exclude` list out of it, which is never the same answer as `entries: []`, and the list
283
+ is then reported untouched. Beside it, `--json` carries
284
+ `projects.created` (the per-service project files written) and `projects.removed` (POSIX paths
285
+ relative to the DOCS ROOT, deleted this run, sorted by path; `[]` otherwise) — and
286
+ `projects.created` is spelled the same way. Both are relative to the docs root, not to the
287
+ repository root, which differ whenever `docsDir` is a subdirectory. A fresh repository is
288
+ scaffolded with
289
+ `exclude: ["**/node_modules/**", "features/**"]`.
290
+
291
+ **Migrating a standalone model to the extending shape** is a documented recipe an agent performs;
292
+ loam does not rewrite a model, because the comments in a real one are somebody's and not loam's to
293
+ reflow. In order:
294
+
295
+ 1. Write into `architecture/landscape.likec4` any element kind the model uses that the map does not
296
+ declare (`element database`, `element queue`), and any element the model reaches that the map
297
+ does not draw — an external system, a broker's topic — as the fleet's own declaration.
298
+ 2. Delete the model's `specification` block.
299
+ 3. Delete every element the map declares: the service's own element, and every re-declared partner.
300
+ 4. Wrap what is left in `model { extend <fqn> { … } }`, `<fqn>` being the fully-qualified id of the
301
+ map's element for this service. Child ids inside the block stay relative; internal edges may be
302
+ written inside it.
303
+ 5. Move the cross-boundary edges outside the `extend` block and spell both endpoints in full —
304
+ `marketplace.orderService.outbox -> kafka.orderEvents`.
305
+ 6. Requalify every id that names one of those elements from outside the file: the `Covers:` lines in
306
+ `arch.spec.md`, the `of <element>` and `include` lines of any `views` block, and the hops of any
307
+ `#req-`-tagged flow beside the model. There is no suffix matching anywhere — `orderService.db`
308
+ does not answer for `marketplace.orderService.db` — so every model-local nested id has to be
309
+ rewritten. `health.yaml`'s `dependencies:` are the one place that usually needs no edit, because
310
+ a dependency resolves when an element of the own slice answers to it **by id, `metadata { service }`
311
+ binding or title**, and the slice holds the service's own elements plus every endpoint of the
312
+ model's own edges and each endpoint's ANCESTORS: `kafka` still answers through an edge naming
313
+ `kafka.orderEvents`, and a peer service still answers through the binding on the map's element.
314
+ Prefer the service id or a top-level system id there — those two spellings resolve under either
315
+ model shape, and a model-local nested id does not.
316
+ 7. Delete `services/<…>/<svc>/likec4.config.json`.
317
+ 8. Run `loam subsystem sync` (it rewrites the root `exclude`), then `loam validate --all`.
318
+
115
319
  ## Subsystems — a navigable tree under `services/`
116
320
 
117
321
  `services/` may be one flat level (the compatibility case, and forever legal) or an unbounded tree
@@ -134,6 +338,13 @@ A directory under `services/` is classified **three ways, never two**:
134
338
  subsystem's marker while another moves a service in, and the group directory would otherwise be
135
339
  read as one empty service. The fleet is never reported smaller than it is.
136
340
 
341
+ A directory named for one of the service's own subdirectories (`adrs/`, `usecases/`, `ui/`) is
342
+ service interior wherever it sits: the walk never reads one as a service or a subsystem, and never
343
+ counts one as the subdirectory that makes its parent a group. Writing the documented
344
+ `services/<id>/usecases/<name>.likec4` slot into a directory that holds no artifact FILE yet used
345
+ to make the walk read `usecases` as a service and its parent as an unmarked group — three errors
346
+ earned by following the instruction `loam init` had just scaffolded.
347
+
137
348
  `subsystem.yaml` is a **marker, not a manifest**:
138
349
 
139
350
  ```yaml
@@ -165,6 +376,30 @@ reports). The output is deterministic and line-oriented — subsystems sorted by
165
376
  one `include` per line, no timestamps, no absolute paths — so it is byte-reproducible on any machine
166
377
  and two concurrent moves into different groups merge in git without intervention.
167
378
 
379
+ **Styling.** Each generated view carries `global style subsystems` — after its `title` and
380
+ `description`, before its `include`/`group` — exactly when the `architecture/` project declares a
381
+ global style with that id: `global { styleGroup subsystems { style element.tag = #external { color gray } } }`
382
+ (or the single-rule `global { style subsystems … }`), in the landscape or in ANY `.likec4` under
383
+ `architecture/` the root `likec4.config.json`'s `exclude` does not cover — a `palette.likec4` beside
384
+ the map works, and one under an excluded path is loaded by neither the renderer nor loam.
385
+ `architecture/landscape.likec4` is the one exception: loam keeps the map in that set however the
386
+ list is written, because an architecture project with no documents in it reads as a fleet that
387
+ models nothing. With the targeted tag declared in the `specification` block.
388
+ Declaring it is the whole opt-in. loam reads only that the id is declared, never what the group
389
+ says, and writes nothing when it is not — a reference to an undeclared style is a parse error that
390
+ blanks the whole `architecture/` project in the renderer. When the project declares style groups
391
+ under other names only, the file opens with one comment line naming them and saying none is
392
+ `subsystems`, and the views carry the renderer's defaults; there is no single-group fallback.
393
+ Both halves restale the file once (`subsystem.views-stale`) until the next `loam subsystem sync`:
394
+ declaring or removing the group named `subsystems` changes whether the views carry the
395
+ `global style subsystems` line, and declaring or removing a group under ANY OTHER name changes the
396
+ disclosure comment that lists what is available. Whether a service's own views can borrow that group depends on the shape of
397
+ its model: a model that EXTENDS the map is in the root project, so a `global styleGroup` declared
398
+ under `architecture/` reaches the views beside it and referencing the id there is legal; a model
399
+ that STANDS ALONE is a project of its own, and a group declared under `architecture/` does not reach
400
+ it — those views need a declaration of their own, or the reference is the parse error that blanks
401
+ that project.
402
+
168
403
  Each view carries the **label a human reads**: `title`, and `description` when the marker authors
169
404
  one. The title is the subsystem's **path** — every marked ancestor's label and its own, joined with
170
405
  ` / `, so `services/platform/order-flow` reads `Platform / Order flow` — and each label is the
@@ -204,6 +439,85 @@ does not parse standalone (its `include` lines reference elements the landscape
204
439
  holds **static** views, whose contents loam never reads in any document. Only the LikeC4 renderer,
205
440
  which merges the whole `architecture/` project, resolves its includes.
206
441
 
442
+ `sync` refuses to rewrite this file while the `architecture/` project cannot be read. The render
443
+ needs the map's elements and its global-style census, and a tolerant "no elements" answer wrote a
444
+ file holding title and description alone — the `global style subsystems` line, every `include` and
445
+ the boundary comment gone — while reporting `updated … 1 subsystem view(s)` and answering `current`
446
+ ever after. So: a load error falls back to the landscape FILE when that parses on its own (a
447
+ duplicate `subsystems` group declared in two `architecture/` documents costs nothing), and when
448
+ that fails too the facts are UNKNOWN. With an existing views file, `sync` then writes nothing about
449
+ it, prints why, and reports the `--json` action `blocked`, with `blocked.reason` reading
450
+ `landscape-invalid` — exit 0, because `loam validate --all`'s `landscape.invalid` is the gating
451
+ surface. With no file yet, and inside a tree-changing verb's commit window (`new`/`move`/`rm`/
452
+ `rename`, whose contract is that the file lands with the change), the tolerant shape is written as
453
+ before and the same note is printed.
454
+
455
+ The tree's **second renderer file** is per service — `services/<…>/<svc>/likec4.config.json`,
456
+ which `loam subsystem sync` also writes, beside every **standalone** `model.likec4` that has none.
457
+ An extending model needs no such file and never gets one: it belongs to the root project, which is
458
+ where it parses, and a project file beside it is `service.likec4-config-stray` — which `sync`
459
+ DELETES, in the same journaled commit as everything else it writes. A standalone model
460
+ needs one because it cannot share a project with the map — it declares its own kinds, and inside
461
+ one project every declaration is a duplicate blamed on the map too — so `sync` also keeps the root
462
+ `exclude` covering exactly the directories of the models that stand alone. Before extending models
463
+ existed, `services/**` was excluded wholesale and every model needed the file; what that exclusion
464
+ cost went unstated for a release: an
465
+ adopted service was a box on the fleet map with nothing renderable inside it. Measured at the
466
+ pinned likec4, a project file INSIDE a service directory is registered as a project of its own
467
+ regardless of the exclusion, so a docs root holding standalone models loads `fleet` plus one
468
+ project per such service, each carrying LikeC4's own `index` view of the containers beside the
469
+ authored one, and a model that does not parse breaks only its own project. (An extending model
470
+ is inside `fleet`, so a model that does not parse breaks the root project instead — the trade the
471
+ shape makes, and `c4.fleet-project-invalid` is what names it before a reader meets it in the
472
+ renderer.) The file is **create-only for a standalone model** —
473
+ never compared, never rewritten — under the root config's ownership rule, not the
474
+ views file's: there is no `-stale` grade because nothing can make it stale. It is removed in
475
+ exactly one state, and only there: beside a model that EXTENDS the map. That nested project claims
476
+ the model out of the root project — the workspace loads two projects, the model is Invalid inside
477
+ its own one (the map's kinds are not in there with it) and `export json --project fleet` then loses
478
+ the service's containers. Which of the two the workspace does was measured both ways at the 1.59.2
479
+ pin: on a seeded fleet whose model extends a top-level id the containers went; on `examples/docs`
480
+ the root project kept the model and the export was byte-identical. What is true in every state is
481
+ the second project itself — `likec4 validate .` at the docs root refuses without `--project` from
482
+ the moment that file exists. So it is not a harmless leftover, and `loam subsystem sync` deletes
483
+ it. One beside a model that is GONE stays: that one really is the team's — though the same second
484
+ project it registers is there too, so `likec4 validate .` and `likec4 build` at the docs root need
485
+ `--project` while it stands (ROADMAP's "Known and carried, 2026-09-04"). Its bytes are a
486
+ function of the id alone (two keys, `name` and `title`), so placement never changes them and a
487
+ `subsystem move` carries the file with no write of its own — it rides inside the rename the move's
488
+ transaction already journals (in a git-backed repo, commit the files `sync` wrote first: `move`
489
+ refuses over untracked paths, `move-uncommitted`); a compare would only fight the keys LikeC4 lets a
490
+ team add (`title`, styles). `name` is the id with every
491
+ `.` folded to `-` (LikeC4's name grammar refuses the byte; loam's id grammar admits it), prefixed
492
+ `service-` when the result would equal the root project's `fleet` (a nested project of that name
493
+ is silently registered as `fleet-1`) or LikeC4's reserved `default`; `title` is the id verbatim,
494
+ and the title is what the renderer's project picker shows. The name is **not injective**, and
495
+ that is disclosed rather than escaped away: an id spelled billing.core beside a service literally
496
+ named billing-core, or `service-fleet` beside `fleet`, share a name and LikeC4 suffixes the later
497
+ one `-1`; no loam
498
+ finding grades it, because the picker still reads the id. Order is a gate — the root file first
499
+ (`loam init --create` writes it, `loam doctor` prints it), then `sync`: without the root file
500
+ nothing is written and the text output says so. No model, no file: a spec-only service is owed
501
+ nothing and gets its file on the first `sync` after its model lands. No legal id, no file either: a
502
+ directory whose name breaks the service-id grammar (`service.id-invalid`) would get a `name` the
503
+ renderer silently drops, and a create-only file the renderer ignores would silence the grade for
504
+ good, so `sync` skips it and the rename that clears `service.id-invalid` earns the file.
505
+ `validate --all` grades presence only — one warning per service with
506
+ a STANDALONE model and no file, `service.likec4-config-missing`, on the fleet `landscape` target,
507
+ never an error, never in `--service`, `status` or `doctor`; the fix is `loam subsystem sync`, never
508
+ a hand-written file. Its two siblings grade the extending shape's version of the same question —
509
+ `service.likec4-config-stray` for a project file that should not be there, `service.model-excluded`
510
+ for a root `exclude` entry that hides the model from the renderer — and `sync` is the repair for
511
+ both: it removes the stray and rewrites the entry. A third reads the same list and is nobody's to
512
+ rewrite: `landscape.excluded` says an entry covers `architecture/landscape.likec4` itself, so the
513
+ renderer loads no map at all — `sync` maintains only the `services/` entries and will not touch that
514
+ line, and the finding quotes it so its author can. It also suspends
515
+ `c4.fleet-project-invalid` for that run, because the renderer's project is exactly what the entry
516
+ broke and grading it would report the one line once per extending model. A
517
+ feature's `delta.likec4` is deliberately out of scope: the directory is transient and
518
+ `loam archive` moves it where a project file would still be registered, so every shipped feature
519
+ would keep a dead project in the list forever.
520
+
207
521
  A move is **one journaled transaction over N directory renames plus the regenerated views file**
208
522
  (`loam subsystem move <name>... --into <sub|.>`; `rename` is a one-rename move; whole subtrees move
209
523
  as one rename). It refuses only when git reports uncommitted changes under a directory being moved
@@ -225,6 +539,7 @@ lives *today*.
225
539
  | Capability | requirement `Capability:` (list; also `Capabilities:`) | `architecture/capabilities.yaml` OR `capabilities/<cap>/spec.md` | declared capability id, `/` allowed for nesting |
226
540
  | 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 |
227
541
  | 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 `-` |
542
+ | 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 |
228
543
 
229
544
  These keys are independent: an operation id is not a permission and a service id is not inferred
230
545
  from a feature directory. A file can therefore be structurally valid while a join is broken; the
@@ -647,7 +962,21 @@ There is no manifest of services: the directories under `services/` **are** the
647
962
  why the two are graded differently;
648
963
  - two elements resolving to the **same** `services/<svc>/` → `landscape.binding-duplicate`
649
964
  (**warning**): every element→service join picks one of them arbitrarily, so the other's edges are
650
- quietly filed under a service that does not own them.
965
+ quietly filed under a service that does not own them;
966
+ - an element resolving to a `services/<svc>/` that no edge in the map touches, while that service's
967
+ `model.likec4` parses and declares at least one call across its own boundary →
968
+ `landscape.service-isolated` (**warning**, never gating): the service is drawn and invisible to
969
+ every cross-service check, and the finding names the attested calls to draw as edges on the
970
+ existing element, collapsed to the service. Evidence-gated on purpose — a service with no model,
971
+ an unparseable one (`c4.invalid`), or a model reaching nothing is silent here, because an edgeless
972
+ element that is TRUE is a fact and an invented edge is a dependency the fleet then plans against;
973
+ the adopt brief's `landscape.touched` is the only place that silent state is named. And BINARY:
974
+ one edge closes it, so a map drawing one of five attested calls is silent too — the check is
975
+ touched/untouched, never a set difference over the attested calls, because loam cannot tell a
976
+ call the map forgot from one the fleet deliberately does not draw at its own altitude. "Edge in
977
+ the map" means an edge in the `architecture/` PROJECT — the landscape plus every other `.likec4`
978
+ under it, minus the generated `subsystems.likec4` — and `loam adopt` reads the same set. An
979
+ `#external` element is never the subject, whatever directory it is bound to.
651
980
 
652
981
  **`architecture/landscape.likec4` is required, not optional.** Its absence is a finding, never a
653
982
  skipped check: an **error** as soon as `services/` holds at least one service directory, and a
@@ -658,7 +987,7 @@ it cannot be silence: with no map, the whole cross-service layer — the C4↔AP
658
987
  `landscape.service-unmodelled`, every derived view — computes nothing and *reports* nothing, which
659
988
  reads exactly like a clean fleet. `loam init` scaffolds the file (empty, commented, parsing clean)
660
989
  so a fresh docs repo starts from a real one rather than from that ambiguity, and `loam adopt` briefs
661
- the block each service still owes it.
990
+ the write each service still owes it — the block, or the edges on an element no edge touches.
662
991
 
663
992
  Absences inside a service directory are graded too. A directory with no `spec.md` is a **warning**
664
993
  (`service.no-spec`) — partial adoption is a supported state, but the checks that file feeds are
@@ -946,7 +1275,9 @@ the source for tests.
946
1275
  diff, tagged to the feature.
947
1276
  - **`loam archive <FEAT>`** merges the delta into the living state on three axes — **requirements**
948
1277
  (`spec.md`, and `arch.spec.md` beside it: the two files ride one merge code path, parameterized by
949
- filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4`) — then archives the
1278
+ filename), **API** (`openapi.yaml`), **architecture** (`landscape.likec4`, plus the
1279
+ `services/<…>/<svc>/model.likec4` of each service whose EXTENDING model owns an addition nested
1280
+ inside it) — then archives the
950
1281
  feature, so the living state stays complete. Archived deltas are the evolution history (like
951
1282
  `git log`). `--dry-run` prints the whole plan and writes nothing.
952
1283
  - **Coherence gate:** `loam validate --feature` checks the three axes agree (C4 edge `op` ↔ OpenAPI
@@ -1106,10 +1437,16 @@ the CI escalation.
1106
1437
  such entry into a false `covers.unknown` "typo" pointing at the spec, when the truth was the file.
1107
1438
  - `health.dependency-unmodelled` (service scope): a `dependencies:` id that nothing in the service's
1108
1439
  OWN `model.likec4` answers to — by element id, `metadata { service }` binding, or title; exact
1109
- match, with a did-you-mean hint. Deliberately the service model and never the landscape: the
1440
+ match, with a did-you-mean hint. Deliberately the service model and never the landscape read on
1441
+ its own: the
1110
1442
  landscape carries only what crosses the service boundary, the model carries everything the service
1111
- touches (a private store as a nested container — `landscape.datastore-private` is the check
1112
- pushing it there), and that is the set the on-call file must agree with.
1443
+ touches (a private store nested inside the service's own element — `landscape.datastore-private`
1444
+ is the check pushing it there, and `landscape.datastore-shared` is what a second consumer of that
1445
+ nested store now earns), and that is the set the on-call file must agree with. For an extending model
1446
+ the set is its own SLICE: the elements it adds, plus the partners its own edges point at and their
1447
+ ancestors — so a dependency named `kafka` resolves through an edge that names `kafka.orderEvents`,
1448
+ and a peer service resolves through the `metadata { service }` binding on the map's element the
1449
+ model points at. A partner the model never reaches still does not answer, which is the check.
1113
1450
 
1114
1451
  The rest of the machinery treats the axis as what it is — requirements. `loam archive` merges an
1115
1452
  `arch.spec.md` delta into the living one through the same code path as `spec.md` (same delta
@@ -1497,6 +1834,21 @@ turn a mistake into a green fleet. And a run that could not READ the flows (a si
1497
1834
  an `architecture/` that did not parse) suspends `capability.requirement-unrealized` entirely: loam
1498
1835
  did not look, which is never the same answer as "there is nothing there".
1499
1836
 
1837
+ **A `#req-` tag inside a service's own project is scoped by the SERVICE, not by a capability.** A
1838
+ `dynamic view` beside model.likec4 carrying `#req-<slug>` resolves against the `Requirement-ID`s of
1839
+ that service's spec.md and arch.spec.md together — flattened by the same rule, an id both declare
1840
+ resolving once, and `many` when the two documents declare ids that flatten alike, because a
1841
+ Requirement-ID is unique inside ONE document and a flow beside the model has two.
1842
+ `usecase.requirement-unresolved` there has four arms: no document at all, no identified requirement,
1843
+ none flattening to the slug (close ids offered), or two that do. No `#cap-` tag belongs there
1844
+ (`usecase.capability-unresolved` says so, vocabulary or not); `usecase.step-unlinked` never fires
1845
+ on a hop whose caller and provider resolve to the same service — a service owes no operationId to
1846
+ itself — and still fires on a hop from such a flow into another service's element (a stand-in the
1847
+ model declares for a sibling, with no `metadata { op }`), exactly as on the fleet map; and a
1848
+ resolved service-local claim is the join being checked and nothing more today: it
1849
+ enters no capability `keptBy`, counts toward no `Covers:` or `c4.uncovered`, appears in no pack,
1850
+ `loam list capabilities`, `loam diff` or `loam delta`, and no feature carries one yet.
1851
+
1500
1852
  **Both corpora are reported together, and neither is derived from the other.**
1501
1853
  `loam list capabilities --json` carries `keptBy` (the flows keeping a promise) beside `realizedBy`
1502
1854
  (the service requirements realizing it) on every `requirements[]` row, with the same three-state
@@ -1770,6 +2122,44 @@ unparseable delta. A living declaration sharing its line with another statement
1770
2122
  splice map cannot see around — its statement scan runs to the end of the line) refuses the same way:
1771
2123
  give each top-level declaration its own line, then re-run.
1772
2124
 
2125
+ **Which document an addition lands in.** The C4 merge asks it per declaration, not per feature. An
2126
+ element whose PARENT resolves to a service whose `model.likec4` extends the map is spliced into that
2127
+ model — and so is the interior a delta writes inside a service element the same archive introduces,
2128
+ since those containers carry no feature tag and would otherwise ride onto the map inside their
2129
+ parent's bytes. The parent chain has to be spelled in the map's own ids for either to happen. The
2130
+ splice goes inside the `extend <fqn> { … }` block for the parent — appended when the model has none, as
2131
+ the `model { }` block itself is appended to a model that has none (an empty file, a `views { }`-only
2132
+ file and a tags-only `specification { }` are all extending models, and all three are legal to
2133
+ archive into). A
2134
+ relationship goes to the model of the service owning its SOURCE when the source is interior to such
2135
+ a service, else to the model owning its TARGET, else to the map: a service-level edge stays where
2136
+ every fleet check reads it. A service with a STANDALONE model, or none yet, owns nothing — the map
2137
+ draws its containers, which is legal and is what every repository did before the extending shape.
2138
+ The merged model is proved the way the grader reads it (the `architecture/` documents with this
2139
+ archive's own merged landscape in place, plus the model) before anything is written, and it appears
2140
+ in the `--json` plan as an ordinary write, so `loam unarchive` restores it from the same
2141
+ `.loam-before` snapshot as any other overwritten file. Two limits are deliberate and each refuses at
2142
+ plan time with nothing written: a merged model that does not parse beside the map (`merge-failed`),
2143
+ and a container-level edge whose other end lives in a THIRD document — another service's interior
2144
+ resolves in neither this model nor the map, so that call is drawn service to service on the map. One
2145
+ more is silent: a delta that spells the parent under its own name rather than the map's (the legacy
2146
+ title-join) keeps landing on the map, because splicing its bytes into a document that shares the
2147
+ map's id namespace would carry ids nothing there resolves. It is the WHOLE chain that decides, not
2148
+ the outermost box — `marketplace.orderSvc.cache` under a map that spells the service
2149
+ `marketplace.orderService` is a title join and stays on the map, even though the group
2150
+ `marketplace` is the map's own. A block a delta writes for a NEW service is split — the box to the
2151
+ map, its containers to the model — unless the block also draws a relationship between those
2152
+ containers. Such an edge names its endpoints by their local names inside the block, so splitting it
2153
+ would leave the map naming children it no longer declares; the block rides whole instead, as it did
2154
+ before routing existed. The block decides once for everything written inside it: a container or an
2155
+ edge the delta tags with the feature follows its block rather than its own tag, so it is placed
2156
+ exactly once whichever way the block goes. Re-archiving is idempotent in the models
2157
+ too: an addition whose id, or whose title inside the same bound service, an extending model already
2158
+ declares is skipped exactly as one the map declares is. The title half is service-scoped in a model
2159
+ and global on the map — the map's global join is the fallback for documents written before
2160
+ `metadata { service }` existed, and a service's interior is where two boxes called `api` would
2161
+ collide.
2162
+
1773
2163
  The OpenAPI merge carries components along with operations, and since the path-item/component
1774
2164
  baselines it decides each one **by verdict rather than by reachability**: a component the feature
1775
2165
  declares with a pin equal to its own content is a quote and is skipped (the living document keeps
@@ -2295,14 +2685,28 @@ through the forge, not through loam features:
2295
2685
  **parsed** model and never computes a view — it reads what a view **declares** (a `dynamic view`'s
2296
2686
  tags and ordered steps, already materialized by the parse loam pays for) and never what a view
2297
2687
  **shows**. It renders nothing, and view *computation* is what is superlinear in edges. (If you
2298
- want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start` in
2299
- the docs repo for the fleet map, `npx likec4 start services/<id>` for one service model — and
2300
- scope the views, because an `include *` over a fleet-sized landscape takes minutes. `loam init`
2688
+ want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start`
2689
+ at the docs root, which renders the fleet map AND every adopted service's model: an extending
2690
+ model is part of the root `fleet` project, beside the map, and a standalone one is the project its
2691
+ own `services/<svc>/likec4.config.json` declares (written by `loam subsystem sync`; the Layout
2692
+ section, "Two shapes of a service model", has the rule), and `npx likec4 start features/<FEAT>`
2693
+ for a delta, which is
2694
+ transient and gets no project — and scope the views, because an `include *` over a fleet-sized
2695
+ landscape takes minutes. Two costs arrive with the first STANDALONE service project, and only
2696
+ with it. Measured at the
2697
+ pinned likec4: with more than one project, `likec4 validate` at the docs root refuses without
2698
+ `--project <name>`, so a CI step that ran it bare changes (`likec4 build` and `likec4 export` take
2699
+ every project and need no flag). And the renderer's project list grows by one per such service
2700
+ beside `fleet` — LikeC4 projects are separate models, so a box on the fleet map is not a link into
2701
+ that service's project. A fleet whose models all extend the map pays neither: one project, one
2702
+ picker entry, and the containers of every adopted service reachable from the map's own boxes.
2703
+ `loam init`
2301
2704
  scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it
2302
2705
  never draws **— and never asks for one: a fleet that draws nothing owes no views block.** It also
2303
- scaffolds a `likec4.config.json` that scopes the root LikeC4 project to `architecture/`: loam
2304
- parses every `.likec4` file **alone**, so each one declares its own `specification` block, and a
2305
- renderer given the whole tree at once merges them and reports every declaration as a duplicate.)
2706
+ scaffolds a `likec4.config.json` whose `exclude` is `["**/node_modules/**", "features/**"]`: a
2707
+ feature's `delta.likec4` re-declares the landscape's own identifiers and carries a `specification`
2708
+ block of its own, so it is read alone, and `services/` is deliberately NOT excluded — the models a
2709
+ fresh repository writes extend the map and the root project is the only place they parse.)
2306
2710
  `loam validate --all --json` over the same 120 services used to be the outlier at ~13–14 s — one
2307
2711
  fresh LikeC4 workspace per service; since the batched loader it parses the run's C4 documents in
2308
2712
  one shared workspace and answers in **~0.7 s median** on the committed 120-service benchmark
@@ -2408,7 +2812,28 @@ repairing anything:
2408
2812
 
2409
2813
  `loam-report` is a separate support protocol, generated as a command and skill in every profile
2410
2814
  but never appended to the lifecycle. It writes
2411
- `loam-reports/YYYY-MM-DD-<short-slug>.md` in the current repository with expected/actual behavior,
2815
+ `loam-reports/NNN-YYYY-MM-DD-<short-slug>.md` — `NNN` an ORDINAL of at least three digits,
2816
+ zero-padded, one more
2817
+ than the highest already in the directory (`001` when it is empty or absent, and widening past
2818
+ three digits rather than reusing a number once a corpus gets there), so a report has a
2819
+ short handle a commit message, an issue or the next report can cite. The document carries a
2820
+ `- Status: open` line beside its classification, whose vocabulary is `open` / `sent` (handed to
2821
+ the author, set by whoever forwards it) / `fixed in <version>` / `superseded by <NNN>`, and
2822
+ `loam doctor` reads the directory: how many reports it holds, how many stand at each status, and
2823
+ which ordinal comes next. `doctor --json` carries that as the `reports` object — `reports.dir`
2824
+ (the ABSOLUTE path of the directory, `loam-reports/` included, so an agent writes inside it rather
2825
+ than joining the name on a second time), resolved beside the `loam.json` that parsed, or beside
2826
+ the one loam FOUND when it did not parse, and only beside the run directory when there is no
2827
+ config file at or above it — `doctor` is the preflight that runs precisely when the config may not
2828
+ resolve, so it counts the corpus that is there rather than handing out an ordinal five existing
2829
+ reports already used — `reports.present`, `reports.total`, `reports.next`, and
2830
+ `reports.entries[]` with a `file`, an `ordinal` (`null` for a name written before the numbering)
2831
+ and a `status`, in ordinal order, numerically, with unnumbered names last (byte order stopped
2832
+ 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
2833
+ the first `##` heading, quoted code skipped in both markdown spellings (a fence, closed only by a
2834
+ run of the same character at least as long as the one that opened it, and an indented block,
2835
+ which is how the protocol prints its own template) — so a report quoting this protocol's own
2836
+ template still counts as whatever its own header says. The body carries expected/actual behavior,
2412
2837
  version, stable envelope and finding codes, relevant locations, write state, a minimal safe
2413
2838
  reproduction, classification and missing evidence. Before writing, the agent redacts secrets and
2414
2839
  home paths and reduces command output to the excerpt that proves the symptom. It never retries a
@@ -2517,20 +2942,43 @@ hand-merges the block. That resolution has a documented cost, not a surprise: a
2517
2942
  landscape moves the file away from the bytes the archive's `.loam-before/` snapshot recorded, so a
2518
2943
  later `loam unarchive` of that feature refuses with `snapshot-stale` and needs `--force` — the
2519
2944
  resolver is trading undo precision for the merge, and should know it. The trigger for revisiting is
2520
- written down so nobody relitigates it early: when landscape merge conflicts become routine — weekly,
2521
- not monthly — move per-service internals fully into `services/<svc>/model.likec4` and thin the
2522
- landscape down to top-level elements plus cross-service edges. Until then, one file keeps the fleet
2523
- map one diffable document.
2945
+ written down so nobody relitigates it early — and half of what it once asked for has already
2946
+ arrived. Per-service internals now live in `services/<svc>/model.likec4` **by default**, and that is
2947
+ where a second team adopting a service writes, so it adds nothing to this file. It is a default and
2948
+ not a prohibition: the map MAY hold a service's interior, and sometimes must. A store a second
2949
+ service will reach has to be declared where that consumer's project can see it — a per-service
2950
+ project is `architecture/` plus one model — so it is nested inside the owner's element ON THE MAP,
2951
+ which is exactly what "Private stores" above prescribes and what `landscape.datastore-private` and
2952
+ `landscape.datastore-shared` offer as a placement. A delta spelling a parent in the legacy
2953
+ title-join lands its container on the map too. What an extending model owns is what it DECLARES,
2954
+ not everything below the service's element. `loam archive` routes rather than forbids — a delta's
2955
+ additions nested under a service whose model extends the map, with the whole parent chain spelled
2956
+ in the map's own ids, are spliced into that model's `extend <fqn> { … }` block instead of this
2957
+ file, and the merged model is proved to parse beside the merged map before anything is written. What is left of the
2958
+ trigger is the CROSS-SERVICE EDGE, which still exists twice — container-level in the model, service
2959
+ level here — because the spine, the platform census and every other fleet check read this file
2960
+ alone. So: when landscape merge conflicts become routine — weekly, not monthly — thin the map to
2961
+ elements plus cross-service edges and teach the fleet checks to read the edges out of the models
2962
+ that attest them. Until then, one file keeps the fleet map one diffable document, and
2963
+ `landscape.service-isolated` remains the nudge that carries an attested call up into it.
2524
2964
 
2525
2965
  ## Two flows
2526
2966
 
2527
2967
  - **Bootstrap (reverse):** `loam adopt --service <id>` emits a **brief**, not an extraction — the
2528
2968
  target paths (and which already exist, to be diffed and never overwritten), the grammar of each
2529
- artifact, what the living landscape already says about the service, the frontmatter to write, the
2969
+ artifact, what the living fleet map — the `architecture/` project, not the landscape file alone —
2970
+ already says about the service, the frontmatter to write, the
2530
2971
  checks `loam validate --service <id>` will then run, and the ones that do not exist. One target is
2531
2972
  not under `services/<id>/`: `architecture/landscape.likec4`, carried with `action: "edit"` and a
2532
- `landscape.instruction` naming the block this service owes the fleet map, and present only while
2533
- nothing in the map resolves to the service. An agent reads the code and writes draft
2973
+ `landscape.instruction` naming the write this service owes the fleet map — the element while
2974
+ nothing in the map resolves to the service, the EDGES while an element does and no edge touches it
2975
+ (`landscape.touched: false`, with `landscape.attested` listing the calls the service's own
2976
+ model.likec4 declares across its boundary); absent, and the instruction `null`, only once an
2977
+ element resolves and an edge touches it. `landscape.callersKnown` says whether loam could read WHO
2978
+ CALLS this service — true when the `architecture/` project parses, and true when it does not but
2979
+ `architecture/landscape.likec4` does. It is what an empty `landscape.expects` has to be read
2980
+ against: only a readable landscape makes "nothing calls this service" evidence, and it is the one
2981
+ fact `openapi.yaml`'s `required` is derived from. An agent reads the code and writes draft
2534
2982
  `model.likec4` + `spec.md` + `openapi.yaml` + `adrs/`, `runbook.md`, `health.yaml`, and makes that
2535
2983
  one addition to the shared map. A human promotes `draft` -> `verified` with `loam vouch`.
2536
2984
  - **Forward (generative):** author `features/<FEAT>/delta.likec4` -> `loam delta <FEAT>` projects it
@@ -2556,9 +3004,9 @@ component-surface pins), `verify`,
2556
3004
  transactional `archive` / `unarchive`, `vouch`, and the OpenSpec audit/migration on-ramp are
2557
3005
  implemented, each with a `--json` contract.
2558
3006
 
2559
- Known and unclosed, so nobody has to discover them: the complete gate still needs repeatable CI and
2560
- installed-package evidence observed from an actual push, even though behavior, coverage and the
2561
- local stress runner pass. Both contract axes now merge a components-only delta: a feature
3007
+ Known and unclosed, so nobody has to discover them: the scheduled CI `stability` job has still to be
3008
+ observed green, even though behavior, coverage, the installed-package smoke and the local stress
3009
+ runner all pass. Both contract axes now merge a components-only delta: a feature
2562
3010
  asyncapi.yaml declaring no channel/operation/message slot (`components.schemas`-only, say) merges
2563
3011
  the component surfaces it declares, pinned by the root `x-loam-baselines` record, and they enter the
2564
3012
  ref sweep like anything else the merge wrote — so a `$ref` whose target the delta itself defines