@ybotok/loam 0.2.0-alpha.5 → 0.2.0-alpha.7

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 (303) hide show
  1. package/CHANGELOG.md +1063 -0
  2. package/MIGRATING-from-OpenSpec.md +7 -4
  3. package/QUICKSTART.md +8 -3
  4. package/README.md +13 -7
  5. package/ROADMAP.md +285 -11
  6. package/SCHEMA.md +566 -45
  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 +53 -23
  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/commit.js +34 -1
  25. package/dist/commands/seed/commit.js.map +1 -1
  26. package/dist/commands/seed/plan.js +158 -13
  27. package/dist/commands/seed/plan.js.map +1 -1
  28. package/dist/commands/seed/seed.js +7 -2
  29. package/dist/commands/seed/seed.js.map +1 -1
  30. package/dist/commands/show/feature.js +2 -1
  31. package/dist/commands/show/feature.js.map +1 -1
  32. package/dist/commands/show/marks.js +3 -5
  33. package/dist/commands/show/marks.js.map +1 -1
  34. package/dist/commands/show/service.js +39 -18
  35. package/dist/commands/show/service.js.map +1 -1
  36. package/dist/commands/subsystem/edit.js +4 -2
  37. package/dist/commands/subsystem/edit.js.map +1 -1
  38. package/dist/commands/subsystem/move.js +55 -17
  39. package/dist/commands/subsystem/move.js.map +1 -1
  40. package/dist/commands/subsystem/sync.js +238 -87
  41. package/dist/commands/subsystem/sync.js.map +1 -1
  42. package/dist/commands/subsystem/txn/exclude.js +132 -0
  43. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  44. package/dist/commands/subsystem/txn/report.js +174 -0
  45. package/dist/commands/subsystem/txn/report.js.map +1 -0
  46. package/dist/commands/subsystem/txn/txn.js +126 -22
  47. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  48. package/dist/commands/subsystem/txn/views.js +159 -19
  49. package/dist/commands/subsystem/txn/views.js.map +1 -1
  50. package/dist/commands/validate/arch-coverage.js +16 -9
  51. package/dist/commands/validate/arch-coverage.js.map +1 -1
  52. package/dist/commands/validate/checks/fleet-shape.js +30 -77
  53. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  54. package/dist/commands/validate/checks/health-deps.js +9 -6
  55. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  56. package/dist/commands/validate/checks/vocabulary.js +4 -19
  57. package/dist/commands/validate/checks/vocabulary.js.map +1 -1
  58. package/dist/commands/validate/feature.js +1 -1
  59. package/dist/commands/validate/feature.js.map +1 -1
  60. package/dist/commands/validate/fleet/census.js +37 -13
  61. package/dist/commands/validate/fleet/census.js.map +1 -1
  62. package/dist/commands/validate/fleet/landscape.js +131 -120
  63. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  64. package/dist/commands/validate/fleet/load.js +66 -0
  65. package/dist/commands/validate/fleet/load.js.map +1 -1
  66. package/dist/commands/validate/fleet/map/attest.js +102 -0
  67. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  68. package/dist/commands/validate/fleet/map/bindings.js +109 -0
  69. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  70. package/dist/commands/validate/fleet/map/consumers.js +219 -0
  71. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  72. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  73. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  74. package/dist/commands/validate/fleet/map/usecases.js +56 -0
  75. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  76. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  77. package/dist/commands/validate/fleet/usecases/steps.js +49 -17
  78. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  79. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  80. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  81. package/dist/commands/validate/fleet/views/ids.js +45 -15
  82. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  83. package/dist/commands/validate/fleet/views/projects.js +258 -70
  84. package/dist/commands/validate/fleet/views/projects.js.map +1 -1
  85. package/dist/commands/validate/fleet/views/root-documents.js +99 -0
  86. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  87. package/dist/commands/validate/fleet/views/stale.js +32 -15
  88. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  89. package/dist/commands/validate/service/model/diverged.js +226 -0
  90. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  91. package/dist/commands/validate/service/model/grade.js +163 -0
  92. package/dist/commands/validate/service/model/grade.js.map +1 -0
  93. package/dist/commands/validate/service/service.js +110 -34
  94. package/dist/commands/validate/service/service.js.map +1 -1
  95. package/dist/commands/validate/service/specs.js +39 -5
  96. package/dist/commands/validate/service/specs.js.map +1 -1
  97. package/dist/commands/validate/service/spine.js +41 -3
  98. package/dist/commands/validate/service/spine.js.map +1 -1
  99. package/dist/commands/validate/service/usecases/tags.js +113 -0
  100. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  101. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  102. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  103. package/dist/commands/validate/validate.js +10 -24
  104. package/dist/commands/validate/validate.js.map +1 -1
  105. package/dist/core/agent/agents-md/artifacts.js +1 -0
  106. package/dist/core/agent/agents-md/artifacts.js.map +1 -1
  107. package/dist/core/agent/agents-md/command-map.js +19 -5
  108. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  109. package/dist/core/agent/agents-md/cycle.js +24 -9
  110. package/dist/core/agent/agents-md/cycle.js.map +1 -1
  111. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  112. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  113. package/dist/core/agent/agents-md/map/subsystem.js +70 -17
  114. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  115. package/dist/core/agent/agents-md/map/usecases.js +45 -3
  116. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  117. package/dist/core/agent/agents-md/refusals.js +5 -3
  118. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  119. package/dist/core/agent/agents-md.js +26 -7
  120. package/dist/core/agent/agents-md.js.map +1 -1
  121. package/dist/core/agent/support/report.js +39 -5
  122. package/dist/core/agent/support/report.js.map +1 -1
  123. package/dist/core/agent/workflows/adopt.js +123 -14
  124. package/dist/core/agent/workflows/adopt.js.map +1 -1
  125. package/dist/core/agent/workflows/check.js +34 -20
  126. package/dist/core/agent/workflows/check.js.map +1 -1
  127. package/dist/core/agent/workflows/closing.js +3 -1
  128. package/dist/core/agent/workflows/closing.js.map +1 -1
  129. package/dist/core/agent/workflows/reference/spine.js +12 -6
  130. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  131. package/dist/core/brief/brief.js +88 -19
  132. package/dist/core/brief/brief.js.map +1 -1
  133. package/dist/core/brief/checks.js +133 -7
  134. package/dist/core/brief/checks.js.map +1 -1
  135. package/dist/core/brief/landscape.js +212 -129
  136. package/dist/core/brief/landscape.js.map +1 -1
  137. package/dist/core/brief/map/extend.js +81 -0
  138. package/dist/core/brief/map/extend.js.map +1 -0
  139. package/dist/core/brief/map/owed.js +284 -0
  140. package/dist/core/brief/map/owed.js.map +1 -0
  141. package/dist/core/brief/targets.js +94 -29
  142. package/dist/core/brief/targets.js.map +1 -1
  143. package/dist/core/brief/unchecked.js +13 -3
  144. package/dist/core/brief/unchecked.js.map +1 -1
  145. package/dist/core/c4/likec4.js +23 -1
  146. package/dist/core/c4/likec4.js.map +1 -1
  147. package/dist/core/c4/parsed/deployment.js +5 -3
  148. package/dist/core/c4/parsed/deployment.js.map +1 -1
  149. package/dist/core/c4/parsed/view-ids.js +39 -1
  150. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  151. package/dist/core/c4/prefetch/fleet.js +142 -0
  152. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  153. package/dist/core/c4/project/architecture.js +77 -7
  154. package/dist/core/c4/project/architecture.js.map +1 -1
  155. package/dist/core/c4/project/batch.js +140 -0
  156. package/dist/core/c4/project/batch.js.map +1 -0
  157. package/dist/core/c4/project/documents.js +12 -6
  158. package/dist/core/c4/project/documents.js.map +1 -1
  159. package/dist/core/c4/project/load.js +85 -42
  160. package/dist/core/c4/project/load.js.map +1 -1
  161. package/dist/core/c4/project/staged.js +53 -15
  162. package/dist/core/c4/project/staged.js.map +1 -1
  163. package/dist/core/c4/resolve/attested.js +0 -0
  164. package/dist/core/c4/resolve/attested.js.map +1 -0
  165. package/dist/core/c4/resolve/service.js +8 -17
  166. package/dist/core/c4/resolve/service.js.map +1 -1
  167. package/dist/core/c4/resolve/steps.js +18 -0
  168. package/dist/core/c4/resolve/steps.js.map +1 -1
  169. package/dist/core/c4/root-project/exclude.js +352 -0
  170. package/dist/core/c4/root-project/exclude.js.map +1 -0
  171. package/dist/core/c4/service-model/documents.js +47 -0
  172. package/dist/core/c4/service-model/documents.js.map +1 -0
  173. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  174. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  175. package/dist/core/c4/service-model/load.js +165 -0
  176. package/dist/core/c4/service-model/load.js.map +1 -0
  177. package/dist/core/c4/service-model/renderer.js +128 -0
  178. package/dist/core/c4/service-model/renderer.js.map +1 -0
  179. package/dist/core/c4/service-model/shape.js +126 -0
  180. package/dist/core/c4/service-model/shape.js.map +1 -0
  181. package/dist/core/c4/service-model/slice.js +188 -0
  182. package/dist/core/c4/service-model/slice.js.map +1 -0
  183. package/dist/core/c4/source-scan.js +26 -0
  184. package/dist/core/c4/source-scan.js.map +1 -1
  185. package/dist/core/c4/splice/authored-source.js +60 -13
  186. package/dist/core/c4/splice/authored-source.js.map +1 -1
  187. package/dist/core/c4/splice/contract.js.map +1 -1
  188. package/dist/core/c4/splice/identity/declared.js +87 -0
  189. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  190. package/dist/core/c4/splice/identity/edges.js +17 -0
  191. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  192. package/dist/core/c4/splice/identity/existing.js +80 -0
  193. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  194. package/dist/core/c4/splice/identity/interior.js +80 -0
  195. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  196. package/dist/core/c4/splice/identity/owner.js +50 -0
  197. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  198. package/dist/core/c4/splice/landscape-merge.js +154 -100
  199. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  200. package/dist/core/c4/splice/model/merge.js +140 -0
  201. package/dist/core/c4/splice/model/merge.js.map +1 -0
  202. package/dist/core/c4/splice/placement.js +14 -4
  203. package/dist/core/c4/splice/placement.js.map +1 -1
  204. package/dist/core/c4/workspace.js +2 -1
  205. package/dist/core/c4/workspace.js.map +1 -1
  206. package/dist/core/coherence/coherence.js +7 -0
  207. package/dist/core/coherence/coherence.js.map +1 -1
  208. package/dist/core/delta/delta.js +9 -0
  209. package/dist/core/delta/delta.js.map +1 -1
  210. package/dist/core/deployment/delta.js +16 -10
  211. package/dist/core/deployment/delta.js.map +1 -1
  212. package/dist/core/diff/base-state.js +21 -5
  213. package/dist/core/diff/base-state.js.map +1 -1
  214. package/dist/core/diff/victims.js +10 -1
  215. package/dist/core/diff/victims.js.map +1 -1
  216. package/dist/core/docs.js +29 -24
  217. package/dist/core/docs.js.map +1 -1
  218. package/dist/core/doctor/config.js +7 -3
  219. package/dist/core/doctor/config.js.map +1 -1
  220. package/dist/core/doctor/doctor.js +36 -8
  221. package/dist/core/doctor/doctor.js.map +1 -1
  222. package/dist/core/doctor/report.js.map +1 -1
  223. package/dist/core/doctor/reports/scan.js +242 -0
  224. package/dist/core/doctor/reports/scan.js.map +1 -0
  225. package/dist/core/explain/refusals.js +1 -1
  226. package/dist/core/explain/refusals.js.map +1 -1
  227. package/dist/core/explore/explore.js +44 -9
  228. package/dist/core/explore/explore.js.map +1 -1
  229. package/dist/core/fleet-context.js +47 -0
  230. package/dist/core/fleet-context.js.map +1 -1
  231. package/dist/core/gate/checks.js +5 -1
  232. package/dist/core/gate/checks.js.map +1 -1
  233. package/dist/core/gate/partners.js +65 -26
  234. package/dist/core/gate/partners.js.map +1 -1
  235. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  236. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  237. package/dist/core/pack/living.js +47 -13
  238. package/dist/core/pack/living.js.map +1 -1
  239. package/dist/core/pack/pack.js +9 -6
  240. package/dist/core/pack/pack.js.map +1 -1
  241. package/dist/core/projection/arch-slice.js +2 -2
  242. package/dist/core/projection/arch-slice.js.map +1 -1
  243. package/dist/core/repo/paths.js +45 -38
  244. package/dist/core/repo/paths.js.map +1 -1
  245. package/dist/core/repo/tree/render/body.js +45 -6
  246. package/dist/core/repo/tree/render/body.js.map +1 -1
  247. package/dist/core/repo/tree/render/projects.js +93 -35
  248. package/dist/core/repo/tree/render/projects.js.map +1 -1
  249. package/dist/core/repo/tree/render/service/header.js +125 -0
  250. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  251. package/dist/core/repo/tree/render/service/peers.js +28 -0
  252. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  253. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  254. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  255. package/dist/core/repo/tree/render/service/views.js +78 -0
  256. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  257. package/dist/core/repo/tree/render/stale.js +20 -4
  258. package/dist/core/repo/tree/render/stale.js.map +1 -1
  259. package/dist/core/repo/tree/render/views.js +149 -32
  260. package/dist/core/repo/tree/render/views.js.map +1 -1
  261. package/dist/core/repo/tree/walk.js +45 -2
  262. package/dist/core/repo/tree/walk.js.map +1 -1
  263. package/dist/core/scaffold/landscape.js +219 -9
  264. package/dist/core/scaffold/landscape.js.map +1 -1
  265. package/dist/core/staging/writes.js.map +1 -1
  266. package/dist/core/usecases/delta/claims.js +1 -0
  267. package/dist/core/usecases/delta/claims.js.map +1 -1
  268. package/dist/core/usecases/delta/overlay.js +4 -0
  269. package/dist/core/usecases/delta/overlay.js.map +1 -1
  270. package/dist/core/usecases/fleet.js +18 -6
  271. package/dist/core/usecases/fleet.js.map +1 -1
  272. package/dist/core/usecases/operations.js +7 -0
  273. package/dist/core/usecases/operations.js.map +1 -1
  274. package/dist/core/usecases/service/exclude.js +119 -0
  275. package/dist/core/usecases/service/exclude.js.map +1 -0
  276. package/dist/core/usecases/service/flows.js +161 -0
  277. package/dist/core/usecases/service/flows.js.map +1 -0
  278. package/dist/core/verify/checklist.js +42 -10
  279. package/dist/core/verify/checklist.js.map +1 -1
  280. package/dist/core/vocabulary/issue.js.map +1 -1
  281. package/dist/core/vocabulary/maturity.js +51 -1
  282. package/dist/core/vocabulary/maturity.js.map +1 -1
  283. package/examples/README.md +30 -8
  284. package/examples/docs/architecture/landscape.likec4 +47 -0
  285. package/examples/docs/architecture/services.likec4 +70 -0
  286. package/examples/docs/architecture/subsystems.likec4 +3 -0
  287. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  288. package/examples/docs/likec4.config.json +0 -1
  289. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  290. package/examples/docs/services/order-service/arch.spec.md +13 -6
  291. package/examples/docs/services/order-service/model.likec4 +58 -70
  292. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  293. package/examples/docs/services/payment-service/model.likec4 +38 -39
  294. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  295. package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
  296. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  297. package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
  298. package/package.json +1 -1
  299. package/examples/docs/services/checkout-web/likec4.config.json +0 -4
  300. package/examples/docs/services/order-service/likec4.config.json +0 -4
  301. package/examples/docs/services/payment-service/likec4.config.json +0 -4
  302. package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
  303. package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
@@ -0,0 +1,102 @@
1
+ /**
2
+ * What the fleet's own `model.likec4` files say, read ONCE for the two map
3
+ * checks that need it: the calls each model attests across its boundary
4
+ * (`./isolation.ts`), and the datastores that live nested inside a service's own
5
+ * element (`../../checks/fleet-shape.ts`).
6
+ *
7
+ * One reader rather than two, because the two checks were answering the same
8
+ * question from different evidence and disagreeing about the fleet. The
9
+ * consumer census walked the MAP's relationships alone, so a store whose
10
+ * consuming edges are all drawn in `services/<…>/model.likec4` had zero
11
+ * consumers and earned nothing at all — and a store reached by three models plus
12
+ * one map edge was graded against the single service the MAP happened to draw
13
+ * (R1). A model is evidence about the fleet exactly as the map is, so it is read
14
+ * here and handed to both.
15
+ *
16
+ * EXTENDING MODELS ONLY, for the consumer join. An extending model's ids ARE the
17
+ * map's fully-qualified ids — one project, one id space — so `counterpartId`
18
+ * joins to a drawn element exactly. A standalone model's ids are its own file's,
19
+ * and `../../../../core/c4/resolve/attested.ts` says outright that a counterpart
20
+ * is never matched to a landscape element: joining one would file a coincidence
21
+ * of spelling as a coupling. Standalone models still contribute their attested
22
+ * CALLS, because `landscape.service-isolated` asks only whether the model
23
+ * declares any, which is shape-independent.
24
+ *
25
+ * A model that does not parse contributes nothing — neither calls nor stores.
26
+ * `c4.invalid` is that file's finding, and a fleet-shape advisory computed from
27
+ * a document nobody could read would be an inference about bytes.
28
+ *
29
+ * A sub-package of `fleet/` for `./isolation.ts`'s reason and with its rule: it
30
+ * imports NOTHING from `fleet/`, so the map facts it needs (the resolver, the
31
+ * ids the map draws at fleet level) arrive on the input record.
32
+ */
33
+ import { existsSync } from "node:fs";
34
+ import {} from "../../../../core/c4/likec4.js";
35
+ import { attestedCalls } from "../../../../core/c4/resolve/attested.js";
36
+ import { FleetContext } from "../../../../core/fleet-context.js";
37
+ import { inOrder } from "../../../../core/kernel/concurrency.js";
38
+ import {} from "../../../../core/repo/entries.js";
39
+ import { servicePathsAt } from "../../../../core/repo/paths.js";
40
+ import { DATASTORE_KIND } from "../../../../core/vocabulary/maturity.js";
41
+ export async function mapAttestation(input) {
42
+ // Through the ONE reader of `model.likec4` (`core/c4/service-model/load.ts`),
43
+ // so a model that EXTENDS the map attests the calls it draws inside the
44
+ // element the map binds — read as a slice of the per-service project — exactly
45
+ // as a standalone one attests the calls it draws in its own file. Memoised in
46
+ // the index, so under `--all` these are hits. Pooled and joined on the entry,
47
+ // never by index.
48
+ const context = input.fleet ?? new FleetContext();
49
+ const present = input.entries.filter((entry) => existsSync(servicePathsAt(entry.dir).model));
50
+ // ONE workspace for the fleet's models rather than one per service. Both
51
+ // answers below are about the WHOLE fleet — who else reaches this store is not
52
+ // a question a narrowed run may answer differently — while `--base` prefetches
53
+ // only the scope it grades, so without this a scoped run would spin a LikeC4
54
+ // workspace per un-narrowed service. An accelerator with `prefetchLikeC4`'s
55
+ // failure story exactly: a batch that cannot run seeds nothing, and every read
56
+ // below falls back to its own load.
57
+ await context.prefetchServiceModels(input.docsDir, present.map((entry) => servicePathsAt(entry.dir)));
58
+ const loaded = await inOrder(present, async (entry) => ({
59
+ entry,
60
+ // CONTAINED, for `fleet/load.ts unreadableLandscape`'s reason: this runs on
61
+ // the landscape target, which sits outside the dispatcher's `guarded`, so a
62
+ // `model.likec4` that is a directory or carries a permission bit would
63
+ // otherwise become the whole run's `repository-unavailable` — one file, and
64
+ // no report at all for the ninety-nine services that are fine. A model that
65
+ // could not be READ contributes nothing here, exactly as one that did not
66
+ // parse does; `service.unreadable` is its own target's finding.
67
+ model: await context.serviceModel(input.docsDir, servicePathsAt(entry.dir)).catch(() => null),
68
+ }));
69
+ const models = [];
70
+ const nestedStores = [];
71
+ for (const { entry, model } of loaded) {
72
+ // A model that does not parse attests nothing: `c4.invalid` is its finding.
73
+ // An extending model whose MAP does not parse could not be read at all, and
74
+ // neither reader runs in that state anyway — the fleet target returns on
75
+ // `landscape.invalid` long before here.
76
+ if (model === null || model.mapUnreadable || model.doc.errors.length > 0)
77
+ continue;
78
+ models.push({
79
+ service: entry.id,
80
+ extending: model.shape === "extending",
81
+ calls: attestedCalls(model.doc, entry.id, input.services),
82
+ });
83
+ if (model.shape !== "extending")
84
+ continue;
85
+ for (const element of model.doc.elements) {
86
+ if (element.kind.toLowerCase() !== DATASTORE_KIND)
87
+ continue;
88
+ // The slice carries the partners this model's own edges point at, and a
89
+ // partner may be another service's store — so the store is kept only when
90
+ // the MAP's resolver files it under this service, and only when the map
91
+ // does not draw it as a peer of the services (which is the fleet-level
92
+ // census's subject, and a different grade).
93
+ if (input.standsForService(element))
94
+ continue;
95
+ if (input.drawnIds.has(element.id) || input.resolve(element.id) !== entry.id)
96
+ continue;
97
+ nestedStores.push({ owner: entry.id, fqn: element.id, title: element.title });
98
+ }
99
+ }
100
+ return { models, nestedStores };
101
+ }
102
+ //# sourceMappingURL=attest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attest.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/map/attest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the fleet's own `model.likec4` files say, read ONCE for the two map\n * checks that need it: the calls each model attests across its boundary\n * (`./isolation.ts`), and the datastores that live nested inside a service's own\n * element (`../../checks/fleet-shape.ts`).\n *\n * One reader rather than two, because the two checks were answering the same\n * question from different evidence and disagreeing about the fleet. The\n * consumer census walked the MAP's relationships alone, so a store whose\n * consuming edges are all drawn in `services/<…>/model.likec4` had zero\n * consumers and earned nothing at all — and a store reached by three models plus\n * one map edge was graded against the single service the MAP happened to draw\n * (R1). A model is evidence about the fleet exactly as the map is, so it is read\n * here and handed to both.\n *\n * EXTENDING MODELS ONLY, for the consumer join. An extending model's ids ARE the\n * map's fully-qualified ids — one project, one id space — so `counterpartId`\n * joins to a drawn element exactly. A standalone model's ids are its own file's,\n * and `../../../../core/c4/resolve/attested.ts` says outright that a counterpart\n * is never matched to a landscape element: joining one would file a coincidence\n * of spelling as a coupling. Standalone models still contribute their attested\n * CALLS, because `landscape.service-isolated` asks only whether the model\n * declares any, which is shape-independent.\n *\n * A model that does not parse contributes nothing — neither calls nor stores.\n * `c4.invalid` is that file's finding, and a fleet-shape advisory computed from\n * a document nobody could read would be an inference about bytes.\n *\n * A sub-package of `fleet/` for `./isolation.ts`'s reason and with its rule: it\n * imports NOTHING from `fleet/`, so the map facts it needs (the resolver, the\n * ids the map draws at fleet level) arrive on the input record.\n */\nimport { existsSync } from \"node:fs\";\nimport { type Elem } from \"../../../../core/c4/likec4.js\";\nimport { attestedCalls, type AttestedCall } from \"../../../../core/c4/resolve/attested.js\";\nimport { FleetContext } from \"../../../../core/fleet-context.js\";\nimport { inOrder } from \"../../../../core/kernel/concurrency.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { type ServiceEntry } from \"../../../../core/repo/entries.js\";\nimport { servicePathsAt } from \"../../../../core/repo/paths.js\";\nimport { DATASTORE_KIND } from \"../../../../core/vocabulary/maturity.js\";\n\n/** One service's model, as the map checks read it. */\nexport interface AttestedModel {\n /** The service directory this model belongs to. */\n service: string;\n /** EXTENDING — and therefore whether `counterpartId` is a map id at all. */\n extending: boolean;\n calls: readonly AttestedCall[];\n}\n\n/** A datastore drawn INSIDE the element that resolves to its owner, rather than beside it. */\nexport interface NestedStore {\n /** The service whose element holds it. */\n owner: string;\n /** The store's id as the one project spells it — `marketplace.orderService.db`. */\n fqn: string;\n title: string;\n}\n\nexport interface MapAttestation {\n models: readonly AttestedModel[];\n nestedStores: readonly NestedStore[];\n}\n\nexport interface AttestInput {\n docsDir: DocsDir;\n /** The enumerated services — every one of them, not a candidate subset: both readers ask about the whole fleet. */\n entries: readonly ServiceEntry[];\n /** The services/<id>/ directories that exist, as the resolver's `known`. */\n services: ReadonlySet<string>;\n /** The MAP's element→service resolver — what decides whose element a nested id sits under. */\n resolve: (id: string) => string;\n /**\n * `census.ts`'s one predicate for \"this element stands for a `services/<id>/`\",\n * injected because a `fleet/map/` module may not import `fleet/`. A `database`\n * that IS a service directory is the fleet itself, not a store: the binding\n * checks grade it, exactly as they do in `./consumers.ts`'s two halves.\n */\n standsForService: (e: Elem) => boolean;\n /** The ids the map draws at FLEET level: a datastore among them is a peer, and the peer census owns it. */\n drawnIds: ReadonlySet<string>;\n /** Under `--all` every model.likec4 is already in this memo, so the reads below are hits. */\n fleet?: FleetContext;\n}\n\nexport async function mapAttestation(input: AttestInput): Promise<MapAttestation> {\n // Through the ONE reader of `model.likec4` (`core/c4/service-model/load.ts`),\n // so a model that EXTENDS the map attests the calls it draws inside the\n // element the map binds — read as a slice of the per-service project — exactly\n // as a standalone one attests the calls it draws in its own file. Memoised in\n // the index, so under `--all` these are hits. Pooled and joined on the entry,\n // never by index.\n const context = input.fleet ?? new FleetContext();\n const present = input.entries.filter((entry) => existsSync(servicePathsAt(entry.dir).model));\n // ONE workspace for the fleet's models rather than one per service. Both\n // answers below are about the WHOLE fleet — who else reaches this store is not\n // a question a narrowed run may answer differently — while `--base` prefetches\n // only the scope it grades, so without this a scoped run would spin a LikeC4\n // workspace per un-narrowed service. An accelerator with `prefetchLikeC4`'s\n // failure story exactly: a batch that cannot run seeds nothing, and every read\n // below falls back to its own load.\n await context.prefetchServiceModels(input.docsDir, present.map((entry) => servicePathsAt(entry.dir)));\n const loaded = await inOrder(present, async (entry) => ({\n entry,\n // CONTAINED, for `fleet/load.ts unreadableLandscape`'s reason: this runs on\n // the landscape target, which sits outside the dispatcher's `guarded`, so a\n // `model.likec4` that is a directory or carries a permission bit would\n // otherwise become the whole run's `repository-unavailable` — one file, and\n // no report at all for the ninety-nine services that are fine. A model that\n // could not be READ contributes nothing here, exactly as one that did not\n // parse does; `service.unreadable` is its own target's finding.\n model: await context.serviceModel(input.docsDir, servicePathsAt(entry.dir)).catch(() => null),\n }));\n\n const models: AttestedModel[] = [];\n const nestedStores: NestedStore[] = [];\n for (const { entry, model } of loaded) {\n // A model that does not parse attests nothing: `c4.invalid` is its finding.\n // An extending model whose MAP does not parse could not be read at all, and\n // neither reader runs in that state anyway — the fleet target returns on\n // `landscape.invalid` long before here.\n if (model === null || model.mapUnreadable || model.doc.errors.length > 0) continue;\n models.push({\n service: entry.id,\n extending: model.shape === \"extending\",\n calls: attestedCalls(model.doc, entry.id, input.services),\n });\n if (model.shape !== \"extending\") continue;\n for (const element of model.doc.elements) {\n if (element.kind.toLowerCase() !== DATASTORE_KIND) continue;\n // The slice carries the partners this model's own edges point at, and a\n // partner may be another service's store — so the store is kept only when\n // the MAP's resolver files it under this service, and only when the map\n // does not draw it as a peer of the services (which is the fleet-level\n // census's subject, and a different grade).\n if (input.standsForService(element)) continue;\n if (input.drawnIds.has(element.id) || input.resolve(element.id) !== (entry.id as string)) continue;\n nestedStores.push({ owner: entry.id, fqn: element.id, title: element.title });\n }\n }\n return { models, nestedStores };\n}\n"]}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The element↔directory passes of the fleet cross-check: which drawn box
3
+ * answers for which `services/<id>/`, and what it means when the answer is
4
+ * none, two, or one nobody owns.
5
+ *
6
+ * Lifted out of `../landscape.ts` when that module reached its line limit, and
7
+ * the seam is a phase rather than a subject: everything here runs after the map
8
+ * has parsed, reads nothing from disk, and grades exactly one join — the census
9
+ * `../census.ts` derives against the enumeration `../landscape.ts` performed.
10
+ * The four grades are ordered as the report reads them, and the ORDER is the
11
+ * caller's business, which is why this is one function rather than four.
12
+ *
13
+ * A sub-package of `fleet/` for `./isolation.ts`'s reason, and with its rule: it
14
+ * imports NOTHING from `fleet/` itself — a child reaching back into its parent
15
+ * while the parent calls the child is the package cycle
16
+ * `scripts/package-graph.mjs` refuses — so the two census derivations arrive as
17
+ * data on the input record rather than as an import of `../census.js`.
18
+ */
19
+ import {} from "../../../../core/c4/likec4.js";
20
+ import { elementService } from "../../../../core/c4/resolve/service.js";
21
+ import { EXTERNAL_TAG, INFRA_KINDS } from "../../../../core/vocabulary/maturity.js";
22
+ import {} from "../../../../core/vocabulary/report.js";
23
+ export function bindingFindings(input) {
24
+ const { elements, drawn, services, pathOf } = input;
25
+ const findings = [];
26
+ // Two boxes standing for one service directory. Every join in loam is
27
+ // `element -> service`, computed by picking the FIRST element that resolves —
28
+ // so with two of them, which one wins is readdir order, and the edges of the
29
+ // loser are attributed to a service they do not belong to. Silent until now,
30
+ // and unfixable by staring at either element on its own.
31
+ const perService = new Map();
32
+ for (const e of drawn) {
33
+ if (e.tags.includes(EXTERNAL_TAG))
34
+ continue;
35
+ const id = elementService(e);
36
+ perService.set(id, [...(perService.get(id) ?? []), e]);
37
+ }
38
+ for (const [id, elems] of perService) {
39
+ if (elems.length < 2)
40
+ continue;
41
+ // A collision only matters where it decides something: a real directory to
42
+ // attribute to, or a binding somebody wrote down on purpose.
43
+ if (!services.has(id) && !elems.some((e) => e.service !== undefined))
44
+ continue;
45
+ findings.push({
46
+ severity: "warn",
47
+ code: "landscape.binding-duplicate",
48
+ subject: id,
49
+ message: `landscape: ${elems.length} elements resolve to service '${id}' (${elems.map((e) => e.id).join(", ")}) — every element→service join picks one of them arbitrarily, so the others' edges are filed under a service that does not own them; keep one element per services/<id>/`,
50
+ });
51
+ }
52
+ for (const id of services) {
53
+ if (input.modelled.has(id))
54
+ continue;
55
+ findings.push({
56
+ severity: "error",
57
+ code: "landscape.service-unmodelled",
58
+ subject: id,
59
+ message: `landscape: ${pathOf(id)}/ exists but nothing in architecture/landscape.likec4 models it — add an element, or bind one with metadata { service '${id}' }`,
60
+ });
61
+ }
62
+ // A binding is a claim about this repo wherever it is written — including
63
+ // inside another element, which the old top-level filter never looked at, so
64
+ // a typo one level down bound an edge to a service that does not exist and
65
+ // nothing said so. Every element with a binding answers for it, at any depth.
66
+ for (const e of elements) {
67
+ if (e.tags.includes(EXTERNAL_TAG) || e.service === undefined)
68
+ continue;
69
+ if (services.has(e.service))
70
+ continue;
71
+ findings.push({
72
+ severity: "error",
73
+ code: "landscape.binding-unknown",
74
+ subject: e.service,
75
+ message: `landscape: '${e.title}' binds to service '${e.service}', but services/${e.service}/ does not exist`,
76
+ });
77
+ }
78
+ // Walked over the SYSTEM census — the same derivation the scorecard counts,
79
+ // so the map's exemptions and the fleet rollup cannot drift — with the two
80
+ // residual skips that are not exemptions: a bound element is the binding
81
+ // pass's subject above, and a title naming a real directory is documented.
82
+ //
83
+ // Infrastructure the fleet OWNS is out of `systems` upstream, in
84
+ // `../census.ts drawnSystems` (INFRA_KINDS), and it had to be: this message
85
+ // offers exactly two remedies and BOTH are false of an owned datastore — there
86
+ // is no `services/<id>/` to bind, and `#external` would claim somebody else
87
+ // runs it. Measured on a one-service fixture drawing an owned `database` at
88
+ // fleet level (2026-09-07), it fired here and had no correct fix.
89
+ for (const e of input.systems) {
90
+ if (e.service !== undefined)
91
+ continue; // graded by the binding pass above
92
+ if (services.has(e.title))
93
+ continue;
94
+ findings.push({
95
+ severity: "warn",
96
+ code: "landscape.service-undocumented",
97
+ subject: e.title,
98
+ // The third remedy is spelled from `INFRA_KINDS` rather than typed out,
99
+ // for the reason the set exists at all: the hook is a kind NAME, so a
100
+ // fleet whose specification spells its store `postgres` still lands here,
101
+ // and this sentence is the only place that tells its author which
102
+ // spellings loam reads. A prose copy would be a second list free to drift
103
+ // from the one the census walks.
104
+ message: `landscape: '${e.title}' has no services/${e.title}/ — bind it with metadata { service '<id>' }, tag it #${EXTERNAL_TAG} if it is not ours, or give it one of the kinds loam reads as infrastructure (${[...INFRA_KINDS].join(", ")}) if it is a store, queue or bucket the fleet runs rather than a system somebody adopts`,
105
+ });
106
+ }
107
+ return findings;
108
+ }
109
+ //# sourceMappingURL=bindings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bindings.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/map/bindings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The element↔directory passes of the fleet cross-check: which drawn box\n * answers for which `services/<id>/`, and what it means when the answer is\n * none, two, or one nobody owns.\n *\n * Lifted out of `../landscape.ts` when that module reached its line limit, and\n * the seam is a phase rather than a subject: everything here runs after the map\n * has parsed, reads nothing from disk, and grades exactly one join — the census\n * `../census.ts` derives against the enumeration `../landscape.ts` performed.\n * The four grades are ordered as the report reads them, and the ORDER is the\n * caller's business, which is why this is one function rather than four.\n *\n * A sub-package of `fleet/` for `./isolation.ts`'s reason, and with its rule: it\n * imports NOTHING from `fleet/` itself — a child reaching back into its parent\n * while the parent calls the child is the package cycle\n * `scripts/package-graph.mjs` refuses — so the two census derivations arrive as\n * data on the input record rather than as an import of `../census.js`.\n */\nimport { type Elem } from \"../../../../core/c4/likec4.js\";\nimport { elementService } from \"../../../../core/c4/resolve/service.js\";\nimport { EXTERNAL_TAG, INFRA_KINDS } from \"../../../../core/vocabulary/maturity.js\";\nimport { type Finding } from \"../../../../core/vocabulary/report.js\";\n\nexport interface BindingInput {\n /** Every element the map declares, at any depth — a binding answers for itself wherever it is written. */\n elements: Elem[];\n /** Service-LEVEL elements (`census.ts serviceLevelElements`) — the fleet map's own boxes. */\n drawn: Elem[];\n /** The drawn SYSTEMS (`census.ts drawnSystems`) — `drawn` minus what the map itself says is no service. */\n systems: Elem[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /** The services some element resolves to — the set `landscape.service-unmodelled` reads. */\n modelled: ReadonlySet<string>;\n /** Where a service's directory sits, repo-relative — the enumeration's answer, never `services/<id>`. */\n pathOf: (id: string) => string;\n}\n\nexport function bindingFindings(input: BindingInput): Finding[] {\n const { elements, drawn, services, pathOf } = input;\n const findings: Finding[] = [];\n\n // Two boxes standing for one service directory. Every join in loam is\n // `element -> service`, computed by picking the FIRST element that resolves —\n // so with two of them, which one wins is readdir order, and the edges of the\n // loser are attributed to a service they do not belong to. Silent until now,\n // and unfixable by staring at either element on its own.\n const perService = new Map<string, Elem[]>();\n for (const e of drawn) {\n if (e.tags.includes(EXTERNAL_TAG)) continue;\n const id = elementService(e);\n perService.set(id, [...(perService.get(id) ?? []), e]);\n }\n for (const [id, elems] of perService) {\n if (elems.length < 2) continue;\n // A collision only matters where it decides something: a real directory to\n // attribute to, or a binding somebody wrote down on purpose.\n if (!services.has(id) && !elems.some((e) => e.service !== undefined)) continue;\n findings.push({\n severity: \"warn\",\n code: \"landscape.binding-duplicate\",\n subject: id,\n message: `landscape: ${elems.length} elements resolve to service '${id}' (${elems.map((e) => e.id).join(\", \")}) — every element→service join picks one of them arbitrarily, so the others' edges are filed under a service that does not own them; keep one element per services/<id>/`,\n });\n }\n\n for (const id of services) {\n if (input.modelled.has(id)) continue;\n findings.push({\n severity: \"error\",\n code: \"landscape.service-unmodelled\",\n subject: id,\n message: `landscape: ${pathOf(id)}/ exists but nothing in architecture/landscape.likec4 models it — add an element, or bind one with metadata { service '${id}' }`,\n });\n }\n\n // A binding is a claim about this repo wherever it is written — including\n // inside another element, which the old top-level filter never looked at, so\n // a typo one level down bound an edge to a service that does not exist and\n // nothing said so. Every element with a binding answers for it, at any depth.\n for (const e of elements) {\n if (e.tags.includes(EXTERNAL_TAG) || e.service === undefined) continue;\n if (services.has(e.service)) continue;\n findings.push({\n severity: \"error\",\n code: \"landscape.binding-unknown\",\n subject: e.service,\n message: `landscape: '${e.title}' binds to service '${e.service}', but services/${e.service}/ does not exist`,\n });\n }\n\n // Walked over the SYSTEM census — the same derivation the scorecard counts,\n // so the map's exemptions and the fleet rollup cannot drift — with the two\n // residual skips that are not exemptions: a bound element is the binding\n // pass's subject above, and a title naming a real directory is documented.\n //\n // Infrastructure the fleet OWNS is out of `systems` upstream, in\n // `../census.ts drawnSystems` (INFRA_KINDS), and it had to be: this message\n // offers exactly two remedies and BOTH are false of an owned datastore — there\n // is no `services/<id>/` to bind, and `#external` would claim somebody else\n // runs it. Measured on a one-service fixture drawing an owned `database` at\n // fleet level (2026-09-07), it fired here and had no correct fix.\n for (const e of input.systems) {\n if (e.service !== undefined) continue; // graded by the binding pass above\n if (services.has(e.title)) continue;\n findings.push({\n severity: \"warn\",\n code: \"landscape.service-undocumented\",\n subject: e.title,\n // The third remedy is spelled from `INFRA_KINDS` rather than typed out,\n // for the reason the set exists at all: the hook is a kind NAME, so a\n // fleet whose specification spells its store `postgres` still lands here,\n // and this sentence is the only place that tells its author which\n // spellings loam reads. A prose copy would be a second list free to drift\n // from the one the census walks.\n message: `landscape: '${e.title}' has no services/${e.title}/ — bind it with metadata { service '<id>' }, tag it #${EXTERNAL_TAG} if it is not ours, or give it one of the kinds loam reads as infrastructure (${[...INFRA_KINDS].join(\", \")}) if it is a store, queue or bucket the fleet runs rather than a system somebody adopts`,\n });\n }\n\n return findings;\n}\n"]}
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Who consumes a drawn element — and the datastore grade that reads the answer.
3
+ *
4
+ * The census used to walk the MAP's relationships alone, and under the extending
5
+ * shape that is a partial view of the fleet: a service draws the container-level
6
+ * call in its own `model.likec4` and the map carries the service-level twin, so a
7
+ * store whose consuming edges were all drawn in models had ZERO consumers and
8
+ * was graded on the one service the map happened to draw (R1). A model is
9
+ * evidence about the fleet exactly as the map is, so both are counted here,
10
+ * once, by one derivation two checks share.
11
+ *
12
+ * WHERE A STORE LIVES IS THE TEAM'S CHOICE, and this module used to say the
13
+ * opposite at length. Nested under the element that resolves to its owner — in
14
+ * that service's `extend <fqn> { store = database … }` block, or inside its
15
+ * element on the map — and drawn as a fleet-level PEER are both first-class
16
+ * placements, and loam grades neither as wrong. `landscape.datastore-private`
17
+ * existed to push every one-consumer store into the first, its whole message a
18
+ * prescription to delete the peer; it is retired (2026-09-07), the arm below is
19
+ * gone, and no message here prescribes a move.
20
+ *
21
+ * What survives the retirement is the one CONSEQUENCE, which is a fact about
22
+ * LikeC4 rather than a preference: a per-service project is `architecture/` plus
23
+ * that ONE model, so an edge naming `<owner-fqn>.<store>` resolves for a SECOND
24
+ * service only when the MAP declares the store. Written in the owner's `extend`
25
+ * block instead, the same edge is two errors on the consumer's own model
26
+ * ("Could not resolve reference to Referenceable named 'store'"). That is the
27
+ * load-bearing half, it is stated by `reversibility` below where it actually
28
+ * bites, and SCHEMA carries the same sentence.
29
+ *
30
+ * The choice is cheap, and the surviving message says exactly HOW cheap, because
31
+ * "reversible" was false in the one state that used to assert it. The two
32
+ * placements do give the store the same id, `<owner-fqn>.<store>` — but the
33
+ * move is only free in ONE direction once a second service names it. Measured
34
+ * on `examples/docs`: moving `db` out of order-service's `extend` block into
35
+ * `orderService` on the map left the model at 12 elements · 8 relationships,
36
+ * left `Covers: marketplace.orderService.db` in its arch.spec.md resolving, and
37
+ * left checkout-web's container-level edge to it resolving too. Moving it BACK
38
+ * with that second edge drawn is two errors on checkout-web's own model
39
+ * ("Could not resolve reference to Referenceable named 'db'"), or
40
+ * `landscape.invalid` plus one `spine.landscape-invalid` per service when the
41
+ * second edge is drawn on the map — because an `extend` block is visible to its
42
+ * own service's project alone. `datastore-shared` printed the unconditional
43
+ * clause in 100% of the states it fires in, every one of which already has the
44
+ * second consumer that makes map→extend break (verification 2026-09-04). So the
45
+ * rule the message states is directional: extend→map is always safe; map→extend
46
+ * only while no other service's model or map edge names the store.
47
+ *
48
+ * A sub-package of `fleet/` for `./isolation.ts`'s reason and with its rule: it
49
+ * imports NOTHING from `fleet/`, so the map facts it reads arrive on the input
50
+ * records the caller fills.
51
+ */
52
+ import {} from "../../../../core/c4/likec4.js";
53
+ import { DATASTORE_KIND } from "../../../../core/vocabulary/maturity.js";
54
+ import {} from "../../../../core/vocabulary/report.js";
55
+ import {} from "./attest.js";
56
+ /**
57
+ * Distinct consumer SERVICES of an element, its nested children included: an
58
+ * edge into `kafka.paymentEvents` is consumption of `kafka`.
59
+ *
60
+ * Two kinds of evidence, unioned. From the MAP: a relationship whose target is
61
+ * the element or a descendant and whose source the shared resolver files under a
62
+ * real `services/<id>/` — persons, other external systems and unresolved sources
63
+ * never count, and two edges from one service are one consumer. From the
64
+ * MODELS: a service whose own model attests an OUTBOUND call at the element or a
65
+ * descendant.
66
+ *
67
+ * The model half reads EXTENDING models only, and the reason is the id space. An
68
+ * extending model's ids are the map's fully-qualified ids — one project — so the
69
+ * join is exact. A standalone model's ids are its own file's, and
70
+ * `core/c4/resolve/attested.ts` says outright that a counterpart is never
71
+ * matched to a landscape element: joining one would file a coincidence of
72
+ * spelling as a coupling between two services.
73
+ */
74
+ export function consumerCensus(input) {
75
+ const reaches = (elementId, id) => id === elementId || id.startsWith(`${elementId}.`);
76
+ return (elementId) => {
77
+ const out = new Set();
78
+ for (const r of input.relationships) {
79
+ if (!reaches(elementId, r.target))
80
+ continue;
81
+ const svc = input.resolve(r.source);
82
+ if (input.services.has(svc))
83
+ out.add(svc);
84
+ }
85
+ for (const model of input.attested) {
86
+ if (!model.extending)
87
+ continue;
88
+ if (model.calls.some((c) => c.direction === "out" && reaches(elementId, c.counterpartId))) {
89
+ out.add(model.service);
90
+ }
91
+ }
92
+ return [...out].sort();
93
+ };
94
+ }
95
+ /**
96
+ * A datastore two or more services reach is the strongest coupling two services
97
+ * can have and the hardest to undo, which deserves to be STATED rather than
98
+ * inferred. That is the whole of this grade now, and it applies to both
99
+ * placements alike: a fleet-level peer and a store nested under its owner earn
100
+ * the same `landscape.datastore-shared` at two consumers, and neither is graded
101
+ * at one.
102
+ *
103
+ * The one-consumer arm is gone (2026-09-07). A fleet-level peer with a single
104
+ * consumer used to be `landscape.datastore-private` — "drawn as a peer it reads
105
+ * as a system in its own right … delete this peer" — which made WHERE a store is
106
+ * drawn a restriction rather than the team's choice. It was never a fact about
107
+ * the fleet: the same store, nested, was silent, so the finding graded the
108
+ * drawing and not the architecture. The nested half was ALREADY silent at one
109
+ * consumer, and it is that half's rule that now holds for both.
110
+ */
111
+ export function datastoreFindings(input) {
112
+ const findings = [];
113
+ for (const e of input.drawn) {
114
+ // An element that stands for a real service is the fleet itself, not its
115
+ // shape; a datastore bound to a directory is graded by the binding checks.
116
+ if (input.standsForService(e))
117
+ continue;
118
+ if (e.kind.toLowerCase() !== DATASTORE_KIND)
119
+ continue;
120
+ const consumers = input.consumers(e.id);
121
+ // One consumer is a store that service owns, drawn where the team chose to
122
+ // draw it — a fact about the map's layout, not about the fleet. Zero is a
123
+ // store nothing reaches, which was never graded here either.
124
+ if (consumers.length < 2)
125
+ continue;
126
+ findings.push(sharedFinding(e.title, consumers, "", ""));
127
+ }
128
+ for (const store of nestedStoresOf(input)) {
129
+ // The owner is a consumer by construction — the store sits inside the element
130
+ // that resolves to it — so silence at one consumer IS the fixed state, and
131
+ // the only question left is whether a second service reaches the same data.
132
+ const consumers = [...new Set([store.owner, ...input.consumers(store.fqn)])].sort();
133
+ if (consumers.length < 2)
134
+ continue;
135
+ findings.push(sharedFinding(store.title, consumers,
136
+ // The store's own id, and said as such. The clause read "nested under
137
+ // '<fqn>'", which sends a reader looking for an element that HOLDS the
138
+ // store — when that id IS the store, and its container is its prefix.
139
+ ` — ${store.owner}'s own store, written as '${store.fqn}'`, reversibility(store, consumers.filter((c) => c !== store.owner))));
140
+ }
141
+ return findings;
142
+ }
143
+ /**
144
+ * Every store nested under the element that resolves to its owner, from both
145
+ * places the stance allows one to be written: inside the service's element ON
146
+ * THE MAP, and in the service's own extending model. Keyed by fqn, so a
147
+ * map-declared store — which the owner's model slice carries too, since a slice
148
+ * is everything resolving to the service — is one store and not two.
149
+ *
150
+ * Both sources are needed and neither is redundant. The map's covers a service
151
+ * whose model is standalone or absent (no slice to read); the model's covers a
152
+ * store the map never draws, which is the placement the adopt brief prescribes.
153
+ */
154
+ function nestedStoresOf(input) {
155
+ const drawnIds = new Set(input.drawn.map((e) => e.id));
156
+ const byFqn = new Map();
157
+ for (const e of input.elements) {
158
+ if (e.kind.toLowerCase() !== DATASTORE_KIND || drawnIds.has(e.id))
159
+ continue;
160
+ // The peer branch's guard, which this half was missing: a `database` bound
161
+ // with `metadata { service '<id>' }` and drawn one level down became a store
162
+ // "owned" by its own binding, so a second reader made it a shared datastore
163
+ // and the fix offered was to give each service its own copy of a service.
164
+ if (input.standsForService(e))
165
+ continue;
166
+ const owner = input.resolve(e.id);
167
+ if (!input.services.has(owner))
168
+ continue;
169
+ byFqn.set(e.id, { owner, fqn: e.id, title: e.title });
170
+ }
171
+ for (const store of input.nestedStores)
172
+ if (!byFqn.has(store.fqn))
173
+ byFqn.set(store.fqn, store);
174
+ return [...byFqn.values()];
175
+ }
176
+ /**
177
+ * Which way the store may still be moved, now that a second service reaches it.
178
+ *
179
+ * The clause this replaces said the two placements were interchangeable — "the
180
+ * two placements give it the same id … so moving the declaration between them
181
+ * rewrites no edge" — and that is false in every state `datastore-shared` fires
182
+ * in, because the finding needs a second consumer and a second consumer's edge
183
+ * resolves only against a store the MAP declares (a per-service project is
184
+ * `architecture/` plus one model). Measured: performing the move the sentence
185
+ * called free turned a clean `examples/docs` into `c4.invalid` on the consumer's
186
+ * model, or into `landscape.invalid` + one `spine.landscape-invalid` per service
187
+ * when the second edge is on the map (verification 2026-09-04).
188
+ *
189
+ * The direction is stated rather than the current placement, and it holds
190
+ * either way round: out of the owner's `extend` block onto the map is always
191
+ * safe, back in is not while another service names the store. (In practice a
192
+ * store this finding reaches is one the MAP declares — a second consumer's edge
193
+ * cannot resolve against an `extend` block, so it could never have been drawn
194
+ * — which is the same fact said from the other side.)
195
+ */
196
+ function reversibility(store, others) {
197
+ const named = others.length === 1 ? `${others[0]}'s edge` : `${others.join(", ")}'s edges`;
198
+ return (`. Both placements give it the same id, ${store.fqn}, but with a second consumer the move is ONE-WAY: ` +
199
+ `${named} resolves only against a declaration the MAP holds — a per-service project is \`architecture/\` ` +
200
+ `plus one model, so an \`extend\` block is visible to its own service's project alone. Out of ` +
201
+ `${store.owner}'s \`extend\` block onto ${store.owner}'s element on the map is always safe; moving it ` +
202
+ `back INTO the \`extend\` block breaks that edge — \`c4.invalid\` on the other service's model, or ` +
203
+ `\`landscape.invalid\` when the edge is drawn on the map`);
204
+ }
205
+ /** The two-or-more message, shared by the peer and the nested store — one word about coupling, once. */
206
+ function sharedFinding(title, consumers, where, tail) {
207
+ return {
208
+ severity: "warn",
209
+ code: "landscape.datastore-shared",
210
+ subject: title,
211
+ message: `landscape: '${title}' is a datastore shared by ${consumers.length} services ` +
212
+ `(${consumers.join(", ")})${where} — the strongest coupling two services can have, and the ` +
213
+ `hardest to undo. Shared means the same DATA: if they read the same tables or keys, keep it ` +
214
+ `where it is and let this warning state the coupling; if they only share a host or cluster ` +
215
+ `(two schemas, two lock paths), that is operational blast radius — a runbook fact — and the ` +
216
+ `honest model is one private store per service${tail}`,
217
+ };
218
+ }
219
+ //# sourceMappingURL=consumers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumers.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/map/consumers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,OAAO,EAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAgB,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAwC,MAAM,aAAa,CAAC;AAanE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAAC,KAAkB;IAC/C,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAE,EAAU,EAAW,EAAE,CAAC,EAAE,KAAK,SAAS,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,SAAS,GAAG,CAAC,CAAC;IAC/G,OAAO,CAAC,SAAiB,EAAY,EAAE;QACrC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;QAC9B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;YACpC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC;gBAAE,SAAS;YAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACpC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5C,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,KAAK,CAAC,SAAS;gBAAE,SAAS;YAC/B,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,KAAK,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;gBAC1F,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;QACD,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC,CAAC;AACJ,CAAC;AA0BD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAqB;IACrD,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAC5B,yEAAyE;QACzE,2EAA2E;QAC3E,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC;YAAE,SAAS;QACxC,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,cAAc;YAAE,SAAS;QACtD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACxC,2EAA2E;QAC3E,0EAA0E;QAC1E,6DAA6D;QAC7D,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QACnC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1C,8EAA8E;QAC9E,2EAA2E;QAC3E,4EAA4E;QAC5E,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACpF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QACnC,QAAQ,CAAC,IAAI,CACX,aAAa,CACX,KAAK,CAAC,KAAK,EACX,SAAS;QACT,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,MAAM,KAAK,CAAC,KAAK,6BAA6B,KAAK,CAAC,GAAG,GAAG,EAC1D,aAAa,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,CACjE,CACF,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,cAAc,CAAC,KAAqB;IAC3C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,cAAc,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,SAAS;QAC5E,2EAA2E;QAC3E,6EAA6E;QAC7E,4EAA4E;QAC5E,0EAA0E;QAC1E,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC;YAAE,SAAS;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,SAAS;QACzC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,YAAY;QAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC/F,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,aAAa,CAAC,KAAkB,EAAE,MAAgB;IACzD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAE,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC;IAC5F,OAAO,CACL,0CAA0C,KAAK,CAAC,GAAG,oDAAoD;QACvG,GAAG,KAAK,kGAAkG;QAC1G,+FAA+F;QAC/F,GAAG,KAAK,CAAC,KAAK,4BAA4B,KAAK,CAAC,KAAK,kDAAkD;QACvG,oGAAoG;QACpG,yDAAyD,CAC1D,CAAC;AACJ,CAAC;AAED,wGAAwG;AACxG,SAAS,aAAa,CAAC,KAAa,EAAE,SAAmB,EAAE,KAAa,EAAE,IAAY;IACpF,OAAO;QACL,QAAQ,EAAE,MAAM;QAChB,IAAI,EAAE,4BAA4B;QAClC,OAAO,EAAE,KAAK;QACd,OAAO,EACL,eAAe,KAAK,8BAA8B,SAAS,CAAC,MAAM,YAAY;YAC9E,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,2DAA2D;YAC5F,6FAA6F;YAC7F,4FAA4F;YAC5F,6FAA6F;YAC7F,gDAAgD,IAAI,EAAE;KACzD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Who consumes a drawn element — and the datastore grade that reads the answer.\n *\n * The census used to walk the MAP's relationships alone, and under the extending\n * shape that is a partial view of the fleet: a service draws the container-level\n * call in its own `model.likec4` and the map carries the service-level twin, so a\n * store whose consuming edges were all drawn in models had ZERO consumers and\n * was graded on the one service the map happened to draw (R1). A model is\n * evidence about the fleet exactly as the map is, so both are counted here,\n * once, by one derivation two checks share.\n *\n * WHERE A STORE LIVES IS THE TEAM'S CHOICE, and this module used to say the\n * opposite at length. Nested under the element that resolves to its owner — in\n * that service's `extend <fqn> { store = database … }` block, or inside its\n * element on the map — and drawn as a fleet-level PEER are both first-class\n * placements, and loam grades neither as wrong. `landscape.datastore-private`\n * existed to push every one-consumer store into the first, its whole message a\n * prescription to delete the peer; it is retired (2026-09-07), the arm below is\n * gone, and no message here prescribes a move.\n *\n * What survives the retirement is the one CONSEQUENCE, which is a fact about\n * LikeC4 rather than a preference: a per-service project is `architecture/` plus\n * that ONE model, so an edge naming `<owner-fqn>.<store>` resolves for a SECOND\n * service only when the MAP declares the store. Written in the owner's `extend`\n * block instead, the same edge is two errors on the consumer's own model\n * (\"Could not resolve reference to Referenceable named 'store'\"). That is the\n * load-bearing half, it is stated by `reversibility` below where it actually\n * bites, and SCHEMA carries the same sentence.\n *\n * The choice is cheap, and the surviving message says exactly HOW cheap, because\n * \"reversible\" was false in the one state that used to assert it. The two\n * placements do give the store the same id, `<owner-fqn>.<store>` — but the\n * move is only free in ONE direction once a second service names it. Measured\n * on `examples/docs`: moving `db` out of order-service's `extend` block into\n * `orderService` on the map left the model at 12 elements · 8 relationships,\n * left `Covers: marketplace.orderService.db` in its arch.spec.md resolving, and\n * left checkout-web's container-level edge to it resolving too. Moving it BACK\n * with that second edge drawn is two errors on checkout-web's own model\n * (\"Could not resolve reference to Referenceable named 'db'\"), or\n * `landscape.invalid` plus one `spine.landscape-invalid` per service when the\n * second edge is drawn on the map — because an `extend` block is visible to its\n * own service's project alone. `datastore-shared` printed the unconditional\n * clause in 100% of the states it fires in, every one of which already has the\n * second consumer that makes map→extend break (verification 2026-09-04). So the\n * rule the message states is directional: extend→map is always safe; map→extend\n * only while no other service's model or map edge names the store.\n *\n * A sub-package of `fleet/` for `./isolation.ts`'s reason and with its rule: it\n * imports NOTHING from `fleet/`, so the map facts it reads arrive on the input\n * records the caller fills.\n */\nimport { type Elem, type Rel } from \"../../../../core/c4/likec4.js\";\nimport { DATASTORE_KIND } from \"../../../../core/vocabulary/maturity.js\";\nimport { type Finding } from \"../../../../core/vocabulary/report.js\";\nimport { type AttestedModel, type NestedStore } from \"./attest.js\";\n\nexport interface CensusInput {\n /** The map's own edges — a service-level twin is what the map carries of a container-level call. */\n relationships: Rel[];\n /** The services/<id>/ directories that exist: a consumer is a source filed under a real one. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver every edge join uses. */\n resolve: (id: string) => string;\n /** What each service's own model attests (`./attest.ts`). */\n attested: readonly AttestedModel[];\n}\n\n/**\n * Distinct consumer SERVICES of an element, its nested children included: an\n * edge into `kafka.paymentEvents` is consumption of `kafka`.\n *\n * Two kinds of evidence, unioned. From the MAP: a relationship whose target is\n * the element or a descendant and whose source the shared resolver files under a\n * real `services/<id>/` — persons, other external systems and unresolved sources\n * never count, and two edges from one service are one consumer. From the\n * MODELS: a service whose own model attests an OUTBOUND call at the element or a\n * descendant.\n *\n * The model half reads EXTENDING models only, and the reason is the id space. An\n * extending model's ids are the map's fully-qualified ids — one project — so the\n * join is exact. A standalone model's ids are its own file's, and\n * `core/c4/resolve/attested.ts` says outright that a counterpart is never\n * matched to a landscape element: joining one would file a coincidence of\n * spelling as a coupling between two services.\n */\nexport function consumerCensus(input: CensusInput): (elementId: string) => string[] {\n const reaches = (elementId: string, id: string): boolean => id === elementId || id.startsWith(`${elementId}.`);\n return (elementId: string): string[] => {\n const out = new Set<string>();\n for (const r of input.relationships) {\n if (!reaches(elementId, r.target)) continue;\n const svc = input.resolve(r.source);\n if (input.services.has(svc)) out.add(svc);\n }\n for (const model of input.attested) {\n if (!model.extending) continue;\n if (model.calls.some((c) => c.direction === \"out\" && reaches(elementId, c.counterpartId))) {\n out.add(model.service);\n }\n }\n return [...out].sort();\n };\n}\n\nexport interface DatastoreInput {\n /** Service-LEVEL elements — the fleet map's own boxes, where a store drawn as a peer is found. */\n drawn: Elem[];\n /** Every element the MAP declares, at any depth — the other place a nested store is written. */\n elements: Elem[];\n /** Stores an extending model declares inside its own element (`./attest.ts`). */\n nestedStores: readonly NestedStore[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /** The shared element→service resolver, for naming the consumer's own element on the map. */\n resolve: (id: string) => string;\n /**\n * `census.ts`'s one predicate for \"this element stands for a `services/<id>/`\",\n * injected because a `fleet/map/` module may not import `fleet/`. Both halves\n * of the grade below skip such an element: it is the fleet itself, not its\n * shape, and a store bound to a directory is the binding checks' subject.\n * `./attest.ts` applies the same predicate to the stores a MODEL declares, so\n * all three places answer it once.\n */\n standsForService: (e: Elem) => boolean;\n /** The census above, built once and shared with the platform-candidate advisory. */\n consumers: (elementId: string) => string[];\n}\n\n/**\n * A datastore two or more services reach is the strongest coupling two services\n * can have and the hardest to undo, which deserves to be STATED rather than\n * inferred. That is the whole of this grade now, and it applies to both\n * placements alike: a fleet-level peer and a store nested under its owner earn\n * the same `landscape.datastore-shared` at two consumers, and neither is graded\n * at one.\n *\n * The one-consumer arm is gone (2026-09-07). A fleet-level peer with a single\n * consumer used to be `landscape.datastore-private` — \"drawn as a peer it reads\n * as a system in its own right … delete this peer\" — which made WHERE a store is\n * drawn a restriction rather than the team's choice. It was never a fact about\n * the fleet: the same store, nested, was silent, so the finding graded the\n * drawing and not the architecture. The nested half was ALREADY silent at one\n * consumer, and it is that half's rule that now holds for both.\n */\nexport function datastoreFindings(input: DatastoreInput): Finding[] {\n const findings: Finding[] = [];\n for (const e of input.drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape; a datastore bound to a directory is graded by the binding checks.\n if (input.standsForService(e)) continue;\n if (e.kind.toLowerCase() !== DATASTORE_KIND) continue;\n const consumers = input.consumers(e.id);\n // One consumer is a store that service owns, drawn where the team chose to\n // draw it — a fact about the map's layout, not about the fleet. Zero is a\n // store nothing reaches, which was never graded here either.\n if (consumers.length < 2) continue;\n findings.push(sharedFinding(e.title, consumers, \"\", \"\"));\n }\n for (const store of nestedStoresOf(input)) {\n // The owner is a consumer by construction — the store sits inside the element\n // that resolves to it — so silence at one consumer IS the fixed state, and\n // the only question left is whether a second service reaches the same data.\n const consumers = [...new Set([store.owner, ...input.consumers(store.fqn)])].sort();\n if (consumers.length < 2) continue;\n findings.push(\n sharedFinding(\n store.title,\n consumers,\n // The store's own id, and said as such. The clause read \"nested under\n // '<fqn>'\", which sends a reader looking for an element that HOLDS the\n // store — when that id IS the store, and its container is its prefix.\n ` — ${store.owner}'s own store, written as '${store.fqn}'`,\n reversibility(store, consumers.filter((c) => c !== store.owner)),\n ),\n );\n }\n\n return findings;\n}\n\n/**\n * Every store nested under the element that resolves to its owner, from both\n * places the stance allows one to be written: inside the service's element ON\n * THE MAP, and in the service's own extending model. Keyed by fqn, so a\n * map-declared store — which the owner's model slice carries too, since a slice\n * is everything resolving to the service — is one store and not two.\n *\n * Both sources are needed and neither is redundant. The map's covers a service\n * whose model is standalone or absent (no slice to read); the model's covers a\n * store the map never draws, which is the placement the adopt brief prescribes.\n */\nfunction nestedStoresOf(input: DatastoreInput): NestedStore[] {\n const drawnIds = new Set(input.drawn.map((e) => e.id));\n const byFqn = new Map<string, NestedStore>();\n for (const e of input.elements) {\n if (e.kind.toLowerCase() !== DATASTORE_KIND || drawnIds.has(e.id)) continue;\n // The peer branch's guard, which this half was missing: a `database` bound\n // with `metadata { service '<id>' }` and drawn one level down became a store\n // \"owned\" by its own binding, so a second reader made it a shared datastore\n // and the fix offered was to give each service its own copy of a service.\n if (input.standsForService(e)) continue;\n const owner = input.resolve(e.id);\n if (!input.services.has(owner)) continue;\n byFqn.set(e.id, { owner, fqn: e.id, title: e.title });\n }\n for (const store of input.nestedStores) if (!byFqn.has(store.fqn)) byFqn.set(store.fqn, store);\n return [...byFqn.values()];\n}\n\n/**\n * Which way the store may still be moved, now that a second service reaches it.\n *\n * The clause this replaces said the two placements were interchangeable — \"the\n * two placements give it the same id … so moving the declaration between them\n * rewrites no edge\" — and that is false in every state `datastore-shared` fires\n * in, because the finding needs a second consumer and a second consumer's edge\n * resolves only against a store the MAP declares (a per-service project is\n * `architecture/` plus one model). Measured: performing the move the sentence\n * called free turned a clean `examples/docs` into `c4.invalid` on the consumer's\n * model, or into `landscape.invalid` + one `spine.landscape-invalid` per service\n * when the second edge is on the map (verification 2026-09-04).\n *\n * The direction is stated rather than the current placement, and it holds\n * either way round: out of the owner's `extend` block onto the map is always\n * safe, back in is not while another service names the store. (In practice a\n * store this finding reaches is one the MAP declares — a second consumer's edge\n * cannot resolve against an `extend` block, so it could never have been drawn\n * — which is the same fact said from the other side.)\n */\nfunction reversibility(store: NestedStore, others: string[]): string {\n const named = others.length === 1 ? `${others[0]!}'s edge` : `${others.join(\", \")}'s edges`;\n return (\n `. Both placements give it the same id, ${store.fqn}, but with a second consumer the move is ONE-WAY: ` +\n `${named} resolves only against a declaration the MAP holds — a per-service project is \\`architecture/\\` ` +\n `plus one model, so an \\`extend\\` block is visible to its own service's project alone. Out of ` +\n `${store.owner}'s \\`extend\\` block onto ${store.owner}'s element on the map is always safe; moving it ` +\n `back INTO the \\`extend\\` block breaks that edge — \\`c4.invalid\\` on the other service's model, or ` +\n `\\`landscape.invalid\\` when the edge is drawn on the map`\n );\n}\n\n/** The two-or-more message, shared by the peer and the nested store — one word about coupling, once. */\nfunction sharedFinding(title: string, consumers: string[], where: string, tail: string): Finding {\n return {\n severity: \"warn\",\n code: \"landscape.datastore-shared\",\n subject: title,\n message:\n `landscape: '${title}' is a datastore shared by ${consumers.length} services ` +\n `(${consumers.join(\", \")})${where} — the strongest coupling two services can have, and the ` +\n `hardest to undo. Shared means the same DATA: if they read the same tables or keys, keep it ` +\n `where it is and let this warning state the coupling; if they only share a host or cluster ` +\n `(two schemas, two lock paths), that is operational blast radius — a runbook fact — and the ` +\n `honest model is one private store per service${tail}`,\n };\n}\n"]}