@ybotok/loam 0.2.0-alpha.5 → 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 (273) hide show
  1. package/CHANGELOG.md +901 -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 +264 -11
  6. package/SCHEMA.md +439 -38
  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 +128 -74
  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 +10 -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 +127 -119
  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 +258 -70
  75. package/dist/commands/validate/fleet/views/projects.js.map +1 -1
  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 -4
  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 -11
  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 +123 -14
  113. package/dist/core/agent/workflows/adopt.js.map +1 -1
  114. package/dist/core/agent/workflows/check.js +35 -20
  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 +133 -7
  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 -29
  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 +29 -24
  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 +36 -8
  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/refusals.js +1 -1
  213. package/dist/core/explain/refusals.js.map +1 -1
  214. package/dist/core/explore/explore.js +44 -9
  215. package/dist/core/explore/explore.js.map +1 -1
  216. package/dist/core/fleet-context.js +47 -0
  217. package/dist/core/fleet-context.js.map +1 -1
  218. package/dist/core/gate/partners.js +5 -9
  219. package/dist/core/gate/partners.js.map +1 -1
  220. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  221. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  222. package/dist/core/pack/living.js +47 -13
  223. package/dist/core/pack/living.js.map +1 -1
  224. package/dist/core/pack/pack.js +9 -6
  225. package/dist/core/pack/pack.js.map +1 -1
  226. package/dist/core/projection/arch-slice.js +2 -2
  227. package/dist/core/projection/arch-slice.js.map +1 -1
  228. package/dist/core/repo/paths.js +11 -9
  229. package/dist/core/repo/paths.js.map +1 -1
  230. package/dist/core/repo/tree/render/projects.js +93 -35
  231. package/dist/core/repo/tree/render/projects.js.map +1 -1
  232. package/dist/core/repo/tree/render/stale.js +2 -3
  233. package/dist/core/repo/tree/render/stale.js.map +1 -1
  234. package/dist/core/repo/tree/render/views.js +51 -3
  235. package/dist/core/repo/tree/render/views.js.map +1 -1
  236. package/dist/core/repo/tree/walk.js +45 -2
  237. package/dist/core/repo/tree/walk.js.map +1 -1
  238. package/dist/core/scaffold/landscape.js +219 -9
  239. package/dist/core/scaffold/landscape.js.map +1 -1
  240. package/dist/core/staging/writes.js.map +1 -1
  241. package/dist/core/usecases/delta/claims.js +1 -0
  242. package/dist/core/usecases/delta/claims.js.map +1 -1
  243. package/dist/core/usecases/delta/overlay.js +4 -0
  244. package/dist/core/usecases/delta/overlay.js.map +1 -1
  245. package/dist/core/usecases/fleet.js +18 -6
  246. package/dist/core/usecases/fleet.js.map +1 -1
  247. package/dist/core/usecases/operations.js +7 -0
  248. package/dist/core/usecases/operations.js.map +1 -1
  249. package/dist/core/usecases/service/exclude.js +119 -0
  250. package/dist/core/usecases/service/exclude.js.map +1 -0
  251. package/dist/core/usecases/service/flows.js +161 -0
  252. package/dist/core/usecases/service/flows.js.map +1 -0
  253. package/dist/core/vocabulary/maturity.js +10 -0
  254. package/dist/core/vocabulary/maturity.js.map +1 -1
  255. package/examples/README.md +24 -6
  256. package/examples/docs/architecture/landscape.likec4 +41 -0
  257. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  258. package/examples/docs/likec4.config.json +0 -1
  259. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  260. package/examples/docs/services/order-service/arch.spec.md +13 -6
  261. package/examples/docs/services/order-service/model.likec4 +53 -67
  262. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  263. package/examples/docs/services/payment-service/model.likec4 +32 -35
  264. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  265. package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
  266. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  267. package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
  268. package/package.json +1 -1
  269. package/examples/docs/services/checkout-web/likec4.config.json +0 -4
  270. package/examples/docs/services/order-service/likec4.config.json +0 -4
  271. package/examples/docs/services/payment-service/likec4.config.json +0 -4
  272. package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
  273. package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
@@ -1,3 +1,19 @@
1
+ /**
2
+ * The landscape's contained load path — the one phase of the fleet target that
3
+ * touches the filesystem before anything can be graded. Split from
4
+ * `./landscape.ts` when the fleet target grew past the line limit; the
5
+ * containment DOCTRINE below travelled with the functions it explains.
6
+ *
7
+ * The load ITSELF is no longer here. `loadArchitecture` moved to
8
+ * `core/c4/project/architecture.ts` when the use-case axis grew a second reader
9
+ * of it, and that module's banner records why; what stayed is the containment,
10
+ * which is command-layer business because it is about which validate TARGET a
11
+ * failed read is filed against.
12
+ */
13
+ import { existsSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import { FleetContext } from "../../../core/fleet-context.js";
16
+ import { featurePaths, landscapePath, servicePathsAt } from "../../../core/repo/paths.js";
1
17
  /**
2
18
  * A landscape that could not be READ, shaped as one that did not PARSE.
3
19
  *
@@ -49,4 +65,54 @@ export async function readLandscape(load) {
49
65
  return unreadableLandscape(err);
50
66
  }
51
67
  }
68
+ /**
69
+ * ONE workspace per document KIND for the whole `--all` run.
70
+ *
71
+ * The per-path load pays a fresh Langium workspace per document (~100 ms each
72
+ * even warm), which made the fleet's main CI command O(documents) workspace
73
+ * spins — 13.7 s median over the 120-service benchmark (docs/BENCHMARKS.md). So
74
+ * `--all` enumerates its documents up front and batch-parses them into the
75
+ * fleet context's memo; every load below, the landscape read included, is then a
76
+ * seeded hit. The enumerations are the same memoised promises the target loops
77
+ * reuse.
78
+ *
79
+ * TWO WORKSPACES, because the fleet's models no longer parse the same way as
80
+ * each other: a model that EXTENDS the map is a PROJECT (the map plus that
81
+ * file) and a model that stands alone is a document. So the shapes are read
82
+ * first, the DOCUMENT batch is asked for everything that is one — the landscape,
83
+ * every feature delta, every standalone model — and `prefetchServiceModels`
84
+ * then batches the extending half as projects, finding its own standalone list
85
+ * already memoised and asking for nothing.
86
+ *
87
+ * The order is load-bearing rather than tidy. `prefetchLikeC4` returns without a
88
+ * workspace when fewer than two documents are missing (one document gains
89
+ * nothing from batch isolation it already has), so splitting the documents
90
+ * across two calls is how a fleet of one service and one feature ends up
91
+ * batching the feature and parsing the model in a workspace of its own — the
92
+ * exact per-document spin this function exists to remove, and what
93
+ * `test/validate-batch-fallback.test.ts` counts.
94
+ *
95
+ * If a batch CANNOT run — a sandbox denying tmpdir writes — the prefetch seeds
96
+ * nothing and every load falls back to today's per-path parse: identical
97
+ * findings, the old speed. Single-service `validate` and `list` keep their
98
+ * untouched code paths on purpose, so the ≤10% regression bound in
99
+ * docs/BENCHMARKS.md holds by construction.
100
+ *
101
+ * It lives HERE rather than in `../validate.ts` because it is fleet-loading
102
+ * business and that module is the argument grammar and the target dispatch; the
103
+ * block also took that file to its line limit, which is the ceiling asking the
104
+ * question it exists to ask.
105
+ */
106
+ export async function prefetchFleetDocuments(input) {
107
+ const { docsDir, fleet } = input;
108
+ const paths = input.services.filter((svc) => svc.has.model).map((svc) => servicePathsAt(svc.dir));
109
+ const shapes = await fleet.modelShapes(paths.map((p) => p.model));
110
+ const landscape = landscapePath(docsDir);
111
+ await fleet.prefetchLikeC4([
112
+ ...(existsSync(landscape) ? [landscape] : []),
113
+ ...input.features.filter((feat) => feat.has.delta).map((feat) => featurePaths(feat.dir).delta),
114
+ ...paths.filter((p) => shapes.get(resolve(p.model)) !== "extending").map((p) => p.model),
115
+ ]);
116
+ await fleet.prefetchServiceModels(docsDir, paths);
117
+ }
52
118
  //# sourceMappingURL=load.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"load.js","sourceRoot":"","sources":["../../../../src/commands/validate/fleet/load.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,OAAO;QACL,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QACvE,QAAQ,EAAE,EAAE;QACZ,aAAa,EAAE,EAAE;KAClB,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAA8B;IAChE,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,mBAAmB,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;AACH,CAAC","sourcesContent":["/**\n * The landscape's contained load path — the one phase of the fleet target that\n * touches the filesystem before anything can be graded. Split from\n * `./landscape.ts` when the fleet target grew past the line limit; the\n * containment DOCTRINE below travelled with the functions it explains.\n *\n * The load ITSELF is no longer here. `loadArchitecture` moved to\n * `core/c4/project/architecture.ts` when the use-case axis grew a second reader\n * of it, and that module's banner records why; what stayed is the containment,\n * which is command-layer business because it is about which validate TARGET a\n * failed read is filed against.\n */\nimport type { LoadedDoc } from \"../../../core/c4/likec4.js\";\n\n/**\n * A landscape that could not be READ, shaped as one that did not PARSE.\n *\n * The landscape is the one artifact no target owns: it is read once for the\n * whole run and it is graded on a target of its own, which runs OUTSIDE\n * `guarded`. So a landscape.likec4 that is a directory, or that carries a\n * permission bit this process cannot open, escaped every per-target catch and\n * became the whole run's `repository-unavailable` — one file, and a fleet gate\n * that said nothing about the ninety-nine services that are fine. A DANGLING\n * symlink is not one of those shapes and never was: every read of the file is\n * gated on an `existsSync` that follows the link, so a broken one resolves as\n * `landscape.missing` long before anything opens it.\n *\n * What this contains is the FLEET-level reads, and only those: the `--all`\n * preload, and both reads inside `validateLandscape` — the conflict-marker\n * `readFile` and the parse. `loam validate --service <id>` and\n * `loam validate --feature <id>` hand in no preloaded doc, so `validateService`\n * and `validateFeature` open the same file again on demand, unwrapped and\n * INSIDE `guarded`: an EISDIR there is still reported as `service.unreadable`\n * or `feature.unreadable`, which files the fleet map's failure against the one\n * target the caller happened to name. That is the wrong subject on a finding\n * that does at least carry the offending path, and it costs those runs nothing\n * further — they grade a single target either way, so there is no report left\n * unwritten — which is why the remainder was left for the change that gives the\n * landscape one load path instead of three.\n *\n * `guarded` is deliberately NOT widened to cover the landscape target instead.\n * Its code ternary knows only services and features, so it would file the fleet\n * map's failure as `feature.unreadable`; and a guarded failure yields no\n * document, so all N services would go on to re-open the same broken file\n * inside their own guards and emit N copies of it. Containing the IO here needs\n * no new code and no new sentence: \"could not be read\" and \"did not parse\" have\n * the same consequence — nothing may be concluded from this file — and\n * `landscape.invalid` is already how that is said.\n */\nexport function unreadableLandscape(err: unknown): LoadedDoc {\n return {\n errors: [{ message: err instanceof Error ? err.message : String(err) }],\n elements: [],\n relationships: [],\n };\n}\n\n/** One landscape load, answering with `unreadableLandscape` rather than throwing. */\nexport async function readLandscape(load: () => Promise<LoadedDoc>): Promise<LoadedDoc> {\n try {\n return await load();\n } catch (err) {\n return unreadableLandscape(err);\n }\n}\n"]}
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../../../../src/commands/validate/fleet/load.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAG9D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,OAAO;QACL,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QACvE,QAAQ,EAAE,EAAE;QACZ,aAAa,EAAE,EAAE;KAClB,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAA8B;IAChE,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,mBAAmB,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;AACH,CAAC;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,KAAoB;IAC/D,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IACjC,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAClG,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,KAAK,CAAC,cAAc,CAAC;QACzB,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;QAC9F,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;KACzF,CAAC,CAAC;IACH,MAAM,KAAK,CAAC,qBAAqB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AACpD,CAAC","sourcesContent":["/**\n * The landscape's contained load path — the one phase of the fleet target that\n * touches the filesystem before anything can be graded. Split from\n * `./landscape.ts` when the fleet target grew past the line limit; the\n * containment DOCTRINE below travelled with the functions it explains.\n *\n * The load ITSELF is no longer here. `loadArchitecture` moved to\n * `core/c4/project/architecture.ts` when the use-case axis grew a second reader\n * of it, and that module's banner records why; what stayed is the containment,\n * which is command-layer business because it is about which validate TARGET a\n * failed read is filed against.\n */\nimport { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport type { FeatureEntry, ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { featurePaths, landscapePath, servicePathsAt } from \"../../../core/repo/paths.js\";\n\n/**\n * A landscape that could not be READ, shaped as one that did not PARSE.\n *\n * The landscape is the one artifact no target owns: it is read once for the\n * whole run and it is graded on a target of its own, which runs OUTSIDE\n * `guarded`. So a landscape.likec4 that is a directory, or that carries a\n * permission bit this process cannot open, escaped every per-target catch and\n * became the whole run's `repository-unavailable` — one file, and a fleet gate\n * that said nothing about the ninety-nine services that are fine. A DANGLING\n * symlink is not one of those shapes and never was: every read of the file is\n * gated on an `existsSync` that follows the link, so a broken one resolves as\n * `landscape.missing` long before anything opens it.\n *\n * What this contains is the FLEET-level reads, and only those: the `--all`\n * preload, and both reads inside `validateLandscape` — the conflict-marker\n * `readFile` and the parse. `loam validate --service <id>` and\n * `loam validate --feature <id>` hand in no preloaded doc, so `validateService`\n * and `validateFeature` open the same file again on demand, unwrapped and\n * INSIDE `guarded`: an EISDIR there is still reported as `service.unreadable`\n * or `feature.unreadable`, which files the fleet map's failure against the one\n * target the caller happened to name. That is the wrong subject on a finding\n * that does at least carry the offending path, and it costs those runs nothing\n * further — they grade a single target either way, so there is no report left\n * unwritten — which is why the remainder was left for the change that gives the\n * landscape one load path instead of three.\n *\n * `guarded` is deliberately NOT widened to cover the landscape target instead.\n * Its code ternary knows only services and features, so it would file the fleet\n * map's failure as `feature.unreadable`; and a guarded failure yields no\n * document, so all N services would go on to re-open the same broken file\n * inside their own guards and emit N copies of it. Containing the IO here needs\n * no new code and no new sentence: \"could not be read\" and \"did not parse\" have\n * the same consequence — nothing may be concluded from this file — and\n * `landscape.invalid` is already how that is said.\n */\nexport function unreadableLandscape(err: unknown): LoadedDoc {\n return {\n errors: [{ message: err instanceof Error ? err.message : String(err) }],\n elements: [],\n relationships: [],\n };\n}\n\n/** One landscape load, answering with `unreadableLandscape` rather than throwing. */\nexport async function readLandscape(load: () => Promise<LoadedDoc>): Promise<LoadedDoc> {\n try {\n return await load();\n } catch (err) {\n return unreadableLandscape(err);\n }\n}\n\n/** Everything a `--all` run parses up front, and the enumerations it was narrowed to. */\nexport interface FleetPrefetch {\n docsDir: DocsDir;\n fleet: FleetContext;\n /** The services this run grades — the `--base` narrowing when there is one. */\n services: readonly ServiceEntry[];\n /** The active features this run grades, likewise. */\n features: readonly FeatureEntry[];\n}\n\n/**\n * ONE workspace per document KIND for the whole `--all` run.\n *\n * The per-path load pays a fresh Langium workspace per document (~100 ms each\n * even warm), which made the fleet's main CI command O(documents) workspace\n * spins — 13.7 s median over the 120-service benchmark (docs/BENCHMARKS.md). So\n * `--all` enumerates its documents up front and batch-parses them into the\n * fleet context's memo; every load below, the landscape read included, is then a\n * seeded hit. The enumerations are the same memoised promises the target loops\n * reuse.\n *\n * TWO WORKSPACES, because the fleet's models no longer parse the same way as\n * each other: a model that EXTENDS the map is a PROJECT (the map plus that\n * file) and a model that stands alone is a document. So the shapes are read\n * first, the DOCUMENT batch is asked for everything that is one — the landscape,\n * every feature delta, every standalone model — and `prefetchServiceModels`\n * then batches the extending half as projects, finding its own standalone list\n * already memoised and asking for nothing.\n *\n * The order is load-bearing rather than tidy. `prefetchLikeC4` returns without a\n * workspace when fewer than two documents are missing (one document gains\n * nothing from batch isolation it already has), so splitting the documents\n * across two calls is how a fleet of one service and one feature ends up\n * batching the feature and parsing the model in a workspace of its own — the\n * exact per-document spin this function exists to remove, and what\n * `test/validate-batch-fallback.test.ts` counts.\n *\n * If a batch CANNOT run — a sandbox denying tmpdir writes — the prefetch seeds\n * nothing and every load falls back to today's per-path parse: identical\n * findings, the old speed. Single-service `validate` and `list` keep their\n * untouched code paths on purpose, so the ≤10% regression bound in\n * docs/BENCHMARKS.md holds by construction.\n *\n * It lives HERE rather than in `../validate.ts` because it is fleet-loading\n * business and that module is the argument grammar and the target dispatch; the\n * block also took that file to its line limit, which is the ceiling asking the\n * question it exists to ask.\n */\nexport async function prefetchFleetDocuments(input: FleetPrefetch): Promise<void> {\n const { docsDir, fleet } = input;\n const paths = input.services.filter((svc) => svc.has.model).map((svc) => servicePathsAt(svc.dir));\n const shapes = await fleet.modelShapes(paths.map((p) => p.model));\n const landscape = landscapePath(docsDir);\n await fleet.prefetchLikeC4([\n ...(existsSync(landscape) ? [landscape] : []),\n ...input.features.filter((feat) => feat.has.delta).map((feat) => featurePaths(feat.dir).delta),\n ...paths.filter((p) => shapes.get(resolve(p.model)) !== \"extending\").map((p) => p.model),\n ]);\n await fleet.prefetchServiceModels(docsDir, paths);\n}\n"]}
@@ -0,0 +1,103 @@
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 reported `landscape.datastore-private` naming the single
13
+ * service the MAP happened to draw (R1). A model is evidence about the fleet
14
+ * exactly as the map is, so it is read 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
+ /** The element kind read as a datastore, compared case-insensitively — `fleet-shape.ts`'s spelling. */
41
+ const DATASTORE_KIND = "database";
42
+ export async function mapAttestation(input) {
43
+ // Through the ONE reader of `model.likec4` (`core/c4/service-model/load.ts`),
44
+ // so a model that EXTENDS the map attests the calls it draws inside the
45
+ // element the map binds — read as a slice of the per-service project — exactly
46
+ // as a standalone one attests the calls it draws in its own file. Memoised in
47
+ // the index, so under `--all` these are hits. Pooled and joined on the entry,
48
+ // never by index.
49
+ const context = input.fleet ?? new FleetContext();
50
+ const present = input.entries.filter((entry) => existsSync(servicePathsAt(entry.dir).model));
51
+ // ONE workspace for the fleet's models rather than one per service. Both
52
+ // answers below are about the WHOLE fleet — who else reaches this store is not
53
+ // a question a narrowed run may answer differently — while `--base` prefetches
54
+ // only the scope it grades, so without this a scoped run would spin a LikeC4
55
+ // workspace per un-narrowed service. An accelerator with `prefetchLikeC4`'s
56
+ // failure story exactly: a batch that cannot run seeds nothing, and every read
57
+ // below falls back to its own load.
58
+ await context.prefetchServiceModels(input.docsDir, present.map((entry) => servicePathsAt(entry.dir)));
59
+ const loaded = await inOrder(present, async (entry) => ({
60
+ entry,
61
+ // CONTAINED, for `fleet/load.ts unreadableLandscape`'s reason: this runs on
62
+ // the landscape target, which sits outside the dispatcher's `guarded`, so a
63
+ // `model.likec4` that is a directory or carries a permission bit would
64
+ // otherwise become the whole run's `repository-unavailable` — one file, and
65
+ // no report at all for the ninety-nine services that are fine. A model that
66
+ // could not be READ contributes nothing here, exactly as one that did not
67
+ // parse does; `service.unreadable` is its own target's finding.
68
+ model: await context.serviceModel(input.docsDir, servicePathsAt(entry.dir)).catch(() => null),
69
+ }));
70
+ const models = [];
71
+ const nestedStores = [];
72
+ for (const { entry, model } of loaded) {
73
+ // A model that does not parse attests nothing: `c4.invalid` is its finding.
74
+ // An extending model whose MAP does not parse could not be read at all, and
75
+ // neither reader runs in that state anyway — the fleet target returns on
76
+ // `landscape.invalid` long before here.
77
+ if (model === null || model.mapUnreadable || model.doc.errors.length > 0)
78
+ continue;
79
+ models.push({
80
+ service: entry.id,
81
+ extending: model.shape === "extending",
82
+ calls: attestedCalls(model.doc, entry.id, input.services),
83
+ });
84
+ if (model.shape !== "extending")
85
+ continue;
86
+ for (const element of model.doc.elements) {
87
+ if (element.kind.toLowerCase() !== DATASTORE_KIND)
88
+ continue;
89
+ // The slice carries the partners this model's own edges point at, and a
90
+ // partner may be another service's store — so the store is kept only when
91
+ // the MAP's resolver files it under this service, and only when the map
92
+ // does not draw it as a peer of the services (which is the fleet-level
93
+ // census's subject, and a different grade).
94
+ if (input.standsForService(element))
95
+ continue;
96
+ if (input.drawnIds.has(element.id) || input.resolve(element.id) !== entry.id)
97
+ continue;
98
+ nestedStores.push({ owner: entry.id, fqn: element.id, title: element.title });
99
+ }
100
+ }
101
+ return { models, nestedStores };
102
+ }
103
+ //# 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 reported `landscape.datastore-private` naming the single\n * service the MAP happened to draw (R1). A model is evidence about the fleet\n * exactly as the map is, so it is read 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\";\n\n/** The element kind read as a datastore, compared case-insensitively — `fleet-shape.ts`'s spelling. */\nconst DATASTORE_KIND = \"database\";\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,96 @@
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 } 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
+ for (const e of input.systems) {
83
+ if (e.service !== undefined)
84
+ continue; // graded by the binding pass above
85
+ if (services.has(e.title))
86
+ continue;
87
+ findings.push({
88
+ severity: "warn",
89
+ code: "landscape.service-undocumented",
90
+ subject: e.title,
91
+ message: `landscape: '${e.title}' has no services/${e.title}/ — bind it with metadata { service '<id>' }, or tag it #${EXTERNAL_TAG} if it is not ours`,
92
+ });
93
+ }
94
+ return findings;
95
+ }
96
+ //# sourceMappingURL=bindings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bindings.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/map/bindings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAa,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,EAAgB,MAAM,uCAAuC,CAAC;AAiBrE,MAAM,UAAU,eAAe,CAAC,KAAmB;IACjD,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IACpD,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,sEAAsE;IACtE,8EAA8E;IAC9E,6EAA6E;IAC7E,6EAA6E;IAC7E,yDAAyD;IACzD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;YAAE,SAAS;QAC5C,MAAM,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;QAC7B,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACzD,CAAC;IACD,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,UAAU,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QAC/B,2EAA2E;QAC3E,6DAA6D;QAC7D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC;YAAE,SAAS;QAC/E,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,6BAA6B;YACnC,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,cAAc,KAAK,CAAC,MAAM,iCAAiC,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,0KAA0K;SACxR,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,SAAS;QACrC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,8BAA8B;YACpC,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,cAAc,MAAM,CAAC,EAAE,CAAC,0HAA0H,EAAE,KAAK;SACnK,CAAC,CAAC;IACL,CAAC;IAED,0EAA0E;IAC1E,6EAA6E;IAC7E,2EAA2E;IAC3E,8EAA8E;IAC9E,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS;YAAE,SAAS;QACvE,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;YAAE,SAAS;QACtC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,2BAA2B;YACjC,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,OAAO,EAAE,eAAe,CAAC,CAAC,KAAK,uBAAuB,CAAC,CAAC,OAAO,mBAAmB,CAAC,CAAC,OAAO,kBAAkB;SAC9G,CAAC,CAAC;IACL,CAAC;IAED,4EAA4E;IAC5E,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAC9B,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS;YAAE,SAAS,CAAC,mCAAmC;QAC1E,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,SAAS;QACpC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,gCAAgC;YACtC,OAAO,EAAE,CAAC,CAAC,KAAK;YAChB,OAAO,EAAE,eAAe,CAAC,CAAC,KAAK,qBAAqB,CAAC,CAAC,KAAK,4DAA4D,YAAY,oBAAoB;SACxJ,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC","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 } 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 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 message: `landscape: '${e.title}' has no services/${e.title}/ — bind it with metadata { service '<id>' }, or tag it #${EXTERNAL_TAG} if it is not ours`,\n });\n }\n\n return findings;\n}\n"]}