@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
@@ -0,0 +1,161 @@
1
+ /**
2
+ * A service's OWN use cases: the `dynamic view`s its LikeC4 project declares,
3
+ * read the way the renderer reads that project.
4
+ *
5
+ * THE SLOT IS THE PROJECT. `loam subsystem sync` writes a
6
+ * `services/<…>/<svc>/likec4.config.json` that registers every `.likec4` under
7
+ * the service directory as one LikeC4 project, so the renderer already shows a
8
+ * `views.likec4` beside the model, or a `usecases/<name>.likec4` under it. loam
9
+ * reads exactly that set — `model.likec4` plus every sibling, recursively, minus
10
+ * whatever that project file's own `exclude` drops (`./exclude.ts`) — and
11
+ * nothing else, because a slot narrower than the renderer's would leave a flow
12
+ * that renders and is graded by nothing, which is the report this module
13
+ * answers: an intra-service flow behind an `arch.spec.md` requirement whose
14
+ * `Covers:` names containers had no home. `architecture/usecases/` is a
15
+ * separate project that cannot resolve a container (one such file turned a
16
+ * whole fleet `landscape.invalid`), and the author's workaround beside the
17
+ * model was invisible to every check. The read is NOT gated on the project
18
+ * file's presence: that file is create-only and a team may delete it, its
19
+ * absence has a warning of its own (`service.likec4-config-missing`), and a
20
+ * flow the renderer cannot show yet is still a flow loam can grade.
21
+ *
22
+ * MODEL FACTS STAY SINGLE-FILE. `c4.valid`'s counts, `Covers:` resolution,
23
+ * `health.dependency-unmodelled` and every other model reader keep the
24
+ * single-file load the service target already made; the project is loaded here
25
+ * for the VIEWS and their `sourcePath` only, and only when there is something
26
+ * to grade. A sibling that declares elements extends what a hop may name, and
27
+ * that is the renderer's reading too.
28
+ *
29
+ * TWO SHAPES, ONE SLOT. A model that stands alone is a project of its own
30
+ * directory, exactly as above. A model that EXTENDS the fleet map is not: its
31
+ * containers are declared inside an element the map owns, so a flow beside it
32
+ * naming one of those containers only resolves in a project that also holds the
33
+ * map (`c4/service-model/documents.ts`, `serviceFlowDocuments`). That project is
34
+ * staged from the DOCS ROOT, so every view comes back spelled
35
+ * `services/<tree>/usecases/x.likec4` — and the service prefix is stripped here,
36
+ * because every finding this scan feeds names a file relative to the service
37
+ * directory and a reader must not have to work out which spelling they were
38
+ * given. The same prefix is the FILTER: the map's own `architecture/usecases/`
39
+ * flows are in that project too, and without it every service in the fleet would
40
+ * be graded for every fleet-level use case.
41
+ *
42
+ * THE CHEAP GATE, in order, so a fleet without the axis pays one recursive
43
+ * readdir per service and nothing else:
44
+ *
45
+ * 1. no sibling → nothing new is loaded, and the model's own tagged views are
46
+ * graded at zero cost against the project the model was READ in. For a
47
+ * standalone model that is the single-file load already in hand, which IS
48
+ * its whole project; for an extending one it is `ServiceModel.project`, the
49
+ * map and this model together — NOT the service's own slice of it, which is
50
+ * what the caller passes as `model` and what every count and `Covers:` line
51
+ * is answered from. A hop backed by a map edge is backed wherever the view
52
+ * is written, and grading the two arms against different documents made one
53
+ * view `usecase.step-unbacked` in model.likec4 and clean in a sibling. A
54
+ * standalone model's views came back named `source.c4`
55
+ * (`core/usecases/place.ts` records the rule) and an extending model's carry
56
+ * their docs-relative path, so the file is spelled here, where it is known,
57
+ * for both: `model.likec4`;
58
+ * 2. siblings, no tagged view in the model, and no reserved prefix in any
59
+ * document (`mentionsTagPrefix`, the fleet's own byte gate) → nothing opted
60
+ * in, no workspace. An untagged `views.likec4` is somebody's hand-drawn
61
+ * diagram, never graded, exactly as an untagged fleet view is — and its
62
+ * parse errors are the renderer's business until a view opts the project
63
+ * in;
64
+ * 3. otherwise `loadProject` over model + siblings, the way the renderer reads
65
+ * them. `own` and the project's views are never merged: when siblings
66
+ * exist the project is the whole answer, which is what keeps one view from
67
+ * being graded twice.
68
+ *
69
+ * A REQUEST RECORD rather than three parameters, for the reason
70
+ * docs/CODE-STYLE.md gives: the paths, the single-file doc and the fleet set
71
+ * are only ever passed together, and a caller handing in a doc loaded from
72
+ * some other path is exactly the inconsistent pair a record makes
73
+ * unrepresentable.
74
+ */
75
+ import { relative } from "node:path";
76
+ import { architectureDocuments } from "../../c4/project/documents.js";
77
+ import { asLoadedDoc, loadProject } from "../../c4/project/load.js";
78
+ import { serviceFlowDocuments } from "../../c4/service-model/documents.js";
79
+ import { ARTIFACT_FILES } from "../../repo/paths.js";
80
+ import { isUseCase, mentionsTagPrefix } from "../fleet.js";
81
+ import { keepIncluded, serviceProjectExclude } from "./exclude.js";
82
+ /**
83
+ * The service's declared use cases, with the model they are drawn over.
84
+ *
85
+ * Reads only, throws nothing: every failure is the `unreadable` arm. The caller
86
+ * has already established that `model.likec4` parses on its own — a project
87
+ * whose model is broken is `c4.invalid`'s business, and loading it again here
88
+ * would only repeat that cascade under a second code.
89
+ */
90
+ export async function readServiceFlows(req) {
91
+ const found = await architectureDocuments(req.paths.dir, [req.paths.model]);
92
+ // What the RENDERER holds, for the standalone shape where this directory's
93
+ // own `likec4.config.json` IS the project config: a sibling its `exclude`
94
+ // covers is in no project loam may claim to be reading (`./exclude.ts`). The
95
+ // extending shape reads the root project instead, so its list does not apply
96
+ // — a per-service config beside an extending model is `service.likec4-config-stray`.
97
+ const siblings = req.extending === undefined
98
+ ? keepIncluded(req.paths.dir, await serviceProjectExclude(req.paths.dir), found)
99
+ : found;
100
+ // The scope of the no-sibling arm, and it is the PROJECT for both shapes: the
101
+ // single-file load for a standalone model (which is its whole project), and
102
+ // the map-plus-model load for an extending one. Reading `req.model` there for
103
+ // an extending model read its own slice instead, so one view was graded two
104
+ // different ways depending on which file it was written in.
105
+ const alone = req.extending?.project ?? req.model;
106
+ const scope = { elements: alone.elements, relationships: alone.relationships, known: req.known };
107
+ const own = (req.model.views ?? []).filter(isUseCase);
108
+ if (siblings.length === 0) {
109
+ if (own.length === 0)
110
+ return { kind: "none" };
111
+ return { kind: "read", views: own.map((view) => ({ ...view, sourcePath: ARTIFACT_FILES.model })), model: scope };
112
+ }
113
+ if (own.length === 0 && !(await mentionsTagPrefix(siblings)))
114
+ return { kind: "none" };
115
+ const base = req.extending?.docsDir ?? req.paths.dir;
116
+ const documents = req.extending === undefined
117
+ ? [req.paths.model, ...siblings]
118
+ : await serviceFlowDocuments(req.extending.docsDir, req.paths);
119
+ const project = await loadProject(base, documents);
120
+ if (!project.clean) {
121
+ // Each error already carries the real path of its document (`loadProject`
122
+ // rewrites the staged path back), and the flattening is `asLoadedDoc`'s —
123
+ // the fleet's own path-sorted order, taken from the one place it is
124
+ // spelled rather than re-spelled here: the caller names the FIRST broken
125
+ // file, and a second sort rule would let the service target name a
126
+ // different first file than the fleet target names for the same project.
127
+ return { kind: "unreadable", errors: asLoadedDoc(project).errors };
128
+ }
129
+ // The prefix the service's own documents are spelled under inside this
130
+ // project: empty for a standalone model (the project IS the directory, so
131
+ // every view in it is this service's) and `services/<tree>/` for an extending
132
+ // one, whose project also holds the whole fleet map.
133
+ const prefix = req.extending === undefined ? "" : `${relative(base, req.paths.dir).split(/[\\/]/).join("/")}/`;
134
+ return {
135
+ kind: "read",
136
+ views: rebase(project.views.filter(isUseCase), prefix),
137
+ model: { elements: project.elements, relationships: project.relationships, known: req.known },
138
+ };
139
+ }
140
+ /**
141
+ * The views this SERVICE declares, spelled relative to its own directory.
142
+ *
143
+ * With an empty prefix — the standalone shape — this is the identity, including
144
+ * for a view whose `sourcePath` is absent: that project holds nothing but the
145
+ * service's own files, so there is nothing to attribute and nothing to strip.
146
+ *
147
+ * With a prefix, it is both the filter and the rebase, and both halves are one
148
+ * rule about which document belongs to whom. The project also holds the fleet
149
+ * map, so `architecture/usecases/checkout.likec4` is in it — and without the
150
+ * filter every service in the fleet would be graded for every fleet-level use
151
+ * case. A view carrying no `sourcePath` cannot be attributed at all and is
152
+ * dropped for the same reason: the path is the only evidence there is.
153
+ */
154
+ function rebase(views, prefix) {
155
+ if (prefix === "")
156
+ return [...views];
157
+ return views
158
+ .filter((view) => view.sourcePath?.startsWith(prefix) === true)
159
+ .map((view) => ({ ...view, sourcePath: (view.sourcePath ?? "").slice(prefix.length) }));
160
+ }
161
+ //# sourceMappingURL=flows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flows.js","sourceRoot":"","sources":["../../../../src/core/usecases/service/flows.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A service's OWN use cases: the `dynamic view`s its LikeC4 project declares,\n * read the way the renderer reads that project.\n *\n * THE SLOT IS THE PROJECT. `loam subsystem sync` writes a\n * `services/<…>/<svc>/likec4.config.json` that registers every `.likec4` under\n * the service directory as one LikeC4 project, so the renderer already shows a\n * `views.likec4` beside the model, or a `usecases/<name>.likec4` under it. loam\n * reads exactly that set — `model.likec4` plus every sibling, recursively, minus\n * whatever that project file's own `exclude` drops (`./exclude.ts`) — and\n * nothing else, because a slot narrower than the renderer's would leave a flow\n * that renders and is graded by nothing, which is the report this module\n * answers: an intra-service flow behind an `arch.spec.md` requirement whose\n * `Covers:` names containers had no home. `architecture/usecases/` is a\n * separate project that cannot resolve a container (one such file turned a\n * whole fleet `landscape.invalid`), and the author's workaround beside the\n * model was invisible to every check. The read is NOT gated on the project\n * file's presence: that file is create-only and a team may delete it, its\n * absence has a warning of its own (`service.likec4-config-missing`), and a\n * flow the renderer cannot show yet is still a flow loam can grade.\n *\n * MODEL FACTS STAY SINGLE-FILE. `c4.valid`'s counts, `Covers:` resolution,\n * `health.dependency-unmodelled` and every other model reader keep the\n * single-file load the service target already made; the project is loaded here\n * for the VIEWS and their `sourcePath` only, and only when there is something\n * to grade. A sibling that declares elements extends what a hop may name, and\n * that is the renderer's reading too.\n *\n * TWO SHAPES, ONE SLOT. A model that stands alone is a project of its own\n * directory, exactly as above. A model that EXTENDS the fleet map is not: its\n * containers are declared inside an element the map owns, so a flow beside it\n * naming one of those containers only resolves in a project that also holds the\n * map (`c4/service-model/documents.ts`, `serviceFlowDocuments`). That project is\n * staged from the DOCS ROOT, so every view comes back spelled\n * `services/<tree>/usecases/x.likec4` — and the service prefix is stripped here,\n * because every finding this scan feeds names a file relative to the service\n * directory and a reader must not have to work out which spelling they were\n * given. The same prefix is the FILTER: the map's own `architecture/usecases/`\n * flows are in that project too, and without it every service in the fleet would\n * be graded for every fleet-level use case.\n *\n * THE CHEAP GATE, in order, so a fleet without the axis pays one recursive\n * readdir per service and nothing else:\n *\n * 1. no sibling → nothing new is loaded, and the model's own tagged views are\n * graded at zero cost against the project the model was READ in. For a\n * standalone model that is the single-file load already in hand, which IS\n * its whole project; for an extending one it is `ServiceModel.project`, the\n * map and this model together — NOT the service's own slice of it, which is\n * what the caller passes as `model` and what every count and `Covers:` line\n * is answered from. A hop backed by a map edge is backed wherever the view\n * is written, and grading the two arms against different documents made one\n * view `usecase.step-unbacked` in model.likec4 and clean in a sibling. A\n * standalone model's views came back named `source.c4`\n * (`core/usecases/place.ts` records the rule) and an extending model's carry\n * their docs-relative path, so the file is spelled here, where it is known,\n * for both: `model.likec4`;\n * 2. siblings, no tagged view in the model, and no reserved prefix in any\n * document (`mentionsTagPrefix`, the fleet's own byte gate) → nothing opted\n * in, no workspace. An untagged `views.likec4` is somebody's hand-drawn\n * diagram, never graded, exactly as an untagged fleet view is — and its\n * parse errors are the renderer's business until a view opts the project\n * in;\n * 3. otherwise `loadProject` over model + siblings, the way the renderer reads\n * them. `own` and the project's views are never merged: when siblings\n * exist the project is the whole answer, which is what keeps one view from\n * being graded twice.\n *\n * A REQUEST RECORD rather than three parameters, for the reason\n * docs/CODE-STYLE.md gives: the paths, the single-file doc and the fleet set\n * are only ever passed together, and a caller handing in a doc loaded from\n * some other path is exactly the inconsistent pair a record makes\n * unrepresentable.\n */\nimport { relative } from \"node:path\";\nimport type { LikeC4Error, LoadedDoc } from \"../../c4/likec4.js\";\nimport type { ParsedView } from \"../../c4/parsed/dynamic-views.js\";\nimport { architectureDocuments } from \"../../c4/project/documents.js\";\nimport { asLoadedDoc, loadProject } from \"../../c4/project/load.js\";\nimport { serviceFlowDocuments } from \"../../c4/service-model/documents.js\";\nimport type { StepScope } from \"../../c4/resolve/steps.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { ARTIFACT_FILES, type ServicePaths } from \"../../repo/paths.js\";\nimport { isUseCase, mentionsTagPrefix } from \"../fleet.js\";\nimport { keepIncluded, serviceProjectExclude } from \"./exclude.js\";\n\nexport interface ServiceFlowRequest {\n paths: ServicePaths;\n /**\n * The single-file load the service target already made and has verified\n * parses. Its views are the project when no sibling exists; its elements are\n * the model a no-sibling flow is graded over.\n */\n model: LoadedDoc;\n /** The enumerated fleet, for `serviceResolver`'s reason: without it a container resolves to a service that has never existed. */\n known: ReadonlySet<string>;\n /**\n * Set only when this model EXTENDS the fleet map, and then both halves of it\n * — a record rather than two optional fields, for docs/CODE-STYLE.md's reason:\n * they are only ever passed together, and a caller that supplied one without\n * the other is exactly the inconsistent pair a record makes unrepresentable.\n *\n * `docsDir` decides which documents the sibling project holds (the map as\n * well, or the directory alone) and which root they are staged from, and\n * therefore how LikeC4 spells every `sourcePath` that comes back.\n *\n * `project` is the per-service project the model was READ in — the map plus\n * this model (`ServiceModel.project`) — and it is the scope a hop resolves\n * against when there is no sibling file. `model` above is then the service's\n * own SLICE of that project, which is the right answer for every count and\n * every `Covers:` line and the wrong one here: a hop backed by a MAP edge is\n * backed, and grading it against the slice reported `usecase.step-unbacked`\n * (exit 1) for a view in model.likec4 that was clean the moment the same view\n * moved to a sibling file, where the loaded project holds the map. One\n * question, two answers, decided by which file the author happened to write\n * the view in.\n *\n * Absent means the standalone shape — the service directory is the project and\n * the single-file load IS that project, exactly as it has always been.\n */\n extending?: { docsDir: DocsDir; project: LoadedDoc };\n}\n\n/**\n * What a service's flows are, or the honest refusal to say.\n *\n * Three variants rather than a record with flags, for `UseCaseScan`'s reason\n * one package up: a project that did not parse has no elements to resolve\n * against and no views to grade, and a caller handed `{ views: [], unreadable:\n * true }` is one `.length` check away from reporting a service as flow-free\n * when nobody could read it. `none` is the gate's answer — nothing opted in,\n * nothing was loaded — and it is distinct from `read` with no views (a project\n * that mentions a prefix in prose and declares no tagged view), because only\n * the second one cost a workspace.\n */\nexport type ServiceFlowScan =\n | { kind: \"none\" }\n | {\n kind: \"read\";\n /** The reserved-tag views ONLY, each carrying a `sourcePath` relative to the SERVICE directory. */\n views: ParsedView[];\n /** What a hop is attributed against — `known` required, as `UseCaseScan.model` requires it. */\n model: StepScope & { known: ReadonlySet<string> };\n }\n | {\n kind: \"unreadable\";\n /**\n * Every error the project raised, each carrying the real absolute path of\n * the document that raised it, sorted by that path — so a caller can name\n * the file, and a project with two broken documents reports them in a\n * stable order.\n */\n errors: LikeC4Error[];\n };\n\n/**\n * The service's declared use cases, with the model they are drawn over.\n *\n * Reads only, throws nothing: every failure is the `unreadable` arm. The caller\n * has already established that `model.likec4` parses on its own — a project\n * whose model is broken is `c4.invalid`'s business, and loading it again here\n * would only repeat that cascade under a second code.\n */\nexport async function readServiceFlows(req: ServiceFlowRequest): Promise<ServiceFlowScan> {\n const found = await architectureDocuments(req.paths.dir, [req.paths.model]);\n // What the RENDERER holds, for the standalone shape where this directory's\n // own `likec4.config.json` IS the project config: a sibling its `exclude`\n // covers is in no project loam may claim to be reading (`./exclude.ts`). The\n // extending shape reads the root project instead, so its list does not apply\n // — a per-service config beside an extending model is `service.likec4-config-stray`.\n const siblings =\n req.extending === undefined\n ? keepIncluded(req.paths.dir, await serviceProjectExclude(req.paths.dir), found)\n : found;\n // The scope of the no-sibling arm, and it is the PROJECT for both shapes: the\n // single-file load for a standalone model (which is its whole project), and\n // the map-plus-model load for an extending one. Reading `req.model` there for\n // an extending model read its own slice instead, so one view was graded two\n // different ways depending on which file it was written in.\n const alone = req.extending?.project ?? req.model;\n const scope = { elements: alone.elements, relationships: alone.relationships, known: req.known };\n const own = (req.model.views ?? []).filter(isUseCase);\n if (siblings.length === 0) {\n if (own.length === 0) return { kind: \"none\" };\n return { kind: \"read\", views: own.map((view) => ({ ...view, sourcePath: ARTIFACT_FILES.model })), model: scope };\n }\n if (own.length === 0 && !(await mentionsTagPrefix(siblings))) return { kind: \"none\" };\n\n const base = req.extending?.docsDir ?? req.paths.dir;\n const documents =\n req.extending === undefined\n ? [req.paths.model, ...siblings]\n : await serviceFlowDocuments(req.extending.docsDir, req.paths);\n const project = await loadProject(base, documents);\n if (!project.clean) {\n // Each error already carries the real path of its document (`loadProject`\n // rewrites the staged path back), and the flattening is `asLoadedDoc`'s —\n // the fleet's own path-sorted order, taken from the one place it is\n // spelled rather than re-spelled here: the caller names the FIRST broken\n // file, and a second sort rule would let the service target name a\n // different first file than the fleet target names for the same project.\n return { kind: \"unreadable\", errors: asLoadedDoc(project).errors };\n }\n // The prefix the service's own documents are spelled under inside this\n // project: empty for a standalone model (the project IS the directory, so\n // every view in it is this service's) and `services/<tree>/` for an extending\n // one, whose project also holds the whole fleet map.\n const prefix = req.extending === undefined ? \"\" : `${relative(base, req.paths.dir).split(/[\\\\/]/).join(\"/\")}/`;\n return {\n kind: \"read\",\n views: rebase(project.views.filter(isUseCase), prefix),\n model: { elements: project.elements, relationships: project.relationships, known: req.known },\n };\n}\n\n/**\n * The views this SERVICE declares, spelled relative to its own directory.\n *\n * With an empty prefix — the standalone shape — this is the identity, including\n * for a view whose `sourcePath` is absent: that project holds nothing but the\n * service's own files, so there is nothing to attribute and nothing to strip.\n *\n * With a prefix, it is both the filter and the rebase, and both halves are one\n * rule about which document belongs to whom. The project also holds the fleet\n * map, so `architecture/usecases/checkout.likec4` is in it — and without the\n * filter every service in the fleet would be graded for every fleet-level use\n * case. A view carrying no `sourcePath` cannot be attributed at all and is\n * dropped for the same reason: the path is the only evidence there is.\n */\nfunction rebase(views: readonly ParsedView[], prefix: string): ParsedView[] {\n if (prefix === \"\") return [...views];\n return views\n .filter((view) => view.sourcePath?.startsWith(prefix) === true)\n .map((view) => ({ ...view, sourcePath: (view.sourcePath ?? \"\").slice(prefix.length) }));\n}\n"]}
@@ -129,6 +129,16 @@ export function landscapeEvidence(req) {
129
129
  const inbound = [];
130
130
  const outbound = [];
131
131
  for (const r of relationships) {
132
+ // An edge whose BOTH endpoints resolve to this service is its own internal
133
+ // wiring (`svc.web -> svc.db`): filed as inbound it made the service its own
134
+ // caller — `← <id> "reads"` in `loam context` (verification 2026-09-04) —
135
+ // and, through `apiExpected` below, told a service nobody calls that it owed
136
+ // an OpenAPI contract. `core/brief/landscape.ts` drops it for the same
137
+ // reason. It still TOUCHES the service, which is why the brief's `touched`
138
+ // and `landscape.service-isolated` read the relationships rather than these
139
+ // two lists.
140
+ if (svcOf(r.source) === id && svcOf(r.target) === id)
141
+ continue;
132
142
  const edge = { op: r.op ?? null, title: r.title ?? null };
133
143
  if (svcOf(r.target) === id)
134
144
  inbound.push({ service: svcOf(r.source), ...edge });
@@ -1 +1 @@
1
- {"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../../src/core/vocabulary/maturity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption-maturity ladder: one monotone word for how far a service's\n * documentation has got, and what stands between it and the next rung.\n *\n * This lived inside `commands/list.ts` while `list` was the only caller. It\n * moved here when `explore` needed the same answer, because the alternative is\n * the failure `core/kernel/ids/service.ts` already records for the service-id grammar: a\n * second, subtly different copy, so two commands grade the same directory\n * differently and nobody can tell which one is lying. The rung is a fleet dial\n * — `list` prints it per service and in a rollup, `explore` prints it for a\n * service somebody is about to build against — and a dial with two readings is\n * not a dial.\n */\nimport type { ServiceEntry } from \"../repo/entries.js\";\n\n/** The ladder, in order. Each rung stands on every rung below it. */\nexport const MATURITY_LADDER = [\"empty\", \"partial\", \"documented\", \"sourced\", \"vouched\"] as const;\nexport type Maturity = (typeof MATURITY_LADDER)[number];\n\n/**\n * A service entry plus the two facts the directory enumeration cannot know on\n * its own. Both change what a rung MEANS, and both are fleet-level: they need\n * the landscape, not `services/<id>/`.\n */\nexport interface MaturityInput {\n entry: ServiceEntry;\n /** `arch.spec.md` — the architecture-obligations axis, absent from ServiceEntry.has. */\n archSpec: boolean;\n /**\n * Does anything in the fleet call an operation on this service? True unless\n * the landscape PROVES otherwise, exactly as `validate`'s no-openapi grace\n * reads it: a missing or unparseable landscape proves nothing.\n */\n apiExpected: boolean;\n}\n\n/**\n * How far a service's documentation has got. Derived from artifact PRESENCE and\n * provenance state only — the data an enumeration already holds — never from\n * what the artifacts say. COMPLETENESS of adopted docs is explicitly on the\n * unchecked list (brief/unchecked.ts): a service with one endpoint documented out of\n * thirty climbs this ladder exactly as fast as a thorough one, which is why no\n * rung is called \"adopted\".\n *\n * empty services/<id>/ exists, no artifact is in it\n * partial some artifacts, but not the required set\n * documented the artifacts the adopt brief marks required are present\n * sourced the living spec declares `sources` — something ties it to code\n * vouched status: verified with a sources_digest behind it — a person\n * stamped it. `verified` with no digest is a claim with nothing\n * behind it and stays below this rung.\n *\n * `openapi.yaml` is required ONLY where an API is expected. The rung used to\n * demand it of everything, which permanently pinned every worker, cron and\n * consumer in the fleet at `partial` — services that are fully documented and\n * vouched for, reported as unfinished forever, leaving the rollup permanently\n * below its true documentation state. The evidence is the same one\n * `validate` uses to keep `service.no-openapi` quiet, so a service cannot be\n * green in one command and unfinished in the other.\n */\nexport function serviceMaturity(v: MaturityInput): Maturity {\n const s = v.entry;\n if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0) return \"empty\";\n if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi))) return \"partial\";\n if (!s.sources.declared) return \"documented\";\n if (!(s.status === \"verified\" && s.sources.stamped)) return \"sourced\";\n return \"vouched\";\n}\n\n/**\n * What stands between this service and `vouched`, named as the thing to go and\n * do. One rung's worth at a time: telling somebody at `empty` that they also\n * need a vouch is noise, and the vouch is not even possible yet.\n */\nexport function maturityGaps(v: MaturityInput): string[] {\n const s = v.entry;\n const files = [\n ...(s.has.model ? [] : [\"model.likec4\"]),\n ...(s.has.spec ? [] : [\"spec.md\"]),\n ...(v.apiExpected && !s.has.openapi ? [\"openapi.yaml\"] : []),\n ];\n if (files.length > 0) return files;\n if (!s.sources.declared) return [\"sources: in the spec.md frontmatter\"];\n if (!(s.status === \"verified\" && s.sources.stamped)) return [`\\`loam vouch --service ${s.id}\\``];\n return [];\n}\n\n/** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */\nexport function maturityRollup(graded: readonly { maturity: Maturity }[]): Record<Maturity, number> {\n const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0])) as Record<Maturity, number>;\n for (const v of graded) out[v.maturity] += 1;\n return out;\n}\n\n/* ------------------------------------------------------------------ */\n/* What the fleet map proves about one service */\n/* ------------------------------------------------------------------ */\n\n/**\n * C4 kinds that model people, and the tag marking an element as somebody\n * else's system. A person is never a service directory, and `#external` is\n * undocumented on purpose — every census and exemption over the fleet map\n * asks both questions.\n *\n * Extracted on the third strike: `commands/validate/checks/vocabulary.ts`,\n * `core/gate/partners.ts` and `core/verify/checklist.ts` each carried a copy.\n * Their real home is `core/c4/likec4.ts` beside the model they describe, but\n * that file sits against the 300-line limit and its package holds five files\n * — so the trio lives here, beside `landscapeEvidence`, which reads the same\n * map for the same per-service questions.\n *\n * `PLATFORM_TAG` joined them from `commands/validate/checks/fleet-shape.ts`,\n * where it was declared beside its one reader. It moved for the rule above the\n * others came for: `fleet/kind-tags.ts` needs to know which tag names loam\n * grades on, and reaching into fleet-shape.ts for a five-character constant\n * would import a module that reads permissions, capabilities and every\n * service's requirements to get it. The two tags loam gives meaning to belong\n * in one place anyway — that list IS what the kind-tag check is about.\n */\nexport const ACTOR_KINDS = new Set([\"person\", \"actor\", \"user\"]);\nexport const EXTERNAL_TAG = \"external\";\n/** Tag marking ubiquitous infrastructure; the scaffolded fleet view excludes it. */\nexport const PLATFORM_TAG = \"platform\";\n\n/**\n * The tags loam GRADES on, as opposed to the ones it merely carries.\n *\n * A tag in this list changes what loam concludes about a fleet: `#external`\n * exempts an element from the landscape↔services reconciliation entirely, and\n * `#platform` silences the hub warning. Every other tag on an element is the\n * author's own vocabulary and loam does not read it.\n *\n * The list exists because since LikeC4 1.59.0 a tag can be declared on a KIND\n * — `specification { element softwareSystem { #external } }` — and LikeC4\n * applies it to every element of that kind. A kind-wide declaration of a tag on\n * this list switches loam's grading off for the whole fleet at once, which is\n * what `kindTagFindings` refuses. A tag NOT on this list is safe to declare\n * kind-wide, so the check must ask exactly this question and no broader one.\n */\nexport const GRADED_TAGS: readonly string[] = [EXTERNAL_TAG, PLATFORM_TAG];\n\n/** One end of a call the fleet map draws, as every per-service view spells it. */\nexport interface LandscapeEdge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\n/**\n * The slice of a parsed relationship the partition reads. Structural on\n * purpose — the smallest parameter type the derivation needs — so this module\n * stays a leaf: `core/c4/likec4.ts`'s `Rel` satisfies it without an import\n * edge from the vocabulary package into the parser.\n */\nexport interface EdgeSource {\n source: string;\n target: string;\n op?: string;\n title?: string;\n}\n\nexport interface LandscapeEvidenceRequest {\n /** The service being asked about. */\n id: string;\n /** Whether the landscape parsed — what makes its silence evidence rather than absence. */\n parses: boolean;\n /** The parsed relationships; empty when the landscape is absent or unparseable. */\n relationships: readonly EdgeSource[];\n /** Every element id in the map, for the `modelled` probe. */\n elementIds: readonly string[];\n /** The shared resolver (`core/c4/resolve/service.ts` `serviceResolver`), widened to plain strings. */\n svcOf: (id: string) => string;\n}\n\nexport interface LandscapeEvidence {\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n /** Whether anything in the fleet map resolves to this service. */\n modelled: boolean;\n /** The `apiExpected` input the ladder above consumes. */\n apiExpected: boolean;\n}\n\n/**\n * The fleet map's evidence about one service: the edges into and out of it,\n * whether the map draws it at all, and whether an API is expected of it.\n *\n * One spelling, because it had grown three. `explore`'s describe and the\n * context pack's living slice partitioned the same relationships with the\n * same positive-evidence rule line for line — an inbound edge carrying an\n * operation proves somebody calls the service; a landscape that is absent or\n * does not parse proves NOTHING about who calls it, so the contract is still\n * owed — and a rule spelled twice is how the same service grades `partial` in\n * one command and `documented` in another. `list`'s serviceViews\n * (`commands/list/views.ts`) derives the same rule fleet-wide in ONE pass —\n * a single `called` set for every service, not a partition per service —\n * so that spelling deliberately stays there, cross-referenced from both\n * sides: calling this per service inside a fleet loop would re-walk the\n * whole relationship list once per service.\n */\nexport function landscapeEvidence(req: LandscapeEvidenceRequest): LandscapeEvidence {\n const { id, parses, relationships, svcOf } = req;\n const inbound: LandscapeEdge[] = [];\n const outbound: LandscapeEdge[] = [];\n for (const r of relationships) {\n const edge = { op: r.op ?? null, title: r.title ?? null };\n if (svcOf(r.target) === id) inbound.push({ service: svcOf(r.source), ...edge });\n else if (svcOf(r.source) === id) outbound.push({ service: svcOf(r.target), ...edge });\n }\n return {\n inbound,\n outbound,\n modelled: req.elementIds.some((e) => svcOf(e) === id),\n apiExpected: !parses || inbound.some((e) => e.op !== null),\n };\n}\n"]}
1
+ {"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../../src/core/vocabulary/maturity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adoption-maturity ladder: one monotone word for how far a service's\n * documentation has got, and what stands between it and the next rung.\n *\n * This lived inside `commands/list.ts` while `list` was the only caller. It\n * moved here when `explore` needed the same answer, because the alternative is\n * the failure `core/kernel/ids/service.ts` already records for the service-id grammar: a\n * second, subtly different copy, so two commands grade the same directory\n * differently and nobody can tell which one is lying. The rung is a fleet dial\n * — `list` prints it per service and in a rollup, `explore` prints it for a\n * service somebody is about to build against — and a dial with two readings is\n * not a dial.\n */\nimport type { ServiceEntry } from \"../repo/entries.js\";\n\n/** The ladder, in order. Each rung stands on every rung below it. */\nexport const MATURITY_LADDER = [\"empty\", \"partial\", \"documented\", \"sourced\", \"vouched\"] as const;\nexport type Maturity = (typeof MATURITY_LADDER)[number];\n\n/**\n * A service entry plus the two facts the directory enumeration cannot know on\n * its own. Both change what a rung MEANS, and both are fleet-level: they need\n * the landscape, not `services/<id>/`.\n */\nexport interface MaturityInput {\n entry: ServiceEntry;\n /** `arch.spec.md` — the architecture-obligations axis, absent from ServiceEntry.has. */\n archSpec: boolean;\n /**\n * Does anything in the fleet call an operation on this service? True unless\n * the landscape PROVES otherwise, exactly as `validate`'s no-openapi grace\n * reads it: a missing or unparseable landscape proves nothing.\n */\n apiExpected: boolean;\n}\n\n/**\n * How far a service's documentation has got. Derived from artifact PRESENCE and\n * provenance state only — the data an enumeration already holds — never from\n * what the artifacts say. COMPLETENESS of adopted docs is explicitly on the\n * unchecked list (brief/unchecked.ts): a service with one endpoint documented out of\n * thirty climbs this ladder exactly as fast as a thorough one, which is why no\n * rung is called \"adopted\".\n *\n * empty services/<id>/ exists, no artifact is in it\n * partial some artifacts, but not the required set\n * documented the artifacts the adopt brief marks required are present\n * sourced the living spec declares `sources` — something ties it to code\n * vouched status: verified with a sources_digest behind it — a person\n * stamped it. `verified` with no digest is a claim with nothing\n * behind it and stays below this rung.\n *\n * `openapi.yaml` is required ONLY where an API is expected. The rung used to\n * demand it of everything, which permanently pinned every worker, cron and\n * consumer in the fleet at `partial` — services that are fully documented and\n * vouched for, reported as unfinished forever, leaving the rollup permanently\n * below its true documentation state. The evidence is the same one\n * `validate` uses to keep `service.no-openapi` quiet, so a service cannot be\n * green in one command and unfinished in the other.\n */\nexport function serviceMaturity(v: MaturityInput): Maturity {\n const s = v.entry;\n if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0) return \"empty\";\n if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi))) return \"partial\";\n if (!s.sources.declared) return \"documented\";\n if (!(s.status === \"verified\" && s.sources.stamped)) return \"sourced\";\n return \"vouched\";\n}\n\n/**\n * What stands between this service and `vouched`, named as the thing to go and\n * do. One rung's worth at a time: telling somebody at `empty` that they also\n * need a vouch is noise, and the vouch is not even possible yet.\n */\nexport function maturityGaps(v: MaturityInput): string[] {\n const s = v.entry;\n const files = [\n ...(s.has.model ? [] : [\"model.likec4\"]),\n ...(s.has.spec ? [] : [\"spec.md\"]),\n ...(v.apiExpected && !s.has.openapi ? [\"openapi.yaml\"] : []),\n ];\n if (files.length > 0) return files;\n if (!s.sources.declared) return [\"sources: in the spec.md frontmatter\"];\n if (!(s.status === \"verified\" && s.sources.stamped)) return [`\\`loam vouch --service ${s.id}\\``];\n return [];\n}\n\n/** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */\nexport function maturityRollup(graded: readonly { maturity: Maturity }[]): Record<Maturity, number> {\n const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0])) as Record<Maturity, number>;\n for (const v of graded) out[v.maturity] += 1;\n return out;\n}\n\n/* ------------------------------------------------------------------ */\n/* What the fleet map proves about one service */\n/* ------------------------------------------------------------------ */\n\n/**\n * C4 kinds that model people, and the tag marking an element as somebody\n * else's system. A person is never a service directory, and `#external` is\n * undocumented on purpose — every census and exemption over the fleet map\n * asks both questions.\n *\n * Extracted on the third strike: `commands/validate/checks/vocabulary.ts`,\n * `core/gate/partners.ts` and `core/verify/checklist.ts` each carried a copy.\n * Their real home is `core/c4/likec4.ts` beside the model they describe, but\n * that file sits against the 300-line limit and its package holds five files\n * — so the trio lives here, beside `landscapeEvidence`, which reads the same\n * map for the same per-service questions.\n *\n * `PLATFORM_TAG` joined them from `commands/validate/checks/fleet-shape.ts`,\n * where it was declared beside its one reader. It moved for the rule above the\n * others came for: `fleet/kind-tags.ts` needs to know which tag names loam\n * grades on, and reaching into fleet-shape.ts for a five-character constant\n * would import a module that reads permissions, capabilities and every\n * service's requirements to get it. The two tags loam gives meaning to belong\n * in one place anyway — that list IS what the kind-tag check is about.\n */\nexport const ACTOR_KINDS = new Set([\"person\", \"actor\", \"user\"]);\nexport const EXTERNAL_TAG = \"external\";\n/** Tag marking ubiquitous infrastructure; the scaffolded fleet view excludes it. */\nexport const PLATFORM_TAG = \"platform\";\n\n/**\n * The tags loam GRADES on, as opposed to the ones it merely carries.\n *\n * A tag in this list changes what loam concludes about a fleet: `#external`\n * exempts an element from the landscape↔services reconciliation entirely, and\n * `#platform` silences the hub warning. Every other tag on an element is the\n * author's own vocabulary and loam does not read it.\n *\n * The list exists because since LikeC4 1.59.0 a tag can be declared on a KIND\n * — `specification { element softwareSystem { #external } }` — and LikeC4\n * applies it to every element of that kind. A kind-wide declaration of a tag on\n * this list switches loam's grading off for the whole fleet at once, which is\n * what `kindTagFindings` refuses. A tag NOT on this list is safe to declare\n * kind-wide, so the check must ask exactly this question and no broader one.\n */\nexport const GRADED_TAGS: readonly string[] = [EXTERNAL_TAG, PLATFORM_TAG];\n\n/** One end of a call the fleet map draws, as every per-service view spells it. */\nexport interface LandscapeEdge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\n/**\n * The slice of a parsed relationship the partition reads. Structural on\n * purpose — the smallest parameter type the derivation needs — so this module\n * stays a leaf: `core/c4/likec4.ts`'s `Rel` satisfies it without an import\n * edge from the vocabulary package into the parser.\n */\nexport interface EdgeSource {\n source: string;\n target: string;\n op?: string;\n title?: string;\n}\n\nexport interface LandscapeEvidenceRequest {\n /** The service being asked about. */\n id: string;\n /** Whether the landscape parsed — what makes its silence evidence rather than absence. */\n parses: boolean;\n /** The parsed relationships; empty when the landscape is absent or unparseable. */\n relationships: readonly EdgeSource[];\n /** Every element id in the map, for the `modelled` probe. */\n elementIds: readonly string[];\n /** The shared resolver (`core/c4/resolve/service.ts` `serviceResolver`), widened to plain strings. */\n svcOf: (id: string) => string;\n}\n\nexport interface LandscapeEvidence {\n inbound: LandscapeEdge[];\n outbound: LandscapeEdge[];\n /** Whether anything in the fleet map resolves to this service. */\n modelled: boolean;\n /** The `apiExpected` input the ladder above consumes. */\n apiExpected: boolean;\n}\n\n/**\n * The fleet map's evidence about one service: the edges into and out of it,\n * whether the map draws it at all, and whether an API is expected of it.\n *\n * One spelling, because it had grown three. `explore`'s describe and the\n * context pack's living slice partitioned the same relationships with the\n * same positive-evidence rule line for line — an inbound edge carrying an\n * operation proves somebody calls the service; a landscape that is absent or\n * does not parse proves NOTHING about who calls it, so the contract is still\n * owed — and a rule spelled twice is how the same service grades `partial` in\n * one command and `documented` in another. `list`'s serviceViews\n * (`commands/list/views.ts`) derives the same rule fleet-wide in ONE pass —\n * a single `called` set for every service, not a partition per service —\n * so that spelling deliberately stays there, cross-referenced from both\n * sides: calling this per service inside a fleet loop would re-walk the\n * whole relationship list once per service.\n */\nexport function landscapeEvidence(req: LandscapeEvidenceRequest): LandscapeEvidence {\n const { id, parses, relationships, svcOf } = req;\n const inbound: LandscapeEdge[] = [];\n const outbound: LandscapeEdge[] = [];\n for (const r of relationships) {\n // An edge whose BOTH endpoints resolve to this service is its own internal\n // wiring (`svc.web -> svc.db`): filed as inbound it made the service its own\n // caller — `← <id> \"reads\"` in `loam context` (verification 2026-09-04) —\n // and, through `apiExpected` below, told a service nobody calls that it owed\n // an OpenAPI contract. `core/brief/landscape.ts` drops it for the same\n // reason. It still TOUCHES the service, which is why the brief's `touched`\n // and `landscape.service-isolated` read the relationships rather than these\n // two lists.\n if (svcOf(r.source) === id && svcOf(r.target) === id) continue;\n const edge = { op: r.op ?? null, title: r.title ?? null };\n if (svcOf(r.target) === id) inbound.push({ service: svcOf(r.source), ...edge });\n else if (svcOf(r.source) === id) outbound.push({ service: svcOf(r.target), ...edge });\n }\n return {\n inbound,\n outbound,\n modelled: req.elementIds.some((e) => svcOf(e) === id),\n apiExpected: !parses || inbound.some((e) => e.op !== null),\n };\n}\n"]}
@@ -149,6 +149,29 @@ and `validate --all` answers `subsystem.views-stale`. (`order-service` and `paym
149
149
  unfiled deliberately: FEAT-088's committed version-2 snapshot restores them by literal path, and the
150
150
  `loam unarchive FEAT-088` walkthrough must keep working byte-for-byte.)
151
151
 
152
+ **All five models are EXTENDING models, and that is the shape this example teaches.** None of them
153
+ declares a `specification` block, none re-declares a partner, and none has a `likec4.config.json`
154
+ beside it. Each one says what is inside its service with `extend marketplace.<svc> { … }` on the
155
+ element `architecture/landscape.likec4` already binds to its directory, so `kafka`, `stripe`,
156
+ `notification-provider` and every peer service are declared exactly once — in the map — and no two
157
+ documents can disagree about their tags. The root `likec4.config.json` therefore excludes only
158
+ `**/node_modules/**` and `features/**`: `npx likec4 validate examples/docs` names ONE project,
159
+ `fleet`, holding the map and all five models, and needs no `--project` flag. The other shape — a
160
+ model that declares its own `specification` and is parsed alone, which is what every model here was
161
+ before — is still legal and still supported; SCHEMA.md's "Two shapes of a service model" has both,
162
+ the eight grades in its table — seven that came with the pair, plus the one it narrowed
163
+ (`service.likec4-config-missing`, now standalone-only) — and the
164
+ ordered recipe for migrating between them.
165
+
166
+ Two consequences are visible in the tree and worth reading for. Element ids carry their full path,
167
+ so `services/order-service/arch.spec.md` writes `Covers: marketplace.orderService.db` — the
168
+ container has no shorter name, because the model extends an element nested in the `marketplace`
169
+ group. And a cross-boundary call is written **twice** on purpose: container-level in the model
170
+ (`marketplace.notificationService.sender -> notificationProvider`) and service-level in the map
171
+ (`marketplace.notificationService -> notificationProvider`), because every fleet check — the spine,
172
+ `landscape.service-isolated`, the platform census — reads the map alone. ROADMAP.md keeps that
173
+ remainder written down rather than hidden.
174
+
152
175
  Plus one count rather than a finding: `sourcesUnverifiableFromHere: 4`. Four specs name paths in
153
176
  their own service repositories, and this is none of those repositories — the fleet gate reports the
154
177
  blind spot instead of resolving it. Running `loam validate --service <id>` inside each service repo
@@ -8,6 +8,16 @@
8
8
  // service needs a directory (warning). An element is tied to its directory by
9
9
  // `metadata { service '<id>' }`, or by its title when there is no binding.
10
10
  // Systems that are not ours carry #external and are exempt.
11
+ //
12
+ // This file is also the SPECIFICATION every service model borrows. Each
13
+ // `services/<…>/<svc>/model.likec4` here is an EXTENDING model: it declares no
14
+ // kinds and no partners, and says what is inside its service with
15
+ // `extend marketplace.<svc> { … }` on the element below that binds to its
16
+ // directory. The root `likec4.config.json` therefore does NOT exclude
17
+ // `services/**` — the map and every model are ONE LikeC4 project, so an element
18
+ // is declared exactly once and no two documents can disagree about its tags.
19
+ // SCHEMA.md, "Two shapes of a service model", has both shapes and the rule that
20
+ // picks between them; a kind or tag a model needs is added HERE, once.
11
21
  specification {
12
22
  // A grouping element — it stands for no service and owns no directory. The
13
23
  // service checks WALK the tree rather than reading only top-level elements:
@@ -20,7 +30,18 @@ specification {
20
30
  // Declared here because archive SPLICES a delta's additions verbatim: FEAT-101's
21
31
  // new system carries its `api` container along, and the kind has to be legal in
22
32
  // THIS document for the merged landscape to parse (archive refuses otherwise).
33
+ // Only a NEW service arrives this way — one that already has an extending model
34
+ // takes the addition into that model instead, and this file never grows an
35
+ // interior.
23
36
  element container
37
+ // Declared here for the SERVICE MODELS, and nothing in this file uses it: a
38
+ // store lives inside the service that owns it (`landscape.datastore-private`
39
+ // is the check that pushes one back in), so every `database` element in this
40
+ // fleet is written in a `services/<svc>/model.likec4` under
41
+ // `extend marketplace.<svc>`. An extending model declares no kinds of its
42
+ // own, so a kind it needs and this block does not declare is ONE parse error
43
+ // on the model — which is what this line prevents.
44
+ element database
24
45
  element person
25
46
  // A topic is an element, and the kind carries #external rather than each
26
47
  // instance doing it: LikeC4 does NOT inherit tags, so a topic nested inside
@@ -137,6 +158,18 @@ model {
137
158
  description 'CRM of record — the fleet reads its events and writes none'
138
159
  }
139
160
 
161
+ // Declared HERE rather than in notification-service's model, and that is the
162
+ // rule an extending model turns on: a system a service reaches is the fleet's
163
+ // element, declared once, and a model that declared its own copy would be
164
+ // `c4.element-unowned` (an extending model may only add elements under the
165
+ // element that resolves to its own service). One consumer, so it never
166
+ // becomes `landscape.platform-candidate`; #external, so no
167
+ // services/notification-provider/ is asked for.
168
+ notificationProvider = softwareSystem 'notification-provider' {
169
+ #external
170
+ description 'Third-party email and SMS gateway — quota-limited, and outside this fleet'
171
+ }
172
+
140
173
  customer -> marketplace.checkoutWeb 'Uses'
141
174
 
142
175
  // The synchronous spine. Every one of these resolves against the TARGET's
@@ -185,6 +218,14 @@ model {
185
218
  // it raises no warning because its title does not claim to be a call into a
186
219
  // fleet service.
187
220
  marketplace.paymentService -> stripe 'Authorizes cards'
221
+ // The second half of the rule the extending shape keeps: the container that
222
+ // really makes the call is drawn in the service's own model
223
+ // (`marketplace.notificationService.sender -> notificationProvider`), and the
224
+ // SERVICE-level twin is drawn here, because every fleet check — the spine,
225
+ // `landscape.service-isolated`, the platform census — reads this file alone.
226
+ // The edge exists twice on purpose; SCHEMA.md says so where it says the map
227
+ // is the one document holding every cross-service edge.
228
+ marketplace.notificationService -> notificationProvider 'Sends email and SMS'
188
229
 
189
230
  // The event spine, the async mirror of `metadata { op }`: the name must be a
190
231
  // message the PRODUCER's own asyncapi.yaml declares an `action: send` for, or
@@ -14,7 +14,10 @@
14
14
  // read as ONE LikeC4 project, so the steps below resolve against the landscape's
15
15
  // elements even though this file declares none — which also means exactly one
16
16
  // file in this directory declares the `specification` block, and it is not this
17
- // one.
17
+ // one. (The renderer's root project is WIDER than what loam grades here: it also
18
+ // holds every `services/<svc>/model.likec4`, because each of them extends the
19
+ // map. A tag declared twice across that whole set is `c4.fleet-project-invalid`,
20
+ // which is why a tag belongs in the landscape's one block.)
18
21
  //
19
22
  // THE TAG MUST BE THE FIRST THING IN THE VIEW BODY. Written after `title` it is
20
23
  // five parse errors, none of which mentions tags. That is the mistake every
@@ -30,7 +33,7 @@
30
33
  // `usecase.step-unlinked` is met except the one that matters, which is that a
31
34
  // person is not a caller. Somebody clicking a button owes no operationId. Delete
32
35
  // that exemption from the checker and this file is what notices: the example
33
- // fleet's warning count moves from eight to nine and test/examples.test.ts
36
+ // fleet's warning count moves from ten to eleven and test/examples.test.ts
34
37
  // fails.
35
38
  //
36
39
  // The hop from checkout-web to order-service is DELIBERATELY NOT DRAWN, and its
@@ -3,7 +3,6 @@
3
3
  "title": "Fleet landscape",
4
4
  "exclude": [
5
5
  "**/node_modules/**",
6
- "services/**",
7
6
  "features/**"
8
7
  ]
9
8
  }
@@ -1,27 +1,24 @@
1
- // Per-service C4 for a UI service. A UI is a first-class C4 system: it has
2
- // containers, it has runtime dependencies, and its pages (ui/pages/*.page.yaml)
3
- // hang off the operations the edges below name.
1
+ // checkout-web's own C4 — what is INSIDE the element the fleet map binds to
2
+ // services/checkout-web/. A UI is a first-class C4 system: it has containers, it
3
+ // has runtime dependencies, and its pages (ui/pages/*.page.yaml) hang off the
4
+ // operations the edges below name.
4
5
  //
5
- // Every .likec4 file is parsed ON ITS OWN — loam never concatenates them — so
6
- // this document declares the kinds it uses even though architecture/landscape.likec4
7
- // declares the same ones. Leaving them out here does not inherit, it fails to parse
8
- // (`c4.invalid`, an error), and the spec/API joins that read the model go quiet with it.
9
- specification {
10
- element softwareSystem
11
- element container
12
- element person
13
- }
14
-
6
+ // An EXTENDING model: no `specification` block, no partner copies. The kinds and
7
+ // every element this file points at — `customer`, `marketplace.orderService`,
8
+ // `marketplace.paymentService` — are declared once in
9
+ // architecture/landscape.likec4, and the root `likec4.config.json` does not
10
+ // exclude `services/**`, so the map and every model are read as ONE LikeC4
11
+ // project. `extend` takes the fully-qualified id, because the element is nested
12
+ // in the `marketplace` group.
13
+ //
14
+ // Note what this file does NOT do: it does not declare `customer = person`. The
15
+ // actor is the map's, and the edge below simply points at it. That is the rule
16
+ // the shape turns on — an element declared here outside
17
+ // `marketplace.checkoutWeb` is `c4.element-unowned` — and it is also what stops
18
+ // two documents from holding two different descriptions of the same person.
19
+ // SCHEMA.md, "Two shapes of a service model", has the other shape.
15
20
  model {
16
- // The actor belongs in a UI's own model in a way it does not in a backend's:
17
- // the pages exist for this person, and the flows in ui/pages/ start at them.
18
- // The landscape draws the same customer -> checkout-web edge at fleet scale;
19
- // a person is exempt from the service/directory cross-check either way.
20
- customer = person 'Customer'
21
-
22
- checkoutWeb = softwareSystem 'checkout-web' {
23
- description 'Customer-facing checkout UI'
24
-
21
+ extend marketplace.checkoutWeb {
25
22
  spa = container 'spa' {
26
23
  description 'Checkout SPA — cart, payment and order status pages'
27
24
  technology 'React'
@@ -37,37 +34,21 @@ model {
37
34
  technology 'Node.js'
38
35
  }
39
36
 
40
- customer -> spa 'Buys'
41
37
  spa -> bff 'Calls the checkout BFF'
42
38
  }
43
39
 
44
- // The services this one actually calls. A service model carries everything the
45
- // service touches, cross-boundary included, drawn with the edge the service
46
- // really has — the landscape carries only the boundary crossing (checkout-web ->
47
- // order-service), while the edge below says WHICH container makes the call.
48
- // Without these three edges this file would be two boxes and a hop, and
49
- // `c4.no-relationships` would be right to say a model that reaches nothing is
50
- // almost never true.
51
- orderService = softwareSystem 'order-service' {
52
- description 'Owns the order lifecycle'
53
- // Bind rather than rely on the title. A renamed box silently stops matching
54
- // its directory everywhere the two are joined — in the landscape that costs
55
- // `landscape.binding-unknown` (error) when the id is wrong, and here it is
56
- // what `health.dependency-unmodelled` resolves a declared dependency
57
- // through in the services that have a health.yaml. checkout-web has none:
58
- // a UI is nobody's runtime dependency, so it declares no SLIs, no alerts
59
- // and no dependency graph, and owes no arch.spec.md to cover them.
60
- metadata {
61
- service 'order-service'
62
- }
63
- }
64
- paymentService = softwareSystem 'payment-service' {
65
- description 'Owns payment authorization/capture'
66
- metadata {
67
- service 'payment-service'
68
- }
69
- }
40
+ // The person is the MAP's element and the hop into this UI is drawn from it.
41
+ // The landscape draws the coarser `customer -> marketplace.checkoutWeb` twin
42
+ // at fleet scale; this one says which container the click reaches. A person is
43
+ // exempt from the service/directory cross-check either way.
44
+ customer -> marketplace.checkoutWeb.spa 'Buys'
70
45
 
46
+ // The services this one actually calls, drawn with the edge the service really
47
+ // has — the landscape carries only the boundary crossing (checkout-web ->
48
+ // order-service), while the edges below say WHICH container makes the call.
49
+ // Without them this file would be two boxes and a hop, and `c4.no-relationships`
50
+ // would be right to say a model that reaches nothing is almost never true.
51
+ //
71
52
  // `metadata { op }` is the C4 half of the operation join: the exact
72
53
  // operationId order-service's openapi.yaml declares. What `loam validate`
73
54
  // grades is the LANDSCAPE's twin of each edge below — `spine.op-undefined`
@@ -77,12 +58,12 @@ model {
77
58
  // nothing would catch it if they did. Two calls to one service are two edges,
78
59
  // not one edge with a list — a view has to be able to show that the status
79
60
  // page and the checkout page stand on different operations.
80
- checkoutWeb.bff -> orderService 'Calls createOrder' {
61
+ marketplace.checkoutWeb.bff -> marketplace.orderService 'Calls createOrder' {
81
62
  metadata {
82
63
  op 'createOrder'
83
64
  }
84
65
  }
85
- checkoutWeb.bff -> orderService 'Calls getOrder' {
66
+ marketplace.checkoutWeb.bff -> marketplace.orderService 'Calls getOrder' {
86
67
  metadata {
87
68
  op 'getOrder'
88
69
  }
@@ -94,16 +75,18 @@ model {
94
75
  // for an edge that SAYS it calls something and names no operation, so nothing
95
76
  // can check the call still exists. Free here and one warning there, which is
96
77
  // the asymmetry worth knowing: the fleet map is what the spine reads.
97
- checkoutWeb.spa -> paymentService 'Calls the payment API'
78
+ marketplace.checkoutWeb.spa -> marketplace.paymentService 'Calls the payment API'
98
79
  }
99
80
 
100
81
  // Views are the LikeC4 renderer's, never loam's — no check reads this block.
101
- // Scoped to the system so the page answers "what does the checkout UI stand on"
102
- // rather than redrawing the fleet, which architecture/landscape.likec4 already does.
82
+ // Scoped to the service's element so the page answers "what does the checkout UI
83
+ // stand on" rather than redrawing the fleet, which architecture/landscape.likec4
84
+ // already does. This file is part of the root project: `npx likec4 start` at the
85
+ // docs root draws it, with no per-service project file to write.
103
86
  views {
104
- view checkout_web of checkoutWeb {
87
+ view checkout_web of marketplace.checkoutWeb {
105
88
  title 'checkout-web — containers and the services they call'
106
89
  include *
107
- include orderService, paymentService
90
+ include marketplace.orderService, marketplace.paymentService
108
91
  }
109
92
  }
@@ -25,9 +25,16 @@ this service's own `model.likec4` plus the landscape (`covers.unknown`, with a d
25
25
  hint, is the typo guard — a mistyped entry otherwise costs exactly the coverage it was written
26
26
  for), and every `slis[].name` and `alerts[].name` in `health.yaml` must be named by one of them
27
27
  (`health.uncovered`, or a signal ships to the on-call rotation with nothing testing it). Edge
28
- entries below are spelled as this model draws them — `orderService.outbox -> kafka.orderEvents`
29
- — because the endpoint that matters is the container; `order-service -> payment-service`
30
- resolves too, through the `metadata { service }` binding on the root element.
28
+ entries below are spelled as this model draws them — `marketplace.orderService.outbox ->
29
+ kafka.orderEvents` — because the endpoint that matters is the container; `order-service ->
30
+ payment-service` resolves too, through the `metadata { service }` binding on the map's element.
31
+
32
+ Element ids here carry the FULL path, `marketplace.` and all. That is a consequence of the model
33
+ shape rather than a style: `model.likec4` extends the element the fleet map declares inside the
34
+ `marketplace` group, so the container it adds is `marketplace.orderService.db` and no shorter
35
+ spelling of it exists. A service whose model declares its own `specification` and is parsed alone
36
+ mints shorter ids and writes them here — see SCHEMA.md, "Two shapes of a service model", which also
37
+ lists requalifying these lines as a step of the migration between the two.
31
38
 
32
39
  ## Requirements
33
40
 
@@ -40,7 +47,7 @@ key rather than placing a second order or requesting a second authorization. che
40
47
  retry on a gateway timeout is not a rare path — it is the normal shape of a mobile checkout —
41
48
  and without the key a customer's flaky connection charges the card twice.
42
49
 
43
- Covers: orderService.db, orderService.domain -> paymentService
50
+ Covers: marketplace.orderService.db, marketplace.orderService.domain -> marketplace.paymentService
44
51
 
45
52
  #### Scenario: checkout-web retries a placement it never saw acknowledged
46
53
  - **Given** an order placed under `Idempotency-Key` "ik-8842" with an authorization already requested
@@ -63,7 +70,7 @@ database and then to kafka. A dual write has no failure mode in which both sides
63
70
  between them either announces an order nobody stored or stores one nobody hears about, and
64
71
  notification-service is the service that pays for the second case.
65
72
 
66
- Covers: orderService.outbox, orderService.outbox -> kafka.orderEvents
73
+ Covers: marketplace.orderService.outbox, marketplace.orderService.outbox -> kafka.orderEvents
67
74
 
68
75
  #### Scenario: The broker is unavailable when an order is placed
69
76
  - **Given** an accepted order whose `OrderPlaced` row is still in the outbox table
@@ -86,7 +93,7 @@ already recorded. The producer's outbox promises at-least-once delivery and a re
86
93
  redelivers whatever was in flight, so a consumer without a ledger is a consumer that confirms
87
94
  an order it already confirmed — and, once ORD-CANCEL has moved on, resurrects a cancelled one.
88
95
 
89
- Covers: orderService.consumer, kafka.paymentEvents -> orderService.consumer
96
+ Covers: marketplace.orderService.consumer, kafka.paymentEvents -> marketplace.orderService.consumer
90
97
  Consumes: payment.PaymentAuthorized
91
98
 
92
99
  #### Scenario: The same authorization is redelivered after a consumer rebalance