@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
@@ -6,7 +6,7 @@
6
6
  * because `core/c4/arch.ts` already exists and a twin name invites the wrong
7
7
  * import.
8
8
  */
9
- import {} from "../c4/likec4.js";
9
+ import { errorText } from "../c4/likec4.js";
10
10
  import { elementService, serviceResolver } from "../c4/resolve/service.js";
11
11
  import { DocsRepoUnavailableError } from "../repo/state.js";
12
12
  import { listServices } from "../repo/repo.js";
@@ -37,7 +37,7 @@ export function archSlice(doc, service, featureId, known) {
37
37
  return empty;
38
38
  const { errors, elements, relationships } = doc;
39
39
  if (errors.length > 0) {
40
- return { ...empty, errors: errors.map((e) => (typeof e.line === "number" ? `L${e.line}: ${e.message}` : e.message)) };
40
+ return { ...empty, errors: errors.map(errorText) };
41
41
  }
42
42
  // Which service an element stands for is the binding's call, not the title's
43
43
  // — and the enumerated fleet rides in so the slice agrees with `validate`
@@ -1 +1 @@
1
- {"version":3,"file":"arch-slice.js","sourceRoot":"","sources":["../../../src/core/projection/arch-slice.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The C4 half of projecting a feature onto one service: which services exist,\n * which the feature introduces, and what its tagged edges add around the one\n * being projected onto. Moved out of `commands/delta/slices.ts` beside\n * `./api.ts` and `./events.ts`; named `arch-slice.ts` rather than `arch.ts`\n * because `core/c4/arch.ts` already exists and a twin name invites the wrong\n * import.\n */\nimport { type Elem, type LoadedDoc, type Rel } from \"../c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../c4/resolve/service.js\";\nimport { DocsRepoUnavailableError } from \"../repo/state.js\";\nimport { listServices } from \"../repo/repo.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/** One end of a feature edge, as seen from the projected service. */\nexport interface Edge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\nexport interface ArchSlice {\n isNew: boolean;\n inbound: Edge[];\n outbound: Edge[];\n errors: string[];\n}\n\n/** Every service in the docs repo, or none when there is no docs repo to ask. */\nexport async function livingServices(docsDir: DocsDir): Promise<string[]> {\n try {\n return (await listServices(docsDir)).map((s) => s.id);\n } catch (err) {\n // \"There is no docs repo at all\" is a different diagnosis with its own\n // command (`loam doctor`); turning it into \"unknown service\" here would\n // send the reader after a typo that is not there.\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n return [];\n }\n}\n\n/** Services the feature's C4 delta introduces — its own tagged top-level elements. */\nexport function introducedServices(doc: LoadedDoc | null, featureId: string): string[] {\n if (doc === null || doc.errors.length > 0) return [];\n return doc.elements.filter((e: Elem) => e.tags.includes(featureId)).map(elementService);\n}\n\n/** The feature's tagged edges around one service, plus whether the service is new. */\nexport function archSlice(\n doc: LoadedDoc | null,\n service: string,\n featureId: string,\n known?: ReadonlySet<string>,\n): ArchSlice {\n const empty: ArchSlice = { isNew: false, inbound: [], outbound: [], errors: [] };\n if (doc === null) return empty;\n\n const { errors, elements, relationships } = doc;\n if (errors.length > 0) {\n return { ...empty, errors: errors.map((e) => (typeof e.line === \"number\" ? `L${e.line}: ${e.message}` : e.message)) };\n }\n\n // Which service an element stands for is the binding's call, not the title's\n // — and the enumerated fleet rides in so the slice agrees with `validate`\n // about an edge drawn into a modelled container. Without it, this projection\n // told the provider \"no inbound calls\" and the consumer \"you call 'api'\" for\n // the same `checkoutWeb -> paymentService.api` edge the validator was\n // grading against payment-service — and the brief an agent implements from\n // must never disagree with the gate it will be checked by.\n const svcOf = serviceResolver(elements, known);\n const edge = (r: Rel, other: string): Edge => ({\n service: other,\n op: r.op ?? null,\n title: r.title ?? null,\n });\n const featRels = relationships.filter((r) => r.tags.includes(featureId));\n\n return {\n isNew: elements.some((e) => elementService(e) === service && e.tags.includes(featureId)),\n inbound: featRels.filter((r) => svcOf(r.target) === service).map((r) => edge(r, svcOf(r.source))),\n outbound: featRels.filter((r) => svcOf(r.source) === service).map((r) => edge(r, svcOf(r.target))),\n errors: [],\n };\n}\n"]}
1
+ {"version":3,"file":"arch-slice.js","sourceRoot":"","sources":["../../../src/core/projection/arch-slice.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The C4 half of projecting a feature onto one service: which services exist,\n * which the feature introduces, and what its tagged edges add around the one\n * being projected onto. Moved out of `commands/delta/slices.ts` beside\n * `./api.ts` and `./events.ts`; named `arch-slice.ts` rather than `arch.ts`\n * because `core/c4/arch.ts` already exists and a twin name invites the wrong\n * import.\n */\nimport { errorText, type Elem, type LoadedDoc, type Rel } from \"../c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../c4/resolve/service.js\";\nimport { DocsRepoUnavailableError } from \"../repo/state.js\";\nimport { listServices } from \"../repo/repo.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\n/** One end of a feature edge, as seen from the projected service. */\nexport interface Edge {\n service: string;\n op: string | null;\n title: string | null;\n}\n\nexport interface ArchSlice {\n isNew: boolean;\n inbound: Edge[];\n outbound: Edge[];\n errors: string[];\n}\n\n/** Every service in the docs repo, or none when there is no docs repo to ask. */\nexport async function livingServices(docsDir: DocsDir): Promise<string[]> {\n try {\n return (await listServices(docsDir)).map((s) => s.id);\n } catch (err) {\n // \"There is no docs repo at all\" is a different diagnosis with its own\n // command (`loam doctor`); turning it into \"unknown service\" here would\n // send the reader after a typo that is not there.\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n return [];\n }\n}\n\n/** Services the feature's C4 delta introduces — its own tagged top-level elements. */\nexport function introducedServices(doc: LoadedDoc | null, featureId: string): string[] {\n if (doc === null || doc.errors.length > 0) return [];\n return doc.elements.filter((e: Elem) => e.tags.includes(featureId)).map(elementService);\n}\n\n/** The feature's tagged edges around one service, plus whether the service is new. */\nexport function archSlice(\n doc: LoadedDoc | null,\n service: string,\n featureId: string,\n known?: ReadonlySet<string>,\n): ArchSlice {\n const empty: ArchSlice = { isNew: false, inbound: [], outbound: [], errors: [] };\n if (doc === null) return empty;\n\n const { errors, elements, relationships } = doc;\n if (errors.length > 0) {\n return { ...empty, errors: errors.map(errorText) };\n }\n\n // Which service an element stands for is the binding's call, not the title's\n // — and the enumerated fleet rides in so the slice agrees with `validate`\n // about an edge drawn into a modelled container. Without it, this projection\n // told the provider \"no inbound calls\" and the consumer \"you call 'api'\" for\n // the same `checkoutWeb -> paymentService.api` edge the validator was\n // grading against payment-service — and the brief an agent implements from\n // must never disagree with the gate it will be checked by.\n const svcOf = serviceResolver(elements, known);\n const edge = (r: Rel, other: string): Edge => ({\n service: other,\n op: r.op ?? null,\n title: r.title ?? null,\n });\n const featRels = relationships.filter((r) => r.tags.includes(featureId));\n\n return {\n isNew: elements.some((e) => elementService(e) === service && e.tags.includes(featureId)),\n inbound: featRels.filter((r) => svcOf(r.target) === service).map((r) => edge(r, svcOf(r.source))),\n outbound: featRels.filter((r) => svcOf(r.source) === service).map((r) => edge(r, svcOf(r.target))),\n errors: [],\n };\n}\n"]}
@@ -274,4 +274,44 @@ export function agentsPath(docsDir) {
274
274
  export function archiveDir(docsDir) {
275
275
  return join(featuresDir(docsDir), ARCHIVE_DIR);
276
276
  }
277
+ /**
278
+ * The LikeC4 project file — the one thing in a docs repo loam writes for a
279
+ * tool other than itself — and it lives at two altitudes.
280
+ *
281
+ * At the ROOT, `loam init --create` writes the project `fleet` (`core/docs.ts`
282
+ * composes those bytes), which holds the fleet map and every service model that
283
+ * EXTENDS it. Beside a model that STANDS ALONE — one declaring its own
284
+ * `specification` block, which can only be parsed on its own — `loam subsystem
285
+ * sync` writes a project of the service's own, and adds that directory to the
286
+ * root's `exclude`; without both, such a model is either a duplicate of the map
287
+ * or a box on it with nothing renderable inside.
288
+ * `core/repo/tree/render/projects.ts` composes the per-service bytes and records
289
+ * what was measured; `core/c4/root-project/exclude.ts` owns reading the root's
290
+ * `exclude` list and deciding what an entry covers; `core/c4/service-model/renderer.ts`
291
+ * owns the list `loam subsystem sync` writes back.
292
+ *
293
+ * Spelled here and NOT in `ARTIFACT_FILES`, and the distinction is
294
+ * load-bearing: a filename in that table classifies a directory as a service,
295
+ * and this file is the renderer's — a stray one must mint nothing. The root's
296
+ * NAME sits beside the filename because the per-service rule needs it too (a
297
+ * service id equal to the root project's name would be silently renamed by
298
+ * the renderer), and two spellings of the word are how the scaffold and the
299
+ * rule drift apart.
300
+ */
301
+ export const LIKEC4_PROJECT_FILENAME = "likec4.config.json";
302
+ /** The root project's name, exactly as `loam init --create` scaffolds it. */
303
+ export const LIKEC4_ROOT_PROJECT = "fleet";
304
+ export function rootProjectPath(docsDir) {
305
+ return join(docsDir, LIKEC4_PROJECT_FILENAME);
306
+ }
307
+ /**
308
+ * The two files the per-service project question is asked of — the model that
309
+ * needs rendering and the project file that renders it — in one spelling for
310
+ * the writer (`subsystem sync`) and the grader (`validate --all`) alike. Handed
311
+ * INTO `render/projects.ts` rather than imported there, because that package
312
+ * cannot import this module: `render/stale.ts` records the cycle.
313
+ */
314
+ export function serviceRenderPaths(dir) {
315
+ return { model: join(dir, ARTIFACT_FILES.model), project: join(dir, LIKEC4_PROJECT_FILENAME) };
316
+ }
277
317
  //# sourceMappingURL=paths.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/repo/paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where every artifact lives. Artifact filenames are spelled here and nowhere\n * else, so a rename is one edit rather than a grep.\n *\n * This is also the module that carries loam's one path guarantee.\n * `unfiledServicePaths(docsDir, service)` spells `<docsDir>/services/<service>/`,\n * so both parts are caller-controlled path input, and `node:path` cannot help:\n * `join(...paths: string[])` accepts every string there is. The guarantee IS\n * the parameter brands and nothing else — `PathableService` for the id (a name\n * whose provenance is the repository, unconstructible from document text),\n * `DocsDir`/`FeatureDir` for the roots (a resolved config or validated\n * `--docs`; a directory an enumeration read), and `ServiceDir` for\n * `servicePathsAt` (the enumeration's own resolved directory, narrower still)\n * — the `kernel/ids/` package holds the only casts, so an unchecked value at\n * these call sites does not compile.\n * Code that spells `services/<id>/` or a feature layout with a bare join is\n * outside the guarantee — `commands/new.ts` and the openspec migrator both do,\n * held instead by `resolveInside` at the write.\n */\nimport { join } from \"node:path\";\nimport { serviceDirOf, type DocsDir, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport type { SubsystemName } from \"../kernel/ids/subsystem.js\";\n\n/** Directory under features/ holding shipped features. Never a feature itself. */\nexport const ARCHIVE_DIR = \"archive\";\n\n/* ------------------------------------------------------------------ */\n/* Paths — artifact filenames are spelled here and nowhere else */\n/* ------------------------------------------------------------------ */\n\n/**\n * The living artifact filenames, as one table, because three questions read\n * them: `servicePathsAt` below spells the paths, the tree walk classifies\n * a directory as a service by whether any of these names sit in it, and\n * `core/diff/base-state.ts` classifies the SAME way over a base git ref's\n * tree listing (exported for that reader — a filename respelled there would\n * be the drift this table exists to prevent). The walk cannot import this\n * module — `repo → repo/tree` is the package direction, and an edge back\n * would be the cycle `scripts/package-graph.mjs` refuses — so `repo.ts`\n * injects `isServiceArtifactName` into the walk request instead.\n */\nexport const ARTIFACT_FILES = {\n model: \"model.likec4\",\n spec: \"spec.md\",\n archSpec: \"arch.spec.md\",\n openapi: \"openapi.yaml\",\n asyncapi: \"asyncapi.yaml\",\n runbook: \"runbook.md\",\n health: \"health.yaml\",\n steps: \"steps.yaml\",\n} as const;\nconst ADRS_DIR = \"adrs\";\n\n/** Does a directory entry name a living service artifact? The classification half of the table above. */\nexport function isServiceArtifactName(name: string, kind: \"file\" | \"dir\"): boolean {\n if (kind === \"dir\") return name === ADRS_DIR;\n return Object.values(ARTIFACT_FILES).some((file) => file === name);\n}\n\nexport interface ServicePaths {\n dir: ServiceDir;\n model: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract — the event axis's sibling of `openapi`. */\n asyncapi: string;\n runbook: string;\n health: string;\n /**\n * The step catalogue — which phrases this service's suite has DECIDED to\n * define. Authored and opt-in; its CONTENTS are read by `loam steps` alone and\n * nothing in `validate` consults them\n * (`core/gherkin/steps/catalogue.ts` records why).\n *\n * Its EXISTENCE is read more widely, and that is not the same statement: the\n * filename is in `ARTIFACT_FILES`, so `isServiceArtifactName` classifies it\n * and the tree walk treats a directory holding one as a service. That is\n * correct — it is a service artifact — and it is worth writing down, because\n * it means a `steps.yaml` written at the wrong path mints a service.\n */\n steps: string;\n adrsDir: string;\n}\n\n/**\n * The artifact paths of a service whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the tree walk found it. This is the\n * spelling every reader of an existing service must use: joining\n * `services/<id>/` at the root is only true for a fleet nobody has filed, and\n * a moved service would silently grade as absent through it.\n */\nexport function servicePathsAt(dir: ServiceDir): ServicePaths {\n return {\n dir,\n model: join(dir, ARTIFACT_FILES.model),\n spec: join(dir, ARTIFACT_FILES.spec),\n archSpec: join(dir, ARTIFACT_FILES.archSpec),\n openapi: join(dir, ARTIFACT_FILES.openapi),\n asyncapi: join(dir, ARTIFACT_FILES.asyncapi),\n runbook: join(dir, ARTIFACT_FILES.runbook),\n health: join(dir, ARTIFACT_FILES.health),\n steps: join(dir, ARTIFACT_FILES.steps),\n adrsDir: join(dir, ADRS_DIR),\n };\n}\n\n/**\n * The UNFILED spelling — `<docsDir>/services/<id>/`, the tree's root level.\n * This is where creation lands (`adopt`'s brief, a new service materialised by\n * `archive`, the OpenSpec migration) and the honest fallback for a service the\n * enumeration does not answer; for a service that EXISTS, resolve through the\n * enumeration instead (`servicePathsAt(entry.dir)`, or\n * `locateServicePaths` in `service-target.ts`, which chooses between the two).\n * Renamed from `servicePaths` when the tree landed, precisely so the compiler\n * would put every remaining caller in front of a reviewer: a root join that\n * survives unreviewed is a moved service silently grading as absent.\n */\nexport function unfiledServicePaths(docsDir: DocsDir, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(docsDir, \"services\", service)));\n}\n\n/**\n * The creation spelling INSIDE a subsystem — `<subsystemDir>/<id>/`, the\n * target `adopt --subsystem` briefs so an adoption need not land unfiled and\n * cost a second command. The same two provenances the unfiled spelling\n * combines, one level down: the subsystem directory came off the tree walk's\n * readdir, and the id passed the service grammar before it reached here.\n */\nexport function servicePathsUnder(subsystemDir: string, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(subsystemDir, service)));\n}\n\n/**\n * The directory a SUBSYSTEM occupies (or will occupy) under a parent — the\n * spelling `subsystem new` creates and `subsystem rename` renames into. The\n * same guarantee-by-brand as the service spellings above: the parent came off\n * the tree walk's readdir (or is `servicesDir` itself), and demanding\n * `SubsystemName` here is what makes the validator unskippable — a raw argv\n * string at this call site does not compile, so no future verb can hoist the\n * join above the check that earns it.\n */\nexport function subsystemPathUnder(parentDir: string, name: SubsystemName): string {\n return join(parentDir, name);\n}\n\n/**\n * The pair of requirement-carrying spec files, living and delta alike: the\n * business spec and the architecture spec. Same grammar, same delta algebra,\n * same merge — everything that walks \"the spec files\" of a service walks this\n * list, so the two axes cannot drift apart in what handles them. `key` indexes\n * ServicePaths/FeatureSpecPaths; `label` is how the axis names itself in prose.\n */\nexport const SPEC_AXES = [\n { key: \"spec\", file: \"spec.md\", label: \"requirements\" },\n { key: \"archSpec\", file: \"arch.spec.md\", label: \"arch requirements\" },\n] as const;\nexport type SpecAxis = (typeof SPEC_AXES)[number];\n\nexport interface FeaturePaths {\n dir: string;\n intent: string;\n delta: string;\n specsDir: string;\n adrsDir: string;\n}\n\nexport function featurePaths(featureDir: FeatureDir): FeaturePaths {\n return {\n dir: featureDir,\n intent: join(featureDir, \"intent.md\"),\n delta: join(featureDir, \"delta.likec4\"),\n specsDir: join(featureDir, \"specs\"),\n adrsDir: join(featureDir, \"adrs\"),\n };\n}\n\nexport interface FeatureSpecPaths {\n dir: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract delta — the event axis's sibling of `openapi`. */\n asyncapi: string;\n}\n\nexport function featureSpecPaths(featureDir: FeatureDir, service: PathableService): FeatureSpecPaths {\n const dir = join(featureDir, \"specs\", service);\n return {\n dir,\n spec: join(dir, \"spec.md\"),\n archSpec: join(dir, \"arch.spec.md\"),\n openapi: join(dir, \"openapi.yaml\"),\n asyncapi: join(dir, \"asyncapi.yaml\"),\n };\n}\n\nexport function landscapePath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", LANDSCAPE_FILE);\n}\n\n/** The fleet's authorization vocabulary — beside the fleet map, for the same reason. */\nexport function permissionsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"permissions.yaml\");\n}\n\n/** The fleet's capability vocabulary — beside the other two fleet documents, for the same reason. */\nexport function capabilitiesPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"capabilities.yaml\");\n}\n\n/**\n * The fleet's ARCHITECTURAL OBLIGATION vocabulary — beside the landscape it tags\n * and the two vocabularies it sits with, for their reason: it is a fact about\n * the fleet that no single service owns.\n */\nexport function obligationsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"obligations.yaml\");\n}\n\n/**\n * The FLEET's decision records — `architecture/adrs/`, beside the landscape and\n * the two vocabularies.\n *\n * ADRs used to exist at two altitudes only, `services/<id>/adrs/` and\n * `features/<FEAT>/adrs/`, so a decision about the fleet itself — \"event\n * publishers use a transactional outbox\", \"cross-service calls carry a circuit\n * breaker\" — had no home but one arbitrary service's directory, where the next\n * reader of the other fifty services never finds it. This is not a new axis: it\n * is the same `ADRS_DIR` name one level up, spelled through the same constant so\n * the two altitudes cannot drift into `adrs/` and `decisions/`.\n *\n * PRESENCE-TRACKED AND NOTHING MORE, exactly like the service directory it\n * mirrors: `loam list` counts the files (`repo.ts`'s `fleetAdrCount`) and no\n * check anywhere reads one. A fleet with no fleet-level ADRs owes nothing and\n * must produce no finding — which is also why `loam init` does NOT scaffold the\n * directory. git does not carry an empty one, so it would vanish on the first\n * clone and come back as a diff on the next `init`; and an empty `adrs/` in a\n * fresh repo reads as an obligation nobody has met, when there is no obligation\n * at all.\n */\nexport function fleetAdrsDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", ADRS_DIR);\n}\n\n/**\n * The GENERATED subsystem views — beside the fleet map because the LikeC4\n * renderer merges the whole `architecture/` project, and a view can only\n * `include` elements the landscape beside it defines. `loam subsystem sync`\n * owns the bytes (`core/repo/tree/render/views.ts` renders them); nothing in loam\n * ever parses the file, and staleness against the tree is a byte compare.\n */\nexport function subsystemViewsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", SUBSYSTEM_VIEWS_FILE);\n}\n\n/** The generated views' file name, spelled once because the reservation below reads it too. */\nconst SUBSYSTEM_VIEWS_FILE = \"subsystems.likec4\";\n\n/**\n * Names inside `architecture/` that LOAM owns, and what goes wrong if a\n * feature's own document claims one.\n *\n * `features/<FEAT>/deployment/<name>.likec4` merges as a whole-file copy\n * straight into `architecture/` — unlike a flow, which lands under\n * `usecases/` — so its `<name>` is free to be any of these, and two of them\n * fail in ways nothing downstream would report:\n *\n * - `subsystems.likec4` is GENERATED and is EXCLUDED from every project load\n * (docs/DESIGN.md rule 26 keeps it a byte compare), so topology landing\n * there is invisible to loam from the moment it archives — and the next\n * `loam subsystem sync` overwrites the file wholesale. Silent both ways,\n * which is the worst shape a failure can have;\n * - `landscape.likec4` is the living map the archive SPLICES; a whole-file\n * copy over it would discard the fleet;\n * - `usecases/` is the flow tree, walked by the use-case scan. A deployment\n * document filed there parses (one project) and is then looked for by a\n * reader asking a different question.\n *\n * Returns the reason, or null when the name is the author's to choose. Takes\n * the `/`-separated rel a directory walk produced, never a string from argv.\n */\nexport function reservedArchitectureName(rel: string): string | null {\n const parts = rel.split(\"/\");\n if (parts[0] === USECASE_DIR) {\n return `\\`${USECASE_DIR}/\\` is the use-case tree — a flow belongs in \\`features/<FEAT>/usecases/\\`, which merges there`;\n }\n if (rel === SUBSYSTEM_VIEWS_FILE) {\n return \"`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives\";\n }\n if (rel === LANDSCAPE_FILE) {\n return \"`landscape.likec4` is the living fleet map, which the archive SPLICES into rather than replaces\";\n }\n return null;\n}\n\n/** The living map's file name, and the flow tree's directory — spelled once, read by the reservation. */\nconst LANDSCAPE_FILE = \"landscape.likec4\";\nconst USECASE_DIR = \"usecases\";\n\nexport function featuresDir(docsDir: DocsDir): string {\n return join(docsDir, \"features\");\n}\n\n/**\n * The root of the tree that IS the fleet — where an unfiled service and a\n * root-level subsystem both sit. The `subsystem` verbs join validated names\n * under it; the enumerating walk spells the same join for itself\n * (`repo/tree/walk.ts`), because it cannot import this module.\n */\nexport function servicesDir(docsDir: DocsDir): string {\n return join(docsDir, \"services\");\n}\n\n/**\n * The agent contract's filename, and where it sits in a docs repo.\n *\n * Both spellings are exported because the two questions are different: `init`,\n * `doctor` and `validate` want a path under a docsDir they already hold, while\n * `docs.ts` builds the scaffold as `[relative path, bytes]` pairs and needs the\n * bare name. Four modules spelled the literal out for themselves, and one of\n * them — `init` — uses it to decide whether a directory IS a docs repo at all,\n * so a rename that reached three of the four would leave `init` recognising a\n * file nothing else writes any more.\n *\n * `openspec-inventory.ts`'s \"AGENTS.md\" is deliberately NOT this constant: it\n * names a file in a foreign OpenSpec tree that happens to share the name.\n */\nexport const AGENTS_FILENAME = \"AGENTS.md\";\n\nexport function agentsPath(docsDir: DocsDir): string {\n return join(docsDir, AGENTS_FILENAME);\n}\n\n/**\n * Where shipped features live. `features/archive/` is spelled here and nowhere\n * else for the same reason every other artifact path is: `archive` and\n * `unarchive` move directories in and out of it, and a layout string that two\n * commands re-derive is a layout two commands can disagree about. The\n * `features/archive/…` strings in refusal PROSE are not this path — they are\n * what a reader types into `ls`, and they stay spelled out.\n */\nexport function archiveDir(docsDir: DocsDir): string {\n return join(featuresDir(docsDir), ARCHIVE_DIR);\n}\n"]}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/repo/paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where every artifact lives. Artifact filenames are spelled here and nowhere\n * else, so a rename is one edit rather than a grep.\n *\n * This is also the module that carries loam's one path guarantee.\n * `unfiledServicePaths(docsDir, service)` spells `<docsDir>/services/<service>/`,\n * so both parts are caller-controlled path input, and `node:path` cannot help:\n * `join(...paths: string[])` accepts every string there is. The guarantee IS\n * the parameter brands and nothing else — `PathableService` for the id (a name\n * whose provenance is the repository, unconstructible from document text),\n * `DocsDir`/`FeatureDir` for the roots (a resolved config or validated\n * `--docs`; a directory an enumeration read), and `ServiceDir` for\n * `servicePathsAt` (the enumeration's own resolved directory, narrower still)\n * — the `kernel/ids/` package holds the only casts, so an unchecked value at\n * these call sites does not compile.\n * Code that spells `services/<id>/` or a feature layout with a bare join is\n * outside the guarantee — `commands/new.ts` and the openspec migrator both do,\n * held instead by `resolveInside` at the write.\n */\nimport { join } from \"node:path\";\nimport { serviceDirOf, type DocsDir, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport type { SubsystemName } from \"../kernel/ids/subsystem.js\";\n\n/** Directory under features/ holding shipped features. Never a feature itself. */\nexport const ARCHIVE_DIR = \"archive\";\n\n/* ------------------------------------------------------------------ */\n/* Paths — artifact filenames are spelled here and nowhere else */\n/* ------------------------------------------------------------------ */\n\n/**\n * The living artifact filenames, as one table, because three questions read\n * them: `servicePathsAt` below spells the paths, the tree walk classifies\n * a directory as a service by whether any of these names sit in it, and\n * `core/diff/base-state.ts` classifies the SAME way over a base git ref's\n * tree listing (exported for that reader — a filename respelled there would\n * be the drift this table exists to prevent). The walk cannot import this\n * module — `repo → repo/tree` is the package direction, and an edge back\n * would be the cycle `scripts/package-graph.mjs` refuses — so `repo.ts`\n * injects `isServiceArtifactName` into the walk request instead.\n */\nexport const ARTIFACT_FILES = {\n model: \"model.likec4\",\n spec: \"spec.md\",\n archSpec: \"arch.spec.md\",\n openapi: \"openapi.yaml\",\n asyncapi: \"asyncapi.yaml\",\n runbook: \"runbook.md\",\n health: \"health.yaml\",\n steps: \"steps.yaml\",\n} as const;\nconst ADRS_DIR = \"adrs\";\n\n/** Does a directory entry name a living service artifact? The classification half of the table above. */\nexport function isServiceArtifactName(name: string, kind: \"file\" | \"dir\"): boolean {\n if (kind === \"dir\") return name === ADRS_DIR;\n return Object.values(ARTIFACT_FILES).some((file) => file === name);\n}\n\nexport interface ServicePaths {\n dir: ServiceDir;\n model: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract — the event axis's sibling of `openapi`. */\n asyncapi: string;\n runbook: string;\n health: string;\n /**\n * The step catalogue — which phrases this service's suite has DECIDED to\n * define. Authored and opt-in; its CONTENTS are read by `loam steps` alone and\n * nothing in `validate` consults them\n * (`core/gherkin/steps/catalogue.ts` records why).\n *\n * Its EXISTENCE is read more widely, and that is not the same statement: the\n * filename is in `ARTIFACT_FILES`, so `isServiceArtifactName` classifies it\n * and the tree walk treats a directory holding one as a service. That is\n * correct — it is a service artifact — and it is worth writing down, because\n * it means a `steps.yaml` written at the wrong path mints a service.\n */\n steps: string;\n adrsDir: string;\n}\n\n/**\n * The artifact paths of a service whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the tree walk found it. This is the\n * spelling every reader of an existing service must use: joining\n * `services/<id>/` at the root is only true for a fleet nobody has filed, and\n * a moved service would silently grade as absent through it.\n */\nexport function servicePathsAt(dir: ServiceDir): ServicePaths {\n return {\n dir,\n model: join(dir, ARTIFACT_FILES.model),\n spec: join(dir, ARTIFACT_FILES.spec),\n archSpec: join(dir, ARTIFACT_FILES.archSpec),\n openapi: join(dir, ARTIFACT_FILES.openapi),\n asyncapi: join(dir, ARTIFACT_FILES.asyncapi),\n runbook: join(dir, ARTIFACT_FILES.runbook),\n health: join(dir, ARTIFACT_FILES.health),\n steps: join(dir, ARTIFACT_FILES.steps),\n adrsDir: join(dir, ADRS_DIR),\n };\n}\n\n/**\n * The UNFILED spelling — `<docsDir>/services/<id>/`, the tree's root level.\n * This is where creation lands (`adopt`'s brief, a new service materialised by\n * `archive`, the OpenSpec migration) and the honest fallback for a service the\n * enumeration does not answer; for a service that EXISTS, resolve through the\n * enumeration instead (`servicePathsAt(entry.dir)`, or\n * `locateServicePaths` in `service-target.ts`, which chooses between the two).\n * Renamed from `servicePaths` when the tree landed, precisely so the compiler\n * would put every remaining caller in front of a reviewer: a root join that\n * survives unreviewed is a moved service silently grading as absent.\n */\nexport function unfiledServicePaths(docsDir: DocsDir, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(docsDir, \"services\", service)));\n}\n\n/**\n * The creation spelling INSIDE a subsystem — `<subsystemDir>/<id>/`, the\n * target `adopt --subsystem` briefs so an adoption need not land unfiled and\n * cost a second command. The same two provenances the unfiled spelling\n * combines, one level down: the subsystem directory came off the tree walk's\n * readdir, and the id passed the service grammar before it reached here.\n */\nexport function servicePathsUnder(subsystemDir: string, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(subsystemDir, service)));\n}\n\n/**\n * The directory a SUBSYSTEM occupies (or will occupy) under a parent — the\n * spelling `subsystem new` creates and `subsystem rename` renames into. The\n * same guarantee-by-brand as the service spellings above: the parent came off\n * the tree walk's readdir (or is `servicesDir` itself), and demanding\n * `SubsystemName` here is what makes the validator unskippable — a raw argv\n * string at this call site does not compile, so no future verb can hoist the\n * join above the check that earns it.\n */\nexport function subsystemPathUnder(parentDir: string, name: SubsystemName): string {\n return join(parentDir, name);\n}\n\n/**\n * The pair of requirement-carrying spec files, living and delta alike: the\n * business spec and the architecture spec. Same grammar, same delta algebra,\n * same merge — everything that walks \"the spec files\" of a service walks this\n * list, so the two axes cannot drift apart in what handles them. `key` indexes\n * ServicePaths/FeatureSpecPaths; `label` is how the axis names itself in prose.\n */\nexport const SPEC_AXES = [\n { key: \"spec\", file: \"spec.md\", label: \"requirements\" },\n { key: \"archSpec\", file: \"arch.spec.md\", label: \"arch requirements\" },\n] as const;\nexport type SpecAxis = (typeof SPEC_AXES)[number];\n\nexport interface FeaturePaths {\n dir: string;\n intent: string;\n delta: string;\n specsDir: string;\n adrsDir: string;\n}\n\nexport function featurePaths(featureDir: FeatureDir): FeaturePaths {\n return {\n dir: featureDir,\n intent: join(featureDir, \"intent.md\"),\n delta: join(featureDir, \"delta.likec4\"),\n specsDir: join(featureDir, \"specs\"),\n adrsDir: join(featureDir, \"adrs\"),\n };\n}\n\nexport interface FeatureSpecPaths {\n dir: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract delta — the event axis's sibling of `openapi`. */\n asyncapi: string;\n}\n\nexport function featureSpecPaths(featureDir: FeatureDir, service: PathableService): FeatureSpecPaths {\n const dir = join(featureDir, \"specs\", service);\n return {\n dir,\n spec: join(dir, \"spec.md\"),\n archSpec: join(dir, \"arch.spec.md\"),\n openapi: join(dir, \"openapi.yaml\"),\n asyncapi: join(dir, \"asyncapi.yaml\"),\n };\n}\n\nexport function landscapePath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", LANDSCAPE_FILE);\n}\n\n/** The fleet's authorization vocabulary — beside the fleet map, for the same reason. */\nexport function permissionsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"permissions.yaml\");\n}\n\n/** The fleet's capability vocabulary — beside the other two fleet documents, for the same reason. */\nexport function capabilitiesPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"capabilities.yaml\");\n}\n\n/**\n * The fleet's ARCHITECTURAL OBLIGATION vocabulary — beside the landscape it tags\n * and the two vocabularies it sits with, for their reason: it is a fact about\n * the fleet that no single service owns.\n */\nexport function obligationsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"obligations.yaml\");\n}\n\n/**\n * The FLEET's decision records — `architecture/adrs/`, beside the landscape and\n * the two vocabularies.\n *\n * ADRs used to exist at two altitudes only, `services/<id>/adrs/` and\n * `features/<FEAT>/adrs/`, so a decision about the fleet itself — \"event\n * publishers use a transactional outbox\", \"cross-service calls carry a circuit\n * breaker\" — had no home but one arbitrary service's directory, where the next\n * reader of the other fifty services never finds it. This is not a new axis: it\n * is the same `ADRS_DIR` name one level up, spelled through the same constant so\n * the two altitudes cannot drift into `adrs/` and `decisions/`.\n *\n * PRESENCE-TRACKED AND NOTHING MORE, exactly like the service directory it\n * mirrors: `loam list` counts the files (`repo.ts`'s `fleetAdrCount`) and no\n * check anywhere reads one. A fleet with no fleet-level ADRs owes nothing and\n * must produce no finding — which is also why `loam init` does NOT scaffold the\n * directory. git does not carry an empty one, so it would vanish on the first\n * clone and come back as a diff on the next `init`; and an empty `adrs/` in a\n * fresh repo reads as an obligation nobody has met, when there is no obligation\n * at all.\n */\nexport function fleetAdrsDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", ADRS_DIR);\n}\n\n/**\n * The GENERATED subsystem views — beside the fleet map because the LikeC4\n * renderer merges the whole `architecture/` project, and a view can only\n * `include` elements the landscape beside it defines. `loam subsystem sync`\n * owns the bytes (`core/repo/tree/render/views.ts` renders them); nothing in loam\n * ever parses the file, and staleness against the tree is a byte compare.\n */\nexport function subsystemViewsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", SUBSYSTEM_VIEWS_FILE);\n}\n\n/** The generated views' file name, spelled once because the reservation below reads it too. */\nconst SUBSYSTEM_VIEWS_FILE = \"subsystems.likec4\";\n\n/**\n * Names inside `architecture/` that LOAM owns, and what goes wrong if a\n * feature's own document claims one.\n *\n * `features/<FEAT>/deployment/<name>.likec4` merges as a whole-file copy\n * straight into `architecture/` — unlike a flow, which lands under\n * `usecases/` — so its `<name>` is free to be any of these, and two of them\n * fail in ways nothing downstream would report:\n *\n * - `subsystems.likec4` is GENERATED and is EXCLUDED from every project load\n * (docs/DESIGN.md rule 26 keeps it a byte compare), so topology landing\n * there is invisible to loam from the moment it archives — and the next\n * `loam subsystem sync` overwrites the file wholesale. Silent both ways,\n * which is the worst shape a failure can have;\n * - `landscape.likec4` is the living map the archive SPLICES; a whole-file\n * copy over it would discard the fleet;\n * - `usecases/` is the flow tree, walked by the use-case scan. A deployment\n * document filed there parses (one project) and is then looked for by a\n * reader asking a different question.\n *\n * Returns the reason, or null when the name is the author's to choose. Takes\n * the `/`-separated rel a directory walk produced, never a string from argv.\n */\nexport function reservedArchitectureName(rel: string): string | null {\n const parts = rel.split(\"/\");\n if (parts[0] === USECASE_DIR) {\n return `\\`${USECASE_DIR}/\\` is the use-case tree — a flow belongs in \\`features/<FEAT>/usecases/\\`, which merges there`;\n }\n if (rel === SUBSYSTEM_VIEWS_FILE) {\n return \"`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives\";\n }\n if (rel === LANDSCAPE_FILE) {\n return \"`landscape.likec4` is the living fleet map, which the archive SPLICES into rather than replaces\";\n }\n return null;\n}\n\n/** The living map's file name, and the flow tree's directory — spelled once, read by the reservation. */\nconst LANDSCAPE_FILE = \"landscape.likec4\";\nconst USECASE_DIR = \"usecases\";\n\nexport function featuresDir(docsDir: DocsDir): string {\n return join(docsDir, \"features\");\n}\n\n/**\n * The root of the tree that IS the fleet — where an unfiled service and a\n * root-level subsystem both sit. The `subsystem` verbs join validated names\n * under it; the enumerating walk spells the same join for itself\n * (`repo/tree/walk.ts`), because it cannot import this module.\n */\nexport function servicesDir(docsDir: DocsDir): string {\n return join(docsDir, \"services\");\n}\n\n/**\n * The agent contract's filename, and where it sits in a docs repo.\n *\n * Both spellings are exported because the two questions are different: `init`,\n * `doctor` and `validate` want a path under a docsDir they already hold, while\n * `docs.ts` builds the scaffold as `[relative path, bytes]` pairs and needs the\n * bare name. Four modules spelled the literal out for themselves, and one of\n * them — `init` — uses it to decide whether a directory IS a docs repo at all,\n * so a rename that reached three of the four would leave `init` recognising a\n * file nothing else writes any more.\n *\n * `openspec-inventory.ts`'s \"AGENTS.md\" is deliberately NOT this constant: it\n * names a file in a foreign OpenSpec tree that happens to share the name.\n */\nexport const AGENTS_FILENAME = \"AGENTS.md\";\n\nexport function agentsPath(docsDir: DocsDir): string {\n return join(docsDir, AGENTS_FILENAME);\n}\n\n/**\n * Where shipped features live. `features/archive/` is spelled here and nowhere\n * else for the same reason every other artifact path is: `archive` and\n * `unarchive` move directories in and out of it, and a layout string that two\n * commands re-derive is a layout two commands can disagree about. The\n * `features/archive/…` strings in refusal PROSE are not this path — they are\n * what a reader types into `ls`, and they stay spelled out.\n */\nexport function archiveDir(docsDir: DocsDir): string {\n return join(featuresDir(docsDir), ARCHIVE_DIR);\n}\n\n/**\n * The LikeC4 project file — the one thing in a docs repo loam writes for a\n * tool other than itself — and it lives at two altitudes.\n *\n * At the ROOT, `loam init --create` writes the project `fleet` (`core/docs.ts`\n * composes those bytes), which holds the fleet map and every service model that\n * EXTENDS it. Beside a model that STANDS ALONE — one declaring its own\n * `specification` block, which can only be parsed on its own — `loam subsystem\n * sync` writes a project of the service's own, and adds that directory to the\n * root's `exclude`; without both, such a model is either a duplicate of the map\n * or a box on it with nothing renderable inside.\n * `core/repo/tree/render/projects.ts` composes the per-service bytes and records\n * what was measured; `core/c4/root-project/exclude.ts` owns reading the root's\n * `exclude` list and deciding what an entry covers; `core/c4/service-model/renderer.ts`\n * owns the list `loam subsystem sync` writes back.\n *\n * Spelled here and NOT in `ARTIFACT_FILES`, and the distinction is\n * load-bearing: a filename in that table classifies a directory as a service,\n * and this file is the renderer's — a stray one must mint nothing. The root's\n * NAME sits beside the filename because the per-service rule needs it too (a\n * service id equal to the root project's name would be silently renamed by\n * the renderer), and two spellings of the word are how the scaffold and the\n * rule drift apart.\n */\nexport const LIKEC4_PROJECT_FILENAME = \"likec4.config.json\";\n\n/** The root project's name, exactly as `loam init --create` scaffolds it. */\nexport const LIKEC4_ROOT_PROJECT = \"fleet\";\n\nexport function rootProjectPath(docsDir: DocsDir): string {\n return join(docsDir, LIKEC4_PROJECT_FILENAME);\n}\n\n/**\n * The two files the per-service project question is asked of — the model that\n * needs rendering and the project file that renders it — in one spelling for\n * the writer (`subsystem sync`) and the grader (`validate --all`) alike. Handed\n * INTO `render/projects.ts` rather than imported there, because that package\n * cannot import this module: `render/stale.ts` records the cycle.\n */\nexport function serviceRenderPaths(dir: ServiceDir): { model: string; project: string } {\n return { model: join(dir, ARTIFACT_FILES.model), project: join(dir, LIKEC4_PROJECT_FILENAME) };\n}\n"]}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The renderer's project file for ONE service — `services/<…>/<id>/likec4.config.json`
3
+ * — the second thing under `services/` that exists only to be rendered.
4
+ *
5
+ * Why it exists, and WHICH MODELS IT IS FOR. A model that STANDS ALONE declares
6
+ * its own `specification` block, so it is parsed alone and the root project must
7
+ * exclude its directory — a renderer merging it into the map reports every kind
8
+ * and element it declares as a duplicate, blamed on both files. The cost of that
9
+ * exclusion went unstated for a release: an adopted service's model — the
10
+ * artifact `loam adopt` spends the most effort on — was a box on the fleet map
11
+ * with nothing renderable inside it from the docs root, and the documented way
12
+ * to see it was one renderer invocation per service directory. Measured at the
13
+ * 1.59.2 pin: a `likec4.config.json` INSIDE a service directory is registered as
14
+ * a project of its own even though the root excludes the directory (the root's
15
+ * `exclude` does not hide a nested project file), the workspace then loads
16
+ * `fleet` plus one project per service with zero errors, and each service
17
+ * project carries LikeC4's own `index` view of the containers beside whatever
18
+ * view the model authors. One two-key file per such directory is therefore what
19
+ * makes that model renderable from the same root, in the same renderer, beside
20
+ * the map.
21
+ *
22
+ * A model that EXTENDS the map is owed NO file, and that is the same
23
+ * measurement read the other way: it declares no kinds, it lives in the ROOT
24
+ * project beside the map, and a nested project file beside it TAKES it out of
25
+ * there — measured at the 1.59.2 pin, the workspace loads two projects and, where
26
+ * the nested one claims the model, it holds it alone (the map's kinds are not in
27
+ * there with it, so it does not resolve) and `export json --project fleet` loses
28
+ * the service's containers. So the survey below takes the shape as a predicate, the writer
29
+ * creates a file only for the standalone half and removes one beside the
30
+ * extending half, and `service.likec4-config-stray` grades what is left.
31
+ *
32
+ * NEVER COMPARED, NEVER REWRITTEN — the root config's ownership rule
33
+ * (`core/docs.ts`: written once, never re-read, the team owns it), NOT the
34
+ * generated views file's. The bytes are a function of the service id alone,
35
+ * which SCHEMA.md defines as the identity no verb ever changes, so nothing can
36
+ * make the file stale; a compare would only fight the per-project keys LikeC4
37
+ * lets a team add (`title`, styles, a contact), and would restale every service
38
+ * on any change to this rule. Presence is the one question loam owes the file,
39
+ * and presence needs no parse: rule 26 stands, and loam still reads none of it.
40
+ *
41
+ * REMOVED IN EXACTLY ONE STATE: beside a model that EXTENDS the map. That is a
42
+ * correction, and it is measured rather than reasoned. The design note said a
43
+ * nested project beside an extending model was registered EMPTY and therefore
44
+ * harmless; on likec4 1.59.2 two agents measured the opposite (verification
45
+ * 2026-09-04, W1) — the nested project CLAIMS the model, the workspace loads
46
+ * "2 projects", the model is Invalid inside its own one (`Could not resolve
47
+ * reference to Element named 'svc_svc_a'`, because the map is not in there with
48
+ * it), and `export json --project fleet` drops the service's containers. So the
49
+ * file is not a leftover, it is a working renderer taken away from the fleet,
50
+ * and `loam subsystem sync` deletes it — which is what SCHEMA, the message and
51
+ * `/loam-adopt` step 5 had been promising all along.
52
+ *
53
+ * THE EXPORT LOSS IS THE WORSE HALF, NOT THE CERTAIN ONE, and the finding's
54
+ * message says so since it was re-measured (re-verification 2026-09-04, area C
55
+ * item 7). Same pin, two fleets: on the seeded fleet above the containers went
56
+ * and came back when the file was deleted; on `examples/docs` a stray beside the
57
+ * extending order-service left `export json --project fleet` byte-identical —
58
+ * 33 elements, every `marketplace.orderService.*` child present — because there
59
+ * the ROOT project kept the model. What is true in every state measured is the
60
+ * second project itself: the docs root stops being a one-project workspace, and
61
+ * `likec4 validate .` goes from ✓ Valid to ✗ Invalid ("Specify exact project,
62
+ * known: [order-service, fleet]"). A message that asserted the export loss
63
+ * unconditionally was refutable on the repository's own example fleet.
64
+ *
65
+ * A leftover beside a model that is GONE stays. That one really is the team's:
66
+ * a nested project holding no `.likec4` file is silently absent from the
67
+ * renderer's project list (measured), so it costs nothing, and a survey that
68
+ * deleted files in directories it found no model in would be a writer acting on
69
+ * an absence.
70
+ *
71
+ * THE NAME. LikeC4's project-name grammar (measured at the pin) refuses an
72
+ * empty name, the word `default`, and the bytes `.`, `@` and `#`; loam's
73
+ * service-id grammar admits `.`. So every `.` becomes `-`, and a name that
74
+ * would then equal the ROOT project's (`fleet` — a nested project of that name
75
+ * is silently registered as `fleet-1`) or LikeC4's reserved `default` is
76
+ * prefixed `service-`. Readable and typeable after `--project`, and NOT
77
+ * injective: an id spelled a.b beside a service literally named a-b, or
78
+ * `service-fleet` beside `fleet`, share a name and the renderer suffixes the
79
+ * later one `-1`. That is disclosed rather than escaped away, because the
80
+ * picker shows the `title` (measured: `title ?? id`), which is ALWAYS the id
81
+ * verbatim, and a collision costs one suffixed label rather than the blanked
82
+ * workspace that made `subsystemViewId` injective. The root's name arrives as
83
+ * a parameter, like every spelling this package needs from `repo/`: `render/`
84
+ * cannot import `repo/paths.ts` (`stale.ts` records the cycle), and a second
85
+ * copy of the word `fleet` here would be the drift the injection prevents.
86
+ *
87
+ * A DIRECTORY THAT IS NOT A LEGAL SERVICE ID IS OWED NO FILE. The tree walk
88
+ * enumerates every directory on purpose — the fleet is never reported smaller
89
+ * than it is — and `service.id-invalid` names the illegal ones. Folding only
90
+ * `.` is right for every LEGAL id, whose remaining bytes LikeC4 accepts; a
91
+ * name like `pay@1` would produce a project file the renderer silently drops
92
+ * (measured: zero errors, the project simply absent from the list), and
93
+ * because the file is create-only the grade below would then be silent
94
+ * forever while the model stayed unrendered — the exact state this file
95
+ * exists to remove, with a label saying it was fixed. So the survey skips
96
+ * such a service: the writer creates nothing, the grader says nothing, and
97
+ * `service.id-invalid`'s own repair — the rename — is what earns the file on
98
+ * the first sync after it.
99
+ *
100
+ * NO MODEL, NO FILE: a service directory without `model.likec4` — one an
101
+ * archive materialised, a spec-only baseline — is owed nothing, and gets its
102
+ * file on the first `loam subsystem sync` after the model lands. A feature's
103
+ * `delta.likec4` is deliberately out of scope: the directory is transient and
104
+ * `loam archive` moves it where a project file would still be registered, so
105
+ * every shipped feature would keep a dead project in the list forever.
106
+ */
107
+ import { existsSync } from "node:fs";
108
+ import { dirNameHazard } from "../../../kernel/ids/service.js";
109
+ /** LikeC4's own reserved project name; a file naming it is refused outright. */
110
+ const LIKEC4_RESERVED_PROJECT = "default";
111
+ /**
112
+ * The project name for a service id — the id itself for every id made of
113
+ * letters, digits, `_` and `-`, which is nearly all of them.
114
+ */
115
+ export function serviceProjectName(id, rootProject) {
116
+ const folded = id.replace(/\./g, "-");
117
+ return folded === rootProject || folded === LIKEC4_RESERVED_PROJECT ? `service-${folded}` : folded;
118
+ }
119
+ /**
120
+ * The exact bytes of a service's project file: two keys, two-space JSON, LF,
121
+ * one trailing newline — the root file's own serialisation. `title` is the id
122
+ * verbatim, always, so whatever the renderer labels a project with is the
123
+ * service's real identity even where `name` had to bend.
124
+ */
125
+ export function renderServiceProject(id, rootProject) {
126
+ return `${JSON.stringify({ name: serviceProjectName(id, rootProject), title: id }, null, 2)}\n`;
127
+ }
128
+ /**
129
+ * ONE predicate for the writer (`subsystem sync`) and the grader
130
+ * (`validate --all`), on `viewsState`'s precedent — two private spellings of
131
+ * "is this model a project" would be two chances to answer differently about
132
+ * one file, and a grader naming a gap the writer would not fill is a loop no
133
+ * command can clear. Both the gaps AND the count live here for the same
134
+ * reason: a caller re-deriving "has a model" beside this function is the
135
+ * second spelling in another coat.
136
+ *
137
+ * Structural over `{ id, dir }` so a walked service and a full entry both fit,
138
+ * and the two file spellings arrive injected (`repo/paths.ts`'s
139
+ * `serviceRenderPaths`) for the package reason the banner gives. Sorted by id,
140
+ * plain lexicographic like every list this package emits, so the writer's
141
+ * report and the grader's findings come out in one stable order.
142
+ */
143
+ export function surveyProjects(services, pathsOf, isStandalone) {
144
+ const models = [];
145
+ for (const service of services) {
146
+ // The banner's rule: a name the renderer would refuse earns no file, and
147
+ // `service.id-invalid` — not this survey — is what names the repair.
148
+ if (dirNameHazard(service.id) !== null)
149
+ continue;
150
+ const paths = pathsOf(service.dir);
151
+ if (!existsSync(paths.model))
152
+ continue;
153
+ models.push({
154
+ service,
155
+ path: paths.project,
156
+ // INJECTED rather than read here, and the reason is this package's
157
+ // standing one: `render/` may not import `c4/` (see `./stale.ts` on the
158
+ // cycle), and the shape is a fact about the model's GRAMMAR that
159
+ // `c4/service-model/shape.ts` owns. A predicate crossing the boundary
160
+ // keeps one scanner for the writer and the grader alike.
161
+ standalone: isStandalone(paths.model),
162
+ configured: existsSync(paths.project),
163
+ });
164
+ }
165
+ models.sort((a, b) => (a.service.id < b.service.id ? -1 : a.service.id > b.service.id ? 1 : 0));
166
+ return {
167
+ // A model that EXTENDS the map is owed no project file: it belongs to the
168
+ // root project, and a nested project file beside it CLAIMS it — the model
169
+ // then parses alone, where the map's kinds are not, and the fleet project
170
+ // loses the service's interior. So the gap set narrowed to the standalone
171
+ // shape, and `strays` below is the grade's and the writer's other direction.
172
+ gaps: models.filter((m) => m.standalone && !m.configured).map(({ service, path }) => ({ service, path })),
173
+ // The other direction, and the one the banner's correction is about: the
174
+ // nested project claims the model and the fleet project loses the service's
175
+ // interior, so the file is removed rather than tolerated.
176
+ strays: models.filter((m) => !m.standalone && m.configured).map(({ service, path }) => ({ service, path })),
177
+ models,
178
+ modelled: models.length,
179
+ standalone: models.filter((m) => m.standalone).length,
180
+ };
181
+ }
182
+ //# sourceMappingURL=projects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/projects.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The renderer's project file for ONE service — `services/<…>/<id>/likec4.config.json`\n * — the second thing under `services/` that exists only to be rendered.\n *\n * Why it exists, and WHICH MODELS IT IS FOR. A model that STANDS ALONE declares\n * its own `specification` block, so it is parsed alone and the root project must\n * exclude its directory — a renderer merging it into the map reports every kind\n * and element it declares as a duplicate, blamed on both files. The cost of that\n * exclusion went unstated for a release: an adopted service's model — the\n * artifact `loam adopt` spends the most effort on — was a box on the fleet map\n * with nothing renderable inside it from the docs root, and the documented way\n * to see it was one renderer invocation per service directory. Measured at the\n * 1.59.2 pin: a `likec4.config.json` INSIDE a service directory is registered as\n * a project of its own even though the root excludes the directory (the root's\n * `exclude` does not hide a nested project file), the workspace then loads\n * `fleet` plus one project per service with zero errors, and each service\n * project carries LikeC4's own `index` view of the containers beside whatever\n * view the model authors. One two-key file per such directory is therefore what\n * makes that model renderable from the same root, in the same renderer, beside\n * the map.\n *\n * A model that EXTENDS the map is owed NO file, and that is the same\n * measurement read the other way: it declares no kinds, it lives in the ROOT\n * project beside the map, and a nested project file beside it TAKES it out of\n * there — measured at the 1.59.2 pin, the workspace loads two projects and, where\n * the nested one claims the model, it holds it alone (the map's kinds are not in\n * there with it, so it does not resolve) and `export json --project fleet` loses\n * the service's containers. So the survey below takes the shape as a predicate, the writer\n * creates a file only for the standalone half and removes one beside the\n * extending half, and `service.likec4-config-stray` grades what is left.\n *\n * NEVER COMPARED, NEVER REWRITTEN — the root config's ownership rule\n * (`core/docs.ts`: written once, never re-read, the team owns it), NOT the\n * generated views file's. The bytes are a function of the service id alone,\n * which SCHEMA.md defines as the identity no verb ever changes, so nothing can\n * make the file stale; a compare would only fight the per-project keys LikeC4\n * lets a team add (`title`, styles, a contact), and would restale every service\n * on any change to this rule. Presence is the one question loam owes the file,\n * and presence needs no parse: rule 26 stands, and loam still reads none of it.\n *\n * REMOVED IN EXACTLY ONE STATE: beside a model that EXTENDS the map. That is a\n * correction, and it is measured rather than reasoned. The design note said a\n * nested project beside an extending model was registered EMPTY and therefore\n * harmless; on likec4 1.59.2 two agents measured the opposite (verification\n * 2026-09-04, W1) — the nested project CLAIMS the model, the workspace loads\n * \"2 projects\", the model is Invalid inside its own one (`Could not resolve\n * reference to Element named 'svc_svc_a'`, because the map is not in there with\n * it), and `export json --project fleet` drops the service's containers. So the\n * file is not a leftover, it is a working renderer taken away from the fleet,\n * and `loam subsystem sync` deletes it — which is what SCHEMA, the message and\n * `/loam-adopt` step 5 had been promising all along.\n *\n * THE EXPORT LOSS IS THE WORSE HALF, NOT THE CERTAIN ONE, and the finding's\n * message says so since it was re-measured (re-verification 2026-09-04, area C\n * item 7). Same pin, two fleets: on the seeded fleet above the containers went\n * and came back when the file was deleted; on `examples/docs` a stray beside the\n * extending order-service left `export json --project fleet` byte-identical —\n * 33 elements, every `marketplace.orderService.*` child present — because there\n * the ROOT project kept the model. What is true in every state measured is the\n * second project itself: the docs root stops being a one-project workspace, and\n * `likec4 validate .` goes from ✓ Valid to ✗ Invalid (\"Specify exact project,\n * known: [order-service, fleet]\"). A message that asserted the export loss\n * unconditionally was refutable on the repository's own example fleet.\n *\n * A leftover beside a model that is GONE stays. That one really is the team's:\n * a nested project holding no `.likec4` file is silently absent from the\n * renderer's project list (measured), so it costs nothing, and a survey that\n * deleted files in directories it found no model in would be a writer acting on\n * an absence.\n *\n * THE NAME. LikeC4's project-name grammar (measured at the pin) refuses an\n * empty name, the word `default`, and the bytes `.`, `@` and `#`; loam's\n * service-id grammar admits `.`. So every `.` becomes `-`, and a name that\n * would then equal the ROOT project's (`fleet` — a nested project of that name\n * is silently registered as `fleet-1`) or LikeC4's reserved `default` is\n * prefixed `service-`. Readable and typeable after `--project`, and NOT\n * injective: an id spelled a.b beside a service literally named a-b, or\n * `service-fleet` beside `fleet`, share a name and the renderer suffixes the\n * later one `-1`. That is disclosed rather than escaped away, because the\n * picker shows the `title` (measured: `title ?? id`), which is ALWAYS the id\n * verbatim, and a collision costs one suffixed label rather than the blanked\n * workspace that made `subsystemViewId` injective. The root's name arrives as\n * a parameter, like every spelling this package needs from `repo/`: `render/`\n * cannot import `repo/paths.ts` (`stale.ts` records the cycle), and a second\n * copy of the word `fleet` here would be the drift the injection prevents.\n *\n * A DIRECTORY THAT IS NOT A LEGAL SERVICE ID IS OWED NO FILE. The tree walk\n * enumerates every directory on purpose — the fleet is never reported smaller\n * than it is — and `service.id-invalid` names the illegal ones. Folding only\n * `.` is right for every LEGAL id, whose remaining bytes LikeC4 accepts; a\n * name like `pay@1` would produce a project file the renderer silently drops\n * (measured: zero errors, the project simply absent from the list), and\n * because the file is create-only the grade below would then be silent\n * forever while the model stayed unrendered — the exact state this file\n * exists to remove, with a label saying it was fixed. So the survey skips\n * such a service: the writer creates nothing, the grader says nothing, and\n * `service.id-invalid`'s own repair — the rename — is what earns the file on\n * the first sync after it.\n *\n * NO MODEL, NO FILE: a service directory without `model.likec4` — one an\n * archive materialised, a spec-only baseline — is owed nothing, and gets its\n * file on the first `loam subsystem sync` after the model lands. A feature's\n * `delta.likec4` is deliberately out of scope: the directory is transient and\n * `loam archive` moves it where a project file would still be registered, so\n * every shipped feature would keep a dead project in the list forever.\n */\nimport { existsSync } from \"node:fs\";\nimport type { ServiceDir } from \"../../../kernel/ids/dirs.js\";\nimport { dirNameHazard } from \"../../../kernel/ids/service.js\";\n\n/** LikeC4's own reserved project name; a file naming it is refused outright. */\nconst LIKEC4_RESERVED_PROJECT = \"default\";\n\n/**\n * The project name for a service id — the id itself for every id made of\n * letters, digits, `_` and `-`, which is nearly all of them.\n */\nexport function serviceProjectName(id: string, rootProject: string): string {\n const folded = id.replace(/\\./g, \"-\");\n return folded === rootProject || folded === LIKEC4_RESERVED_PROJECT ? `service-${folded}` : folded;\n}\n\n/**\n * The exact bytes of a service's project file: two keys, two-space JSON, LF,\n * one trailing newline — the root file's own serialisation. `title` is the id\n * verbatim, always, so whatever the renderer labels a project with is the\n * service's real identity even where `name` had to bend.\n */\nexport function renderServiceProject(id: string, rootProject: string): string {\n return `${JSON.stringify({ name: serviceProjectName(id, rootProject), title: id }, null, 2)}\\n`;\n}\n\n/**\n * A service whose model has no project file beside it, and where the file\n * goes. The SERVICE travels whole — a walked service or a full entry,\n * whichever the caller surveyed — rather than its id: the grader spells the\n * finding's path from the entry's own placement, and a join back by id would\n * name the wrong directory the moment two services share a leaf name under\n * different subsystems (`subsystem.name-collision` — a broken tree the walk\n * still enumerates in full, and one this grade is emitted on by design).\n */\nexport interface ProjectGap<T> {\n service: T;\n /** Absolute path of the file `loam subsystem sync` will create. */\n path: string;\n}\n\n/**\n * One surveyed model: everything the writer and the four renderer grades ask\n * about a single `model.likec4`, answered in one walk.\n *\n * The SHAPE is on here rather than re-derived per grade because it decides\n * opposite things for the two of them: a model that stands alone is owed a\n * project file and must be excluded from the root project, and a model that\n * extends the map is owed neither and must NOT be excluded. Two readers of the\n * same bytes would be two chances to answer that backwards, and backwards is\n * not a warning — it is a renderer that shows nothing.\n */\nexport interface SurveyedModel<T> {\n service: T;\n /** Absolute path of the per-service project file, whether or not it exists. */\n path: string;\n /** True when the model declares its own element kinds — the caller's byte scan, injected. */\n standalone: boolean;\n /** True when a `likec4.config.json` already sits beside the model. */\n configured: boolean;\n}\n\n/** What one survey of the tree found: the gaps, the models behind them, and the domain they were counted against. */\nexport interface ProjectSurvey<T> {\n /** Every service owed a file and lacking one, sorted by id. */\n gaps: ProjectGap<T>[];\n /**\n * Every service owed NO file that has one anyway — an extending model with a\n * `likec4.config.json` beside it, sorted by id. The mirror of `gaps`, and the\n * same shape, because the writer does the same thing with it: `subsystem sync`\n * plans a delete where it plans a create, and `service.likec4-config-stray`\n * grades what is left. One predicate for both, so the grader can never name a\n * stray the writer would decline to remove.\n */\n strays: ProjectGap<T>[];\n /** Every model the survey considered, sorted by id — the four renderer grades walk this. */\n models: SurveyedModel<T>[];\n /**\n * Every service the survey considered at all — a model on disk, a legal id\n * — file or no file, whichever shape the model has. It is what the\n * root-missing note is owed to: without the root project file NOTHING under\n * `services/` is written, and the count a person needs there is how many\n * models the run walked past.\n */\n modelled: number;\n /**\n * The subset of those whose model STANDS ALONE — the only services owed a\n * project file at all, and therefore the domain `gaps` was drawn from.\n *\n * Apart from `modelled` because the two stopped meaning the same thing when a\n * model could extend the map, and one subtraction rested on their being\n * equal: `modelled - gaps.length` was \"already had a file\", and it counted\n * every extending model as one — a fleet that had just migrated reported N\n * services holding a file that must not exist. `standalone - gaps.length` is\n * the same subtraction over the domain that is actually owed the file.\n */\n standalone: number;\n}\n\n/**\n * ONE predicate for the writer (`subsystem sync`) and the grader\n * (`validate --all`), on `viewsState`'s precedent — two private spellings of\n * \"is this model a project\" would be two chances to answer differently about\n * one file, and a grader naming a gap the writer would not fill is a loop no\n * command can clear. Both the gaps AND the count live here for the same\n * reason: a caller re-deriving \"has a model\" beside this function is the\n * second spelling in another coat.\n *\n * Structural over `{ id, dir }` so a walked service and a full entry both fit,\n * and the two file spellings arrive injected (`repo/paths.ts`'s\n * `serviceRenderPaths`) for the package reason the banner gives. Sorted by id,\n * plain lexicographic like every list this package emits, so the writer's\n * report and the grader's findings come out in one stable order.\n */\nexport function surveyProjects<T extends { id: string; dir: ServiceDir }>(\n services: readonly T[],\n pathsOf: (dir: ServiceDir) => { model: string; project: string },\n isStandalone: (modelPath: string) => boolean,\n): ProjectSurvey<T> {\n const models: SurveyedModel<T>[] = [];\n for (const service of services) {\n // The banner's rule: a name the renderer would refuse earns no file, and\n // `service.id-invalid` — not this survey — is what names the repair.\n if (dirNameHazard(service.id) !== null) continue;\n const paths = pathsOf(service.dir);\n if (!existsSync(paths.model)) continue;\n models.push({\n service,\n path: paths.project,\n // INJECTED rather than read here, and the reason is this package's\n // standing one: `render/` may not import `c4/` (see `./stale.ts` on the\n // cycle), and the shape is a fact about the model's GRAMMAR that\n // `c4/service-model/shape.ts` owns. A predicate crossing the boundary\n // keeps one scanner for the writer and the grader alike.\n standalone: isStandalone(paths.model),\n configured: existsSync(paths.project),\n });\n }\n models.sort((a, b) => (a.service.id < b.service.id ? -1 : a.service.id > b.service.id ? 1 : 0));\n return {\n // A model that EXTENDS the map is owed no project file: it belongs to the\n // root project, and a nested project file beside it CLAIMS it — the model\n // then parses alone, where the map's kinds are not, and the fleet project\n // loses the service's interior. So the gap set narrowed to the standalone\n // shape, and `strays` below is the grade's and the writer's other direction.\n gaps: models.filter((m) => m.standalone && !m.configured).map(({ service, path }) => ({ service, path })),\n // The other direction, and the one the banner's correction is about: the\n // nested project claims the model and the fleet project loses the service's\n // interior, so the file is removed rather than tolerated.\n strays: models.filter((m) => !m.standalone && m.configured).map(({ service, path }) => ({ service, path })),\n models,\n modelled: models.length,\n standalone: models.filter((m) => m.standalone).length,\n };\n}\n"]}
@@ -16,10 +16,9 @@
16
16
  */
17
17
  import { existsSync } from "node:fs";
18
18
  import { readFile } from "node:fs/promises";
19
- import {} from "../../../c4/likec4.js";
20
19
  import { renderSubsystemViews, viewsAgree } from "./views.js";
21
- export async function viewsState(path, tree, elements) {
22
- const expected = renderSubsystemViews(tree, elements);
20
+ export async function viewsState(path, tree, map) {
21
+ const expected = renderSubsystemViews(tree, map);
23
22
  const actual = existsSync(path) ? await readFile(path, "utf8") : null;
24
23
  return { actual, expected, agrees: viewsAgree(actual, expected) };
25
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/stale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAa,MAAM,uBAAuB,CAAC;AAElD,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgB9D,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY,EAAE,IAAe,EAAE,QAAgB;IAC9E,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["/**\n * Is the generated file on disk what this tree and this map render to?\n *\n * One function, because the question now has three askers — `validate --all`\n * (which turns a disagreement into `subsystem.views-stale`), `loam subsystem\n * sync` and the write verbs (which repair it), and `loam archive` (which must\n * not report a repo current when it has just made the file stale). Three\n * private compares would be three chances to answer differently about one\n * file, and the module that mints the bytes is the one that owes the answer.\n *\n * The PATH arrives as a parameter rather than a `DocsDir`: `repo/paths.ts`\n * owns the spelling of every file in a docs repo, and importing it here would\n * close the package cycle `repo → repo/tree → repo/tree/render → repo` that\n * `scripts/package-graph.mjs` exists to refuse. `walk.ts` pays the same price\n * for the same reason and says so.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type Elem } from \"../../../c4/likec4.js\";\nimport type { FleetTree } from \"../walk.js\";\nimport { renderSubsystemViews, viewsAgree } from \"./views.js\";\n\n/**\n * The three states, kept apart: absent, must-be-absent, and\n * present-but-different are three different sentences, and a fleet whose views\n * file does not exist is not a fleet whose views file is blank.\n */\nexport interface ViewsState {\n /** The bytes on disk, or null when the file is not there. */\n actual: string | null;\n /** The bytes the tree renders to, or null when the file must be absent. */\n expected: string | null;\n /** Do the two say the same thing? Content, not bytes — `viewsAgree` says why. */\n agrees: boolean;\n}\n\nexport async function viewsState(path: string, tree: FleetTree, elements: Elem[]): Promise<ViewsState> {\n const expected = renderSubsystemViews(tree, elements);\n const actual = existsSync(path) ? await readFile(path, \"utf8\") : null;\n return { actual, expected, agrees: viewsAgree(actual, expected) };\n}\n"]}
1
+ {"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/stale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAiB,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgB7E,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY,EAAE,IAAe,EAAE,GAAa;IAC3E,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["/**\n * Is the generated file on disk what this tree and this map render to?\n *\n * One function, because the question now has three askers — `validate --all`\n * (which turns a disagreement into `subsystem.views-stale`), `loam subsystem\n * sync` and the write verbs (which repair it), and `loam archive` (which must\n * not report a repo current when it has just made the file stale). Three\n * private compares would be three chances to answer differently about one\n * file, and the module that mints the bytes is the one that owes the answer.\n *\n * The PATH arrives as a parameter rather than a `DocsDir`: `repo/paths.ts`\n * owns the spelling of every file in a docs repo, and importing it here would\n * close the package cycle `repo → repo/tree → repo/tree/render → repo` that\n * `scripts/package-graph.mjs` exists to refuse. `walk.ts` pays the same price\n * for the same reason and says so.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { FleetTree } from \"../walk.js\";\nimport { type MapFacts, renderSubsystemViews, viewsAgree } from \"./views.js\";\n\n/**\n * The three states, kept apart: absent, must-be-absent, and\n * present-but-different are three different sentences, and a fleet whose views\n * file does not exist is not a fleet whose views file is blank.\n */\nexport interface ViewsState {\n /** The bytes on disk, or null when the file is not there. */\n actual: string | null;\n /** The bytes the tree renders to, or null when the file must be absent. */\n expected: string | null;\n /** Do the two say the same thing? Content, not bytes — `viewsAgree` says why. */\n agrees: boolean;\n}\n\nexport async function viewsState(path: string, tree: FleetTree, map: MapFacts): Promise<ViewsState> {\n const expected = renderSubsystemViews(tree, map);\n const actual = existsSync(path) ? await readFile(path, \"utf8\") : null;\n return { actual, expected, agrees: viewsAgree(actual, expected) };\n}\n"]}
@@ -50,6 +50,27 @@
50
50
  import {} from "../../../c4/likec4.js";
51
51
  import { renderContext, viewBody } from "./body.js";
52
52
  import { likec4String, viewTitle } from "./text.js";
53
+ /**
54
+ * The global style group a generated view references when the map declares
55
+ * one: `global { styleGroup subsystems { … } }` (or the single-rule form
56
+ * `global { style subsystems … }` — LikeC4 files both under one id table).
57
+ * Presence is the opt-in and it is read off a DECLARATION, never a view: a
58
+ * generated view cannot carry an inline `style` rule of its own (the file is
59
+ * regenerated wholesale), and the one mechanism LikeC4 offers a view for
60
+ * borrowing a repository's palette is a reference to a global style by id.
61
+ * Referencing one that is not declared is a parse error that blanks the whole
62
+ * `architecture/` project in the renderer, which is why the line is written
63
+ * only when the id is declared and why its absence leaves the file exactly
64
+ * what it was before this constant existed.
65
+ *
66
+ * One conventional name rather than "the single declared group": a second
67
+ * group somebody declares for a hand-drawn view would otherwise silently
68
+ * switch the generated views' styling OFF and restale the file on a change
69
+ * that touched no subsystem — behaviour flipping on an unrelated declaration
70
+ * is the shape `body.ts` calls "a picture that looks the same whether or not
71
+ * the two agree". The name mirrors the file it styles.
72
+ */
73
+ export const SUBSYSTEM_STYLE_GROUP = "subsystems";
53
74
  /**
54
75
  * The header every generated file opens with — stable bytes, part of the
55
76
  * byte-compare contract, naming the regenerator so a reader who found the
@@ -59,16 +80,35 @@ const HEADER = "// generated by loam — the subsystem tree under services/, one
59
80
  "// Do not edit: `loam subsystem sync` regenerates this file from the directories,\n" +
60
81
  "// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\n" +
61
82
  "// A scoping aid, not the fleet's architecture: author your own diagrams elsewhere.\n";
83
+ /**
84
+ * The one file-level comment the render may write: the map declares global
85
+ * style groups, and none is the conventional one. Written between `HEADER`
86
+ * and `views {` — outside every view body, graded by nothing — so a team
87
+ * that already declared a palette under another name reads the fix in the
88
+ * diff of the sync that regenerated the file, instead of wondering why the
89
+ * generated views ignore it. Silent when the map declares no group at all: a
90
+ * fleet with no palette is owed no sentence about one. Ids go unescaped
91
+ * because LikeC4's grammar admits only identifier bytes in a style id.
92
+ */
93
+ function stylesDisclosure(declared) {
94
+ if (declared.length === 0 || declared.includes(SUBSYSTEM_STYLE_GROUP))
95
+ return null;
96
+ return (`// styles: the project declares global style group(s) ${[...declared].sort().join(", ")}, ` +
97
+ `and none named \`${SUBSYSTEM_STYLE_GROUP}\` — these views carry the renderer's defaults until one is.`);
98
+ }
62
99
  /**
63
100
  * Render the views file for a walked tree, or null when the tree has no
64
101
  * subsystems — the file must then be ABSENT, so a flat fleet never carries a
65
102
  * generated artifact that says nothing.
66
103
  */
67
- export function renderSubsystemViews(tree, elements) {
104
+ export function renderSubsystemViews(tree, map) {
68
105
  if (tree.subsystems.length === 0)
69
106
  return null;
70
- const ctx = renderContext(tree, elements);
71
- const lines = [HEADER, "views {"];
107
+ const ctx = renderContext(tree, map.elements);
108
+ const declared = map.globalStyles ?? [];
109
+ const styled = declared.includes(SUBSYSTEM_STYLE_GROUP);
110
+ const disclosure = stylesDisclosure(declared);
111
+ const lines = [HEADER, ...(disclosure === null ? [] : [disclosure]), "views {"];
72
112
  const subsystems = [...tree.subsystems].sort((a, b) => (pathKey(a) < pathKey(b) ? -1 : 1));
73
113
  for (const [i, sub] of subsystems.entries()) {
74
114
  if (i > 0)
@@ -87,6 +127,14 @@ export function renderSubsystemViews(tree, elements) {
87
127
  const description = likec4String(sub.meta.description ?? "");
88
128
  if (description !== "")
89
129
  lines.push(` description '${description}'`);
130
+ // After the label, before anything that includes: measured at the pin
131
+ // (test/likec4-view-shape.test.ts), a `global style <id>` line here
132
+ // parses with zero errors, while the same line before `title` is the
133
+ // parse error the comment above records. The palette is the map's own —
134
+ // the group is declared in the same project this file renders into — so
135
+ // the reference resolves exactly when the presence test says it does.
136
+ if (styled)
137
+ lines.push(` global style ${SUBSYSTEM_STYLE_GROUP}`);
90
138
  lines.push(...body.lines);
91
139
  lines.push(" }");
92
140
  }