@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
@@ -1 +1 @@
1
- {"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The generated views file: `architecture/subsystems.likec4`, one LikeC4 view\n * per subsystem enumerating the services beneath it. GENERATED, never\n * authored — authored bytes stay authored (archive's verbatim-splice promise),\n * so the tree is mirrored into a file of its own that `loam subsystem sync`\n * owns outright, and staleness against the tree is a byte compare\n * (`subsystem.views-stale` in `validate --all`), never a parse: a spike\n * confirmed a views-only document does NOT parse standalone (its `include`\n * lines reference elements the landscape defines), so nothing in loam ever\n * loads this file (and rule 26 does not change that: this file is GENERATED and\n * holds STATIC views, whose contents loam may not read in any document) — only\n * the LikeC4 renderer, which merges the whole `architecture/` project per\n * likec4.config.json, resolves it.\n *\n * Each view opens with the LABEL a human reads — `title`, and `description`\n * when the marker carries one — because this file exists only to be rendered\n * and an unlabelled view is shown by its id, which is hex-escaped for\n * injectivity and reads as `subsystem_platform__order_2dflow`. Both\n * strings are somebody else's words and go through `likec4String`, which\n * records what a raw one does.\n *\n * The output is deterministic and line-oriented, and both halves are the\n * contract: subsystems sorted by path, members sorted by id, ONE `include`\n * per line — so two concurrent moves into different groups touch different\n * lines and git merges the file without intervention, exactly as the\n * service-grouped landscape splice already achieves. Byte-reproducible on any\n * machine: no timestamps, no absolute paths, no readdir-order dependence (the\n * walk sorts, and every list is re-sorted here). Plain lexicographic sorts on\n * purpose — the ids' own grammar is ASCII, an INVALID name is still ordered\n * deterministically, and `compareIds`' numeric tokenizing lives in `repo/`,\n * which this package must not import (`walk.ts` says why the edge only points\n * the other way).\n *\n * Membership is TRANSITIVE: a subsystem's view includes every service beneath\n * it at any depth, so a parent's view is the whole group's map rather than an\n * empty frame around its children. An empty view body is legal LikeC4 (same\n * spike), so an empty subsystem renders as one — `subsystem new` must be\n * usable before anything moves in.\n *\n * Include lines name the landscape ELEMENT id, resolved through the same\n * element→service join `validate` uses (`serviceResolver`: explicit\n * `metadata { service }` binding first, then a title naming a real service) —\n * because element ids in real landscapes are not service ids, and a view of\n * raw service ids would silently render empty for every such fleet. A member\n * with no landscape element is omitted from the includes: the omission is\n * already an error (`landscape.service-unmodelled`), and rendering only what\n * the committed landscape resolves keeps the output a function of committed\n * bytes alone.\n */\nimport { type Elem } from \"../../../c4/likec4.js\";\nimport { renderContext, viewBody } from \"./body.js\";\nimport { likec4String, viewTitle } from \"./text.js\";\nimport type { FleetTree, SubsystemEntry } from \"../walk.js\";\n\n/**\n * The header every generated file opens with — stable bytes, part of the\n * byte-compare contract, naming the regenerator so a reader who found the\n * file first knows what owns it.\n */\nconst HEADER =\n \"// generated by loam — the subsystem tree under services/, one view per subsystem.\\n\" +\n \"// Do not edit: `loam subsystem sync` regenerates this file from the directories,\\n\" +\n \"// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\\n\" +\n \"// A scoping aid, not the fleet's architecture: author your own diagrams elsewhere.\\n\";\n\n/**\n * Render the views file for a walked tree, or null when the tree has no\n * subsystems — the file must then be ABSENT, so a flat fleet never carries a\n * generated artifact that says nothing.\n */\nexport function renderSubsystemViews(tree: FleetTree, elements: Elem[]): string | null {\n if (tree.subsystems.length === 0) return null;\n const ctx = renderContext(tree, elements);\n const lines: string[] = [HEADER, \"views {\"];\n const subsystems = [...tree.subsystems].sort((a, b) => (pathKey(a) < pathKey(b) ? -1 : 1));\n for (const [i, sub] of subsystems.entries()) {\n if (i > 0) lines.push(\"\");\n lines.push(` // services/${pathKey(sub)}`);\n const body = viewBody(ctx, sub);\n if (body.disclosure !== null) lines.push(` // ${body.disclosure}`);\n lines.push(` view ${subsystemViewId(sub)} {`);\n // Metadata FIRST, and that ordering is a hard constraint rather than a\n // taste: measured, an `include` OR a `group { … }` line followed by\n // `title` is a parse error (\"Expecting token of type '}' but found\n // `title`\"), and one parse error in this file blanks the whole\n // `architecture/` project in the renderer.\n lines.push(` title '${viewTitle(titleChain(tree, sub))}'`);\n const description = likec4String(sub.meta.description ?? \"\");\n if (description !== \"\") lines.push(` description '${description}'`);\n lines.push(...body.lines);\n lines.push(\" }\");\n }\n lines.push(\"}\");\n return lines.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * This subsystem and every MARKED ancestor above it, outermost first — the\n * chain `viewTitle` composes a path out of.\n *\n * Marked, because an intermediate directory holding no `subsystem.yaml` is\n * `subsystem.unmarked`, an error the walk already names: it is not a group,\n * so it contributes no folder level here either. Read off the walked tree\n * rather than off the path segments, so what the title says and what the\n * finding says about the same directory cannot disagree.\n */\nfunction titleChain(tree: FleetTree, sub: SubsystemEntry): SubsystemEntry[] {\n return tree.subsystems\n .filter((s) => s.path.length <= sub.path.length && s.path.every((name, i) => sub.path[i] === name))\n .sort((a, b) => a.path.length - b.path.length);\n}\n\n/** The sort key and the comment spelling: the subsystem's path from services/, joined. */\nfunction pathKey(sub: SubsystemEntry): string {\n return sub.path.join(\"/\");\n}\n\n/**\n * A LikeC4 identifier for the view, derived from the subsystem's path so\n * nesting reads in the name — and INJECTIVE over every path the name grammar\n * admits, because the renderer refuses the whole `architecture/` project over\n * a duplicate view id. The naive folding (every non-identifier byte and the\n * join separator alike becoming `_`) collided three DISTINCT healthy names\n * onto one identifier — `a_b`, and `b` under `a`, both rendered\n * `subsystem_a_b` — so `validate --all` was green while the renderer refused\n * the fleet map; `subsystem.name-collision` never fires because the flat\n * namespace really is intact. Hence the encoding: an identifier byte\n * (`[A-Za-z0-9]`) passes through, any other byte becomes `_` + two lowercase\n * hex digits (`.` → `_2e`, `-` → `_2d`, `_` itself → `_5f` — the underscore\n * must be escaped precisely because it is BOTH legal in a name and the\n * separator), and segments join on `__`. Underscore runs then decode\n * uniquely — one is an escape, two a separator, three a separator followed\n * by an escape — so no two distinct legal paths share an identifier. A name\n * `subsystem.name-invalid` already flagged may hold a code point above 0xff\n * whose longer hex could in principle re-collide; it is deterministic still,\n * and that directory is already an error finding by name.\n *\n * EXPORTED, with its prefix, because `subsystem.view-id-collision` has to ask\n * the generator what it mints rather than re-deriving it. A check carrying its\n * own copy of the escaping above would go quiet the day the encoding changed,\n * and going quiet is the failure this whole family exists to prevent.\n */\nexport const SUBSYSTEM_VIEW_PREFIX = \"subsystem_\";\n\nexport function subsystemViewId(sub: SubsystemEntry): string {\n const segment = (name: string): string =>\n [...name].map((ch) => (/[A-Za-z0-9]/.test(ch) ? ch : `_${ch.codePointAt(0)!.toString(16).padStart(2, \"0\")}`)).join(\"\");\n return `subsystem_${sub.path.map(segment).join(\"__\")}`;\n}\n\n/**\n * Does the file on disk already SAY what `renderSubsystemViews` renders?\n *\n * The generator above only ever emits LF. `core.autocrlf=true` is Git for\n * Windows' installer default and a docs repo ships no `.gitattributes`, so an\n * ordinary Windows clone hands the file back with CRLF and not one fact\n * changed. Four call sites asked this question with a byte equality and all\n * four answered \"stale\" on such a clone: `loam validate` reported an error,\n * `loam subsystem sync` reported `updated` and rewrote with LF, git then showed\n * the file permanently modified, and the next checkout restored the CRLF. That\n * is a loop rather than a diagnosis, which is why the comparison lives here\n * now — the module that mints the bytes is the one that owes an answer about\n * them, and three near-copies of this rule would go stale one at a time.\n *\n * Still never a parse: a views-only document does not parse standalone, and no\n * check anywhere reads this file's content. The narrowing is from BYTES to\n * CONTENT, which is what `subsystem.views-stale`'s own message (\"does not match\n * the tree\") already claims. The symmetric cost is accepted and is the one\n * `core/verify/pins/pin.ts` documents for `pinnedDigest`: a pure line-ending\n * change is invisible, correctly, because it is not a change to what the file\n * says.\n *\n * `null` is carried through as itself rather than collapsing into `\"\"`: absent,\n * must-be-absent and present-but-different are three distinct states with three\n * distinct messages, and a fleet whose views file does not exist is not a fleet\n * whose views file is blank.\n */\nexport function viewsAgree(actual: string | null, expected: string | null): boolean {\n const lf = (text: string | null): string | null => (text === null ? null : text.replace(/\\r\\n|\\r/g, \"\\n\"));\n return lf(actual) === lf(expected);\n}\n"]}
1
+ {"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../../src/core/repo/tree/render/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The generated views file: `architecture/subsystems.likec4`, one LikeC4 view\n * per subsystem enumerating the services beneath it. GENERATED, never\n * authored — authored bytes stay authored (archive's verbatim-splice promise),\n * so the tree is mirrored into a file of its own that `loam subsystem sync`\n * owns outright, and staleness against the tree is a byte compare\n * (`subsystem.views-stale` in `validate --all`), never a parse: a spike\n * confirmed a views-only document does NOT parse standalone (its `include`\n * lines reference elements the landscape defines), so nothing in loam ever\n * loads this file (and rule 26 does not change that: this file is GENERATED and\n * holds STATIC views, whose contents loam may not read in any document) — only\n * the LikeC4 renderer, which merges the whole `architecture/` project per\n * likec4.config.json, resolves it.\n *\n * Each view opens with the LABEL a human reads — `title`, and `description`\n * when the marker carries one — because this file exists only to be rendered\n * and an unlabelled view is shown by its id, which is hex-escaped for\n * injectivity and reads as `subsystem_platform__order_2dflow`. Both\n * strings are somebody else's words and go through `likec4String`, which\n * records what a raw one does.\n *\n * The output is deterministic and line-oriented, and both halves are the\n * contract: subsystems sorted by path, members sorted by id, ONE `include`\n * per line — so two concurrent moves into different groups touch different\n * lines and git merges the file without intervention, exactly as the\n * service-grouped landscape splice already achieves. Byte-reproducible on any\n * machine: no timestamps, no absolute paths, no readdir-order dependence (the\n * walk sorts, and every list is re-sorted here). Plain lexicographic sorts on\n * purpose — the ids' own grammar is ASCII, an INVALID name is still ordered\n * deterministically, and `compareIds`' numeric tokenizing lives in `repo/`,\n * which this package must not import (`walk.ts` says why the edge only points\n * the other way).\n *\n * Membership is TRANSITIVE: a subsystem's view includes every service beneath\n * it at any depth, so a parent's view is the whole group's map rather than an\n * empty frame around its children. An empty view body is legal LikeC4 (same\n * spike), so an empty subsystem renders as one — `subsystem new` must be\n * usable before anything moves in.\n *\n * Include lines name the landscape ELEMENT id, resolved through the same\n * element→service join `validate` uses (`serviceResolver`: explicit\n * `metadata { service }` binding first, then a title naming a real service) —\n * because element ids in real landscapes are not service ids, and a view of\n * raw service ids would silently render empty for every such fleet. A member\n * with no landscape element is omitted from the includes: the omission is\n * already an error (`landscape.service-unmodelled`), and rendering only what\n * the committed landscape resolves keeps the output a function of committed\n * bytes alone.\n */\nimport { type LoadedDoc } from \"../../../c4/likec4.js\";\nimport { renderContext, viewBody } from \"./body.js\";\nimport { likec4String, viewTitle } from \"./text.js\";\nimport type { FleetTree, SubsystemEntry } from \"../walk.js\";\n\n/**\n * The global style group a generated view references when the map declares\n * one: `global { styleGroup subsystems { … } }` (or the single-rule form\n * `global { style subsystems … }` — LikeC4 files both under one id table).\n * Presence is the opt-in and it is read off a DECLARATION, never a view: a\n * generated view cannot carry an inline `style` rule of its own (the file is\n * regenerated wholesale), and the one mechanism LikeC4 offers a view for\n * borrowing a repository's palette is a reference to a global style by id.\n * Referencing one that is not declared is a parse error that blanks the whole\n * `architecture/` project in the renderer, which is why the line is written\n * only when the id is declared and why its absence leaves the file exactly\n * what it was before this constant existed.\n *\n * One conventional name rather than \"the single declared group\": a second\n * group somebody declares for a hand-drawn view would otherwise silently\n * switch the generated views' styling OFF and restale the file on a change\n * that touched no subsystem — behaviour flipping on an unrelated declaration\n * is the shape `body.ts` calls \"a picture that looks the same whether or not\n * the two agree\". The name mirrors the file it styles.\n */\nexport const SUBSYSTEM_STYLE_GROUP = \"subsystems\";\n\n/**\n * What the render reads off the parsed map: the elements the member join\n * resolves, and the global style ids a view may reference. A record rather\n * than a third parameter because the two come out of ONE parsed document, and\n * passing them apart would make an inconsistent pair representable. Every\n * `LoadedDoc` is structurally a `MapFacts`, so a caller holding the document\n * passes the document.\n */\nexport type MapFacts = Pick<LoadedDoc, \"elements\" | \"globalStyles\">;\n\n/**\n * The header every generated file opens with — stable bytes, part of the\n * byte-compare contract, naming the regenerator so a reader who found the\n * file first knows what owns it.\n */\nconst HEADER =\n \"// generated by loam — the subsystem tree under services/, one view per subsystem.\\n\" +\n \"// Do not edit: `loam subsystem sync` regenerates this file from the directories,\\n\" +\n \"// and `loam validate --all` reports `subsystem.views-stale` when it drifts.\\n\" +\n \"// A scoping aid, not the fleet's architecture: author your own diagrams elsewhere.\\n\";\n\n/**\n * The one file-level comment the render may write: the map declares global\n * style groups, and none is the conventional one. Written between `HEADER`\n * and `views {` — outside every view body, graded by nothing — so a team\n * that already declared a palette under another name reads the fix in the\n * diff of the sync that regenerated the file, instead of wondering why the\n * generated views ignore it. Silent when the map declares no group at all: a\n * fleet with no palette is owed no sentence about one. Ids go unescaped\n * because LikeC4's grammar admits only identifier bytes in a style id.\n */\nfunction stylesDisclosure(declared: readonly string[]): string | null {\n if (declared.length === 0 || declared.includes(SUBSYSTEM_STYLE_GROUP)) return null;\n return (\n `// styles: the project declares global style group(s) ${[...declared].sort().join(\", \")}, ` +\n `and none named \\`${SUBSYSTEM_STYLE_GROUP}\\` — these views carry the renderer's defaults until one is.`\n );\n}\n\n/**\n * Render the views file for a walked tree, or null when the tree has no\n * subsystems — the file must then be ABSENT, so a flat fleet never carries a\n * generated artifact that says nothing.\n */\nexport function renderSubsystemViews(tree: FleetTree, map: MapFacts): string | null {\n if (tree.subsystems.length === 0) return null;\n const ctx = renderContext(tree, map.elements);\n const declared = map.globalStyles ?? [];\n const styled = declared.includes(SUBSYSTEM_STYLE_GROUP);\n const disclosure = stylesDisclosure(declared);\n const lines: string[] = [HEADER, ...(disclosure === null ? [] : [disclosure]), \"views {\"];\n const subsystems = [...tree.subsystems].sort((a, b) => (pathKey(a) < pathKey(b) ? -1 : 1));\n for (const [i, sub] of subsystems.entries()) {\n if (i > 0) lines.push(\"\");\n lines.push(` // services/${pathKey(sub)}`);\n const body = viewBody(ctx, sub);\n if (body.disclosure !== null) lines.push(` // ${body.disclosure}`);\n lines.push(` view ${subsystemViewId(sub)} {`);\n // Metadata FIRST, and that ordering is a hard constraint rather than a\n // taste: measured, an `include` OR a `group { … }` line followed by\n // `title` is a parse error (\"Expecting token of type '}' but found\n // `title`\"), and one parse error in this file blanks the whole\n // `architecture/` project in the renderer.\n lines.push(` title '${viewTitle(titleChain(tree, sub))}'`);\n const description = likec4String(sub.meta.description ?? \"\");\n if (description !== \"\") lines.push(` description '${description}'`);\n // After the label, before anything that includes: measured at the pin\n // (test/likec4-view-shape.test.ts), a `global style <id>` line here\n // parses with zero errors, while the same line before `title` is the\n // parse error the comment above records. The palette is the map's own —\n // the group is declared in the same project this file renders into — so\n // the reference resolves exactly when the presence test says it does.\n if (styled) lines.push(` global style ${SUBSYSTEM_STYLE_GROUP}`);\n lines.push(...body.lines);\n lines.push(\" }\");\n }\n lines.push(\"}\");\n return lines.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * This subsystem and every MARKED ancestor above it, outermost first — the\n * chain `viewTitle` composes a path out of.\n *\n * Marked, because an intermediate directory holding no `subsystem.yaml` is\n * `subsystem.unmarked`, an error the walk already names: it is not a group,\n * so it contributes no folder level here either. Read off the walked tree\n * rather than off the path segments, so what the title says and what the\n * finding says about the same directory cannot disagree.\n */\nfunction titleChain(tree: FleetTree, sub: SubsystemEntry): SubsystemEntry[] {\n return tree.subsystems\n .filter((s) => s.path.length <= sub.path.length && s.path.every((name, i) => sub.path[i] === name))\n .sort((a, b) => a.path.length - b.path.length);\n}\n\n/** The sort key and the comment spelling: the subsystem's path from services/, joined. */\nfunction pathKey(sub: SubsystemEntry): string {\n return sub.path.join(\"/\");\n}\n\n/**\n * A LikeC4 identifier for the view, derived from the subsystem's path so\n * nesting reads in the name — and INJECTIVE over every path the name grammar\n * admits, because the renderer refuses the whole `architecture/` project over\n * a duplicate view id. The naive folding (every non-identifier byte and the\n * join separator alike becoming `_`) collided three DISTINCT healthy names\n * onto one identifier — `a_b`, and `b` under `a`, both rendered\n * `subsystem_a_b` — so `validate --all` was green while the renderer refused\n * the fleet map; `subsystem.name-collision` never fires because the flat\n * namespace really is intact. Hence the encoding: an identifier byte\n * (`[A-Za-z0-9]`) passes through, any other byte becomes `_` + two lowercase\n * hex digits (`.` → `_2e`, `-` → `_2d`, `_` itself → `_5f` — the underscore\n * must be escaped precisely because it is BOTH legal in a name and the\n * separator), and segments join on `__`. Underscore runs then decode\n * uniquely — one is an escape, two a separator, three a separator followed\n * by an escape — so no two distinct legal paths share an identifier. A name\n * `subsystem.name-invalid` already flagged may hold a code point above 0xff\n * whose longer hex could in principle re-collide; it is deterministic still,\n * and that directory is already an error finding by name.\n *\n * EXPORTED, with its prefix, because `subsystem.view-id-collision` has to ask\n * the generator what it mints rather than re-deriving it. A check carrying its\n * own copy of the escaping above would go quiet the day the encoding changed,\n * and going quiet is the failure this whole family exists to prevent.\n */\nexport const SUBSYSTEM_VIEW_PREFIX = \"subsystem_\";\n\nexport function subsystemViewId(sub: SubsystemEntry): string {\n const segment = (name: string): string =>\n [...name].map((ch) => (/[A-Za-z0-9]/.test(ch) ? ch : `_${ch.codePointAt(0)!.toString(16).padStart(2, \"0\")}`)).join(\"\");\n return `subsystem_${sub.path.map(segment).join(\"__\")}`;\n}\n\n/**\n * Does the file on disk already SAY what `renderSubsystemViews` renders?\n *\n * The generator above only ever emits LF. `core.autocrlf=true` is Git for\n * Windows' installer default and a docs repo ships no `.gitattributes`, so an\n * ordinary Windows clone hands the file back with CRLF and not one fact\n * changed. Four call sites asked this question with a byte equality and all\n * four answered \"stale\" on such a clone: `loam validate` reported an error,\n * `loam subsystem sync` reported `updated` and rewrote with LF, git then showed\n * the file permanently modified, and the next checkout restored the CRLF. That\n * is a loop rather than a diagnosis, which is why the comparison lives here\n * now — the module that mints the bytes is the one that owes an answer about\n * them, and three near-copies of this rule would go stale one at a time.\n *\n * Still never a parse: a views-only document does not parse standalone, and no\n * check anywhere reads this file's content. The narrowing is from BYTES to\n * CONTENT, which is what `subsystem.views-stale`'s own message (\"does not match\n * the tree\") already claims. The symmetric cost is accepted and is the one\n * `core/verify/pins/pin.ts` documents for `pinnedDigest`: a pure line-ending\n * change is invisible, correctly, because it is not a change to what the file\n * says.\n *\n * `null` is carried through as itself rather than collapsing into `\"\"`: absent,\n * must-be-absent and present-but-different are three distinct states with three\n * distinct messages, and a fleet whose views file does not exist is not a fleet\n * whose views file is blank.\n */\nexport function viewsAgree(actual: string | null, expected: string | null): boolean {\n const lf = (text: string | null): string | null => (text === null ? null : text.replace(/\\r\\n|\\r/g, \"\\n\"));\n return lf(actual) === lf(expected);\n}\n"]}
@@ -23,6 +23,21 @@
23
23
  * this walk would close the exact package cycle `scripts/package-graph.mjs`
24
24
  * exists to refuse.
25
25
  *
26
+ * A SERVICE'S OWN SUBDIRECTORIES ARE NEVER CLASSIFIED (`RESERVED_INTERIOR`).
27
+ * That is a different question from `isServiceArtifact`, which asks whether a
28
+ * name makes the directory holding it a service; this asks whether a child
29
+ * directory may be READ as one at all, and the two answers diverged the moment
30
+ * SCHEMA's service layout grew a slot the artifact table does not carry. A
31
+ * `services/<svc>/` holding only the documented `usecases/` flow directory was
32
+ * classified as a group: the walk descended, filed `usecases` as a service, and
33
+ * one `loam validate --all` reported `subsystem.unmarked`,
34
+ * `landscape.service-unmodelled` for `services/<svc>/usecases/` and
35
+ * `landscape.binding-unknown` for the real service — three errors whose text
36
+ * says the fleet is losing services, earned by following the instruction `loam
37
+ * init` had just scaffolded (verification 2026-09-04). The names are SCHEMA's
38
+ * service layout, and a directory named for one is service interior wherever it
39
+ * sits, so no branch of the classification descends into one.
40
+ *
26
41
  * Cost: exactly one readdir per directory walked, service directories included
27
42
  * (their own listing is what classifies them), plus one realpath per directory
28
43
  * for the symlink-cycle guard — `entryIs` follows symlinks on purpose, and a
@@ -36,6 +51,29 @@ import { rawServiceId } from "../../kernel/ids/service.js";
36
51
  import { subsystemNameProblem } from "../../kernel/ids/subsystem.js";
37
52
  import { entryIs } from "./fs.js";
38
53
  import { readSubsystemMarker, SUBSYSTEM_MARKER } from "./marker.js";
54
+ /**
55
+ * The subdirectory names SCHEMA's service layout reserves for a service's own
56
+ * interior (`SCHEMA.md`, "services/<svc>/"): `adrs/` the decisions, `usecases/`
57
+ * the flows drawn over its containers, `ui/` the page-specs. A directory with
58
+ * one of these names is never a service and never a subsystem — it is inside
59
+ * one — so the walk neither classifies it nor counts it as a subdirectory that
60
+ * makes its parent a group.
61
+ *
62
+ * Kept here rather than folded into `isServiceArtifact` because the two
63
+ * questions are not the same one: `adrs/` answers both (it is an artifact AND
64
+ * interior) and `usecases/` answers only this one today. Widening the injected
65
+ * artifact table instead would also make a lone `usecases/` classify its parent
66
+ * as a service by ARTIFACT, which hides a real service filed beside it — the
67
+ * silent shrink this module refuses.
68
+ *
69
+ * Exported for the one other reader of this rule: `core/diff/base-state.ts`
70
+ * classifies a base git ref's tree listing, which no walk can readdir, and its
71
+ * banner already commits it to restating this module's branches rather than
72
+ * inventing its own. It shares the SET for the reason it shares the artifact
73
+ * table — a name respelled there is the drift both modules are written to
74
+ * avoid.
75
+ */
76
+ export const RESERVED_INTERIOR = new Set(["adrs", "usecases", "ui"]);
39
77
  export async function walkServicesTree(request) {
40
78
  const ctx = {
41
79
  request,
@@ -82,10 +120,15 @@ async function visitDir(spot, ctx) {
82
120
  const rel = ["services", ...spot.chain, spot.name].join("/");
83
121
  const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith("."));
84
122
  const files = entries.filter((e) => entryIs(spot.dir, e, "file")).map((e) => e.name);
85
- const dirs = entries.filter((e) => entryIs(spot.dir, e, "dir")).map((e) => e.name).sort();
123
+ const all = entries.filter((e) => entryIs(spot.dir, e, "dir")).map((e) => e.name).sort();
124
+ // The classifiable children — a service's own interior removed once, here, so
125
+ // every branch below agrees about it (see `RESERVED_INTERIOR`). `all` is still
126
+ // what the artifact probe reads: `adrs/` makes its parent a service, and
127
+ // `usecases/` is interior the parent may hold without becoming a group.
128
+ const dirs = all.filter((d) => !RESERVED_INTERIOR.has(d));
86
129
  const marker = files.includes(SUBSYSTEM_MARKER);
87
130
  const artifact = files.some((f) => ctx.request.isServiceArtifact(f, "file")) ||
88
- dirs.some((d) => ctx.request.isServiceArtifact(d, "dir"));
131
+ all.some((d) => ctx.request.isServiceArtifact(d, "dir"));
89
132
  if (marker && artifact) {
90
133
  // Artifacts win the classification: the documents are somebody's work and
91
134
  // the marker is one stray file, so the directory stays an addressable
@@ -1 +1 @@
1
- {"version":3,"file":"walk.js","sourceRoot":"","sources":["../../../../src/core/repo/tree/walk.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The tree read model: three-way classification of everything under\n * `services/`, in one walk.\n *\n * A directory holding `subsystem.yaml` is a SUBSYSTEM and is walked; one\n * holding any service artifact is a SERVICE and is not walked deeper — unless\n * a marker sits BESIDE the artifacts, where the walk descends anyway (the\n * marker says a group was here, and its members must not vanish); one holding\n * neither while containing subdirectories is an ERROR that still names every\n * service found beneath it. The third branch is the whole point:\n * two-way classification loses a service silently on an ordinary clean merge —\n * one branch deletes an emptied subsystem's marker while another moves a\n * service into it, and the group directory is then read as a service whose\n * real services vanish from the fleet with no finding. No directory under\n * `services/` may ever be reinterpreted as a different kind without saying so,\n * and the fleet is never reported smaller: the walk descends into the broken\n * group precisely so the stranded services stay enumerated and named.\n *\n * The package direction is `repo → repo/tree`, never back. That is why the\n * artifact-name question arrives INJECTED (`TreeWalkRequest.isServiceArtifact`\n * — `repo/paths.ts` owns the spellings and `repo/repo.ts` builds the request):\n * importing `paths.ts` from here while `repo.ts` delegates its enumeration to\n * this walk would close the exact package cycle `scripts/package-graph.mjs`\n * exists to refuse.\n *\n * Cost: exactly one readdir per directory walked, service directories included\n * (their own listing is what classifies them), plus one realpath per directory\n * for the symlink-cycle guard — `entryIs` follows symlinks on purpose, and a\n * followed cycle without the visited-set is an unbounded recursion.\n */\nimport { realpathSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { serviceDirOf, serviceTreePath, type DocsDir, type ServiceDir } from \"../../kernel/ids/dirs.js\";\nimport { rawServiceId, type RawServiceId } from \"../../kernel/ids/service.js\";\nimport { subsystemNameProblem } from \"../../kernel/ids/subsystem.js\";\nimport type { Finding } from \"../../vocabulary/report.js\";\nimport { entryIs } from \"./fs.js\";\nimport { readSubsystemMarker, SUBSYSTEM_MARKER, type SubsystemMeta } from \"./marker.js\";\n\nexport interface WalkedService {\n id: RawServiceId;\n dir: ServiceDir;\n /**\n * Names of the directories between `services/` and the service, outermost\n * first. Empty = unfiled — directly under `services/`, which is permanent,\n * normal and SILENT. For a service stranded under an unmarked group the\n * chain still records the physical placement: the path is a fact even while\n * `subsystem.unmarked` says the grouping is broken.\n */\n subsystem: string[];\n}\n\nexport interface SubsystemEntry {\n /** The directory name — the subsystem's name in the flat namespace. */\n name: string;\n /** Names from `services/` down to and including this one. */\n path: string[];\n /** Absolute path of the subsystem directory. */\n dir: string;\n meta: SubsystemMeta;\n}\n\nexport interface FleetTree {\n services: WalkedService[];\n subsystems: SubsystemEntry[];\n findings: Finding[];\n}\n\nexport interface TreeWalkRequest {\n docsDir: DocsDir;\n /** Does this entry name a service artifact? `repo/paths.ts`'s table, injected — the module doc says why. */\n isServiceArtifact: (name: string, kind: \"file\" | \"dir\") => boolean;\n}\n\ninterface WalkContext {\n request: TreeWalkRequest;\n tree: FleetTree;\n /** Realpaths already DESCENDED into — the symlink-cycle guard. */\n visited: Set<string>;\n /** Every name in the flat namespace → the directories that claim it. */\n names: Map<string, string[]>;\n}\n\n/** One directory the walk is looking at: where it is, and how it is reached. */\ninterface Spot {\n dir: string;\n name: string;\n /** Directory names between `services/` and this one, outermost first. */\n chain: string[];\n}\n\nexport async function walkServicesTree(request: TreeWalkRequest): Promise<FleetTree> {\n const ctx: WalkContext = {\n request,\n tree: { services: [], subsystems: [], findings: [] },\n visited: new Set(),\n names: new Map(),\n };\n const root = join(request.docsDir, \"services\");\n guardDescent(root, ctx);\n for (const name of await subdirNames(root)) {\n await visitDir({ dir: join(root, name), name, chain: [] }, ctx);\n }\n namespaceFindings(ctx);\n return ctx.tree;\n}\n\n/**\n * Mark a directory as descended-into; false when it (or another name for it)\n * already was. Guards SUBTREES, not entries: a directory reachable under two\n * names is classified twice — both names exist and hiding the second would be\n * the silent shrink this module refuses — but its children are enumerated\n * once, so a symlink cycle terminates instead of recursing forever. A path\n * realpath cannot resolve is one whose children cannot be read either.\n */\nfunction guardDescent(dir: string, ctx: WalkContext): boolean {\n let real: string;\n try {\n real = realpathSync(dir);\n } catch {\n return false;\n }\n if (ctx.visited.has(real)) return false;\n ctx.visited.add(real);\n return true;\n}\n\n/** Sorted non-dot subdirectory names — the walk's deterministic child order. */\nasync function subdirNames(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => !e.name.startsWith(\".\") && entryIs(dir, e, \"dir\")).map((e) => e.name).sort();\n}\n\n/** Classify one directory and, for the group kinds, walk what is inside it. */\nasync function visitDir(spot: Spot, ctx: WalkContext): Promise<void> {\n const rel = [\"services\", ...spot.chain, spot.name].join(\"/\");\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n const files = entries.filter((e) => entryIs(spot.dir, e, \"file\")).map((e) => e.name);\n const dirs = entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort();\n const marker = files.includes(SUBSYSTEM_MARKER);\n const artifact =\n files.some((f) => ctx.request.isServiceArtifact(f, \"file\")) ||\n dirs.some((d) => ctx.request.isServiceArtifact(d, \"dir\"));\n\n if (marker && artifact) {\n // Artifacts win the classification: the documents are somebody's work and\n // the marker is one stray file, so the directory stays an addressable\n // service and the finding names the contradiction instead of the walk\n // quietly picking a reading. But the walk STILL descends (the module\n // invariant: no reinterpretation may shrink the fleet) — the marker says\n // a GROUP was here, and returning without walking made every service\n // beneath it vanish with only this finding to hint why. Child directories\n // that are themselves artifacts (`adrs/`) are service interior, not walked.\n recordService(spot, ctx);\n const stranded = await descendInto(spot, dirs.filter((d) => !ctx.request.isServiceArtifact(d, \"dir\")), ctx);\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.marker-misplaced\",\n subject: spot.name,\n message:\n `${rel}/ holds ${SUBSYSTEM_MARKER} BESIDE service artifacts — a directory is a subsystem or a service, never both. ` +\n `It stays classified as a service; delete ${rel}/${SUBSYSTEM_MARKER}, ` +\n `or move the artifacts into a service directory of their own inside the group.` +\n (stranded.length > 0 ? ` ${stranded.length} service(s) beneath it stay enumerated meanwhile: ${stranded.map((s) => s.id).join(\", \")}.` : \"\"),\n ...(stranded.length > 0 ? { details: stranded.map(serviceTreePath) } : {}),\n });\n return;\n }\n if (marker) {\n await visitSubsystem(spot, { rel, dirs }, ctx);\n return;\n }\n if (artifact) {\n recordService(spot, ctx);\n return;\n }\n if (dirs.length > 0) {\n await visitUnmarked(spot, { rel, dirs }, ctx);\n return;\n }\n // Neither marker nor artifact, and nothing beneath: today's flat behaviour —\n // an empty (or not-yet-adopted) directory under services/ is a service.\n recordService(spot, ctx);\n}\n\nfunction recordService(spot: Spot, ctx: WalkContext): void {\n ctx.tree.services.push({\n id: rawServiceId(spot.name),\n dir: serviceDirOf(spot.dir),\n subsystem: spot.chain,\n });\n claimName(spot, ctx);\n}\n\n/** A subsystem: record it, report a broken marker or an illegal name, walk in. */\nasync function visitSubsystem(spot: Spot, at: { rel: string; dirs: string[] }, ctx: WalkContext): Promise<void> {\n const meta = await readSubsystemMarker(join(spot.dir, SUBSYSTEM_MARKER));\n if (meta.invalid !== undefined) {\n // Exactly one finding per marker file: presence already classified the\n // directory, so the content failure is metadata, not identity.\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.invalid\",\n subject: at.rel,\n message:\n `${at.rel}/${SUBSYSTEM_MARKER} cannot be read as subsystem metadata — ${meta.invalid}. ` +\n `The directory still counts as a subsystem (presence classifies; content is metadata). ` +\n `Fix the file: a YAML mapping with optional title, description and owner — an empty file is also a valid marker.`,\n });\n }\n const problem = subsystemNameProblem(spot.name, \"subsystem directory name\");\n if (problem !== null) {\n // Walked anyway, mirroring how an illegal service id stays enumerated\n // (`idProblem`): the directory exists and hiding its subtree would shrink\n // the fleet over a naming mistake.\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.name-invalid\",\n subject: spot.name,\n message: `${at.rel}/ — ${problem} Rename the directory; its services stay enumerated meanwhile.`,\n });\n }\n ctx.tree.subsystems.push({\n name: spot.name,\n path: [...spot.chain, spot.name],\n dir: spot.dir,\n meta,\n });\n claimName(spot, ctx);\n await descendInto(spot, at.dirs, ctx);\n}\n\n/**\n * Walk the children of a group directory (symlink-cycle guarded), returning\n * the services newly recorded beneath it — the ONE spelling of descent, so\n * the three descending branches agree about the guard and the chain.\n */\nasync function descendInto(spot: Spot, dirs: string[], ctx: WalkContext): Promise<WalkedService[]> {\n const before = ctx.tree.services.length;\n if (guardDescent(spot.dir, ctx)) {\n for (const child of dirs) {\n await visitDir({ dir: join(spot.dir, child), name: child, chain: [...spot.chain, spot.name] }, ctx);\n }\n }\n return ctx.tree.services.slice(before);\n}\n\n/**\n * The refusal branch — a group directory nothing marks. The walk STILL\n * descends, so the services beneath stay in the enumeration and the finding\n * can name them: the fleet must never be reported smaller than it is.\n */\nasync function visitUnmarked(spot: Spot, at: { rel: string; dirs: string[] }, ctx: WalkContext): Promise<void> {\n const stranded = await descendInto(spot, at.dirs, ctx);\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.unmarked\",\n subject: at.rel,\n message:\n `${at.rel}/ is neither a subsystem (no ${SUBSYSTEM_MARKER}) nor a service (no service artifact), ` +\n `but ${stranded.length} service(s) sit beneath it` +\n (stranded.length > 0 ? `: ${stranded.map((s) => s.id).join(\", \")}. ` : `. `) +\n `Without the marker this directory would be read as a service and everything under it would vanish from the fleet — ` +\n `the shape an ordinary merge produces when one branch deletes an emptied subsystem's marker while another moves a service in. ` +\n `Restore ${at.rel}/${SUBSYSTEM_MARKER} (an empty file is a valid marker), or move the directories out.`,\n ...(stranded.length > 0 ? { details: stranded.map(serviceTreePath) } : {}),\n });\n}\n\nfunction claimName(spot: Spot, ctx: WalkContext): void {\n const rel = [\"services\", ...spot.chain, spot.name].join(\"/\");\n ctx.names.set(spot.name, [...(ctx.names.get(spot.name) ?? []), rel]);\n}\n\n/**\n * One flat namespace: every subsystem name and service id, unique across the\n * whole tree at any depth, never colliding with each other. The identity\n * decision demands it — a service id is its leaf directory name ALONE, so two\n * leaves spelling one name are one identity claimed twice, wherever they sit.\n * One finding per name, naming every claimant, so the fix is a choice rather\n * than a scavenger hunt.\n */\nfunction namespaceFindings(ctx: WalkContext): void {\n for (const [name, dirs] of [...ctx.names].sort(([a], [b]) => (a < b ? -1 : 1))) {\n if (dirs.length < 2) continue;\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.name-collision\",\n subject: name,\n message:\n `'${name}' is claimed by ${dirs.length} directories in the services/ tree: ${dirs.join(\", \")} — ` +\n `service ids and subsystem names share one flat namespace, unique at any depth, ` +\n `because a service's identity is its leaf directory name alone and placement never disambiguates it. ` +\n `Rename all but one.`,\n details: dirs,\n });\n }\n}\n"]}
1
+ {"version":3,"file":"walk.js","sourceRoot":"","sources":["../../../../src/core/repo/tree/walk.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The tree read model: three-way classification of everything under\n * `services/`, in one walk.\n *\n * A directory holding `subsystem.yaml` is a SUBSYSTEM and is walked; one\n * holding any service artifact is a SERVICE and is not walked deeper — unless\n * a marker sits BESIDE the artifacts, where the walk descends anyway (the\n * marker says a group was here, and its members must not vanish); one holding\n * neither while containing subdirectories is an ERROR that still names every\n * service found beneath it. The third branch is the whole point:\n * two-way classification loses a service silently on an ordinary clean merge —\n * one branch deletes an emptied subsystem's marker while another moves a\n * service into it, and the group directory is then read as a service whose\n * real services vanish from the fleet with no finding. No directory under\n * `services/` may ever be reinterpreted as a different kind without saying so,\n * and the fleet is never reported smaller: the walk descends into the broken\n * group precisely so the stranded services stay enumerated and named.\n *\n * The package direction is `repo → repo/tree`, never back. That is why the\n * artifact-name question arrives INJECTED (`TreeWalkRequest.isServiceArtifact`\n * — `repo/paths.ts` owns the spellings and `repo/repo.ts` builds the request):\n * importing `paths.ts` from here while `repo.ts` delegates its enumeration to\n * this walk would close the exact package cycle `scripts/package-graph.mjs`\n * exists to refuse.\n *\n * A SERVICE'S OWN SUBDIRECTORIES ARE NEVER CLASSIFIED (`RESERVED_INTERIOR`).\n * That is a different question from `isServiceArtifact`, which asks whether a\n * name makes the directory holding it a service; this asks whether a child\n * directory may be READ as one at all, and the two answers diverged the moment\n * SCHEMA's service layout grew a slot the artifact table does not carry. A\n * `services/<svc>/` holding only the documented `usecases/` flow directory was\n * classified as a group: the walk descended, filed `usecases` as a service, and\n * one `loam validate --all` reported `subsystem.unmarked`,\n * `landscape.service-unmodelled` for `services/<svc>/usecases/` and\n * `landscape.binding-unknown` for the real service — three errors whose text\n * says the fleet is losing services, earned by following the instruction `loam\n * init` had just scaffolded (verification 2026-09-04). The names are SCHEMA's\n * service layout, and a directory named for one is service interior wherever it\n * sits, so no branch of the classification descends into one.\n *\n * Cost: exactly one readdir per directory walked, service directories included\n * (their own listing is what classifies them), plus one realpath per directory\n * for the symlink-cycle guard — `entryIs` follows symlinks on purpose, and a\n * followed cycle without the visited-set is an unbounded recursion.\n */\nimport { realpathSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { serviceDirOf, serviceTreePath, type DocsDir, type ServiceDir } from \"../../kernel/ids/dirs.js\";\nimport { rawServiceId, type RawServiceId } from \"../../kernel/ids/service.js\";\nimport { subsystemNameProblem } from \"../../kernel/ids/subsystem.js\";\nimport type { Finding } from \"../../vocabulary/report.js\";\nimport { entryIs } from \"./fs.js\";\nimport { readSubsystemMarker, SUBSYSTEM_MARKER, type SubsystemMeta } from \"./marker.js\";\n\n/**\n * The subdirectory names SCHEMA's service layout reserves for a service's own\n * interior (`SCHEMA.md`, \"services/<svc>/\"): `adrs/` the decisions, `usecases/`\n * the flows drawn over its containers, `ui/` the page-specs. A directory with\n * one of these names is never a service and never a subsystem — it is inside\n * one — so the walk neither classifies it nor counts it as a subdirectory that\n * makes its parent a group.\n *\n * Kept here rather than folded into `isServiceArtifact` because the two\n * questions are not the same one: `adrs/` answers both (it is an artifact AND\n * interior) and `usecases/` answers only this one today. Widening the injected\n * artifact table instead would also make a lone `usecases/` classify its parent\n * as a service by ARTIFACT, which hides a real service filed beside it — the\n * silent shrink this module refuses.\n *\n * Exported for the one other reader of this rule: `core/diff/base-state.ts`\n * classifies a base git ref's tree listing, which no walk can readdir, and its\n * banner already commits it to restating this module's branches rather than\n * inventing its own. It shares the SET for the reason it shares the artifact\n * table — a name respelled there is the drift both modules are written to\n * avoid.\n */\nexport const RESERVED_INTERIOR: ReadonlySet<string> = new Set([\"adrs\", \"usecases\", \"ui\"]);\n\nexport interface WalkedService {\n id: RawServiceId;\n dir: ServiceDir;\n /**\n * Names of the directories between `services/` and the service, outermost\n * first. Empty = unfiled — directly under `services/`, which is permanent,\n * normal and SILENT. For a service stranded under an unmarked group the\n * chain still records the physical placement: the path is a fact even while\n * `subsystem.unmarked` says the grouping is broken.\n */\n subsystem: string[];\n}\n\nexport interface SubsystemEntry {\n /** The directory name — the subsystem's name in the flat namespace. */\n name: string;\n /** Names from `services/` down to and including this one. */\n path: string[];\n /** Absolute path of the subsystem directory. */\n dir: string;\n meta: SubsystemMeta;\n}\n\nexport interface FleetTree {\n services: WalkedService[];\n subsystems: SubsystemEntry[];\n findings: Finding[];\n}\n\nexport interface TreeWalkRequest {\n docsDir: DocsDir;\n /** Does this entry name a service artifact? `repo/paths.ts`'s table, injected — the module doc says why. */\n isServiceArtifact: (name: string, kind: \"file\" | \"dir\") => boolean;\n}\n\ninterface WalkContext {\n request: TreeWalkRequest;\n tree: FleetTree;\n /** Realpaths already DESCENDED into — the symlink-cycle guard. */\n visited: Set<string>;\n /** Every name in the flat namespace → the directories that claim it. */\n names: Map<string, string[]>;\n}\n\n/** One directory the walk is looking at: where it is, and how it is reached. */\ninterface Spot {\n dir: string;\n name: string;\n /** Directory names between `services/` and this one, outermost first. */\n chain: string[];\n}\n\nexport async function walkServicesTree(request: TreeWalkRequest): Promise<FleetTree> {\n const ctx: WalkContext = {\n request,\n tree: { services: [], subsystems: [], findings: [] },\n visited: new Set(),\n names: new Map(),\n };\n const root = join(request.docsDir, \"services\");\n guardDescent(root, ctx);\n for (const name of await subdirNames(root)) {\n await visitDir({ dir: join(root, name), name, chain: [] }, ctx);\n }\n namespaceFindings(ctx);\n return ctx.tree;\n}\n\n/**\n * Mark a directory as descended-into; false when it (or another name for it)\n * already was. Guards SUBTREES, not entries: a directory reachable under two\n * names is classified twice — both names exist and hiding the second would be\n * the silent shrink this module refuses — but its children are enumerated\n * once, so a symlink cycle terminates instead of recursing forever. A path\n * realpath cannot resolve is one whose children cannot be read either.\n */\nfunction guardDescent(dir: string, ctx: WalkContext): boolean {\n let real: string;\n try {\n real = realpathSync(dir);\n } catch {\n return false;\n }\n if (ctx.visited.has(real)) return false;\n ctx.visited.add(real);\n return true;\n}\n\n/** Sorted non-dot subdirectory names — the walk's deterministic child order. */\nasync function subdirNames(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => !e.name.startsWith(\".\") && entryIs(dir, e, \"dir\")).map((e) => e.name).sort();\n}\n\n/** Classify one directory and, for the group kinds, walk what is inside it. */\nasync function visitDir(spot: Spot, ctx: WalkContext): Promise<void> {\n const rel = [\"services\", ...spot.chain, spot.name].join(\"/\");\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n const files = entries.filter((e) => entryIs(spot.dir, e, \"file\")).map((e) => e.name);\n const all = entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort();\n // The classifiable children — a service's own interior removed once, here, so\n // every branch below agrees about it (see `RESERVED_INTERIOR`). `all` is still\n // what the artifact probe reads: `adrs/` makes its parent a service, and\n // `usecases/` is interior the parent may hold without becoming a group.\n const dirs = all.filter((d) => !RESERVED_INTERIOR.has(d));\n const marker = files.includes(SUBSYSTEM_MARKER);\n const artifact =\n files.some((f) => ctx.request.isServiceArtifact(f, \"file\")) ||\n all.some((d) => ctx.request.isServiceArtifact(d, \"dir\"));\n\n if (marker && artifact) {\n // Artifacts win the classification: the documents are somebody's work and\n // the marker is one stray file, so the directory stays an addressable\n // service and the finding names the contradiction instead of the walk\n // quietly picking a reading. But the walk STILL descends (the module\n // invariant: no reinterpretation may shrink the fleet) — the marker says\n // a GROUP was here, and returning without walking made every service\n // beneath it vanish with only this finding to hint why. Child directories\n // that are themselves artifacts (`adrs/`) are service interior, not walked.\n recordService(spot, ctx);\n const stranded = await descendInto(spot, dirs.filter((d) => !ctx.request.isServiceArtifact(d, \"dir\")), ctx);\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.marker-misplaced\",\n subject: spot.name,\n message:\n `${rel}/ holds ${SUBSYSTEM_MARKER} BESIDE service artifacts — a directory is a subsystem or a service, never both. ` +\n `It stays classified as a service; delete ${rel}/${SUBSYSTEM_MARKER}, ` +\n `or move the artifacts into a service directory of their own inside the group.` +\n (stranded.length > 0 ? ` ${stranded.length} service(s) beneath it stay enumerated meanwhile: ${stranded.map((s) => s.id).join(\", \")}.` : \"\"),\n ...(stranded.length > 0 ? { details: stranded.map(serviceTreePath) } : {}),\n });\n return;\n }\n if (marker) {\n await visitSubsystem(spot, { rel, dirs }, ctx);\n return;\n }\n if (artifact) {\n recordService(spot, ctx);\n return;\n }\n if (dirs.length > 0) {\n await visitUnmarked(spot, { rel, dirs }, ctx);\n return;\n }\n // Neither marker nor artifact, and nothing beneath: today's flat behaviour —\n // an empty (or not-yet-adopted) directory under services/ is a service.\n recordService(spot, ctx);\n}\n\nfunction recordService(spot: Spot, ctx: WalkContext): void {\n ctx.tree.services.push({\n id: rawServiceId(spot.name),\n dir: serviceDirOf(spot.dir),\n subsystem: spot.chain,\n });\n claimName(spot, ctx);\n}\n\n/** A subsystem: record it, report a broken marker or an illegal name, walk in. */\nasync function visitSubsystem(spot: Spot, at: { rel: string; dirs: string[] }, ctx: WalkContext): Promise<void> {\n const meta = await readSubsystemMarker(join(spot.dir, SUBSYSTEM_MARKER));\n if (meta.invalid !== undefined) {\n // Exactly one finding per marker file: presence already classified the\n // directory, so the content failure is metadata, not identity.\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.invalid\",\n subject: at.rel,\n message:\n `${at.rel}/${SUBSYSTEM_MARKER} cannot be read as subsystem metadata — ${meta.invalid}. ` +\n `The directory still counts as a subsystem (presence classifies; content is metadata). ` +\n `Fix the file: a YAML mapping with optional title, description and owner — an empty file is also a valid marker.`,\n });\n }\n const problem = subsystemNameProblem(spot.name, \"subsystem directory name\");\n if (problem !== null) {\n // Walked anyway, mirroring how an illegal service id stays enumerated\n // (`idProblem`): the directory exists and hiding its subtree would shrink\n // the fleet over a naming mistake.\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.name-invalid\",\n subject: spot.name,\n message: `${at.rel}/ — ${problem} Rename the directory; its services stay enumerated meanwhile.`,\n });\n }\n ctx.tree.subsystems.push({\n name: spot.name,\n path: [...spot.chain, spot.name],\n dir: spot.dir,\n meta,\n });\n claimName(spot, ctx);\n await descendInto(spot, at.dirs, ctx);\n}\n\n/**\n * Walk the children of a group directory (symlink-cycle guarded), returning\n * the services newly recorded beneath it — the ONE spelling of descent, so\n * the three descending branches agree about the guard and the chain.\n */\nasync function descendInto(spot: Spot, dirs: string[], ctx: WalkContext): Promise<WalkedService[]> {\n const before = ctx.tree.services.length;\n if (guardDescent(spot.dir, ctx)) {\n for (const child of dirs) {\n await visitDir({ dir: join(spot.dir, child), name: child, chain: [...spot.chain, spot.name] }, ctx);\n }\n }\n return ctx.tree.services.slice(before);\n}\n\n/**\n * The refusal branch — a group directory nothing marks. The walk STILL\n * descends, so the services beneath stay in the enumeration and the finding\n * can name them: the fleet must never be reported smaller than it is.\n */\nasync function visitUnmarked(spot: Spot, at: { rel: string; dirs: string[] }, ctx: WalkContext): Promise<void> {\n const stranded = await descendInto(spot, at.dirs, ctx);\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.unmarked\",\n subject: at.rel,\n message:\n `${at.rel}/ is neither a subsystem (no ${SUBSYSTEM_MARKER}) nor a service (no service artifact), ` +\n `but ${stranded.length} service(s) sit beneath it` +\n (stranded.length > 0 ? `: ${stranded.map((s) => s.id).join(\", \")}. ` : `. `) +\n `Without the marker this directory would be read as a service and everything under it would vanish from the fleet — ` +\n `the shape an ordinary merge produces when one branch deletes an emptied subsystem's marker while another moves a service in. ` +\n `Restore ${at.rel}/${SUBSYSTEM_MARKER} (an empty file is a valid marker), or move the directories out.`,\n ...(stranded.length > 0 ? { details: stranded.map(serviceTreePath) } : {}),\n });\n}\n\nfunction claimName(spot: Spot, ctx: WalkContext): void {\n const rel = [\"services\", ...spot.chain, spot.name].join(\"/\");\n ctx.names.set(spot.name, [...(ctx.names.get(spot.name) ?? []), rel]);\n}\n\n/**\n * One flat namespace: every subsystem name and service id, unique across the\n * whole tree at any depth, never colliding with each other. The identity\n * decision demands it — a service id is its leaf directory name ALONE, so two\n * leaves spelling one name are one identity claimed twice, wherever they sit.\n * One finding per name, naming every claimant, so the fix is a choice rather\n * than a scavenger hunt.\n */\nfunction namespaceFindings(ctx: WalkContext): void {\n for (const [name, dirs] of [...ctx.names].sort(([a], [b]) => (a < b ? -1 : 1))) {\n if (dirs.length < 2) continue;\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.name-collision\",\n subject: name,\n message:\n `'${name}' is claimed by ${dirs.length} directories in the services/ tree: ${dirs.join(\", \")} — ` +\n `service ids and subsystem names share one flat namespace, unique at any depth, ` +\n `because a service's identity is its leaf directory name alone and placement never disambiguates it. ` +\n `Rename all but one.`,\n details: dirs,\n });\n }\n}\n"]}
@@ -60,6 +60,185 @@ export const LANDSCAPE_STUB = `// The fleet map: every service in services/ appe
60
60
  //
61
61
  // The \`views\` block at the bottom is for LikeC4's own tooling — loam reads
62
62
  // the model and never a view. Render with \`npx likec4 start\` from the docs
63
+ // repo root, which likec4.config.json scopes to this directory. A service's
64
+ // \`services/<id>/model.likec4\` EXTENDS this map: it declares no
65
+ // \`specification\` block of its own, so it belongs to THIS project and renders
66
+ // with it, containers and all. A model that declares its own \`specification\`
67
+ // stands alone instead — it renders only from its own directory
68
+ // (\`npx likec4 start services/<id>\`), and the \`exclude\` list in the root
69
+ // likec4.config.json has to cover that directory or its declarations collide
70
+ // with this file's. \`loam subsystem sync\` maintains both, so neither is
71
+ // hand-edited. Keep views
72
+ // scoped, because computing one is superlinear in the number of edges — the
73
+ // \`fleet\` view below stays affordable precisely because it excludes the
74
+ // platform hubs.
75
+ //
76
+ // One shape is worth getting right before the fleet is drawn: a shared broker.
77
+ // Kafka as a single element becomes the node every service points at, and any
78
+ // view over it is a star nobody can read. Model the TOPIC instead — a \`topic\`
79
+ // nested inside the broker's element, edges pointing at \`kafka.<topic>\` — which
80
+ // splits one hub of degree sixty into a dozen small ones and is the truer model
81
+ // besides. Tag the KIND, not just the broker: LikeC4 does not inherit tags, so a
82
+ // topic under an \`#external\` broker is not external itself and \`validate\` asks
83
+ // for a services/ directory nobody owes.
84
+ //
85
+ // specification {
86
+ // element topic {
87
+ // #external
88
+ // style { shape queue }
89
+ // }
90
+ // }
91
+
92
+ specification {
93
+ element person
94
+ element softwareSystem
95
+ element container
96
+ element database
97
+
98
+ // Ubiquitous infrastructure — logging Kafka, auth, service discovery — takes
99
+ // one inbound edge per service, and by the third service the fleet view is a
100
+ // hairball. Tag those elements #platform: the fleet view excludes them
101
+ // without losing "who depends on the Identity Provider", which the platform
102
+ // view answers.
103
+ tag platform
104
+ }
105
+
106
+ model {
107
+ }
108
+
109
+ views {
110
+ view fleet {
111
+ include *
112
+ exclude element.tag = #platform
113
+ }
114
+ // The obvious spelling — \`include element.tag = #platform\` — draws the
115
+ // platform boxes with NO edges and no consumers. The predicate that works is
116
+ // the relationship form below.
117
+ view platform {
118
+ include * -> element.tag = #platform
119
+ }
120
+ }
121
+ `;
122
+ /**
123
+ * Stubs earlier versions of loam wrote, FROZEN — never re-derived from the
124
+ * constant above, or an edit to it would silently rewrite history.
125
+ *
126
+ * `isLandscapeStub` is a suffix match on the exact bytes, and `loam seed`
127
+ * refuses (`seed-landscape-edited`) on a map it judges authored. So editing
128
+ * the stub reclassifies every untouched older scaffold as hand-edited, in
129
+ * repositories nobody has touched — the teaching step disappears from `loam
130
+ * status` and `seed` refuses a map its own scaffold wrote. Each entry is one
131
+ * shipped generation of the file, OLDEST FIRST, appended when the text above
132
+ * changes — every generation loam ever wrote is here, not just the previous
133
+ * one, since a repository scaffolded two releases ago is as untouched as one
134
+ * scaffolded yesterday.
135
+ *
136
+ * Generation 1 (v0.1.0-beta.1, .2): the stub lived in core/docs.ts, carried no
137
+ * `views` block at all ("on purpose: loam reads the model and never a view")
138
+ * and no `tag platform`.
139
+ * Generation 2 (v0.1.0-beta.3): the same file with the render paragraph
140
+ * rewritten — still no `views` block, still no `tag platform`.
141
+ * Generation 3 (v0.2.0-alpha.1 through .5): the `views` paragraph taught the
142
+ * retired shape — that a service model "renders from its OWN directory" and
143
+ * "each declares its own `specification` block" — which was written before an
144
+ * extending model belonged to the root project.
145
+ */
146
+ export const LEGACY_LANDSCAPE_STUBS = [
147
+ `// The fleet map: every service in services/ appears here, and every call
148
+ // between two of them is an edge. This file is written by hand — loam never
149
+ // guesses it, because "who calls whom" is the one fact no generator can read
150
+ // off a repository.
151
+ //
152
+ // After \`loam adopt --service <id>\`, add the service here:
153
+ //
154
+ // paymentService = softwareSystem 'payment-service' {
155
+ // description 'Owns payment authorization/capture'
156
+ // metadata { service 'payment-service' } // binds the box to services/<id>/
157
+ // }
158
+ //
159
+ // and give each call the operationId it uses, so requirements, C4 and OpenAPI
160
+ // can be cross-checked:
161
+ //
162
+ // checkoutWeb -> paymentService 'Authorizes' {
163
+ // metadata { op 'authorizePayment' }
164
+ // }
165
+ //
166
+ // There is no \`views\` block, on purpose: loam reads the model and never a
167
+ // view, so it would draw nothing. Add views here if you want diagrams, and
168
+ // render them with LikeC4's own tooling (\`npx likec4 start\`) — but scope them,
169
+ // because computing a view is superlinear in the number of edges and an
170
+ // \`include *\` over a whole fleet takes minutes.
171
+
172
+ specification {
173
+ element person
174
+ element softwareSystem
175
+ element container
176
+ element database
177
+ }
178
+
179
+ model {
180
+ }
181
+ `,
182
+ `// The fleet map: every service in services/ appears here, and every call
183
+ // between two of them is an edge. This file is written by hand — loam never
184
+ // guesses it, because "who calls whom" is the one fact no generator can read
185
+ // off a repository.
186
+ //
187
+ // After \`loam adopt --service <id>\`, add the service here:
188
+ //
189
+ // paymentService = softwareSystem 'payment-service' {
190
+ // description 'Owns payment authorization/capture'
191
+ // metadata { service 'payment-service' } // binds the box to services/<id>/
192
+ // }
193
+ //
194
+ // and give each call the operationId it uses, so requirements, C4 and OpenAPI
195
+ // can be cross-checked:
196
+ //
197
+ // checkoutWeb -> paymentService 'Authorizes' {
198
+ // metadata { op 'authorizePayment' }
199
+ // }
200
+ //
201
+ // There is no \`views\` block, on purpose: loam reads the model and never a
202
+ // view, so it would draw nothing. Add views here if you want diagrams, and
203
+ // render them with LikeC4's own tooling — \`npx likec4 start\` from the docs
204
+ // repo root, which likec4.config.json scopes to this directory. (A service
205
+ // model or a feature delta renders from its OWN directory, e.g.
206
+ // \`npx likec4 start services/<id>\`: each declares its own \`specification\`
207
+ // block, so the renderer can only be given one of them at a time.) Scope your
208
+ // views, because computing one is superlinear in the number of edges and an
209
+ // \`include *\` over a whole fleet takes minutes.
210
+
211
+ specification {
212
+ element person
213
+ element softwareSystem
214
+ element container
215
+ element database
216
+ }
217
+
218
+ model {
219
+ }
220
+ `,
221
+ `// The fleet map: every service in services/ appears here, and every call
222
+ // between two of them is an edge. This file is written by hand — loam never
223
+ // guesses it, because "who calls whom" is the one fact no generator can read
224
+ // off a repository.
225
+ //
226
+ // After \`loam adopt --service <id>\`, add the service here:
227
+ //
228
+ // paymentService = softwareSystem 'payment-service' {
229
+ // description 'Owns payment authorization/capture'
230
+ // metadata { service 'payment-service' } // binds the box to services/<id>/
231
+ // }
232
+ //
233
+ // and give each call the operationId it uses, so requirements, C4 and OpenAPI
234
+ // can be cross-checked:
235
+ //
236
+ // checkoutWeb -> paymentService 'Authorizes' {
237
+ // metadata { op 'authorizePayment' }
238
+ // }
239
+ //
240
+ // The \`views\` block at the bottom is for LikeC4's own tooling — loam reads
241
+ // the model and never a view. Render with \`npx likec4 start\` from the docs
63
242
  // repo root, which likec4.config.json scopes to this directory. (A service
64
243
  // model or a feature delta renders from its OWN directory, e.g.
65
244
  // \`npx likec4 start services/<id>\`: each declares its own \`specification\`
@@ -113,19 +292,50 @@ views {
113
292
  include * -> element.tag = #platform
114
293
  }
115
294
  }
116
- `;
295
+ `,
296
+ ];
117
297
  /**
118
- * Whether the fleet map is still the scaffold's own bytes — the sentinel
298
+ * The one thing loam itself ever writes ABOVE the stub: `core/docs.ts`'s
299
+ * `landscapePreamble`, spliced in as `${preamble.trimEnd()}\n//\n` — comment
300
+ * lines, closed by a bare `//` that separates the caller's sentence from the
301
+ * scaffold's own opening paragraph. `migrate-openspec` is the only caller.
302
+ *
303
+ * Matched as a SHAPE rather than as the migrator's exact sentence because the
304
+ * preamble is a parameter, and because the alternative is a second copy of that
305
+ * sentence in a package `core/scaffold/` must not import from
306
+ * (`commands/migrate-openspec/materialize/target.ts`). The shape is tight where
307
+ * it has to be: the closing bare `//` is what a hand-written note above the map
308
+ * does not have.
309
+ */
310
+ const LOAM_PREAMBLE = /^(?:\/\/[^\n]*\n)*\/\/\n$/;
311
+ /**
312
+ * Whether the fleet map is still a scaffold's own bytes — the sentinel
119
313
  * doctrine of core/coherence/authoring/sentinels.ts: the writer above and this
120
- * checker read ONE string, so they cannot drift apart. `endsWith`, not
121
- * equality, because `migrate-openspec` prepends a `landscapePreamble` to the
122
- * identical stub; CRLF-normalised because a checkout under core.autocrlf is
123
- * still untouched. A hand-edit inside the stub body breaks the suffix match —
124
- * the safe failure mode is a false "authored", which merely drops one teaching
125
- * step from `loam status`'s empty-fleet ladder (core/status/fleet/next.ts).
314
+ * checker read ONE string, so they cannot drift apart. CRLF-normalised because
315
+ * a checkout under core.autocrlf is still untouched. Every generation loam ever
316
+ * wrote counts, not just the current one — see `LEGACY_LANDSCAPE_STUBS`. A
317
+ * hand-edit inside the stub body breaks the match — the safe failure mode is a
318
+ * false "authored", which merely drops one teaching step from `loam status`'s
319
+ * empty-fleet ladder (core/status/fleet/next.ts).
320
+ *
321
+ * ANCHORED AT BOTH ENDS, past a preamble loam wrote itself. This was a bare
322
+ * `endsWith`, so anything a person put ABOVE the stub was invisible to the
323
+ * guard: `loam seed` overwrote a two-line "OWNED BY THE ARCHITECTURE GUILD — do
324
+ * not regenerate" header while printing "replaced the scaffold's untouched
325
+ * stub", against its own promise that "Seed never overwrites human work"
326
+ * (`commands/seed/commit.ts`; verification 2026-09-04, second pass). The suffix
327
+ * match existed for the migration preamble alone, so that is now the only
328
+ * prefix accepted, and everything else above the stub makes the file authored.
126
329
  */
127
330
  export function isLandscapeStub(content) {
128
331
  const normal = (s) => s.replace(/\r\n/g, "\n").trimEnd();
129
- return normal(content).endsWith(normal(LANDSCAPE_STUB));
332
+ const seen = normal(content);
333
+ return [LANDSCAPE_STUB, ...LEGACY_LANDSCAPE_STUBS].some((stub) => {
334
+ const body = normal(stub);
335
+ if (!seen.endsWith(body))
336
+ return false;
337
+ const prefix = seen.slice(0, seen.length - body.length);
338
+ return prefix === "" || LOAM_PREAMBLE.test(prefix);
339
+ });
130
340
  }
131
341
  //# sourceMappingURL=landscape.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"landscape.js","sourceRoot":"","sources":["../../../src/core/scaffold/landscape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0E7B,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,MAAM,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;IACzE,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["/**\n * The bytes a brand-new docs repo's fleet map starts with, and the one\n * question anybody asks of them afterwards: has a human touched this yet?\n *\n * Split out of `../docs.ts` when the README joined the scaffold and the file\n * reached its line limit, and the seam was already there: `docs.ts` says what\n * a docs repo is MADE OF — which directories, which files, in what order —\n * while this package holds the CONTENT of the authored templates it lays down.\n * The two change for different reasons: a new artifact edits the first, better\n * advice to a new fleet edits the second.\n */\n\n/**\n * The fleet map, empty but valid.\n *\n * Nobody writes this file automatically — `adopt` deliberately does not touch\n * it, because who calls whom is a human judgement and a generated landscape\n * would be a guess presented as the map. But \"nobody writes it\" used to mean\n * \"it is simply absent\", and an absent landscape is the one artifact whose\n * absence silences the fleet-wide checks entirely: every cross-service breach\n * `validate` exists to find is invisible on a repo with no landscape.\n *\n * So the scaffold lays down the empty map instead: the four element kinds a\n * fleet actually uses are declared, the model is empty, and the comments say\n * what to add and why. The first `loam adopt` then has somewhere to be drawn.\n *\n * A `views` block IS scaffolded — reversing the decision this comment used to\n * record, which was: no views, because computing one is superlinear in edge\n * count and a scaffolded `include *` handed every repo a fleet-sized bill for\n * the first tool that renders. That cost was real and still is; what changed\n * is the shape of the block. A fleet map stops being readable at the third\n * service unless platform infrastructure is split out, and the split's\n * `exclude element.tag = #platform` is exactly the\n * pruning that keeps the fleet view's render affordable — the scaffold now\n * pays the old objection instead of ignoring it. loam itself still reads\n * elements and relationships and never a view. The platform view's predicate\n * is scaffolded because it is the line users mistype: the obvious spelling\n * draws boxes with no edges.\n * The `views` block below has a deliberate second copy: `core/c4/seed/template.ts` emits the\n * same two views into a seeded landscape (two strikes; extract on a third), and test/seed.test.ts\n * compares the two blocks byte for byte — comments included — so whichever copy drifts fails.\n */\nexport const LANDSCAPE_STUB = `// The fleet map: every service in services/ appears here, and every call\n// between two of them is an edge. This file is written by hand — loam never\n// guesses it, because \"who calls whom\" is the one fact no generator can read\n// off a repository.\n//\n// After \\`loam adopt --service <id>\\`, add the service here:\n//\n// paymentService = softwareSystem 'payment-service' {\n// description 'Owns payment authorization/capture'\n// metadata { service 'payment-service' } // binds the box to services/<id>/\n// }\n//\n// and give each call the operationId it uses, so requirements, C4 and OpenAPI\n// can be cross-checked:\n//\n// checkoutWeb -> paymentService 'Authorizes' {\n// metadata { op 'authorizePayment' }\n// }\n//\n// The \\`views\\` block at the bottom is for LikeC4's own tooling — loam reads\n// the model and never a view. Render with \\`npx likec4 start\\` from the docs\n// repo root, which likec4.config.json scopes to this directory. (A service\n// model or a feature delta renders from its OWN directory, e.g.\n// \\`npx likec4 start services/<id>\\`: each declares its own \\`specification\\`\n// block, so the renderer can only be given one of them at a time.) Keep views\n// scoped, because computing one is superlinear in the number of edges — the\n// \\`fleet\\` view below stays affordable precisely because it excludes the\n// platform hubs.\n//\n// One shape is worth getting right before the fleet is drawn: a shared broker.\n// Kafka as a single element becomes the node every service points at, and any\n// view over it is a star nobody can read. Model the TOPIC instead — a \\`topic\\`\n// nested inside the broker's element, edges pointing at \\`kafka.<topic>\\` — which\n// splits one hub of degree sixty into a dozen small ones and is the truer model\n// besides. Tag the KIND, not just the broker: LikeC4 does not inherit tags, so a\n// topic under an \\`#external\\` broker is not external itself and \\`validate\\` asks\n// for a services/ directory nobody owes.\n//\n// specification {\n// element topic {\n// #external\n// style { shape queue }\n// }\n// }\n\nspecification {\n element person\n element softwareSystem\n element container\n element database\n\n // Ubiquitous infrastructure — logging Kafka, auth, service discovery — takes\n // one inbound edge per service, and by the third service the fleet view is a\n // hairball. Tag those elements #platform: the fleet view excludes them\n // without losing \"who depends on the Identity Provider\", which the platform\n // view answers.\n tag platform\n}\n\nmodel {\n}\n\nviews {\n view fleet {\n include *\n exclude element.tag = #platform\n }\n // The obvious spelling — \\`include element.tag = #platform\\` — draws the\n // platform boxes with NO edges and no consumers. The predicate that works is\n // the relationship form below.\n view platform {\n include * -> element.tag = #platform\n }\n}\n`;\n\n/**\n * Whether the fleet map is still the scaffold's own bytes — the sentinel\n * doctrine of core/coherence/authoring/sentinels.ts: the writer above and this\n * checker read ONE string, so they cannot drift apart. `endsWith`, not\n * equality, because `migrate-openspec` prepends a `landscapePreamble` to the\n * identical stub; CRLF-normalised because a checkout under core.autocrlf is\n * still untouched. A hand-edit inside the stub body breaks the suffix match —\n * the safe failure mode is a false \"authored\", which merely drops one teaching\n * step from `loam status`'s empty-fleet ladder (core/status/fleet/next.ts).\n */\nexport function isLandscapeStub(content: string): boolean {\n const normal = (s: string): string => s.replace(/\\r\\n/g, \"\\n\").trimEnd();\n return normal(content).endsWith(normal(LANDSCAPE_STUB));\n}\n"]}
1
+ {"version":3,"file":"landscape.js","sourceRoot":"","sources":["../../../src/core/scaffold/landscape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+E7B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB;IACvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCD;IACC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCD;IACC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0ED;CACA,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAElD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,MAAM,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;IACzE,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7B,OAAO,CAAC,cAAc,EAAE,GAAG,sBAAsB,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QAC/D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;QACxD,OAAO,MAAM,KAAK,EAAE,IAAI,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * The bytes a brand-new docs repo's fleet map starts with, and the one\n * question anybody asks of them afterwards: has a human touched this yet?\n *\n * Split out of `../docs.ts` when the README joined the scaffold and the file\n * reached its line limit, and the seam was already there: `docs.ts` says what\n * a docs repo is MADE OF — which directories, which files, in what order —\n * while this package holds the CONTENT of the authored templates it lays down.\n * The two change for different reasons: a new artifact edits the first, better\n * advice to a new fleet edits the second.\n */\n\n/**\n * The fleet map, empty but valid.\n *\n * Nobody writes this file automatically — `adopt` deliberately does not touch\n * it, because who calls whom is a human judgement and a generated landscape\n * would be a guess presented as the map. But \"nobody writes it\" used to mean\n * \"it is simply absent\", and an absent landscape is the one artifact whose\n * absence silences the fleet-wide checks entirely: every cross-service breach\n * `validate` exists to find is invisible on a repo with no landscape.\n *\n * So the scaffold lays down the empty map instead: the four element kinds a\n * fleet actually uses are declared, the model is empty, and the comments say\n * what to add and why. The first `loam adopt` then has somewhere to be drawn.\n *\n * A `views` block IS scaffolded — reversing the decision this comment used to\n * record, which was: no views, because computing one is superlinear in edge\n * count and a scaffolded `include *` handed every repo a fleet-sized bill for\n * the first tool that renders. That cost was real and still is; what changed\n * is the shape of the block. A fleet map stops being readable at the third\n * service unless platform infrastructure is split out, and the split's\n * `exclude element.tag = #platform` is exactly the\n * pruning that keeps the fleet view's render affordable — the scaffold now\n * pays the old objection instead of ignoring it. loam itself still reads\n * elements and relationships and never a view. The platform view's predicate\n * is scaffolded because it is the line users mistype: the obvious spelling\n * draws boxes with no edges.\n * The `views` block below has a deliberate second copy: `core/c4/seed/template.ts` emits the\n * same two views into a seeded landscape (two strikes; extract on a third), and test/seed.test.ts\n * compares the two blocks byte for byte — comments included — so whichever copy drifts fails.\n */\nexport const LANDSCAPE_STUB = `// The fleet map: every service in services/ appears here, and every call\n// between two of them is an edge. This file is written by hand — loam never\n// guesses it, because \"who calls whom\" is the one fact no generator can read\n// off a repository.\n//\n// After \\`loam adopt --service <id>\\`, add the service here:\n//\n// paymentService = softwareSystem 'payment-service' {\n// description 'Owns payment authorization/capture'\n// metadata { service 'payment-service' } // binds the box to services/<id>/\n// }\n//\n// and give each call the operationId it uses, so requirements, C4 and OpenAPI\n// can be cross-checked:\n//\n// checkoutWeb -> paymentService 'Authorizes' {\n// metadata { op 'authorizePayment' }\n// }\n//\n// The \\`views\\` block at the bottom is for LikeC4's own tooling — loam reads\n// the model and never a view. Render with \\`npx likec4 start\\` from the docs\n// repo root, which likec4.config.json scopes to this directory. A service's\n// \\`services/<id>/model.likec4\\` EXTENDS this map: it declares no\n// \\`specification\\` block of its own, so it belongs to THIS project and renders\n// with it, containers and all. A model that declares its own \\`specification\\`\n// stands alone instead — it renders only from its own directory\n// (\\`npx likec4 start services/<id>\\`), and the \\`exclude\\` list in the root\n// likec4.config.json has to cover that directory or its declarations collide\n// with this file's. \\`loam subsystem sync\\` maintains both, so neither is\n// hand-edited. Keep views\n// scoped, because computing one is superlinear in the number of edges — the\n// \\`fleet\\` view below stays affordable precisely because it excludes the\n// platform hubs.\n//\n// One shape is worth getting right before the fleet is drawn: a shared broker.\n// Kafka as a single element becomes the node every service points at, and any\n// view over it is a star nobody can read. Model the TOPIC instead — a \\`topic\\`\n// nested inside the broker's element, edges pointing at \\`kafka.<topic>\\` — which\n// splits one hub of degree sixty into a dozen small ones and is the truer model\n// besides. Tag the KIND, not just the broker: LikeC4 does not inherit tags, so a\n// topic under an \\`#external\\` broker is not external itself and \\`validate\\` asks\n// for a services/ directory nobody owes.\n//\n// specification {\n// element topic {\n// #external\n// style { shape queue }\n// }\n// }\n\nspecification {\n element person\n element softwareSystem\n element container\n element database\n\n // Ubiquitous infrastructure — logging Kafka, auth, service discovery — takes\n // one inbound edge per service, and by the third service the fleet view is a\n // hairball. Tag those elements #platform: the fleet view excludes them\n // without losing \"who depends on the Identity Provider\", which the platform\n // view answers.\n tag platform\n}\n\nmodel {\n}\n\nviews {\n view fleet {\n include *\n exclude element.tag = #platform\n }\n // The obvious spelling — \\`include element.tag = #platform\\` — draws the\n // platform boxes with NO edges and no consumers. The predicate that works is\n // the relationship form below.\n view platform {\n include * -> element.tag = #platform\n }\n}\n`;\n\n/**\n * Stubs earlier versions of loam wrote, FROZEN — never re-derived from the\n * constant above, or an edit to it would silently rewrite history.\n *\n * `isLandscapeStub` is a suffix match on the exact bytes, and `loam seed`\n * refuses (`seed-landscape-edited`) on a map it judges authored. So editing\n * the stub reclassifies every untouched older scaffold as hand-edited, in\n * repositories nobody has touched — the teaching step disappears from `loam\n * status` and `seed` refuses a map its own scaffold wrote. Each entry is one\n * shipped generation of the file, OLDEST FIRST, appended when the text above\n * changes — every generation loam ever wrote is here, not just the previous\n * one, since a repository scaffolded two releases ago is as untouched as one\n * scaffolded yesterday.\n *\n * Generation 1 (v0.1.0-beta.1, .2): the stub lived in core/docs.ts, carried no\n * `views` block at all (\"on purpose: loam reads the model and never a view\")\n * and no `tag platform`.\n * Generation 2 (v0.1.0-beta.3): the same file with the render paragraph\n * rewritten — still no `views` block, still no `tag platform`.\n * Generation 3 (v0.2.0-alpha.1 through .5): the `views` paragraph taught the\n * retired shape — that a service model \"renders from its OWN directory\" and\n * \"each declares its own `specification` block\" — which was written before an\n * extending model belonged to the root project.\n */\nexport const LEGACY_LANDSCAPE_STUBS: readonly string[] = [\n `// The fleet map: every service in services/ appears here, and every call\n// between two of them is an edge. This file is written by hand — loam never\n// guesses it, because \"who calls whom\" is the one fact no generator can read\n// off a repository.\n//\n// After \\`loam adopt --service <id>\\`, add the service here:\n//\n// paymentService = softwareSystem 'payment-service' {\n// description 'Owns payment authorization/capture'\n// metadata { service 'payment-service' } // binds the box to services/<id>/\n// }\n//\n// and give each call the operationId it uses, so requirements, C4 and OpenAPI\n// can be cross-checked:\n//\n// checkoutWeb -> paymentService 'Authorizes' {\n// metadata { op 'authorizePayment' }\n// }\n//\n// There is no \\`views\\` block, on purpose: loam reads the model and never a\n// view, so it would draw nothing. Add views here if you want diagrams, and\n// render them with LikeC4's own tooling (\\`npx likec4 start\\`) — but scope them,\n// because computing a view is superlinear in the number of edges and an\n// \\`include *\\` over a whole fleet takes minutes.\n\nspecification {\n element person\n element softwareSystem\n element container\n element database\n}\n\nmodel {\n}\n`,\n `// The fleet map: every service in services/ appears here, and every call\n// between two of them is an edge. This file is written by hand — loam never\n// guesses it, because \"who calls whom\" is the one fact no generator can read\n// off a repository.\n//\n// After \\`loam adopt --service <id>\\`, add the service here:\n//\n// paymentService = softwareSystem 'payment-service' {\n// description 'Owns payment authorization/capture'\n// metadata { service 'payment-service' } // binds the box to services/<id>/\n// }\n//\n// and give each call the operationId it uses, so requirements, C4 and OpenAPI\n// can be cross-checked:\n//\n// checkoutWeb -> paymentService 'Authorizes' {\n// metadata { op 'authorizePayment' }\n// }\n//\n// There is no \\`views\\` block, on purpose: loam reads the model and never a\n// view, so it would draw nothing. Add views here if you want diagrams, and\n// render them with LikeC4's own tooling — \\`npx likec4 start\\` from the docs\n// repo root, which likec4.config.json scopes to this directory. (A service\n// model or a feature delta renders from its OWN directory, e.g.\n// \\`npx likec4 start services/<id>\\`: each declares its own \\`specification\\`\n// block, so the renderer can only be given one of them at a time.) Scope your\n// views, because computing one is superlinear in the number of edges and an\n// \\`include *\\` over a whole fleet takes minutes.\n\nspecification {\n element person\n element softwareSystem\n element container\n element database\n}\n\nmodel {\n}\n`,\n `// The fleet map: every service in services/ appears here, and every call\n// between two of them is an edge. This file is written by hand — loam never\n// guesses it, because \"who calls whom\" is the one fact no generator can read\n// off a repository.\n//\n// After \\`loam adopt --service <id>\\`, add the service here:\n//\n// paymentService = softwareSystem 'payment-service' {\n// description 'Owns payment authorization/capture'\n// metadata { service 'payment-service' } // binds the box to services/<id>/\n// }\n//\n// and give each call the operationId it uses, so requirements, C4 and OpenAPI\n// can be cross-checked:\n//\n// checkoutWeb -> paymentService 'Authorizes' {\n// metadata { op 'authorizePayment' }\n// }\n//\n// The \\`views\\` block at the bottom is for LikeC4's own tooling — loam reads\n// the model and never a view. Render with \\`npx likec4 start\\` from the docs\n// repo root, which likec4.config.json scopes to this directory. (A service\n// model or a feature delta renders from its OWN directory, e.g.\n// \\`npx likec4 start services/<id>\\`: each declares its own \\`specification\\`\n// block, so the renderer can only be given one of them at a time.) Keep views\n// scoped, because computing one is superlinear in the number of edges — the\n// \\`fleet\\` view below stays affordable precisely because it excludes the\n// platform hubs.\n//\n// One shape is worth getting right before the fleet is drawn: a shared broker.\n// Kafka as a single element becomes the node every service points at, and any\n// view over it is a star nobody can read. Model the TOPIC instead — a \\`topic\\`\n// nested inside the broker's element, edges pointing at \\`kafka.<topic>\\` — which\n// splits one hub of degree sixty into a dozen small ones and is the truer model\n// besides. Tag the KIND, not just the broker: LikeC4 does not inherit tags, so a\n// topic under an \\`#external\\` broker is not external itself and \\`validate\\` asks\n// for a services/ directory nobody owes.\n//\n// specification {\n// element topic {\n// #external\n// style { shape queue }\n// }\n// }\n\nspecification {\n element person\n element softwareSystem\n element container\n element database\n\n // Ubiquitous infrastructure — logging Kafka, auth, service discovery — takes\n // one inbound edge per service, and by the third service the fleet view is a\n // hairball. Tag those elements #platform: the fleet view excludes them\n // without losing \"who depends on the Identity Provider\", which the platform\n // view answers.\n tag platform\n}\n\nmodel {\n}\n\nviews {\n view fleet {\n include *\n exclude element.tag = #platform\n }\n // The obvious spelling — \\`include element.tag = #platform\\` — draws the\n // platform boxes with NO edges and no consumers. The predicate that works is\n // the relationship form below.\n view platform {\n include * -> element.tag = #platform\n }\n}\n`,\n];\n\n/**\n * The one thing loam itself ever writes ABOVE the stub: `core/docs.ts`'s\n * `landscapePreamble`, spliced in as `${preamble.trimEnd()}\\n//\\n` — comment\n * lines, closed by a bare `//` that separates the caller's sentence from the\n * scaffold's own opening paragraph. `migrate-openspec` is the only caller.\n *\n * Matched as a SHAPE rather than as the migrator's exact sentence because the\n * preamble is a parameter, and because the alternative is a second copy of that\n * sentence in a package `core/scaffold/` must not import from\n * (`commands/migrate-openspec/materialize/target.ts`). The shape is tight where\n * it has to be: the closing bare `//` is what a hand-written note above the map\n * does not have.\n */\nconst LOAM_PREAMBLE = /^(?:\\/\\/[^\\n]*\\n)*\\/\\/\\n$/;\n\n/**\n * Whether the fleet map is still a scaffold's own bytes — the sentinel\n * doctrine of core/coherence/authoring/sentinels.ts: the writer above and this\n * checker read ONE string, so they cannot drift apart. CRLF-normalised because\n * a checkout under core.autocrlf is still untouched. Every generation loam ever\n * wrote counts, not just the current one — see `LEGACY_LANDSCAPE_STUBS`. A\n * hand-edit inside the stub body breaks the match — the safe failure mode is a\n * false \"authored\", which merely drops one teaching step from `loam status`'s\n * empty-fleet ladder (core/status/fleet/next.ts).\n *\n * ANCHORED AT BOTH ENDS, past a preamble loam wrote itself. This was a bare\n * `endsWith`, so anything a person put ABOVE the stub was invisible to the\n * guard: `loam seed` overwrote a two-line \"OWNED BY THE ARCHITECTURE GUILD — do\n * not regenerate\" header while printing \"replaced the scaffold's untouched\n * stub\", against its own promise that \"Seed never overwrites human work\"\n * (`commands/seed/commit.ts`; verification 2026-09-04, second pass). The suffix\n * match existed for the migration preamble alone, so that is now the only\n * prefix accepted, and everything else above the stub makes the file authored.\n */\nexport function isLandscapeStub(content: string): boolean {\n const normal = (s: string): string => s.replace(/\\r\\n/g, \"\\n\").trimEnd();\n const seen = normal(content);\n return [LANDSCAPE_STUB, ...LEGACY_LANDSCAPE_STUBS].some((stub) => {\n const body = normal(stub);\n if (!seen.endsWith(body)) return false;\n const prefix = seen.slice(0, seen.length - body.length);\n return prefix === \"\" || LOAM_PREAMBLE.test(prefix);\n });\n}\n"]}