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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +947 -0
  2. package/MIGRATING-from-OpenSpec.md +7 -1
  3. package/QUICKSTART.md +8 -3
  4. package/README.md +20 -13
  5. package/ROADMAP.md +329 -22
  6. package/SCHEMA.md +472 -24
  7. package/WORKFLOW.md +23 -10
  8. package/dist/commands/adopt/render.js +47 -8
  9. package/dist/commands/adopt/render.js.map +1 -1
  10. package/dist/commands/archive/plan/landscape.js +45 -1
  11. package/dist/commands/archive/plan/landscape.js.map +1 -1
  12. package/dist/commands/archive/plan/model/extending.js +85 -0
  13. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  14. package/dist/commands/archive/run.js +5 -2
  15. package/dist/commands/archive/run.js.map +1 -1
  16. package/dist/commands/context/print.js +11 -1
  17. package/dist/commands/context/print.js.map +1 -1
  18. package/dist/commands/doctor.js +46 -0
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/explore.js +12 -2
  21. package/dist/commands/explore.js.map +1 -1
  22. package/dist/commands/list/views.js +5 -1
  23. package/dist/commands/list/views.js.map +1 -1
  24. package/dist/commands/seed/plan.js +97 -3
  25. package/dist/commands/seed/plan.js.map +1 -1
  26. package/dist/commands/seed/seed.js +7 -2
  27. package/dist/commands/seed/seed.js.map +1 -1
  28. package/dist/commands/show/feature.js +2 -1
  29. package/dist/commands/show/feature.js.map +1 -1
  30. package/dist/commands/show/marks.js +3 -5
  31. package/dist/commands/show/marks.js.map +1 -1
  32. package/dist/commands/show/service.js +39 -18
  33. package/dist/commands/show/service.js.map +1 -1
  34. package/dist/commands/subsystem/sync.js +184 -39
  35. package/dist/commands/subsystem/sync.js.map +1 -1
  36. package/dist/commands/subsystem/txn/exclude.js +132 -0
  37. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  38. package/dist/commands/subsystem/txn/report.js +124 -0
  39. package/dist/commands/subsystem/txn/report.js.map +1 -0
  40. package/dist/commands/subsystem/txn/txn.js +29 -3
  41. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  42. package/dist/commands/subsystem/txn/views.js +113 -16
  43. package/dist/commands/subsystem/txn/views.js.map +1 -1
  44. package/dist/commands/validate/arch-coverage.js +16 -9
  45. package/dist/commands/validate/arch-coverage.js.map +1 -1
  46. package/dist/commands/validate/checks/fleet-shape.js +30 -77
  47. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  48. package/dist/commands/validate/checks/vocabulary.js +4 -19
  49. package/dist/commands/validate/checks/vocabulary.js.map +1 -1
  50. package/dist/commands/validate/feature.js +1 -1
  51. package/dist/commands/validate/feature.js.map +1 -1
  52. package/dist/commands/validate/fleet/census.js +11 -10
  53. package/dist/commands/validate/fleet/census.js.map +1 -1
  54. package/dist/commands/validate/fleet/landscape.js +128 -114
  55. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  56. package/dist/commands/validate/fleet/load.js +66 -0
  57. package/dist/commands/validate/fleet/load.js.map +1 -1
  58. package/dist/commands/validate/fleet/map/attest.js +103 -0
  59. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  60. package/dist/commands/validate/fleet/map/bindings.js +96 -0
  61. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  62. package/dist/commands/validate/fleet/map/consumers.js +298 -0
  63. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  64. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  65. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  66. package/dist/commands/validate/fleet/map/usecases.js +56 -0
  67. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  68. package/dist/commands/validate/fleet/usecases/steps.js +49 -17
  69. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  70. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  71. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  72. package/dist/commands/validate/fleet/views/ids.js +13 -9
  73. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  74. package/dist/commands/validate/fleet/views/projects.js +278 -0
  75. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  76. package/dist/commands/validate/fleet/views/root-documents.js +97 -0
  77. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  78. package/dist/commands/validate/fleet/views/stale.js +2 -2
  79. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  80. package/dist/commands/validate/service/model/diverged.js +226 -0
  81. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  82. package/dist/commands/validate/service/model/grade.js +163 -0
  83. package/dist/commands/validate/service/model/grade.js.map +1 -0
  84. package/dist/commands/validate/service/service.js +110 -34
  85. package/dist/commands/validate/service/service.js.map +1 -1
  86. package/dist/commands/validate/service/specs.js +39 -5
  87. package/dist/commands/validate/service/specs.js.map +1 -1
  88. package/dist/commands/validate/service/spine.js +41 -3
  89. package/dist/commands/validate/service/spine.js.map +1 -1
  90. package/dist/commands/validate/service/usecases/tags.js +113 -0
  91. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  92. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  93. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  94. package/dist/commands/validate/validate.js +10 -24
  95. package/dist/commands/validate/validate.js.map +1 -1
  96. package/dist/core/agent/agents-md/artifacts.js +1 -0
  97. package/dist/core/agent/agents-md/artifacts.js.map +1 -1
  98. package/dist/core/agent/agents-md/command-map.js +18 -2
  99. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  100. package/dist/core/agent/agents-md/cycle.js +24 -9
  101. package/dist/core/agent/agents-md/cycle.js.map +1 -1
  102. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  103. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  104. package/dist/core/agent/agents-md/map/subsystem.js +51 -2
  105. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  106. package/dist/core/agent/agents-md/map/usecases.js +45 -3
  107. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  108. package/dist/core/agent/agents-md.js +14 -4
  109. package/dist/core/agent/agents-md.js.map +1 -1
  110. package/dist/core/agent/support/report.js +39 -5
  111. package/dist/core/agent/support/report.js.map +1 -1
  112. package/dist/core/agent/workflows/adopt.js +125 -6
  113. package/dist/core/agent/workflows/adopt.js.map +1 -1
  114. package/dist/core/agent/workflows/check.js +35 -19
  115. package/dist/core/agent/workflows/check.js.map +1 -1
  116. package/dist/core/agent/workflows/closing.js +3 -1
  117. package/dist/core/agent/workflows/closing.js.map +1 -1
  118. package/dist/core/brief/brief.js +63 -11
  119. package/dist/core/brief/brief.js.map +1 -1
  120. package/dist/core/brief/checks.js +138 -3
  121. package/dist/core/brief/checks.js.map +1 -1
  122. package/dist/core/brief/landscape.js +212 -129
  123. package/dist/core/brief/landscape.js.map +1 -1
  124. package/dist/core/brief/map/extend.js +81 -0
  125. package/dist/core/brief/map/extend.js.map +1 -0
  126. package/dist/core/brief/map/owed.js +284 -0
  127. package/dist/core/brief/map/owed.js.map +1 -0
  128. package/dist/core/brief/targets.js +30 -26
  129. package/dist/core/brief/targets.js.map +1 -1
  130. package/dist/core/brief/unchecked.js +12 -2
  131. package/dist/core/brief/unchecked.js.map +1 -1
  132. package/dist/core/c4/likec4.js +23 -1
  133. package/dist/core/c4/likec4.js.map +1 -1
  134. package/dist/core/c4/parsed/deployment.js +5 -3
  135. package/dist/core/c4/parsed/deployment.js.map +1 -1
  136. package/dist/core/c4/parsed/view-ids.js +39 -1
  137. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  138. package/dist/core/c4/prefetch/fleet.js +142 -0
  139. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  140. package/dist/core/c4/project/architecture.js +52 -4
  141. package/dist/core/c4/project/architecture.js.map +1 -1
  142. package/dist/core/c4/project/batch.js +140 -0
  143. package/dist/core/c4/project/batch.js.map +1 -0
  144. package/dist/core/c4/project/documents.js +9 -4
  145. package/dist/core/c4/project/documents.js.map +1 -1
  146. package/dist/core/c4/project/load.js +77 -38
  147. package/dist/core/c4/project/load.js.map +1 -1
  148. package/dist/core/c4/project/staged.js +46 -12
  149. package/dist/core/c4/project/staged.js.map +1 -1
  150. package/dist/core/c4/resolve/attested.js +0 -0
  151. package/dist/core/c4/resolve/attested.js.map +1 -0
  152. package/dist/core/c4/resolve/service.js +8 -17
  153. package/dist/core/c4/resolve/service.js.map +1 -1
  154. package/dist/core/c4/resolve/steps.js +18 -0
  155. package/dist/core/c4/resolve/steps.js.map +1 -1
  156. package/dist/core/c4/root-project/exclude.js +352 -0
  157. package/dist/core/c4/root-project/exclude.js.map +1 -0
  158. package/dist/core/c4/service-model/documents.js +47 -0
  159. package/dist/core/c4/service-model/documents.js.map +1 -0
  160. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  161. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  162. package/dist/core/c4/service-model/load.js +165 -0
  163. package/dist/core/c4/service-model/load.js.map +1 -0
  164. package/dist/core/c4/service-model/renderer.js +128 -0
  165. package/dist/core/c4/service-model/renderer.js.map +1 -0
  166. package/dist/core/c4/service-model/shape.js +126 -0
  167. package/dist/core/c4/service-model/shape.js.map +1 -0
  168. package/dist/core/c4/service-model/slice.js +188 -0
  169. package/dist/core/c4/service-model/slice.js.map +1 -0
  170. package/dist/core/c4/source-scan.js +26 -0
  171. package/dist/core/c4/source-scan.js.map +1 -1
  172. package/dist/core/c4/splice/authored-source.js +60 -13
  173. package/dist/core/c4/splice/authored-source.js.map +1 -1
  174. package/dist/core/c4/splice/contract.js.map +1 -1
  175. package/dist/core/c4/splice/identity/declared.js +87 -0
  176. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  177. package/dist/core/c4/splice/identity/edges.js +17 -0
  178. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  179. package/dist/core/c4/splice/identity/existing.js +80 -0
  180. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  181. package/dist/core/c4/splice/identity/interior.js +80 -0
  182. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  183. package/dist/core/c4/splice/identity/owner.js +50 -0
  184. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  185. package/dist/core/c4/splice/landscape-merge.js +154 -100
  186. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  187. package/dist/core/c4/splice/model/merge.js +140 -0
  188. package/dist/core/c4/splice/model/merge.js.map +1 -0
  189. package/dist/core/c4/splice/placement.js +14 -4
  190. package/dist/core/c4/splice/placement.js.map +1 -1
  191. package/dist/core/c4/workspace.js +2 -1
  192. package/dist/core/c4/workspace.js.map +1 -1
  193. package/dist/core/coherence/coherence.js +7 -0
  194. package/dist/core/coherence/coherence.js.map +1 -1
  195. package/dist/core/delta/delta.js +9 -0
  196. package/dist/core/delta/delta.js.map +1 -1
  197. package/dist/core/deployment/delta.js +4 -0
  198. package/dist/core/deployment/delta.js.map +1 -1
  199. package/dist/core/diff/base-state.js +21 -5
  200. package/dist/core/diff/base-state.js.map +1 -1
  201. package/dist/core/diff/victims.js +10 -1
  202. package/dist/core/diff/victims.js.map +1 -1
  203. package/dist/core/docs.js +31 -22
  204. package/dist/core/docs.js.map +1 -1
  205. package/dist/core/doctor/config.js +7 -3
  206. package/dist/core/doctor/config.js.map +1 -1
  207. package/dist/core/doctor/doctor.js +48 -9
  208. package/dist/core/doctor/doctor.js.map +1 -1
  209. package/dist/core/doctor/report.js.map +1 -1
  210. package/dist/core/doctor/reports/scan.js +242 -0
  211. package/dist/core/doctor/reports/scan.js.map +1 -0
  212. package/dist/core/explain/families.js +1 -1
  213. package/dist/core/explain/families.js.map +1 -1
  214. package/dist/core/explain/refusals.js +1 -1
  215. package/dist/core/explain/refusals.js.map +1 -1
  216. package/dist/core/explore/explore.js +44 -9
  217. package/dist/core/explore/explore.js.map +1 -1
  218. package/dist/core/fleet-context.js +47 -0
  219. package/dist/core/fleet-context.js.map +1 -1
  220. package/dist/core/gate/partners.js +5 -9
  221. package/dist/core/gate/partners.js.map +1 -1
  222. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  223. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  224. package/dist/core/pack/living.js +47 -13
  225. package/dist/core/pack/living.js.map +1 -1
  226. package/dist/core/pack/pack.js +9 -6
  227. package/dist/core/pack/pack.js.map +1 -1
  228. package/dist/core/projection/arch-slice.js +2 -2
  229. package/dist/core/projection/arch-slice.js.map +1 -1
  230. package/dist/core/repo/paths.js +40 -0
  231. package/dist/core/repo/paths.js.map +1 -1
  232. package/dist/core/repo/tree/render/projects.js +182 -0
  233. package/dist/core/repo/tree/render/projects.js.map +1 -0
  234. package/dist/core/repo/tree/render/stale.js +2 -3
  235. package/dist/core/repo/tree/render/stale.js.map +1 -1
  236. package/dist/core/repo/tree/render/views.js +51 -3
  237. package/dist/core/repo/tree/render/views.js.map +1 -1
  238. package/dist/core/repo/tree/walk.js +45 -2
  239. package/dist/core/repo/tree/walk.js.map +1 -1
  240. package/dist/core/scaffold/landscape.js +219 -9
  241. package/dist/core/scaffold/landscape.js.map +1 -1
  242. package/dist/core/staging/writes.js.map +1 -1
  243. package/dist/core/usecases/delta/claims.js +1 -0
  244. package/dist/core/usecases/delta/claims.js.map +1 -1
  245. package/dist/core/usecases/delta/overlay.js +4 -0
  246. package/dist/core/usecases/delta/overlay.js.map +1 -1
  247. package/dist/core/usecases/fleet.js +18 -6
  248. package/dist/core/usecases/fleet.js.map +1 -1
  249. package/dist/core/usecases/operations.js +7 -0
  250. package/dist/core/usecases/operations.js.map +1 -1
  251. package/dist/core/usecases/service/exclude.js +119 -0
  252. package/dist/core/usecases/service/exclude.js.map +1 -0
  253. package/dist/core/usecases/service/flows.js +161 -0
  254. package/dist/core/usecases/service/flows.js.map +1 -0
  255. package/dist/core/vocabulary/maturity.js +10 -0
  256. package/dist/core/vocabulary/maturity.js.map +1 -1
  257. package/examples/README.md +23 -0
  258. package/examples/docs/architecture/landscape.likec4 +41 -0
  259. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  260. package/examples/docs/likec4.config.json +0 -1
  261. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  262. package/examples/docs/services/order-service/arch.spec.md +13 -6
  263. package/examples/docs/services/order-service/model.likec4 +53 -67
  264. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  265. package/examples/docs/services/payment-service/model.likec4 +32 -33
  266. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  267. package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
  268. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  269. package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
  270. package/package.json +1 -1
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Every service model ONE command invocation has read, memoised — the fleet
3
+ * map, each model's shape, each per-service project, and each assembled
4
+ * `ServiceModel`.
5
+ *
6
+ * A package of its own rather than four more maps on `core/fleet-context.ts`,
7
+ * and the seam is the one that module's banner already states for its leaves:
8
+ * the rules being memoised here are `../service-model/`'s (which shape a file
9
+ * has, which documents its project holds, how a failed batch degrades), and a
10
+ * memo whose rules live an import away is a memo somebody fills under the wrong
11
+ * key. The read index keeps what it owns — the document parses, the
12
+ * enumerations — and hands them in. It also keeps that file under its line
13
+ * limit, which is the honest second reason: this axis is four memos and a batch
14
+ * plan, and it arrived at a module that was already 336 lines of other axes.
15
+ *
16
+ * The direction is one-way and load-bearing: `fleet-context.ts` imports this,
17
+ * never the reverse. Everything this class needs from the read index arrives as
18
+ * a function on the request (`ModelRead`), so nothing here knows that a
19
+ * FleetContext exists — which is what keeps `core/` → `core/c4/` acyclic.
20
+ *
21
+ * REQUEST-SCOPED exactly as `FleetContext` is: one instance per invocation,
22
+ * dropped when the command finishes. No module-level state, so a later run
23
+ * cannot observe an earlier one's answer, and a `chdir` between two tests
24
+ * cannot leak one docs repo into the next.
25
+ */
26
+ import { resolve } from "node:path";
27
+ import { loadArchitecture } from "../project/architecture.js";
28
+ import { asLoadedDoc, loadProject } from "../project/load.js";
29
+ import { modelProjectDocuments } from "../service-model/documents.js";
30
+ import { batchServiceProjects, serviceModelAt, } from "../service-model/load.js";
31
+ import { readModelShape, readModelShapes } from "../service-model/shape.js";
32
+ /** One in-flight promise per key, created on first ask — the memo idiom, once. */
33
+ function memo(store, k, make) {
34
+ let pending = store.get(k);
35
+ if (pending === undefined) {
36
+ pending = make();
37
+ store.set(k, pending);
38
+ }
39
+ return pending;
40
+ }
41
+ export class ServiceModels {
42
+ deps;
43
+ architectures = new Map();
44
+ shapeOf = new Map();
45
+ projects = new Map();
46
+ models = new Map();
47
+ constructor(deps) {
48
+ this.deps = deps;
49
+ }
50
+ /**
51
+ * The fleet map as the PROJECT it is (`../project/architecture.ts`). Read
52
+ * twice over for every service now rather than once for the run — it is the
53
+ * gate on whether an extending model can be read at all, and the other half
54
+ * of the diff that decides what such a model actually declares — so the memo
55
+ * is what keeps a fleet run to one parse of it.
56
+ *
57
+ * Filled ONLY when somebody asks. `model` below hands `loadServiceModel` a
58
+ * thunk rather than an awaited value, so a fleet whose models all stand alone
59
+ * never parses this project at all — a standalone model is read as one file,
60
+ * and paying for the map (and inheriting its failures) for `loam show` or the
61
+ * adopt brief was cost and risk for an answer nobody used.
62
+ */
63
+ architecture(docsDir) {
64
+ return memo(this.architectures, resolve(docsDir), () => loadArchitecture(docsDir));
65
+ }
66
+ /**
67
+ * Which shape each model has, keyed by resolved path — ONE read of each
68
+ * file's bytes per invocation, however many questions are asked of it.
69
+ *
70
+ * The bulk form is the primary one because every caller asks in bulk: the
71
+ * fleet target needs the shapes of the WHOLE enumeration before it can grade
72
+ * a single renderer-file finding, and the prefetch needs them before it can
73
+ * decide which workspace each model belongs in.
74
+ */
75
+ async shapes(paths) {
76
+ const keys = [...new Set(paths.map((path) => resolve(path)))];
77
+ const missing = keys.filter((k) => !this.shapeOf.has(k));
78
+ for (const [k, shape] of await readModelShapes(missing)) {
79
+ // A concurrent read's promise stays authoritative — it is the one another
80
+ // caller is already holding — so a second scan of the same bytes can
81
+ // never replace it with a duplicate.
82
+ if (!this.shapeOf.has(k))
83
+ this.shapeOf.set(k, Promise.resolve(shape));
84
+ }
85
+ const answered = await Promise.all(keys.map(async (k) => [k, await this.shape(k)]));
86
+ return new Map(answered);
87
+ }
88
+ /** One model's shape, out of the same memo the bulk form fills. */
89
+ shape(modelPath) {
90
+ return memo(this.shapeOf, resolve(modelPath), () => readModelShape(modelPath));
91
+ }
92
+ /** A service's model, read the way its own shape demands. Memoised per model PATH, which is what identifies the file. */
93
+ model(docsDir, paths) {
94
+ return memo(this.models, resolve(paths.model), async () => serviceModelAt({
95
+ docsDir,
96
+ paths,
97
+ shape: await this.shape(paths.model),
98
+ known: await this.deps.known(docsDir),
99
+ // A thunk, so a standalone model never spins the map's workspace: the
100
+ // memo makes the extending case one parse for the run either way.
101
+ architecture: () => this.architecture(docsDir),
102
+ standalone: () => this.deps.standalone(paths.model),
103
+ project: () => this.project(docsDir, paths.model),
104
+ }));
105
+ }
106
+ /**
107
+ * Prepare every EXTENDING model's project in one workspace, and answer with
108
+ * the STANDALONE models' paths for the caller to prefetch through its own
109
+ * document batch — the two shapes need different workspaces, and only the
110
+ * caller has the memo the second one seeds.
111
+ *
112
+ * Purely an accelerator: a batch that could not be created seeds nothing and
113
+ * every reader falls back to its own load, so findings can never change
114
+ * because of a tmpdir — only the speed can.
115
+ */
116
+ async prefetch(docsDir, paths) {
117
+ const shapes = await this.shapes(paths.map((p) => p.model));
118
+ const models = (shape) => [...shapes].filter(([, found]) => found === shape).map(([model]) => model);
119
+ const extending = models("extending").filter((model) => !this.projects.has(model));
120
+ for (const [k, doc] of await batchServiceProjects(docsDir, extending)) {
121
+ // A concurrent load's promise stays authoritative: it is the one every
122
+ // reader is already holding, and replacing it would count the parse twice.
123
+ if (this.projects.has(k))
124
+ continue;
125
+ this.deps.onProjectLoad();
126
+ this.projects.set(k, Promise.resolve(doc));
127
+ }
128
+ return models("standalone");
129
+ }
130
+ /**
131
+ * One service's project — the architecture documents and its model, parsed
132
+ * together. Keyed on the model, which is what `prefetch` seeds above, so a
133
+ * batched fleet run and a single-service run answer out of the same memo.
134
+ */
135
+ project(docsDir, modelPath) {
136
+ return memo(this.projects, resolve(modelPath), async () => {
137
+ this.deps.onProjectLoad();
138
+ return asLoadedDoc(await loadProject(docsDir, await modelProjectDocuments(docsDir, modelPath)));
139
+ });
140
+ }
141
+ }
142
+ //# sourceMappingURL=fleet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fleet.js","sourceRoot":"","sources":["../../../../src/core/c4/prefetch/fleet.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every service model ONE command invocation has read, memoised — the fleet\n * map, each model's shape, each per-service project, and each assembled\n * `ServiceModel`.\n *\n * A package of its own rather than four more maps on `core/fleet-context.ts`,\n * and the seam is the one that module's banner already states for its leaves:\n * the rules being memoised here are `../service-model/`'s (which shape a file\n * has, which documents its project holds, how a failed batch degrades), and a\n * memo whose rules live an import away is a memo somebody fills under the wrong\n * key. The read index keeps what it owns — the document parses, the\n * enumerations — and hands them in. It also keeps that file under its line\n * limit, which is the honest second reason: this axis is four memos and a batch\n * plan, and it arrived at a module that was already 336 lines of other axes.\n *\n * The direction is one-way and load-bearing: `fleet-context.ts` imports this,\n * never the reverse. Everything this class needs from the read index arrives as\n * a function on the request (`ModelRead`), so nothing here knows that a\n * FleetContext exists — which is what keeps `core/` → `core/c4/` acyclic.\n *\n * REQUEST-SCOPED exactly as `FleetContext` is: one instance per invocation,\n * dropped when the command finishes. No module-level state, so a later run\n * cannot observe an earlier one's answer, and a `chdir` between two tests\n * cannot leak one docs repo into the next.\n */\nimport { resolve } from \"node:path\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport type { ServicePaths } from \"../../repo/paths.js\";\nimport type { LoadedDoc } from \"../likec4.js\";\nimport { loadArchitecture } from \"../project/architecture.js\";\nimport { asLoadedDoc, loadProject } from \"../project/load.js\";\nimport { modelProjectDocuments } from \"../service-model/documents.js\";\nimport {\n batchServiceProjects,\n serviceModelAt,\n type ServiceModel,\n} from \"../service-model/load.js\";\nimport { readModelShape, readModelShapes, type ModelShape } from \"../service-model/shape.js\";\n\n/**\n * The three things this memo cannot know for itself, supplied once by whoever\n * owns it. Functions rather than values because each is itself memoised on the\n * caller's side: asking again is what makes an answer shared rather than\n * copied.\n */\nexport interface ModelDeps {\n /** Fires once per per-service project actually parsed, batched or not — the caller owns its counters. */\n onProjectLoad: () => void;\n /** The service directories that exist — the caller has enumerated them; this package must not enumerate them again. */\n known: (docsDir: DocsDir) => Promise<ReadonlySet<string>>;\n /** Parse one `.likec4` document ALONE, through the caller's own document memo, so a prefetched parse is a hit. */\n standalone: (path: string) => Promise<LoadedDoc>;\n}\n\n/** One in-flight promise per key, created on first ask — the memo idiom, once. */\nfunction memo<T>(store: Map<string, Promise<T>>, k: string, make: () => Promise<T>): Promise<T> {\n let pending = store.get(k);\n if (pending === undefined) {\n pending = make();\n store.set(k, pending);\n }\n return pending;\n}\n\nexport class ServiceModels {\n private readonly architectures = new Map<string, Promise<LoadedDoc>>();\n private readonly shapeOf = new Map<string, Promise<ModelShape>>();\n private readonly projects = new Map<string, Promise<LoadedDoc>>();\n private readonly models = new Map<string, Promise<ServiceModel>>();\n\n constructor(private readonly deps: ModelDeps) {}\n\n /**\n * The fleet map as the PROJECT it is (`../project/architecture.ts`). Read\n * twice over for every service now rather than once for the run — it is the\n * gate on whether an extending model can be read at all, and the other half\n * of the diff that decides what such a model actually declares — so the memo\n * is what keeps a fleet run to one parse of it.\n *\n * Filled ONLY when somebody asks. `model` below hands `loadServiceModel` a\n * thunk rather than an awaited value, so a fleet whose models all stand alone\n * never parses this project at all — a standalone model is read as one file,\n * and paying for the map (and inheriting its failures) for `loam show` or the\n * adopt brief was cost and risk for an answer nobody used.\n */\n architecture(docsDir: DocsDir): Promise<LoadedDoc> {\n return memo(this.architectures, resolve(docsDir), () => loadArchitecture(docsDir));\n }\n\n /**\n * Which shape each model has, keyed by resolved path — ONE read of each\n * file's bytes per invocation, however many questions are asked of it.\n *\n * The bulk form is the primary one because every caller asks in bulk: the\n * fleet target needs the shapes of the WHOLE enumeration before it can grade\n * a single renderer-file finding, and the prefetch needs them before it can\n * decide which workspace each model belongs in.\n */\n async shapes(paths: readonly string[]): Promise<Map<string, ModelShape>> {\n const keys = [...new Set(paths.map((path) => resolve(path)))];\n const missing = keys.filter((k) => !this.shapeOf.has(k));\n for (const [k, shape] of await readModelShapes(missing)) {\n // A concurrent read's promise stays authoritative — it is the one another\n // caller is already holding — so a second scan of the same bytes can\n // never replace it with a duplicate.\n if (!this.shapeOf.has(k)) this.shapeOf.set(k, Promise.resolve(shape));\n }\n const answered = await Promise.all(keys.map(async (k) => [k, await this.shape(k)] as const));\n return new Map(answered);\n }\n\n /** One model's shape, out of the same memo the bulk form fills. */\n shape(modelPath: string): Promise<ModelShape> {\n return memo(this.shapeOf, resolve(modelPath), () => readModelShape(modelPath));\n }\n\n /** A service's model, read the way its own shape demands. Memoised per model PATH, which is what identifies the file. */\n model(docsDir: DocsDir, paths: ServicePaths): Promise<ServiceModel> {\n return memo(this.models, resolve(paths.model), async () =>\n serviceModelAt({\n docsDir,\n paths,\n shape: await this.shape(paths.model),\n known: await this.deps.known(docsDir),\n // A thunk, so a standalone model never spins the map's workspace: the\n // memo makes the extending case one parse for the run either way.\n architecture: () => this.architecture(docsDir),\n standalone: () => this.deps.standalone(paths.model),\n project: () => this.project(docsDir, paths.model),\n }),\n );\n }\n\n /**\n * Prepare every EXTENDING model's project in one workspace, and answer with\n * the STANDALONE models' paths for the caller to prefetch through its own\n * document batch — the two shapes need different workspaces, and only the\n * caller has the memo the second one seeds.\n *\n * Purely an accelerator: a batch that could not be created seeds nothing and\n * every reader falls back to its own load, so findings can never change\n * because of a tmpdir — only the speed can.\n */\n async prefetch(docsDir: DocsDir, paths: readonly ServicePaths[]): Promise<string[]> {\n const shapes = await this.shapes(paths.map((p) => p.model));\n const models = (shape: ModelShape): string[] =>\n [...shapes].filter(([, found]) => found === shape).map(([model]) => model);\n const extending = models(\"extending\").filter((model) => !this.projects.has(model));\n for (const [k, doc] of await batchServiceProjects(docsDir, extending)) {\n // A concurrent load's promise stays authoritative: it is the one every\n // reader is already holding, and replacing it would count the parse twice.\n if (this.projects.has(k)) continue;\n this.deps.onProjectLoad();\n this.projects.set(k, Promise.resolve(doc));\n }\n return models(\"standalone\");\n }\n\n /**\n * One service's project — the architecture documents and its model, parsed\n * together. Keyed on the model, which is what `prefetch` seeds above, so a\n * batched fleet run and a single-service run answer out of the same memo.\n */\n private project(docsDir: DocsDir, modelPath: string): Promise<LoadedDoc> {\n return memo(this.projects, resolve(modelPath), async () => {\n this.deps.onProjectLoad();\n return asLoadedDoc(await loadProject(docsDir, await modelProjectDocuments(docsDir, modelPath)));\n });\n }\n}\n"]}
@@ -18,14 +18,63 @@
18
18
  * command-layer business: it is about which validate target a failed read is
19
19
  * filed against.
20
20
  */
21
- import { join } from "node:path";
21
+ import { join, relative, resolve } from "node:path";
22
+ import { excludingPath, readRootExclude } from "../root-project/exclude.js";
22
23
  import { architectureDocuments } from "./documents.js";
23
24
  import { asLoadedDoc, loadProject } from "./load.js";
24
- import { subsystemViewsPath } from "../../repo/paths.js";
25
+ import { landscapePath, subsystemViewsPath } from "../../repo/paths.js";
25
26
  /** `<docsDir>/architecture` — the LikeC4 project root, spelled once. */
26
27
  export function architectureDir(docsDir) {
27
28
  return join(docsDir, "architecture");
28
29
  }
30
+ /**
31
+ * Exactly the documents `loadArchitecture` puts in the fleet map's project, as
32
+ * one named answer.
33
+ *
34
+ * It exists because a SECOND project now has to contain the same set: a service
35
+ * model that extends the map is parsed beside it (`c4/service-model/`), and the
36
+ * set it must be staged with is this one plus the model. Two spellings of "the
37
+ * architecture project's documents" would be two chances to forget the
38
+ * generated `subsystems.likec4` exclusion — a correctness rule, not an
39
+ * optimisation (`./documents.ts` says what it prevents) — and the reader that
40
+ * forgot it would blank a service's whole model over a stale generated file it
41
+ * has nothing to do with.
42
+ *
43
+ * THE ROOT `exclude` IS HONOURED HERE, and that is the second correctness rule
44
+ * this one list carries. A `.likec4` under `architecture/` that the root project
45
+ * excludes is a document the RENDERER never loads, so a global style group
46
+ * declared in one is a name the fleet project cannot resolve — and until this
47
+ * filter existed the generated subsystem views referenced it anyway: `likec4
48
+ * validate` Invalid on every run, `loam validate --all` at 0 errors, `doctor`
49
+ * healthy and `sync` "current" (verification 2026-09-04, W5). A root config loam
50
+ * cannot read an `exclude` list out of filters nothing: asserting an exclusion on
51
+ * evidence loam does not have is worse than loading one document too many.
52
+ *
53
+ * THE MAP ITSELF IS THE FLOOR, and it is not a special case for tidiness. The
54
+ * filter had none, so an entry covering `architecture/landscape.likec4` — an
55
+ * `architecture/*.likec4` a team wrote for a palette, say — left this list
56
+ * EMPTY, and an empty project parses with zero errors. Every reader then took
57
+ * that for "the map declares nothing": `validate --all` reported
58
+ * `landscape.service-unmodelled` naming the very file that models the service
59
+ * and `c4.invalid` on every extending model, `subsystem sync` rewrote a good
60
+ * generated file down to a title, and nothing in either run mentioned the
61
+ * `exclude` entry that caused it (verification 2026-09-04, review C). Keeping
62
+ * the landscape is the tolerant direction this same banner already chooses for
63
+ * an unreadable config: loam grades one document the renderer may skip, rather
64
+ * than asserting a fleet's whole architecture says nothing. The entry is NAMED
65
+ * to its author one target over — `landscape.excluded`, in
66
+ * `commands/validate/fleet/views/projects.ts`, which reads this same list
67
+ * through this same matcher so the two can never disagree about which file the
68
+ * renderer drops.
69
+ */
70
+ export async function architectureProjectDocuments(docsDir) {
71
+ const paths = await architectureDocuments(architectureDir(docsDir), [subsystemViewsPath(docsDir)]);
72
+ const exclude = await readRootExclude(docsDir);
73
+ if (exclude === null || exclude.length === 0)
74
+ return paths;
75
+ const map = resolve(landscapePath(docsDir));
76
+ return paths.filter((path) => resolve(path) === map || excludingPath(exclude, relative(docsDir, path)) === null);
77
+ }
29
78
  /**
30
79
  * The fleet map, read as the PROJECT it actually is.
31
80
  *
@@ -39,7 +88,6 @@ export function architectureDir(docsDir) {
39
88
  * always did, which is what keeps this a widening rather than a change.
40
89
  */
41
90
  export async function loadArchitecture(docsDir) {
42
- const dir = architectureDir(docsDir);
43
- return asLoadedDoc(await loadProject(dir, await architectureDocuments(dir, [subsystemViewsPath(docsDir)])));
91
+ return asLoadedDoc(await loadProject(architectureDir(docsDir), await architectureProjectDocuments(docsDir)));
44
92
  }
45
93
  //# sourceMappingURL=architecture.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"architecture.js","sourceRoot":"","sources":["../../../../src/core/c4/project/architecture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgB;IACrD,MAAM,GAAG,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,WAAW,CAAC,MAAM,WAAW,CAAC,GAAG,EAAE,MAAM,qBAAqB,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9G,CAAC","sourcesContent":["/**\n * `architecture/` as ONE LikeC4 project — the docsDir-aware wrapper over the two\n * modules beside it.\n *\n * It lived in `commands/validate/fleet/load.ts` until the use-case axis grew a\n * second reader. That was the right home while `validate --all` was the only\n * caller: the command layer is where a `DocsDir` becomes paths. It stopped being\n * the right home the moment `core/usecases/fleet.ts` needed the same load —\n * `core/` may not import from `commands/` (AGENTS.md), so the choice was a second\n * copy of the two-line composition or one copy one level down. A second copy is\n * the copy that drifts, and the thing that would drift is which documents are in\n * the project: the generated `architecture/subsystems.likec4` exclusion is a\n * correctness rule (see `./documents.ts`), not a preference, and a reader that\n * forgot it would blank a fleet's whole map over a stale generated file.\n *\n * So the wrapper moved here and `validate`'s own module kept the landscape\n * CONTAINMENT — `readLandscape` / `unreadableLandscape` — which is genuinely\n * command-layer business: it is about which validate target a failed read is\n * filed against.\n */\nimport { join } from \"node:path\";\nimport type { LoadedDoc } from \"../likec4.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { subsystemViewsPath } from \"../../repo/paths.js\";\n\n/** `<docsDir>/architecture` — the LikeC4 project root, spelled once. */\nexport function architectureDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\");\n}\n\n/**\n * The fleet map, read as the PROJECT it actually is.\n *\n * `architecture/landscape.likec4` plus every `architecture/usecases/*.likec4`,\n * merged the way the renderer merges them — because a use case declares views\n * over the landscape's elements and does not parse standalone (measured: five\n * errors). `architectureDocuments` owns which files are in and why the\n * generated one is not.\n *\n * A fleet with no use cases loads exactly the landscape and behaves as it\n * always did, which is what keeps this a widening rather than a change.\n */\nexport async function loadArchitecture(docsDir: DocsDir): Promise<LoadedDoc> {\n const dir = architectureDir(docsDir);\n return asLoadedDoc(await loadProject(dir, await architectureDocuments(dir, [subsystemViewsPath(docsDir)])));\n}\n"]}
1
+ {"version":3,"file":"architecture.js","sourceRoot":"","sources":["../../../../src/core/c4/project/architecture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAExE,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,OAAgB;IACjE,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACnG,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AACnH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgB;IACrD,OAAO,WAAW,CAAC,MAAM,WAAW,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC/G,CAAC","sourcesContent":["/**\n * `architecture/` as ONE LikeC4 project — the docsDir-aware wrapper over the two\n * modules beside it.\n *\n * It lived in `commands/validate/fleet/load.ts` until the use-case axis grew a\n * second reader. That was the right home while `validate --all` was the only\n * caller: the command layer is where a `DocsDir` becomes paths. It stopped being\n * the right home the moment `core/usecases/fleet.ts` needed the same load —\n * `core/` may not import from `commands/` (AGENTS.md), so the choice was a second\n * copy of the two-line composition or one copy one level down. A second copy is\n * the copy that drifts, and the thing that would drift is which documents are in\n * the project: the generated `architecture/subsystems.likec4` exclusion is a\n * correctness rule (see `./documents.ts`), not a preference, and a reader that\n * forgot it would blank a fleet's whole map over a stale generated file.\n *\n * So the wrapper moved here and `validate`'s own module kept the landscape\n * CONTAINMENT — `readLandscape` / `unreadableLandscape` — which is genuinely\n * command-layer business: it is about which validate target a failed read is\n * filed against.\n */\nimport { join, relative, resolve } from \"node:path\";\nimport type { LoadedDoc } from \"../likec4.js\";\nimport { excludingPath, readRootExclude } from \"../root-project/exclude.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { landscapePath, subsystemViewsPath } from \"../../repo/paths.js\";\n\n/** `<docsDir>/architecture` — the LikeC4 project root, spelled once. */\nexport function architectureDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\");\n}\n\n/**\n * Exactly the documents `loadArchitecture` puts in the fleet map's project, as\n * one named answer.\n *\n * It exists because a SECOND project now has to contain the same set: a service\n * model that extends the map is parsed beside it (`c4/service-model/`), and the\n * set it must be staged with is this one plus the model. Two spellings of \"the\n * architecture project's documents\" would be two chances to forget the\n * generated `subsystems.likec4` exclusion — a correctness rule, not an\n * optimisation (`./documents.ts` says what it prevents) — and the reader that\n * forgot it would blank a service's whole model over a stale generated file it\n * has nothing to do with.\n *\n * THE ROOT `exclude` IS HONOURED HERE, and that is the second correctness rule\n * this one list carries. A `.likec4` under `architecture/` that the root project\n * excludes is a document the RENDERER never loads, so a global style group\n * declared in one is a name the fleet project cannot resolve — and until this\n * filter existed the generated subsystem views referenced it anyway: `likec4\n * validate` Invalid on every run, `loam validate --all` at 0 errors, `doctor`\n * healthy and `sync` \"current\" (verification 2026-09-04, W5). A root config loam\n * cannot read an `exclude` list out of filters nothing: asserting an exclusion on\n * evidence loam does not have is worse than loading one document too many.\n *\n * THE MAP ITSELF IS THE FLOOR, and it is not a special case for tidiness. The\n * filter had none, so an entry covering `architecture/landscape.likec4` — an\n * `architecture/*.likec4` a team wrote for a palette, say — left this list\n * EMPTY, and an empty project parses with zero errors. Every reader then took\n * that for \"the map declares nothing\": `validate --all` reported\n * `landscape.service-unmodelled` naming the very file that models the service\n * and `c4.invalid` on every extending model, `subsystem sync` rewrote a good\n * generated file down to a title, and nothing in either run mentioned the\n * `exclude` entry that caused it (verification 2026-09-04, review C). Keeping\n * the landscape is the tolerant direction this same banner already chooses for\n * an unreadable config: loam grades one document the renderer may skip, rather\n * than asserting a fleet's whole architecture says nothing. The entry is NAMED\n * to its author one target over — `landscape.excluded`, in\n * `commands/validate/fleet/views/projects.ts`, which reads this same list\n * through this same matcher so the two can never disagree about which file the\n * renderer drops.\n */\nexport async function architectureProjectDocuments(docsDir: DocsDir): Promise<string[]> {\n const paths = await architectureDocuments(architectureDir(docsDir), [subsystemViewsPath(docsDir)]);\n const exclude = await readRootExclude(docsDir);\n if (exclude === null || exclude.length === 0) return paths;\n const map = resolve(landscapePath(docsDir));\n return paths.filter((path) => resolve(path) === map || excludingPath(exclude, relative(docsDir, path)) === null);\n}\n\n/**\n * The fleet map, read as the PROJECT it actually is.\n *\n * `architecture/landscape.likec4` plus every `architecture/usecases/*.likec4`,\n * merged the way the renderer merges them — because a use case declares views\n * over the landscape's elements and does not parse standalone (measured: five\n * errors). `architectureDocuments` owns which files are in and why the\n * generated one is not.\n *\n * A fleet with no use cases loads exactly the landscape and behaves as it\n * always did, which is what keeps this a widening rather than a change.\n */\nexport async function loadArchitecture(docsDir: DocsDir): Promise<LoadedDoc> {\n return asLoadedDoc(await loadProject(architectureDir(docsDir), await architectureProjectDocuments(docsDir)));\n}\n"]}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Many PROJECTS — each a directory-shaped set of documents that must see each
3
+ * other — through ONE LikeC4 workspace.
4
+ *
5
+ * The third loader, and the seam is the same one that already separates the
6
+ * two beside it: `../workspace.ts` batches N documents as N isolated
7
+ * single-file projects, `./load.ts` loads ONE project of N documents, and this
8
+ * loads N projects of N documents each. The shape exists because a service
9
+ * model that EXTENDS the fleet map is only readable beside the map (measured:
10
+ * an `extend <fqn> { … }` model parsed alone is every id unresolved), so a
11
+ * fleet run that used to parse 56 models as 56 isolated documents now needs 56
12
+ * projects — the architecture documents plus one model each. Measured at the
13
+ * 1.59.2 pin on an example landscape with 56 generated models: 56 such projects
14
+ * in ONE workspace take ~700 ms, while 20 SEPARATE workspaces take 2.2 s. The
15
+ * workspace, not the parse, is what a fleet run pays for.
16
+ *
17
+ * Isolation is `../workspace.ts`'s rule verbatim, and for its reason: every
18
+ * project folder is named `p<i>_<token>` with one crypto-random token per
19
+ * invocation, so an author-written `import … from '<name>'` cannot resolve
20
+ * against a sibling project it guessed the name of. Here the sibling projects
21
+ * are other services' — a model importing another service's project would
22
+ * silently grade against documents its own repo never named.
23
+ *
24
+ * The failure story is the caller's, exactly as it is for the other two loaders:
25
+ * a project whose documents could not be staged at all is ABSENT from the
26
+ * result (the caller's ordinary per-project load reproduces the error), and a
27
+ * batch-INFRASTRUCTURE failure — tmpdir denied, the sandboxed-runner class
28
+ * ROADMAP documents — rejects the whole call so `FleetContext` can degrade to
29
+ * per-project loads. Findings can never change because a workspace could not be
30
+ * created; only the speed can.
31
+ */
32
+ import { randomBytes } from "node:crypto";
33
+ import { mkdtemp, rm } from "node:fs/promises";
34
+ import { tmpdir } from "node:os";
35
+ import { join, resolve } from "node:path";
36
+ import { LikeC4 } from "likec4";
37
+ import { inOrder } from "../../kernel/concurrency.js";
38
+ import { cleanProjectDoc, emptyProject, groupErrorsByRel, stageProject, } from "./load.js";
39
+ /**
40
+ * Load many projects in one workspace. Returns `key` → `ProjectDoc`; a project
41
+ * that could not be staged at all is absent from the result.
42
+ *
43
+ * Each project's verdict is `loadProject`'s, applied per project rather than
44
+ * per workspace: errors mean no model, and the model here is that ONE
45
+ * project's — a broken service model leaves the other 55 projects with their
46
+ * models intact, which is the whole reason the fleet is staged as many projects
47
+ * rather than as one.
48
+ */
49
+ export async function loadProjectBatch(projects) {
50
+ const out = new Map();
51
+ if (projects.length === 0)
52
+ return out;
53
+ const root = await mkdtemp(join(tmpdir(), "loam-c4-"));
54
+ try {
55
+ const staged = await stageAll(root, projects);
56
+ if (staged.length === 0)
57
+ return out;
58
+ // One workspace for every project — the entire point. The logger is
59
+ // disabled exactly as in the two loaders beside this one: diagnostics are
60
+ // returned per document, not written out-of-band to stderr (core never
61
+ // prints).
62
+ const likec4 = await LikeC4.fromWorkspace(root, { logger: false });
63
+ try {
64
+ const byProject = groupByProject(likec4.getErrors(), new Set(staged.map((s) => s.project)));
65
+ for (const { key, project, byRel } of staged) {
66
+ const errors = groupErrorsByRel(byProject.get(project) ?? [], byRel);
67
+ if (errors.size > 0) {
68
+ out.set(key, emptyProject(errors, false));
69
+ }
70
+ else {
71
+ out.set(key, cleanProjectDoc((await likec4.parsedModel(project))));
72
+ }
73
+ }
74
+ }
75
+ finally {
76
+ await likec4.dispose();
77
+ }
78
+ return out;
79
+ }
80
+ finally {
81
+ // The workspace is this invocation's alone; nothing else may observe it.
82
+ // A kill between mkdtemp and here can strand a loam-c4-* dir in OS tmp —
83
+ // never in the docs repo — which docs/DESIGN.md records as accepted.
84
+ await rm(root, { recursive: true, force: true });
85
+ }
86
+ }
87
+ /**
88
+ * Every project staged into its own folder under `root`, in input order.
89
+ *
90
+ * ONE token for the whole invocation, and the index makes each name unique
91
+ * inside it — `../workspace.ts`'s scheme exactly, so the two batch loaders
92
+ * cannot drift into two answers about whether a sibling project is nameable.
93
+ * A project none of whose documents could be copied is dropped rather than
94
+ * staged empty: an empty project in the workspace would parse clean and grade a
95
+ * service against nothing at all, where absence sends the caller back to its
96
+ * own per-project load and today's error.
97
+ */
98
+ async function stageAll(root, projects) {
99
+ const token = randomBytes(8).toString("hex");
100
+ // Concurrent, capped, in input order (kernel/concurrency's contract): the
101
+ // per-project folders share nothing, and on a network-mounted docs repo the
102
+ // sequential form pays a round-trip per document. Nulls mark the drops so the
103
+ // p<i> names stay tied to the caller's own ordering.
104
+ const results = await inOrder([...projects.entries()], async ([i, req]) => {
105
+ const project = `p${i}_${token}`;
106
+ const targets = [...new Set(req.paths.map((path) => resolve(path)))];
107
+ const byRel = await stageProject(root, project, resolve(req.base), targets);
108
+ return byRel.size === 0 ? null : { key: req.key, project, byRel };
109
+ });
110
+ return results.filter((r) => r !== null);
111
+ }
112
+ /**
113
+ * One workspace's diagnostics split per PROJECT, keyed on the unique
114
+ * `p<i>_<token>` path SEGMENT.
115
+ *
116
+ * Never a tmp-root prefix compare, for `../workspace.ts`'s measured reason:
117
+ * darwin hands back realpathed workspace paths (`/var/…` becomes
118
+ * `/private/var/…`) in some environments and the literal mkdtemp path in
119
+ * others, and a prefix compare silently attributes every error to nobody in
120
+ * exactly one of those two worlds. The segment survives both.
121
+ *
122
+ * An error this walk cannot attribute throws, and the whole batch with it: a
123
+ * dropped error grades its project clean, which is failing open in the fleet
124
+ * gate. The caller degrades to per-project loads, where the error reappears
125
+ * against the document that raised it.
126
+ */
127
+ function groupByProject(errors, projects) {
128
+ const grouped = new Map();
129
+ for (const err of errors) {
130
+ const project = err.sourceFsPath.split(/[\\/]/).find((segment) => projects.has(segment));
131
+ if (project === undefined) {
132
+ throw new Error(`unattributable LikeC4 error in batch workspace: ${err.message}`);
133
+ }
134
+ const list = grouped.get(project) ?? [];
135
+ list.push(err);
136
+ grouped.set(project, list);
137
+ }
138
+ return grouped;
139
+ }
140
+ //# sourceMappingURL=batch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.js","sourceRoot":"","sources":["../../../../src/core/c4/project/batch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Many PROJECTS — each a directory-shaped set of documents that must see each\n * other — through ONE LikeC4 workspace.\n *\n * The third loader, and the seam is the same one that already separates the\n * two beside it: `../workspace.ts` batches N documents as N isolated\n * single-file projects, `./load.ts` loads ONE project of N documents, and this\n * loads N projects of N documents each. The shape exists because a service\n * model that EXTENDS the fleet map is only readable beside the map (measured:\n * an `extend <fqn> { … }` model parsed alone is every id unresolved), so a\n * fleet run that used to parse 56 models as 56 isolated documents now needs 56\n * projects — the architecture documents plus one model each. Measured at the\n * 1.59.2 pin on an example landscape with 56 generated models: 56 such projects\n * in ONE workspace take ~700 ms, while 20 SEPARATE workspaces take 2.2 s. The\n * workspace, not the parse, is what a fleet run pays for.\n *\n * Isolation is `../workspace.ts`'s rule verbatim, and for its reason: every\n * project folder is named `p<i>_<token>` with one crypto-random token per\n * invocation, so an author-written `import … from '<name>'` cannot resolve\n * against a sibling project it guessed the name of. Here the sibling projects\n * are other services' — a model importing another service's project would\n * silently grade against documents its own repo never named.\n *\n * The failure story is the caller's, exactly as it is for the other two loaders:\n * a project whose documents could not be staged at all is ABSENT from the\n * result (the caller's ordinary per-project load reproduces the error), and a\n * batch-INFRASTRUCTURE failure — tmpdir denied, the sandboxed-runner class\n * ROADMAP documents — rejects the whole call so `FleetContext` can degrade to\n * per-project loads. Findings can never change because a workspace could not be\n * created; only the speed can.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { inOrder } from \"../../kernel/concurrency.js\";\nimport type { LikeC4Error, ReadableModel } from \"../likec4.js\";\nimport {\n cleanProjectDoc,\n emptyProject,\n groupErrorsByRel,\n stageProject,\n type ProjectDoc,\n} from \"./load.js\";\n\n/** One project to load: the caller's key for it, the root its documents are mirrored relative to, and the documents. */\nexport interface ProjectRequest {\n /**\n * How the caller will look the answer up. Never derived here from a path,\n * because the caller's identity for a project is not this module's business:\n * `FleetContext` keys on the resolved `model.likec4` path, and a second\n * spelling of that key is a memo the caller can never hit.\n */\n key: string;\n /**\n * The directory the documents' relative structure is preserved against —\n * the docs root for a per-service project, which is what makes a view's\n * `sourcePath` come back spelled `services/<tree>/model.likec4`, the way the\n * renderer spells it from the same root.\n */\n base: string;\n paths: string[];\n}\n\n/** A project that reached the workspace: the caller's key, its folder name, and rel → real path. */\ninterface StagedProject {\n key: string;\n /** The workspace folder name — `parsedModel`'s argument AND the error-attribution segment. */\n project: string;\n byRel: Map<string, string>;\n}\n\n/**\n * Load many projects in one workspace. Returns `key` → `ProjectDoc`; a project\n * that could not be staged at all is absent from the result.\n *\n * Each project's verdict is `loadProject`'s, applied per project rather than\n * per workspace: errors mean no model, and the model here is that ONE\n * project's — a broken service model leaves the other 55 projects with their\n * models intact, which is the whole reason the fleet is staged as many projects\n * rather than as one.\n */\nexport async function loadProjectBatch(\n projects: readonly ProjectRequest[],\n): Promise<Map<string, ProjectDoc>> {\n const out = new Map<string, ProjectDoc>();\n if (projects.length === 0) return out;\n const root = await mkdtemp(join(tmpdir(), \"loam-c4-\"));\n try {\n const staged = await stageAll(root, projects);\n if (staged.length === 0) return out;\n // One workspace for every project — the entire point. The logger is\n // disabled exactly as in the two loaders beside this one: diagnostics are\n // returned per document, not written out-of-band to stderr (core never\n // prints).\n const likec4 = await LikeC4.fromWorkspace(root, { logger: false });\n try {\n const byProject = groupByProject(likec4.getErrors(), new Set(staged.map((s) => s.project)));\n for (const { key, project, byRel } of staged) {\n const errors = groupErrorsByRel(byProject.get(project) ?? [], byRel);\n if (errors.size > 0) {\n out.set(key, emptyProject(errors, false));\n } else {\n out.set(key, cleanProjectDoc((await likec4.parsedModel(project)) as ReadableModel));\n }\n }\n } finally {\n await likec4.dispose();\n }\n return out;\n } finally {\n // The workspace is this invocation's alone; nothing else may observe it.\n // A kill between mkdtemp and here can strand a loam-c4-* dir in OS tmp —\n // never in the docs repo — which docs/DESIGN.md records as accepted.\n await rm(root, { recursive: true, force: true });\n }\n}\n\n/**\n * Every project staged into its own folder under `root`, in input order.\n *\n * ONE token for the whole invocation, and the index makes each name unique\n * inside it — `../workspace.ts`'s scheme exactly, so the two batch loaders\n * cannot drift into two answers about whether a sibling project is nameable.\n * A project none of whose documents could be copied is dropped rather than\n * staged empty: an empty project in the workspace would parse clean and grade a\n * service against nothing at all, where absence sends the caller back to its\n * own per-project load and today's error.\n */\nasync function stageAll(root: string, projects: readonly ProjectRequest[]): Promise<StagedProject[]> {\n const token = randomBytes(8).toString(\"hex\");\n // Concurrent, capped, in input order (kernel/concurrency's contract): the\n // per-project folders share nothing, and on a network-mounted docs repo the\n // sequential form pays a round-trip per document. Nulls mark the drops so the\n // p<i> names stay tied to the caller's own ordering.\n const results = await inOrder([...projects.entries()], async ([i, req]) => {\n const project = `p${i}_${token}`;\n const targets = [...new Set(req.paths.map((path) => resolve(path)))];\n const byRel = await stageProject(root, project, resolve(req.base), targets);\n return byRel.size === 0 ? null : { key: req.key, project, byRel };\n });\n return results.filter((r): r is StagedProject => r !== null);\n}\n\n/**\n * One workspace's diagnostics split per PROJECT, keyed on the unique\n * `p<i>_<token>` path SEGMENT.\n *\n * Never a tmp-root prefix compare, for `../workspace.ts`'s measured reason:\n * darwin hands back realpathed workspace paths (`/var/…` becomes\n * `/private/var/…`) in some environments and the literal mkdtemp path in\n * others, and a prefix compare silently attributes every error to nobody in\n * exactly one of those two worlds. The segment survives both.\n *\n * An error this walk cannot attribute throws, and the whole batch with it: a\n * dropped error grades its project clean, which is failing open in the fleet\n * gate. The caller degrades to per-project loads, where the error reappears\n * against the document that raised it.\n */\nfunction groupByProject(\n errors: readonly (LikeC4Error & { sourceFsPath: string })[],\n projects: ReadonlySet<string>,\n): Map<string, (LikeC4Error & { sourceFsPath: string })[]> {\n const grouped = new Map<string, (LikeC4Error & { sourceFsPath: string })[]>();\n for (const err of errors) {\n const project = err.sourceFsPath.split(/[\\\\/]/).find((segment) => projects.has(segment));\n if (project === undefined) {\n throw new Error(`unattributable LikeC4 error in batch workspace: ${err.message}`);\n }\n const list = grouped.get(project) ?? [];\n list.push(err);\n grouped.set(project, list);\n }\n return grouped;\n}\n"]}
@@ -2,10 +2,15 @@
2
2
  * Which `.likec4` documents in a docs repo's `architecture/` loam loads as one
3
3
  * project.
4
4
  *
5
- * The renderer's answer is "all of them" — `likec4.config.json` scopes the root
6
- * LikeC4 project to everything but `services/**` and `features/**`. loam's
7
- * answer is all of them EXCEPT the generated `architecture/subsystems.likec4`,
8
- * and that exception is not an optimisation:
5
+ * The renderer's answer is "all of them". The root `likec4.config.json` no
6
+ * longer excludes `services/**`: the root project holds the map AND every
7
+ * service model that EXTENDS it, because such a model is only readable beside
8
+ * the map, and `loam subsystem sync` excludes only the directory of each model
9
+ * that STANDS ALONE — one `services/<tree>/**` entry per standalone model,
10
+ * never the whole subtree (`c4/service-model/renderer.ts` owns that list, and
11
+ * is the one place loam reads that file). loam's own answer here is all of them
12
+ * EXCEPT the generated `architecture/subsystems.likec4`, and that exception is
13
+ * not an optimisation:
9
14
  *
10
15
  * - docs/DESIGN.md rule 26 excludes the generated file from every read loam
11
16
  * performs. Its staleness is a byte compare (`subsystem.views-stale`) and
@@ -1 +1 @@
1
- {"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../../src/core/c4/project/documents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,GAAW,EAAE,IAAI,GAAa,EAAE;IAC1E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,KAAK,EAAE,EAAU,EAAiB,EAAE;QAC/C,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;iBACrC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC","sourcesContent":["/**\n * Which `.likec4` documents in a docs repo's `architecture/` loam loads as one\n * project.\n *\n * The renderer's answer is \"all of them\" — `likec4.config.json` scopes the root\n * LikeC4 project to everything but `services/**` and `features/**`. loam's\n * answer is all of them EXCEPT the generated `architecture/subsystems.likec4`,\n * and that exception is not an optimisation:\n *\n * - docs/DESIGN.md rule 26 excludes the generated file from every read loam\n * performs. Its staleness is a byte compare (`subsystem.views-stale`) and\n * nothing anywhere reads its contents.\n * - Including it would invent a failure mode. Its `include` lines name\n * landscape element ids, so a file that has drifted — a service removed\n * since the last `loam subsystem sync` — would be a parse error, and a parse\n * error in one project document blanks the model for all of them. A fleet\n * whose only problem is a stale generated file would lose its whole map\n * instead of getting the one finding that names the repair.\n *\n * The skip is passed in by the caller rather than spelled here, so the filename\n * stays written in `repo/paths.ts` alone.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\n/**\n * Every `.likec4` file under `dir`, recursively, minus `skip`, in a stable\n * order.\n *\n * Sorted because it decides the staging order and therefore nothing at all —\n * LikeC4 merges a project regardless — but a stable list makes a failure\n * reproducible and a diff of one readable. A directory that does not exist is\n * an empty list, never a throw: a fleet with no `architecture/` is a fleet with\n * nothing to load, which is already `landscape.missing`'s question, not this\n * function's.\n */\nexport async function architectureDocuments(dir: string, skip: string[] = []): Promise<string[]> {\n const excluded = new Set(skip.map((path) => resolve(path)));\n const found: string[] = [];\n const walk = async (at: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(at, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const path = join(at, entry.name);\n if (entry.isDirectory()) await walk(path);\n else if (entry.name.endsWith(\".likec4\") && !excluded.has(resolve(path))) found.push(path);\n }\n };\n await walk(resolve(dir));\n return found.sort();\n}\n"]}
1
+ {"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../../src/core/c4/project/documents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,GAAW,EAAE,IAAI,GAAa,EAAE;IAC1E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,KAAK,EAAE,EAAU,EAAiB,EAAE;QAC/C,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;iBACrC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC","sourcesContent":["/**\n * Which `.likec4` documents in a docs repo's `architecture/` loam loads as one\n * project.\n *\n * The renderer's answer is \"all of them\". The root `likec4.config.json` no\n * longer excludes `services/**`: the root project holds the map AND every\n * service model that EXTENDS it, because such a model is only readable beside\n * the map, and `loam subsystem sync` excludes only the directory of each model\n * that STANDS ALONE — one `services/<tree>/**` entry per standalone model,\n * never the whole subtree (`c4/service-model/renderer.ts` owns that list, and\n * is the one place loam reads that file). loam's own answer here is all of them\n * EXCEPT the generated `architecture/subsystems.likec4`, and that exception is\n * not an optimisation:\n *\n * - docs/DESIGN.md rule 26 excludes the generated file from every read loam\n * performs. Its staleness is a byte compare (`subsystem.views-stale`) and\n * nothing anywhere reads its contents.\n * - Including it would invent a failure mode. Its `include` lines name\n * landscape element ids, so a file that has drifted — a service removed\n * since the last `loam subsystem sync` — would be a parse error, and a parse\n * error in one project document blanks the model for all of them. A fleet\n * whose only problem is a stale generated file would lose its whole map\n * instead of getting the one finding that names the repair.\n *\n * The skip is passed in by the caller rather than spelled here, so the filename\n * stays written in `repo/paths.ts` alone.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\n/**\n * Every `.likec4` file under `dir`, recursively, minus `skip`, in a stable\n * order.\n *\n * Sorted because it decides the staging order and therefore nothing at all —\n * LikeC4 merges a project regardless — but a stable list makes a failure\n * reproducible and a diff of one readable. A directory that does not exist is\n * an empty list, never a throw: a fleet with no `architecture/` is a fleet with\n * nothing to load, which is already `landscape.missing`'s question, not this\n * function's.\n */\nexport async function architectureDocuments(dir: string, skip: string[] = []): Promise<string[]> {\n const excluded = new Set(skip.map((path) => resolve(path)));\n const found: string[] = [];\n const walk = async (at: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(at, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const path = join(at, entry.name);\n if (entry.isDirectory()) await walk(path);\n else if (entry.name.endsWith(\".likec4\") && !excluded.has(resolve(path))) found.push(path);\n }\n };\n await walk(resolve(dir));\n return found.sort();\n}\n"]}