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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +947 -0
  2. package/MIGRATING-from-OpenSpec.md +7 -1
  3. package/QUICKSTART.md +8 -3
  4. package/README.md +20 -13
  5. package/ROADMAP.md +329 -22
  6. package/SCHEMA.md +472 -24
  7. package/WORKFLOW.md +23 -10
  8. package/dist/commands/adopt/render.js +47 -8
  9. package/dist/commands/adopt/render.js.map +1 -1
  10. package/dist/commands/archive/plan/landscape.js +45 -1
  11. package/dist/commands/archive/plan/landscape.js.map +1 -1
  12. package/dist/commands/archive/plan/model/extending.js +85 -0
  13. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  14. package/dist/commands/archive/run.js +5 -2
  15. package/dist/commands/archive/run.js.map +1 -1
  16. package/dist/commands/context/print.js +11 -1
  17. package/dist/commands/context/print.js.map +1 -1
  18. package/dist/commands/doctor.js +46 -0
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/explore.js +12 -2
  21. package/dist/commands/explore.js.map +1 -1
  22. package/dist/commands/list/views.js +5 -1
  23. package/dist/commands/list/views.js.map +1 -1
  24. package/dist/commands/seed/plan.js +97 -3
  25. package/dist/commands/seed/plan.js.map +1 -1
  26. package/dist/commands/seed/seed.js +7 -2
  27. package/dist/commands/seed/seed.js.map +1 -1
  28. package/dist/commands/show/feature.js +2 -1
  29. package/dist/commands/show/feature.js.map +1 -1
  30. package/dist/commands/show/marks.js +3 -5
  31. package/dist/commands/show/marks.js.map +1 -1
  32. package/dist/commands/show/service.js +39 -18
  33. package/dist/commands/show/service.js.map +1 -1
  34. package/dist/commands/subsystem/sync.js +184 -39
  35. package/dist/commands/subsystem/sync.js.map +1 -1
  36. package/dist/commands/subsystem/txn/exclude.js +132 -0
  37. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  38. package/dist/commands/subsystem/txn/report.js +124 -0
  39. package/dist/commands/subsystem/txn/report.js.map +1 -0
  40. package/dist/commands/subsystem/txn/txn.js +29 -3
  41. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  42. package/dist/commands/subsystem/txn/views.js +113 -16
  43. package/dist/commands/subsystem/txn/views.js.map +1 -1
  44. package/dist/commands/validate/arch-coverage.js +16 -9
  45. package/dist/commands/validate/arch-coverage.js.map +1 -1
  46. package/dist/commands/validate/checks/fleet-shape.js +30 -77
  47. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  48. package/dist/commands/validate/checks/vocabulary.js +4 -19
  49. package/dist/commands/validate/checks/vocabulary.js.map +1 -1
  50. package/dist/commands/validate/feature.js +1 -1
  51. package/dist/commands/validate/feature.js.map +1 -1
  52. package/dist/commands/validate/fleet/census.js +11 -10
  53. package/dist/commands/validate/fleet/census.js.map +1 -1
  54. package/dist/commands/validate/fleet/landscape.js +128 -114
  55. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  56. package/dist/commands/validate/fleet/load.js +66 -0
  57. package/dist/commands/validate/fleet/load.js.map +1 -1
  58. package/dist/commands/validate/fleet/map/attest.js +103 -0
  59. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  60. package/dist/commands/validate/fleet/map/bindings.js +96 -0
  61. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  62. package/dist/commands/validate/fleet/map/consumers.js +298 -0
  63. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  64. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  65. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  66. package/dist/commands/validate/fleet/map/usecases.js +56 -0
  67. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  68. package/dist/commands/validate/fleet/usecases/steps.js +49 -17
  69. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  70. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  71. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  72. package/dist/commands/validate/fleet/views/ids.js +13 -9
  73. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  74. package/dist/commands/validate/fleet/views/projects.js +278 -0
  75. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  76. package/dist/commands/validate/fleet/views/root-documents.js +97 -0
  77. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  78. package/dist/commands/validate/fleet/views/stale.js +2 -2
  79. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  80. package/dist/commands/validate/service/model/diverged.js +226 -0
  81. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  82. package/dist/commands/validate/service/model/grade.js +163 -0
  83. package/dist/commands/validate/service/model/grade.js.map +1 -0
  84. package/dist/commands/validate/service/service.js +110 -34
  85. package/dist/commands/validate/service/service.js.map +1 -1
  86. package/dist/commands/validate/service/specs.js +39 -5
  87. package/dist/commands/validate/service/specs.js.map +1 -1
  88. package/dist/commands/validate/service/spine.js +41 -3
  89. package/dist/commands/validate/service/spine.js.map +1 -1
  90. package/dist/commands/validate/service/usecases/tags.js +113 -0
  91. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  92. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  93. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  94. package/dist/commands/validate/validate.js +10 -24
  95. package/dist/commands/validate/validate.js.map +1 -1
  96. package/dist/core/agent/agents-md/artifacts.js +1 -0
  97. package/dist/core/agent/agents-md/artifacts.js.map +1 -1
  98. package/dist/core/agent/agents-md/command-map.js +18 -2
  99. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  100. package/dist/core/agent/agents-md/cycle.js +24 -9
  101. package/dist/core/agent/agents-md/cycle.js.map +1 -1
  102. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  103. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  104. package/dist/core/agent/agents-md/map/subsystem.js +51 -2
  105. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  106. package/dist/core/agent/agents-md/map/usecases.js +45 -3
  107. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  108. package/dist/core/agent/agents-md.js +14 -4
  109. package/dist/core/agent/agents-md.js.map +1 -1
  110. package/dist/core/agent/support/report.js +39 -5
  111. package/dist/core/agent/support/report.js.map +1 -1
  112. package/dist/core/agent/workflows/adopt.js +125 -6
  113. package/dist/core/agent/workflows/adopt.js.map +1 -1
  114. package/dist/core/agent/workflows/check.js +35 -19
  115. package/dist/core/agent/workflows/check.js.map +1 -1
  116. package/dist/core/agent/workflows/closing.js +3 -1
  117. package/dist/core/agent/workflows/closing.js.map +1 -1
  118. package/dist/core/brief/brief.js +63 -11
  119. package/dist/core/brief/brief.js.map +1 -1
  120. package/dist/core/brief/checks.js +138 -3
  121. package/dist/core/brief/checks.js.map +1 -1
  122. package/dist/core/brief/landscape.js +212 -129
  123. package/dist/core/brief/landscape.js.map +1 -1
  124. package/dist/core/brief/map/extend.js +81 -0
  125. package/dist/core/brief/map/extend.js.map +1 -0
  126. package/dist/core/brief/map/owed.js +284 -0
  127. package/dist/core/brief/map/owed.js.map +1 -0
  128. package/dist/core/brief/targets.js +30 -26
  129. package/dist/core/brief/targets.js.map +1 -1
  130. package/dist/core/brief/unchecked.js +12 -2
  131. package/dist/core/brief/unchecked.js.map +1 -1
  132. package/dist/core/c4/likec4.js +23 -1
  133. package/dist/core/c4/likec4.js.map +1 -1
  134. package/dist/core/c4/parsed/deployment.js +5 -3
  135. package/dist/core/c4/parsed/deployment.js.map +1 -1
  136. package/dist/core/c4/parsed/view-ids.js +39 -1
  137. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  138. package/dist/core/c4/prefetch/fleet.js +142 -0
  139. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  140. package/dist/core/c4/project/architecture.js +52 -4
  141. package/dist/core/c4/project/architecture.js.map +1 -1
  142. package/dist/core/c4/project/batch.js +140 -0
  143. package/dist/core/c4/project/batch.js.map +1 -0
  144. package/dist/core/c4/project/documents.js +9 -4
  145. package/dist/core/c4/project/documents.js.map +1 -1
  146. package/dist/core/c4/project/load.js +77 -38
  147. package/dist/core/c4/project/load.js.map +1 -1
  148. package/dist/core/c4/project/staged.js +46 -12
  149. package/dist/core/c4/project/staged.js.map +1 -1
  150. package/dist/core/c4/resolve/attested.js +0 -0
  151. package/dist/core/c4/resolve/attested.js.map +1 -0
  152. package/dist/core/c4/resolve/service.js +8 -17
  153. package/dist/core/c4/resolve/service.js.map +1 -1
  154. package/dist/core/c4/resolve/steps.js +18 -0
  155. package/dist/core/c4/resolve/steps.js.map +1 -1
  156. package/dist/core/c4/root-project/exclude.js +352 -0
  157. package/dist/core/c4/root-project/exclude.js.map +1 -0
  158. package/dist/core/c4/service-model/documents.js +47 -0
  159. package/dist/core/c4/service-model/documents.js.map +1 -0
  160. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  161. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  162. package/dist/core/c4/service-model/load.js +165 -0
  163. package/dist/core/c4/service-model/load.js.map +1 -0
  164. package/dist/core/c4/service-model/renderer.js +128 -0
  165. package/dist/core/c4/service-model/renderer.js.map +1 -0
  166. package/dist/core/c4/service-model/shape.js +126 -0
  167. package/dist/core/c4/service-model/shape.js.map +1 -0
  168. package/dist/core/c4/service-model/slice.js +188 -0
  169. package/dist/core/c4/service-model/slice.js.map +1 -0
  170. package/dist/core/c4/source-scan.js +26 -0
  171. package/dist/core/c4/source-scan.js.map +1 -1
  172. package/dist/core/c4/splice/authored-source.js +60 -13
  173. package/dist/core/c4/splice/authored-source.js.map +1 -1
  174. package/dist/core/c4/splice/contract.js.map +1 -1
  175. package/dist/core/c4/splice/identity/declared.js +87 -0
  176. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  177. package/dist/core/c4/splice/identity/edges.js +17 -0
  178. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  179. package/dist/core/c4/splice/identity/existing.js +80 -0
  180. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  181. package/dist/core/c4/splice/identity/interior.js +80 -0
  182. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  183. package/dist/core/c4/splice/identity/owner.js +50 -0
  184. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  185. package/dist/core/c4/splice/landscape-merge.js +154 -100
  186. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  187. package/dist/core/c4/splice/model/merge.js +140 -0
  188. package/dist/core/c4/splice/model/merge.js.map +1 -0
  189. package/dist/core/c4/splice/placement.js +14 -4
  190. package/dist/core/c4/splice/placement.js.map +1 -1
  191. package/dist/core/c4/workspace.js +2 -1
  192. package/dist/core/c4/workspace.js.map +1 -1
  193. package/dist/core/coherence/coherence.js +7 -0
  194. package/dist/core/coherence/coherence.js.map +1 -1
  195. package/dist/core/delta/delta.js +9 -0
  196. package/dist/core/delta/delta.js.map +1 -1
  197. package/dist/core/deployment/delta.js +4 -0
  198. package/dist/core/deployment/delta.js.map +1 -1
  199. package/dist/core/diff/base-state.js +21 -5
  200. package/dist/core/diff/base-state.js.map +1 -1
  201. package/dist/core/diff/victims.js +10 -1
  202. package/dist/core/diff/victims.js.map +1 -1
  203. package/dist/core/docs.js +31 -22
  204. package/dist/core/docs.js.map +1 -1
  205. package/dist/core/doctor/config.js +7 -3
  206. package/dist/core/doctor/config.js.map +1 -1
  207. package/dist/core/doctor/doctor.js +48 -9
  208. package/dist/core/doctor/doctor.js.map +1 -1
  209. package/dist/core/doctor/report.js.map +1 -1
  210. package/dist/core/doctor/reports/scan.js +242 -0
  211. package/dist/core/doctor/reports/scan.js.map +1 -0
  212. package/dist/core/explain/families.js +1 -1
  213. package/dist/core/explain/families.js.map +1 -1
  214. package/dist/core/explain/refusals.js +1 -1
  215. package/dist/core/explain/refusals.js.map +1 -1
  216. package/dist/core/explore/explore.js +44 -9
  217. package/dist/core/explore/explore.js.map +1 -1
  218. package/dist/core/fleet-context.js +47 -0
  219. package/dist/core/fleet-context.js.map +1 -1
  220. package/dist/core/gate/partners.js +5 -9
  221. package/dist/core/gate/partners.js.map +1 -1
  222. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  223. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  224. package/dist/core/pack/living.js +47 -13
  225. package/dist/core/pack/living.js.map +1 -1
  226. package/dist/core/pack/pack.js +9 -6
  227. package/dist/core/pack/pack.js.map +1 -1
  228. package/dist/core/projection/arch-slice.js +2 -2
  229. package/dist/core/projection/arch-slice.js.map +1 -1
  230. package/dist/core/repo/paths.js +40 -0
  231. package/dist/core/repo/paths.js.map +1 -1
  232. package/dist/core/repo/tree/render/projects.js +182 -0
  233. package/dist/core/repo/tree/render/projects.js.map +1 -0
  234. package/dist/core/repo/tree/render/stale.js +2 -3
  235. package/dist/core/repo/tree/render/stale.js.map +1 -1
  236. package/dist/core/repo/tree/render/views.js +51 -3
  237. package/dist/core/repo/tree/render/views.js.map +1 -1
  238. package/dist/core/repo/tree/walk.js +45 -2
  239. package/dist/core/repo/tree/walk.js.map +1 -1
  240. package/dist/core/scaffold/landscape.js +219 -9
  241. package/dist/core/scaffold/landscape.js.map +1 -1
  242. package/dist/core/staging/writes.js.map +1 -1
  243. package/dist/core/usecases/delta/claims.js +1 -0
  244. package/dist/core/usecases/delta/claims.js.map +1 -1
  245. package/dist/core/usecases/delta/overlay.js +4 -0
  246. package/dist/core/usecases/delta/overlay.js.map +1 -1
  247. package/dist/core/usecases/fleet.js +18 -6
  248. package/dist/core/usecases/fleet.js.map +1 -1
  249. package/dist/core/usecases/operations.js +7 -0
  250. package/dist/core/usecases/operations.js.map +1 -1
  251. package/dist/core/usecases/service/exclude.js +119 -0
  252. package/dist/core/usecases/service/exclude.js.map +1 -0
  253. package/dist/core/usecases/service/flows.js +161 -0
  254. package/dist/core/usecases/service/flows.js.map +1 -0
  255. package/dist/core/vocabulary/maturity.js +10 -0
  256. package/dist/core/vocabulary/maturity.js.map +1 -1
  257. package/examples/README.md +23 -0
  258. package/examples/docs/architecture/landscape.likec4 +41 -0
  259. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  260. package/examples/docs/likec4.config.json +0 -1
  261. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  262. package/examples/docs/services/order-service/arch.spec.md +13 -6
  263. package/examples/docs/services/order-service/model.likec4 +53 -67
  264. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  265. package/examples/docs/services/payment-service/model.likec4 +32 -33
  266. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  267. package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
  268. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  269. package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
  270. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exclude.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/exclude.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The ROOT `likec4.config.json`'s `exclude` list, as `loam subsystem sync`\n * derives it from the shapes on disk.\n *\n * It sits beside `./views.ts` for that module's reason and by the same seam:\n * both answer \"what should this renderer-only file say, given the tree?\", both\n * are pure derivations one command writes, and keeping them together is what\n * stops the writer and the grader spelling one rule twice. The difference is\n * ownership, and it is stated in `core/c4/service-model/renderer.ts`: every byte\n * of the generated views file is loam's, while this file is the team's and loam\n * maintains exactly the `services/` entries in it. What an entry COVERS is\n * `core/c4/root-project/exclude.ts`'s question, asked of the directory the entry\n * names rather than of its spelling.\n *\n * It is a module rather than three functions in `../sync.ts` because that file\n * reached its line limit carrying them, and this is the phase boundary inside\n * it: `sync.ts` is the lock, the transaction and the report; this is one of the\n * three things it decides to write.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { excludingEntry, parseRootProject, readRootExclude } from \"../../../core/c4/root-project/exclude.js\";\nimport { standaloneExclude } from \"../../../core/c4/service-model/renderer.js\";\nimport { serviceTreePath, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { rootProjectPath } from \"../../../core/repo/paths.js\";\nimport type { SurveyedModel } from \"../../../core/repo/tree/render/projects.js\";\nimport type { WalkedService } from \"../../../core/repo/tree/walk.js\";\nimport { planWrite, type PlannedWrite } from \"../../../core/staging/writes.js\";\n\n/** What one sync did to the ROOT project's `exclude` list. */\nexport interface ExcludePlan {\n /** Whether this run rewrites the list. */\n updated: boolean;\n /** The list as it stands after this run — the current one when nothing changed. */\n entries: string[];\n added: string[];\n removed: string[];\n /** The rewrite, staged with everything else. Null when nothing is owed. */\n write: PlannedWrite | null;\n /** The root file exists and loam could not read an `exclude` list out of it — reported, never rewritten. */\n unreadable: boolean;\n}\n\n/** The answer for a run that has no root file to read, and the shape every other arm fills. */\nexport const NO_EXCLUDE: ExcludePlan = {\n updated: false,\n entries: [],\n added: [],\n removed: [],\n write: null,\n unreadable: false,\n};\n\n/**\n * The root project's `exclude` list, recomputed from the shapes on disk.\n *\n * WHAT MAKES THIS SAFE TO RUN ON EVERY SYNC is the split `standaloneExclude`\n * enforces: the `services/` half of the list is loam's and every other entry is\n * the team's, kept in order. The scaffold's node_modules glob, `features/**`, a\n * team's own `drafts/**` all survive; only the entries that decide whether a\n * model is renderable are derived. A standalone model MUST be excluded (inside\n * the root project every kind it declares is a duplicate blamed on the map as\n * well) and an extending model must NOT be (the root project is the only place\n * it parses), so a service that migrates from one shape to the other has its\n * entry added or dropped on the next sync instead of quietly blanking a project.\n *\n * THE GATE keeps an old repository untouched. A fleet whose models all stand\n * alone and whose root already says `services/**` is already correct, and\n * rewriting it into one entry per service would be a diff with no fact behind\n * it — so the rewrite happens only when at least one model extends the map (the\n * blanket exclusion is then wrong) or some standalone model is not covered at\n * all (the renderer is merging it into the map right now). Comparing the parsed\n * ARRAYS rather than the bytes is the second half of the same rule: a file whose\n * `exclude` already says what this run would write is left exactly as the team\n * formatted it.\n *\n * A root config loam cannot read an `exclude` list out of is REPORTED and never\n * rewritten. Guessing at the JSON a team hand-wrote is how a writer destroys a\n * file it did not understand.\n */\nexport async function planExclude(\n docsDir: DocsDir,\n services: readonly WalkedService[],\n models: readonly SurveyedModel<WalkedService>[],\n): Promise<ExcludePlan> {\n const current = await readRootExclude(docsDir);\n if (current === null) return { ...NO_EXCLUDE, unreadable: true };\n const standalone = models.filter((m) => m.standalone).map((m) => serviceTreePath(m.service));\n // Both halves of the shape scan travel, not just the standalone one: an entry\n // is loam's to take back either because loam wrote it (a tree the enumeration\n // returns) or because it HIDES a model that only parses in the root project,\n // and only the extending list can answer the second question. Without it a\n // subsystem-wide `services/platform/**` survived every sync while\n // `service.model-excluded` named that same sync as the repair.\n const extending = models.filter((m) => !m.standalone).map((m) => serviceTreePath(m.service));\n const wanted = standaloneExclude(current, {\n standalone,\n extending,\n enumerated: services.map((svc) => serviceTreePath(svc)),\n });\n const uncovered = standalone.some((tree) => excludingEntry(current, tree) === null);\n if (extending.length === 0 && !uncovered) return { ...NO_EXCLUDE, entries: [...current] };\n if (current.length === wanted.length && current.every((entry, i) => entry === wanted[i])) {\n return { ...NO_EXCLUDE, entries: wanted };\n }\n const bytes = await rewrittenRootProject(docsDir, wanted);\n if (bytes === null) return { ...NO_EXCLUDE, entries: [...current], unreadable: true };\n return {\n updated: true,\n entries: wanted,\n added: wanted.filter((entry) => !current.includes(entry)),\n removed: current.filter((entry) => !wanted.includes(entry)),\n // `planWrite` decides create-vs-overwrite from the filesystem, so the root\n // file — which by definition already exists here — is staged as an\n // OVERWRITE, and `swapStaged` compares it against its pre-image immediately\n // before the rename: a team member editing the same file during the sync\n // stops the commit instead of having their bytes buried.\n write: planWrite(rootProjectPath(docsDir), bytes),\n unreadable: false,\n };\n}\n\n/**\n * The root project file with its `exclude` replaced and NOTHING else touched —\n * every other key kept, in the order the team wrote them, re-serialised as\n * 2-space JSON with a trailing newline (`core/docs.ts`'s own form, so a\n * scaffolded file round-trips byte for byte).\n *\n * Null when the file stopped being a JSON object between `readRootExclude` and\n * here, which is a concurrent edit rather than a state loam may write over.\n */\nasync function rewrittenRootProject(docsDir: DocsDir, exclude: readonly string[]): Promise<string | null> {\n let text: string;\n try {\n text = await readFile(rootProjectPath(docsDir), \"utf8\");\n } catch {\n // Unreadable is not \"excludes nothing\": the caller reports it and writes\n // nothing at all.\n return null;\n }\n // The SAME parse the grader read the list with — `parseRootProject`, which\n // tolerates the byte-order mark PowerShell's `Out-File` writes. Two parses\n // would mean a run that read an `exclude` here and then declared the file\n // unreadable at the write, which is exactly what a BOM produced before\n // (re-verification 2026-09-04, area C item 3). Re-read rather than carried:\n // the point of this read is that it happens immediately before the swap.\n const parsed = parseRootProject(text);\n if (parsed === null) return null;\n return `${JSON.stringify({ ...parsed, exclude: [...exclude] }, null, 2)}\\n`;\n}\n"]}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * What one `loam subsystem sync` SAYS it did — the `--json` envelope and the
3
+ * human lines, in one place.
4
+ *
5
+ * It sits beside `./views.ts` and `./exclude.ts` because it is the third answer
6
+ * about the same three files, and it moved out of `../sync.ts` for that file's
7
+ * standing reason: `sync.ts` is the lock, the transaction and the plan, and it
8
+ * reached the 400-line limit carrying the output too. Composing a report is not
9
+ * a phase of the commit, so nothing here reads the disk or decides anything — a
10
+ * caller hands over what happened and this spells it.
11
+ */
12
+ import { emitJson } from "../../../core/envelope/json.js";
13
+ import { LANDSCAPE_UNREADABLE_NOTE } from "./views.js";
14
+ export function reportSync(json, out) {
15
+ // One reason today, and it is exactly the `action` it explains — spelled as a
16
+ // record so a second cause is an added value rather than a re-read of the
17
+ // sentence a person sees.
18
+ const blocked = out.action === "blocked" ? { reason: "landscape-invalid" } : null;
19
+ if (json) {
20
+ emitJson({
21
+ command: "subsystem",
22
+ path: "architecture/subsystems.likec4",
23
+ action: out.action,
24
+ subsystems: out.subsystems,
25
+ ...(blocked === null ? {} : { blocked }),
26
+ ...(out.recovered === null ? {} : { recovered: out.recovered }),
27
+ projects: {
28
+ root: out.projects.root,
29
+ created: out.projects.created,
30
+ removed: out.projects.removed,
31
+ current: out.projects.current,
32
+ // Additive, and emitted in every mode including the untouched one: a
33
+ // consumer must be able to read what the root project now excludes
34
+ // without branching on whether this run happened to change it.
35
+ //
36
+ // `unreadable` travels with it because `entries: []` alone collapses the
37
+ // two answers `readRootExclude` insists must stay apart — "the file
38
+ // excludes nothing" and "loam could not read the file" are opposite
39
+ // facts about what the renderer will load, and the text view had the
40
+ // distinction while `--json` did not (verification 2026-09-04, review C).
41
+ exclude: {
42
+ updated: out.projects.exclude.updated,
43
+ entries: out.projects.exclude.entries,
44
+ added: out.projects.exclude.added,
45
+ removed: out.projects.exclude.removed,
46
+ unreadable: out.projects.exclude.unreadable,
47
+ },
48
+ },
49
+ });
50
+ return;
51
+ }
52
+ if (out.recovered !== null && out.recovered.outcome !== "consistent") {
53
+ console.log(`note: recovered an interrupted \`loam ${out.recovered.command}\` commit first (${out.recovered.outcome}).`);
54
+ }
55
+ console.log(views(out));
56
+ // Owed on BOTH arms: the run that declined to rewrite an existing file, and
57
+ // the run that wrote the tolerant shape because there was none to protect.
58
+ if (out.mapUnreadable)
59
+ console.log(LANDSCAPE_UNREADABLE_NOTE);
60
+ projects(out.projects);
61
+ exclude(out.projects);
62
+ }
63
+ /** The one line about the generated views file. */
64
+ function views(out) {
65
+ // "nothing to write" is a claim about the WHOLE run, and it is false the moment
66
+ // any of this verb's three writers landed something beside a current views
67
+ // file: a project file created, one removed, or the root `exclude` rewritten.
68
+ // The third was missed when it was added, so the tail contradicted the very
69
+ // next line of output (verification 2026-09-04, review C).
70
+ const quiet = out.projects.created.length === 0 && out.projects.removed.length === 0 && !out.projects.exclude.updated;
71
+ const sentence = {
72
+ current: `architecture/subsystems.likec4 is current (${out.subsystems} subsystem(s))${quiet ? " — nothing to write." : "."}`,
73
+ created: `wrote architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,
74
+ updated: `updated architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,
75
+ removed: `removed architecture/subsystems.likec4 — the tree has no subsystems, so the generated file must be absent.`,
76
+ blocked: "architecture/subsystems.likec4 left as it is — architecture/ does not parse as one project, so the " +
77
+ "render's element join is the landscape file's alone (a use-case document that binds an element of its " +
78
+ "own is not in it), and rewriting the file would take its `include` lines with it.",
79
+ };
80
+ return sentence[out.action];
81
+ }
82
+ /** The per-service project files this run created and removed. */
83
+ function projects(out) {
84
+ if (out.created.length > 0) {
85
+ console.log(`wrote ${out.created.length} services/<…>/likec4.config.json — one LikeC4 project per service model whose ` +
86
+ "model stands alone (an existing file is yours and is never rewritten; commit the new ones before a " +
87
+ "`loam subsystem move`, which refuses over untracked paths). With more than one project, " +
88
+ "likec4 validate at the docs root needs --project <name>; build and export take every project.");
89
+ }
90
+ // Named one per line: this is a DELETE of a file a team may have written, and
91
+ // a count would leave a reader nothing to check against their own diff.
92
+ for (const path of out.removed)
93
+ console.log(`removed ${path}`);
94
+ if (out.removed.length > 0) {
95
+ console.log("— that model extends the fleet map, and a project file beside it registers a second renderer project " +
96
+ "rooted at the service directory: `likec4 validate .` then refuses without `--project`, and wherever " +
97
+ "that project claims the model it loads it alone, where the map's kinds are not, and the fleet project " +
98
+ "loses the service's interior. This is what `service.likec4-config-stray` names.");
99
+ }
100
+ if (!out.root && out.modelled > 0) {
101
+ console.log("note: no likec4.config.json at the docs root, so no service project files were written — " +
102
+ "`loam doctor` prints the root file.");
103
+ }
104
+ }
105
+ /** The root project's `exclude`, and the one state loam refuses to touch. */
106
+ function exclude(out) {
107
+ const plan = out.exclude;
108
+ if (plan.updated) {
109
+ // Named entries rather than a count: this line is the whole record of a
110
+ // rewrite to a file the team owns, and "updated the exclude list" gives a
111
+ // reader nothing to check against their own diff.
112
+ const parts = [
113
+ ...(plan.added.length > 0 ? [`+${plan.added.join(" +")}`] : []),
114
+ ...(plan.removed.length > 0 ? [`-${plan.removed.join(" -")}`] : []),
115
+ ];
116
+ console.log(`rewrote likec4.config.json's \`exclude\` (${parts.join(" ")}) — the root project excludes exactly the ` +
117
+ "models that stand alone; a model that extends the fleet map renders from the docs root and must not be excluded.");
118
+ }
119
+ if (plan.unreadable) {
120
+ console.log("note: likec4.config.json at the docs root is not a JSON object with a string `exclude` list, so it was " +
121
+ "left exactly as it is — loam does not rewrite a project file it cannot read.");
122
+ }
123
+ }
124
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What one `loam subsystem sync` SAYS it did — the `--json` envelope and the\n * human lines, in one place.\n *\n * It sits beside `./views.ts` and `./exclude.ts` because it is the third answer\n * about the same three files, and it moved out of `../sync.ts` for that file's\n * standing reason: `sync.ts` is the lock, the transaction and the plan, and it\n * reached the 400-line limit carrying the output too. Composing a report is not\n * a phase of the commit, so nothing here reads the disk or decides anything — a\n * caller hands over what happened and this spells it.\n */\nimport { emitJson } from \"../../../core/envelope/json.js\";\nimport type { CommitRecovery } from \"../../../core/staging/interrupted.js\";\nimport type { ExcludePlan } from \"./exclude.js\";\nimport { LANDSCAPE_UNREADABLE_NOTE } from \"./views.js\";\n\n/**\n * What one sync did to the views file — and ONLY that file; the projects report\n * separately.\n *\n * `blocked` is the fifth answer: the map cannot be read, a generated file\n * already exists, and this run left it exactly as it is. The tolerant render\n * used to run anyway and rewrote a good file down to title and description\n * (verification 2026-09-04, W5). The project half is unaffected and still runs —\n * neither the per-service files nor the root `exclude` depend on the map.\n */\nexport type SyncAction = \"current\" | \"created\" | \"updated\" | \"removed\" | \"blocked\";\n\n/** Why a run answered `blocked`. One reason today; a key rather than a bare string so a second one is additive. */\ninterface SyncBlocked {\n reason: \"landscape-invalid\";\n}\n\n/** The additive `projects` payload key, and what the text lines are composed from. */\nexport interface ProjectsReport {\n root: boolean;\n /** POSIX paths relative to the DOCS ROOT, written this run, sorted by path. */\n created: string[];\n /**\n * POSIX paths relative to the DOCS ROOT, DELETED this run, sorted by path — a\n * `likec4.config.json` beside a model that extends the map. `[]` rather than\n * absent on a run that removed none, so a consumer reads the key.\n *\n * SORTED BY PATH rather than by service id, which is the order the survey\n * hands them over in: `../sync.ts` sorts both arrays where it spells them, so\n * the `--json` list and the `removed <path>` lines below can never disagree.\n */\n removed: string[];\n current: number;\n modelled: number;\n exclude: ExcludePlan;\n}\n\n/** Everything one run has to say, as the caller knows it. */\nexport interface SyncReport {\n action: SyncAction;\n subsystems: number;\n recovered: CommitRecovery | null;\n projects: ProjectsReport;\n /** The map could not be read at all — the note is owed whether or not a file was protected. */\n mapUnreadable: boolean;\n}\n\nexport function reportSync(json: boolean, out: SyncReport): void {\n // One reason today, and it is exactly the `action` it explains — spelled as a\n // record so a second cause is an added value rather than a re-read of the\n // sentence a person sees.\n const blocked: SyncBlocked | null = out.action === \"blocked\" ? { reason: \"landscape-invalid\" } : null;\n if (json) {\n emitJson({\n command: \"subsystem\",\n path: \"architecture/subsystems.likec4\",\n action: out.action,\n subsystems: out.subsystems,\n ...(blocked === null ? {} : { blocked }),\n ...(out.recovered === null ? {} : { recovered: out.recovered }),\n projects: {\n root: out.projects.root,\n created: out.projects.created,\n removed: out.projects.removed,\n current: out.projects.current,\n // Additive, and emitted in every mode including the untouched one: a\n // consumer must be able to read what the root project now excludes\n // without branching on whether this run happened to change it.\n //\n // `unreadable` travels with it because `entries: []` alone collapses the\n // two answers `readRootExclude` insists must stay apart — \"the file\n // excludes nothing\" and \"loam could not read the file\" are opposite\n // facts about what the renderer will load, and the text view had the\n // distinction while `--json` did not (verification 2026-09-04, review C).\n exclude: {\n updated: out.projects.exclude.updated,\n entries: out.projects.exclude.entries,\n added: out.projects.exclude.added,\n removed: out.projects.exclude.removed,\n unreadable: out.projects.exclude.unreadable,\n },\n },\n });\n return;\n }\n if (out.recovered !== null && out.recovered.outcome !== \"consistent\") {\n console.log(\n `note: recovered an interrupted \\`loam ${out.recovered.command}\\` commit first (${out.recovered.outcome}).`,\n );\n }\n console.log(views(out));\n // Owed on BOTH arms: the run that declined to rewrite an existing file, and\n // the run that wrote the tolerant shape because there was none to protect.\n if (out.mapUnreadable) console.log(LANDSCAPE_UNREADABLE_NOTE);\n projects(out.projects);\n exclude(out.projects);\n}\n\n/** The one line about the generated views file. */\nfunction views(out: SyncReport): string {\n // \"nothing to write\" is a claim about the WHOLE run, and it is false the moment\n // any of this verb's three writers landed something beside a current views\n // file: a project file created, one removed, or the root `exclude` rewritten.\n // The third was missed when it was added, so the tail contradicted the very\n // next line of output (verification 2026-09-04, review C).\n const quiet =\n out.projects.created.length === 0 && out.projects.removed.length === 0 && !out.projects.exclude.updated;\n const sentence: Record<SyncAction, string> = {\n current: `architecture/subsystems.likec4 is current (${out.subsystems} subsystem(s))${quiet ? \" — nothing to write.\" : \".\"}`,\n created: `wrote architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,\n updated: `updated architecture/subsystems.likec4 — ${out.subsystems} subsystem view(s).`,\n removed: `removed architecture/subsystems.likec4 — the tree has no subsystems, so the generated file must be absent.`,\n blocked:\n \"architecture/subsystems.likec4 left as it is — architecture/ does not parse as one project, so the \" +\n \"render's element join is the landscape file's alone (a use-case document that binds an element of its \" +\n \"own is not in it), and rewriting the file would take its `include` lines with it.\",\n };\n return sentence[out.action];\n}\n\n/** The per-service project files this run created and removed. */\nfunction projects(out: ProjectsReport): void {\n if (out.created.length > 0) {\n console.log(\n `wrote ${out.created.length} services/<…>/likec4.config.json — one LikeC4 project per service model whose ` +\n \"model stands alone (an existing file is yours and is never rewritten; commit the new ones before a \" +\n \"`loam subsystem move`, which refuses over untracked paths). With more than one project, \" +\n \"likec4 validate at the docs root needs --project <name>; build and export take every project.\",\n );\n }\n // Named one per line: this is a DELETE of a file a team may have written, and\n // a count would leave a reader nothing to check against their own diff.\n for (const path of out.removed) console.log(`removed ${path}`);\n if (out.removed.length > 0) {\n console.log(\n \"— that model extends the fleet map, and a project file beside it registers a second renderer project \" +\n \"rooted at the service directory: `likec4 validate .` then refuses without `--project`, and wherever \" +\n \"that project claims the model it loads it alone, where the map's kinds are not, and the fleet project \" +\n \"loses the service's interior. This is what `service.likec4-config-stray` names.\",\n );\n }\n if (!out.root && out.modelled > 0) {\n console.log(\n \"note: no likec4.config.json at the docs root, so no service project files were written — \" +\n \"`loam doctor` prints the root file.\",\n );\n }\n}\n\n/** The root project's `exclude`, and the one state loam refuses to touch. */\nfunction exclude(out: ProjectsReport): void {\n const plan = out.exclude;\n if (plan.updated) {\n // Named entries rather than a count: this line is the whole record of a\n // rewrite to a file the team owns, and \"updated the exclude list\" gives a\n // reader nothing to check against their own diff.\n const parts = [\n ...(plan.added.length > 0 ? [`+${plan.added.join(\" +\")}`] : []),\n ...(plan.removed.length > 0 ? [`-${plan.removed.join(\" -\")}`] : []),\n ];\n console.log(\n `rewrote likec4.config.json's \\`exclude\\` (${parts.join(\" \")}) — the root project excludes exactly the ` +\n \"models that stand alone; a model that extends the fleet map renders from the docs root and must not be excluded.\",\n );\n }\n if (plan.unreadable) {\n console.log(\n \"note: likec4.config.json at the docs root is not a JSON object with a string `exclude` list, so it was \" +\n \"left exactly as it is — loam does not rewrite a project file it cannot read.\",\n );\n }\n}\n"]}
@@ -7,6 +7,25 @@
7
7
  * the same commit as whatever changed the tree — a marker without its view,
8
8
  * or renames without theirs, is exactly the `subsystem.views-stale` state
9
9
  * the generated file's contract refuses to leave between two commits.
10
+ *
11
+ * This window does NOT write the per-service project files
12
+ * (`services/<…>/<id>/likec4.config.json`); only `../sync.ts` does, and the
13
+ * asymmetry is deliberate rather than an omission. The views file must ride
14
+ * in the verb's commit because its bytes depend on the PLACEMENT the verb is
15
+ * changing. A project file's bytes depend on the service id alone, which no
16
+ * verb here changes — and the file lives INSIDE the service directory, so a
17
+ * `move` carries it to the new location as part of the rename it already
18
+ * journals (in a git-backed repo that move REFUSES first, `move-uncommitted`,
19
+ * until the files `sync` created are committed — the refusal names them, and
20
+ * `sync`'s own output says to commit them), and `rm` never reaches one,
21
+ * because it refuses any subsystem that still holds a member
22
+ * (`subsystem-not-empty`, `../edit.ts`). No verb can
23
+ * therefore leave a project file stale or orphaned, and a verb that filled
24
+ * gaps in passing would silently write files a `--json` reader of `new` or
25
+ * `rename` was never told to expect. The one state a verb can leave is a
26
+ * model with no file beside it, and that is the state `validate --all` names
27
+ * as `service.likec4-config-missing`, with `loam subsystem sync` as the
28
+ * repair — one writer, so the create-only rule has one place to hold.
10
29
  */
11
30
  import { existsSync } from "node:fs";
12
31
  import { readFile } from "node:fs/promises";
@@ -20,7 +39,7 @@ import { recoverInterruptedCommit } from "../../../core/staging/recovery/recover
20
39
  import { commitStaged } from "../../../core/staging/txn/transaction.js";
21
40
  import {} from "../../../core/staging/writes.js";
22
41
  import { viewsAgree } from "../../../core/repo/tree/render/views.js";
23
- import { expectedViews } from "./views.js";
42
+ import { expectedViews, LANDSCAPE_UNREADABLE_NOTE } from "./views.js";
24
43
  /** The text tail a verb's success line carries about the generated file. */
25
44
  export function reportViews(views) {
26
45
  return views === "current" ? "" : ` — architecture/subsystems.likec4 ${views}`;
@@ -57,14 +76,14 @@ export async function commitWindow(docsDir, json, plan) {
57
76
  return;
58
77
  const path = subsystemViewsPath(docsDir);
59
78
  const expected = await expectedViews(docsDir, txn.tree);
60
- const bytes = expected === null ? null : Buffer.from(expected, "utf8");
79
+ const bytes = expected.content === null ? null : Buffer.from(expected.content, "utf8");
61
80
  // Content, not bytes — `viewsAgree` carries the reason. Without it every
62
81
  // subsystem move on a Windows clone folded a rewrite of this file into its
63
82
  // transaction that changed not one fact, and journalled it.
64
83
  const current = existsSync(path) ? await readFile(path, "utf8") : null;
65
84
  let views = "current";
66
85
  const writes = [...txn.writes];
67
- if (!viewsAgree(current, expected)) {
86
+ if (!viewsAgree(current, expected.content)) {
68
87
  views = current === null ? "created" : bytes === null ? "removed" : "updated";
69
88
  writes.push({ path, content: bytes });
70
89
  }
@@ -86,6 +105,13 @@ export async function commitWindow(docsDir, json, plan) {
86
105
  return;
87
106
  }
88
107
  await txn.report(views, recovered);
108
+ // A verb CANNOT leave this file behind the tree — its bytes depend on the
109
+ // placement the verb just changed, so the tolerant render lands even when the
110
+ // map is unreadable and the file loses its includes. `sync` may decline that
111
+ // rewrite (`action: "blocked"`); a verb may not, so the person is told
112
+ // instead, and the same note names the run that puts the includes back.
113
+ if (!json && !expected.known)
114
+ console.log(LANDSCAPE_UNREADABLE_NOTE);
89
115
  }
90
116
  finally {
91
117
  await releaseLock();
@@ -1 +1 @@
1
- {"version":3,"file":"txn.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/txn.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared commit window every subsystem WRITE verb goes through — `new`,\n * `rm`, `move` and `rename` all land here: lock, recover a predecessor's\n * journal, plan against the walked tree, regenerate the views file from the\n * PLANNED post-state, commit through the journaled transaction (directory\n * renames included), report. One window, because the views file must land in\n * the same commit as whatever changed the tree — a marker without its view,\n * or renames without theirs, is exactly the `subsystem.views-stale` state\n * the generated file's contract refuses to leave between two commits.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { fail, type ErrorCode } from \"../../../core/envelope/json.js\";\nimport { subsystemViewsPath } from \"../../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../../core/repo/repo.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../../core/staging/interrupted.js\";\nimport { acquireDocsLock, DocsBusyError } from \"../../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../../core/staging/txn/transaction.js\";\nimport { type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { viewsAgree } from \"../../../core/repo/tree/render/views.js\";\nimport { expectedViews } from \"./views.js\";\n\nexport type ViewsAction = \"created\" | \"updated\" | \"removed\" | \"current\";\n\n/** The text tail a verb's success line carries about the generated file. */\nexport function reportViews(views: ViewsAction): string {\n return views === \"current\" ? \"\" : ` — architecture/subsystems.likec4 ${views}`;\n}\n\n/** What one subsystem verb asks the shared commit window to land. */\nexport interface SubsystemTxn {\n /** The journal's `target` — what this commit is FOR. */\n target: string;\n /** Marker writes (create/delete); the views write is derived and appended. */\n writes: PlannedWrite[];\n /** The tree AS THE COMMIT LEAVES IT — the views file is rendered from this. */\n tree: FleetTree;\n /** Directory renames, performed after the swaps, in this order. */\n moves?: { from: string; to: string }[];\n /** Remap of a RACED commit failure (new: a lost marker create is `already-exists`). */\n racedCode?: ErrorCode;\n /** Remap of a rolled-back commit failure (move: `move-failed` — no merge was computed). */\n failedCode?: ErrorCode;\n /** The rollback sentence's verb for half-state files. */\n what: string;\n report: (views: ViewsAction, recovered: CommitRecovery | null) => void | Promise<void>;\n}\n\n/**\n * Lock → recover → plan → stage (views included) → journaled commit → report.\n * `plan` returning null means it already reported a refusal; nothing was\n * staged and nothing is written.\n */\nexport async function commitWindow(\n docsDir: DocsDir,\n json: boolean,\n plan: (tree: FleetTree) => Promise<SubsystemTxn | null>,\n): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLock(docsDir);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n fail(json, \"commit-interrupted\", err.message);\n return;\n }\n const txn = await plan(await listFleetTree(docsDir));\n if (txn === null) return;\n\n const path = subsystemViewsPath(docsDir);\n const expected = await expectedViews(docsDir, txn.tree);\n const bytes = expected === null ? null : Buffer.from(expected, \"utf8\");\n // Content, not bytes — `viewsAgree` carries the reason. Without it every\n // subsystem move on a Windows clone folded a rewrite of this file into its\n // transaction that changed not one fact, and journalled it.\n const current = existsSync(path) ? await readFile(path, \"utf8\") : null;\n let views: ViewsAction = \"current\";\n const writes = [...txn.writes];\n if (!viewsAgree(current, expected)) {\n views = current === null ? \"created\" : bytes === null ? \"removed\" : \"updated\";\n writes.push({ path, content: bytes });\n }\n const staged = await stageWrites(writes);\n const committed = await commitStaged(\n {\n root: docsDir,\n command: \"subsystem\",\n rerun: \"loam subsystem sync\",\n target: txn.target,\n ...(txn.moves === undefined ? {} : { moves: txn.moves }),\n },\n staged,\n txn.what,\n );\n if (!committed.ok) {\n const code =\n committed.code === \"rollback-incomplete\"\n ? committed.code\n : committed.raced\n ? (txn.racedCode ?? txn.failedCode ?? committed.code)\n : (txn.failedCode ?? committed.code);\n fail(json, code, committed.message);\n return;\n }\n await txn.report(views, recovered);\n } finally {\n await releaseLock();\n }\n}\n"]}
1
+ {"version":3,"file":"txn.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/txn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,IAAI,EAAkB,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAC9D,OAAO,EAAuB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnG,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AACxE,OAAO,EAAqB,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAItE,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,KAAkB;IAC5C,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,qCAAqC,KAAK,EAAE,CAAC;AACjF,CAAC;AAqBD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAgB,EAChB,IAAa,EACb,IAAuD;IAEvD,IAAI,WAAgC,CAAC;IACrC,IAAI,CAAC;QACH,WAAW,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,GAAG,YAAY,aAAa,CAAC;YAAE,MAAM,GAAG,CAAC;QAC/C,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,IAAI,SAAgC,CAAC;QACrC,IAAI,CAAC;YACH,SAAS,GAAG,MAAM,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACtD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,CAAC,GAAG,YAAY,sBAAsB,CAAC;gBAAE,MAAM,GAAG,CAAC;YACxD,IAAI,CAAC,IAAI,EAAE,oBAAoB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YAC9C,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO;QAEzB,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACvF,yEAAyE;QACzE,2EAA2E;QAC3E,4DAA4D;QAC5D,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvE,IAAI,KAAK,GAAgB,SAAS,CAAC;QACnC,MAAM,MAAM,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3C,KAAK,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;YAC9E,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,SAAS,GAAG,MAAM,YAAY,CAClC;YACE,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,WAAW;YACpB,KAAK,EAAE,qBAAqB;YAC5B,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC;SACzD,EACD,MAAM,EACN,GAAG,CAAC,IAAI,CACT,CAAC;QACF,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;YAClB,MAAM,IAAI,GACR,SAAS,CAAC,IAAI,KAAK,qBAAqB;gBACtC,CAAC,CAAC,SAAS,CAAC,IAAI;gBAChB,CAAC,CAAC,SAAS,CAAC,KAAK;oBACf,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,CAAC;oBACrD,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,MAAM,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QACnC,0EAA0E;QAC1E,8EAA8E;QAC9E,6EAA6E;QAC7E,uEAAuE;QACvE,wEAAwE;QACxE,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK;YAAE,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACvE,CAAC;YAAS,CAAC;QACT,MAAM,WAAW,EAAE,CAAC;IACtB,CAAC;AACH,CAAC","sourcesContent":["/**\n * The shared commit window every subsystem WRITE verb goes through — `new`,\n * `rm`, `move` and `rename` all land here: lock, recover a predecessor's\n * journal, plan against the walked tree, regenerate the views file from the\n * PLANNED post-state, commit through the journaled transaction (directory\n * renames included), report. One window, because the views file must land in\n * the same commit as whatever changed the tree — a marker without its view,\n * or renames without theirs, is exactly the `subsystem.views-stale` state\n * the generated file's contract refuses to leave between two commits.\n *\n * This window does NOT write the per-service project files\n * (`services/<…>/<id>/likec4.config.json`); only `../sync.ts` does, and the\n * asymmetry is deliberate rather than an omission. The views file must ride\n * in the verb's commit because its bytes depend on the PLACEMENT the verb is\n * changing. A project file's bytes depend on the service id alone, which no\n * verb here changes — and the file lives INSIDE the service directory, so a\n * `move` carries it to the new location as part of the rename it already\n * journals (in a git-backed repo that move REFUSES first, `move-uncommitted`,\n * until the files `sync` created are committed — the refusal names them, and\n * `sync`'s own output says to commit them), and `rm` never reaches one,\n * because it refuses any subsystem that still holds a member\n * (`subsystem-not-empty`, `../edit.ts`). No verb can\n * therefore leave a project file stale or orphaned, and a verb that filled\n * gaps in passing would silently write files a `--json` reader of `new` or\n * `rename` was never told to expect. The one state a verb can leave is a\n * model with no file beside it, and that is the state `validate --all` names\n * as `service.likec4-config-missing`, with `loam subsystem sync` as the\n * repair — one writer, so the create-only rule has one place to hold.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { fail, type ErrorCode } from \"../../../core/envelope/json.js\";\nimport { subsystemViewsPath } from \"../../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../../core/repo/repo.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../../core/staging/interrupted.js\";\nimport { acquireDocsLock, DocsBusyError } from \"../../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../../core/staging/txn/transaction.js\";\nimport { type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { viewsAgree } from \"../../../core/repo/tree/render/views.js\";\nimport { expectedViews, LANDSCAPE_UNREADABLE_NOTE } from \"./views.js\";\n\nexport type ViewsAction = \"created\" | \"updated\" | \"removed\" | \"current\";\n\n/** The text tail a verb's success line carries about the generated file. */\nexport function reportViews(views: ViewsAction): string {\n return views === \"current\" ? \"\" : ` — architecture/subsystems.likec4 ${views}`;\n}\n\n/** What one subsystem verb asks the shared commit window to land. */\nexport interface SubsystemTxn {\n /** The journal's `target` — what this commit is FOR. */\n target: string;\n /** Marker writes (create/delete); the views write is derived and appended. */\n writes: PlannedWrite[];\n /** The tree AS THE COMMIT LEAVES IT — the views file is rendered from this. */\n tree: FleetTree;\n /** Directory renames, performed after the swaps, in this order. */\n moves?: { from: string; to: string }[];\n /** Remap of a RACED commit failure (new: a lost marker create is `already-exists`). */\n racedCode?: ErrorCode;\n /** Remap of a rolled-back commit failure (move: `move-failed` — no merge was computed). */\n failedCode?: ErrorCode;\n /** The rollback sentence's verb for half-state files. */\n what: string;\n report: (views: ViewsAction, recovered: CommitRecovery | null) => void | Promise<void>;\n}\n\n/**\n * Lock → recover → plan → stage (views included) → journaled commit → report.\n * `plan` returning null means it already reported a refusal; nothing was\n * staged and nothing is written.\n */\nexport async function commitWindow(\n docsDir: DocsDir,\n json: boolean,\n plan: (tree: FleetTree) => Promise<SubsystemTxn | null>,\n): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLock(docsDir);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n fail(json, \"commit-interrupted\", err.message);\n return;\n }\n const txn = await plan(await listFleetTree(docsDir));\n if (txn === null) return;\n\n const path = subsystemViewsPath(docsDir);\n const expected = await expectedViews(docsDir, txn.tree);\n const bytes = expected.content === null ? null : Buffer.from(expected.content, \"utf8\");\n // Content, not bytes — `viewsAgree` carries the reason. Without it every\n // subsystem move on a Windows clone folded a rewrite of this file into its\n // transaction that changed not one fact, and journalled it.\n const current = existsSync(path) ? await readFile(path, \"utf8\") : null;\n let views: ViewsAction = \"current\";\n const writes = [...txn.writes];\n if (!viewsAgree(current, expected.content)) {\n views = current === null ? \"created\" : bytes === null ? \"removed\" : \"updated\";\n writes.push({ path, content: bytes });\n }\n const staged = await stageWrites(writes);\n const committed = await commitStaged(\n {\n root: docsDir,\n command: \"subsystem\",\n rerun: \"loam subsystem sync\",\n target: txn.target,\n ...(txn.moves === undefined ? {} : { moves: txn.moves }),\n },\n staged,\n txn.what,\n );\n if (!committed.ok) {\n const code =\n committed.code === \"rollback-incomplete\"\n ? committed.code\n : committed.raced\n ? (txn.racedCode ?? txn.failedCode ?? committed.code)\n : (txn.failedCode ?? committed.code);\n fail(json, code, committed.message);\n return;\n }\n await txn.report(views, recovered);\n // A verb CANNOT leave this file behind the tree — its bytes depend on the\n // placement the verb just changed, so the tolerant render lands even when the\n // map is unreadable and the file loses its includes. `sync` may decline that\n // rewrite (`action: \"blocked\"`); a verb may not, so the person is told\n // instead, and the same note names the run that puts the includes back.\n if (!json && !expected.known) console.log(LANDSCAPE_UNREADABLE_NOTE);\n } finally {\n await releaseLock();\n }\n}\n"]}
@@ -6,19 +6,16 @@
6
6
  * disagree with `validate`'s staleness check.
7
7
  */
8
8
  import { existsSync } from "node:fs";
9
- import { relative, sep } from "node:path";
10
- import {} from "../../../core/c4/likec4.js";
11
- import { loadArchitecture } from "../../../core/c4/project/architecture.js";
9
+ import { relative, resolve, sep } from "node:path";
10
+ import { loadFile } from "../../../core/c4/likec4.js";
11
+ import { architectureProjectDocuments, loadArchitecture } from "../../../core/c4/project/architecture.js";
12
12
  import { serviceDirOf } from "../../../core/kernel/ids/dirs.js";
13
13
  import { landscapePath } from "../../../core/repo/paths.js";
14
14
  import { renderSubsystemViews } from "../../../core/repo/tree/render/views.js";
15
15
  /**
16
- * The elements of the whole `architecture/` PROJECT for the member join, or
17
- * the empty list when the map is absent or does not parse. Tolerant on
18
- * purpose: sync must stay runnable in a repo whose landscape is broken — it
19
- * renders what the committed bytes resolve (no includes), deterministically,
20
- * and the next sync after the landscape is repaired catches the file up.
21
- * `validate` skips the staleness question in exactly those states.
16
+ * The elements and declared global style ids of the whole `architecture/`
17
+ * PROJECT — the member join, and the one line a view may borrow a palette
18
+ * with — with the provenance of that answer beside it.
22
19
  *
23
20
  * The PROJECT, not the landscape file alone, and that is a correctness rule
24
21
  * rather than a preference: `validate` grades this file against
@@ -31,22 +28,122 @@ import { renderSubsystemViews } from "../../../core/repo/tree/render/views.js";
31
28
  * could clear, reproduced on `examples/docs` before this call changed. Two
32
29
  * readers of "what should the views say" must read the same documents, or the
33
30
  * shared render in `core/repo/tree/render/views.ts` is shared in name only.
31
+ *
32
+ * WHY THE FALLBACK AND THE FLAG ARE TWO ANSWERS RATHER THAN ONE. The fallback
33
+ * exists because one broken use-case document must not cost the render its
34
+ * member join: the landscape's own elements are still the truth about who
35
+ * belongs in which subsystem, and re-rendering the file without them takes every
36
+ * `include` line off it (verification 2026-09-04, W5). But the STYLE CENSUS is
37
+ * not the landscape file's alone — SCHEMA blesses a palette in any `.likec4`
38
+ * under `architecture/` — so the fallback takes it over EVERY document that
39
+ * parses on its own (`declaredStyles`). Reading it off the landscape alone was
40
+ * enough to drop `global style subsystems` from a good generated file whenever
41
+ * the palette sat in a sibling and an unrelated sibling was broken
42
+ * (re-verification 2026-09-04, area C item 8).
43
+ *
44
+ * `known` still means the PROJECT parsed, and nothing weaker, because the MEMBER
45
+ * JOIN is what it licences: a use-case document that binds an element of its own
46
+ * contributes to the join and cannot be read document by document. It is
47
+ * `../sync.ts`'s licence to leave an existing file alone.
48
+ *
49
+ * An ABSENT map is not a failure: there is nothing to read, the empty facts are
50
+ * the truth, and the tolerant render is what a fleet being scaffolded needs. An
51
+ * EMPTY project is no longer reachable here — `architectureProjectDocuments`
52
+ * keeps the landscape whatever the root `exclude` says, and its banner records
53
+ * what an empty one cost.
34
54
  */
35
- export async function landscapeElements(docsDir) {
36
- if (!existsSync(landscapePath(docsDir)))
37
- return [];
55
+ export async function landscapeFacts(docsDir) {
56
+ const path = landscapePath(docsDir);
57
+ if (!existsSync(path))
58
+ return { facts: { elements: [] }, known: true };
38
59
  try {
39
60
  const doc = await loadArchitecture(docsDir);
40
- return doc.errors.length > 0 ? [] : doc.elements;
61
+ // The whole document on success, so the render reads the same record
62
+ // `validate --all` grades against; `globalStyles` rides along without
63
+ // being named here, and a field the loaders add later does too.
64
+ if (doc.errors.length === 0)
65
+ return { facts: doc, known: true };
41
66
  }
42
67
  catch {
43
- return [];
68
+ // A throw and a parse error are the same evidence: fall through to the file.
69
+ }
70
+ try {
71
+ const file = await loadFile(path);
72
+ // THE STYLE CENSUS IS TAKEN OVER EVERY DOCUMENT, not over the landscape, and
73
+ // that is what makes the fallback safe for a verb that has to write. A
74
+ // palette declared in a SIBLING under `architecture/` is invisible to a read
75
+ // of the landscape alone, so a `subsystem new` run standing beside one
76
+ // unrelated broken sibling silently dropped `global style subsystems` from a
77
+ // good generated file — the landscape parsed, the element join survived, and
78
+ // nothing graded the loss because `sync` answers `blocked` for as long as the
79
+ // broken file stands (re-verification 2026-09-04, area C item 8). Reading
80
+ // each document ON ITS OWN is the widening the fallback is entitled to: it
81
+ // cannot merge them (that is what failed one line up), but a document that
82
+ // parses alone certainly declares the style groups it declares.
83
+ if (file.errors.length === 0) {
84
+ return { facts: { elements: file.elements, globalStyles: await declaredStyles(docsDir, file) }, known: false };
85
+ }
44
86
  }
87
+ catch {
88
+ // Unreadable bytes; the caller is told loam could not say.
89
+ }
90
+ return { facts: null, known: false };
91
+ }
92
+ /**
93
+ * Every global style id declared by an `architecture/` document that parses ON
94
+ * ITS OWN, the landscape's included — the union, sorted, so the render is a
95
+ * function of the set rather than of readdir order.
96
+ *
97
+ * The document set is `architectureProjectDocuments`: the same list the PROJECT
98
+ * read uses, so the fallback can never consider a file the merged read would
99
+ * have excluded — the generated views file itself, or one the root `exclude`
100
+ * hides from the renderer. A document that does not parse alone contributes
101
+ * nothing and is not an error here: it is named by `loam validate --all`, and
102
+ * the note this module carries already tells the reader the census came from
103
+ * outside a project read.
104
+ */
105
+ async function declaredStyles(docsDir, landscape) {
106
+ const ids = new Set(landscape.globalStyles ?? []);
107
+ let paths;
108
+ try {
109
+ paths = await architectureProjectDocuments(docsDir);
110
+ }
111
+ catch {
112
+ return [...ids].sort();
113
+ }
114
+ const map = resolve(landscapePath(docsDir));
115
+ for (const path of paths) {
116
+ if (resolve(path) === map)
117
+ continue;
118
+ try {
119
+ const doc = await loadFile(path);
120
+ if (doc.errors.length === 0)
121
+ for (const id of doc.globalStyles ?? [])
122
+ ids.add(id);
123
+ }
124
+ catch {
125
+ // One unreadable sibling costs its own styles and nothing else.
126
+ }
127
+ }
128
+ return [...ids].sort();
45
129
  }
46
- /** The bytes the views file must hold for this tree, or null for "must be absent". */
47
130
  export async function expectedViews(docsDir, tree) {
48
- return renderSubsystemViews(tree, await landscapeElements(docsDir));
131
+ const read = await landscapeFacts(docsDir);
132
+ return { known: read.known, content: renderSubsystemViews(tree, read.facts ?? { elements: [] }) };
49
133
  }
134
+ /**
135
+ * The one line a person is owed when the map cannot be read — the same sentence
136
+ * from `../sync.ts` (which then leaves an existing file alone) and from a
137
+ * tree-changing verb (which renders the tolerant shape anyway, because its own
138
+ * contract is that the file lands in the verb's commit). It names the gating
139
+ * surface rather than repeating its findings: `loam validate --all` is where the
140
+ * broken document is named. It lives here, beside the facts it is about, because
141
+ * both callers are on this side of the `txn/` boundary and a constant in
142
+ * `../sync.ts` would point the package edge back up.
143
+ */
144
+ export const LANDSCAPE_UNREADABLE_NOTE = "note: architecture/ does not parse as one project, so the element join is whatever " +
145
+ "architecture/landscape.likec4 alone says and the style census is the union of the documents that parse on " +
146
+ "their own — fix the map (`loam validate --all` names the file), then re-run `loam subsystem sync`.";
50
147
  /**
51
148
  * The tree AS THE RENAMES LEAVE IT, computed in memory: every dir under a
52
149
  * moved root is remapped by prefix, and each entry's placement chain is
@@ -1 +1 @@
1
- {"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/views.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAgB,MAAM,kCAAkC,CAAC;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAG/E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAAgB;IACtD,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IACnD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC5C,OAAO,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAgB,EAAE,IAAe;IACnE,OAAO,oBAAoB,CAAC,IAAI,EAAE,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAC,IAAe,EAAE,OAAuC,EAAE,YAAoB;IACtG,MAAM,KAAK,GAAG,CAAC,GAAW,EAAU,EAAE;QACpC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC;gBAAE,OAAO,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7F,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,CAAC,GAAW,EAAY,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClF,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YAChC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACzB,iEAAiE;YACjE,gEAAgE;YAChE,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChF,CAAC,CAAC;QACF,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACpC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACzB,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3C,CAAC,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["/**\n * What the generated views file MUST say — the shared answer every subsystem\n * writer and `subsystem sync` render from. A module beneath the verbs (in the\n * txn/ subpackage) because the commit window and sync both consume it, and a\n * second spelling of \"what should the views say\" would be a second chance to\n * disagree with `validate`'s staleness check.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, sep } from \"node:path\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { serviceDirOf, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { landscapePath } from \"../../../core/repo/paths.js\";\nimport { renderSubsystemViews } from \"../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\n\n/**\n * The elements of the whole `architecture/` PROJECT for the member join, or\n * the empty list when the map is absent or does not parse. Tolerant on\n * purpose: sync must stay runnable in a repo whose landscape is broken — it\n * renders what the committed bytes resolve (no includes), deterministically,\n * and the next sync after the landscape is repaired catches the file up.\n * `validate` skips the staleness question in exactly those states.\n *\n * The PROJECT, not the landscape file alone, and that is a correctness rule\n * rather than a preference: `validate` grades this file against\n * `loadArchitecture` — the landscape merged with every\n * `architecture/usecases/*.likec4`, which may `extend` the model and bind\n * elements of its own. While this side read the landscape alone, a use-case\n * document declaring one element bound to a filed service was enough to make\n * `sync` answer `current` while `validate --all` reported\n * `subsystem.views-stale` and named `sync` as the repair — a loop no command\n * could clear, reproduced on `examples/docs` before this call changed. Two\n * readers of \"what should the views say\" must read the same documents, or the\n * shared render in `core/repo/tree/render/views.ts` is shared in name only.\n */\nexport async function landscapeElements(docsDir: DocsDir): Promise<Elem[]> {\n if (!existsSync(landscapePath(docsDir))) return [];\n try {\n const doc = await loadArchitecture(docsDir);\n return doc.errors.length > 0 ? [] : doc.elements;\n } catch {\n return [];\n }\n}\n\n/** The bytes the views file must hold for this tree, or null for \"must be absent\". */\nexport async function expectedViews(docsDir: DocsDir, tree: FleetTree): Promise<string | null> {\n return renderSubsystemViews(tree, await landscapeElements(docsDir));\n}\n\n/**\n * The tree AS THE RENAMES LEAVE IT, computed in memory: every dir under a\n * moved root is remapped by prefix, and each entry's placement chain is\n * re-derived from its remapped dir. The views file is rendered from this, so\n * the commit lands file and renames agreeing — never from a re-walk that\n * would have to happen between two states. Beside `expectedViews` because the\n * two are one promise: what the commit renames and what the file says must\n * come from the same in-memory answer.\n */\nexport function movedTree(tree: FleetTree, renames: { from: string; to: string }[], servicesRoot: string): FleetTree {\n const remap = (dir: string): string => {\n for (const r of renames) {\n if (dir === r.from || dir.startsWith(r.from + sep)) return r.to + dir.slice(r.from.length);\n }\n return dir;\n };\n const chainOf = (dir: string): string[] => relative(servicesRoot, dir).split(sep);\n return {\n findings: tree.findings,\n services: tree.services.map((s) => {\n const dir = remap(s.dir);\n // The constructor call records provenance: a readdir-established\n // directory carried through a rename this same commit performs.\n return { ...s, dir: serviceDirOf(dir), subsystem: chainOf(dir).slice(0, -1) };\n }),\n subsystems: tree.subsystems.map((s) => {\n const dir = remap(s.dir);\n return { ...s, dir, path: chainOf(dir) };\n }),\n };\n}\n"]}
1
+ {"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the generated views file MUST say — the shared answer every subsystem\n * writer and `subsystem sync` render from. A module beneath the verbs (in the\n * txn/ subpackage) because the commit window and sync both consume it, and a\n * second spelling of \"what should the views say\" would be a second chance to\n * disagree with `validate`'s staleness check.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, resolve, sep } from \"node:path\";\nimport { loadFile } from \"../../../core/c4/likec4.js\";\nimport { architectureProjectDocuments, loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { serviceDirOf, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { landscapePath } from \"../../../core/repo/paths.js\";\nimport { type MapFacts, renderSubsystemViews } from \"../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\n\n/** What one read of the map yielded, and whether it came from the whole project. */\nexport interface LandscapeRead {\n /** The element join and the style census, or null when neither reading was available. */\n facts: MapFacts | null;\n /**\n * The `architecture/` PROJECT parsed. FALSE means the facts below, if there\n * are any, were assembled document by document — the member join from the\n * landscape file alone (so an element a use-case document binds is missing\n * from it), the style census from every document that parses on its own.\n */\n known: boolean;\n}\n\n/**\n * The elements and declared global style ids of the whole `architecture/`\n * PROJECT — the member join, and the one line a view may borrow a palette\n * with — with the provenance of that answer beside it.\n *\n * The PROJECT, not the landscape file alone, and that is a correctness rule\n * rather than a preference: `validate` grades this file against\n * `loadArchitecture` — the landscape merged with every\n * `architecture/usecases/*.likec4`, which may `extend` the model and bind\n * elements of its own. While this side read the landscape alone, a use-case\n * document declaring one element bound to a filed service was enough to make\n * `sync` answer `current` while `validate --all` reported\n * `subsystem.views-stale` and named `sync` as the repair — a loop no command\n * could clear, reproduced on `examples/docs` before this call changed. Two\n * readers of \"what should the views say\" must read the same documents, or the\n * shared render in `core/repo/tree/render/views.ts` is shared in name only.\n *\n * WHY THE FALLBACK AND THE FLAG ARE TWO ANSWERS RATHER THAN ONE. The fallback\n * exists because one broken use-case document must not cost the render its\n * member join: the landscape's own elements are still the truth about who\n * belongs in which subsystem, and re-rendering the file without them takes every\n * `include` line off it (verification 2026-09-04, W5). But the STYLE CENSUS is\n * not the landscape file's alone — SCHEMA blesses a palette in any `.likec4`\n * under `architecture/` — so the fallback takes it over EVERY document that\n * parses on its own (`declaredStyles`). Reading it off the landscape alone was\n * enough to drop `global style subsystems` from a good generated file whenever\n * the palette sat in a sibling and an unrelated sibling was broken\n * (re-verification 2026-09-04, area C item 8).\n *\n * `known` still means the PROJECT parsed, and nothing weaker, because the MEMBER\n * JOIN is what it licences: a use-case document that binds an element of its own\n * contributes to the join and cannot be read document by document. It is\n * `../sync.ts`'s licence to leave an existing file alone.\n *\n * An ABSENT map is not a failure: there is nothing to read, the empty facts are\n * the truth, and the tolerant render is what a fleet being scaffolded needs. An\n * EMPTY project is no longer reachable here — `architectureProjectDocuments`\n * keeps the landscape whatever the root `exclude` says, and its banner records\n * what an empty one cost.\n */\nexport async function landscapeFacts(docsDir: DocsDir): Promise<LandscapeRead> {\n const path = landscapePath(docsDir);\n if (!existsSync(path)) return { facts: { elements: [] }, known: true };\n try {\n const doc = await loadArchitecture(docsDir);\n // The whole document on success, so the render reads the same record\n // `validate --all` grades against; `globalStyles` rides along without\n // being named here, and a field the loaders add later does too.\n if (doc.errors.length === 0) return { facts: doc, known: true };\n } catch {\n // A throw and a parse error are the same evidence: fall through to the file.\n }\n try {\n const file = await loadFile(path);\n // THE STYLE CENSUS IS TAKEN OVER EVERY DOCUMENT, not over the landscape, and\n // that is what makes the fallback safe for a verb that has to write. A\n // palette declared in a SIBLING under `architecture/` is invisible to a read\n // of the landscape alone, so a `subsystem new` run standing beside one\n // unrelated broken sibling silently dropped `global style subsystems` from a\n // good generated file — the landscape parsed, the element join survived, and\n // nothing graded the loss because `sync` answers `blocked` for as long as the\n // broken file stands (re-verification 2026-09-04, area C item 8). Reading\n // each document ON ITS OWN is the widening the fallback is entitled to: it\n // cannot merge them (that is what failed one line up), but a document that\n // parses alone certainly declares the style groups it declares.\n if (file.errors.length === 0) {\n return { facts: { elements: file.elements, globalStyles: await declaredStyles(docsDir, file) }, known: false };\n }\n } catch {\n // Unreadable bytes; the caller is told loam could not say.\n }\n return { facts: null, known: false };\n}\n\n/**\n * Every global style id declared by an `architecture/` document that parses ON\n * ITS OWN, the landscape's included — the union, sorted, so the render is a\n * function of the set rather than of readdir order.\n *\n * The document set is `architectureProjectDocuments`: the same list the PROJECT\n * read uses, so the fallback can never consider a file the merged read would\n * have excluded — the generated views file itself, or one the root `exclude`\n * hides from the renderer. A document that does not parse alone contributes\n * nothing and is not an error here: it is named by `loam validate --all`, and\n * the note this module carries already tells the reader the census came from\n * outside a project read.\n */\nasync function declaredStyles(docsDir: DocsDir, landscape: MapFacts): Promise<string[]> {\n const ids = new Set(landscape.globalStyles ?? []);\n let paths: string[];\n try {\n paths = await architectureProjectDocuments(docsDir);\n } catch {\n return [...ids].sort();\n }\n const map = resolve(landscapePath(docsDir));\n for (const path of paths) {\n if (resolve(path) === map) continue;\n try {\n const doc = await loadFile(path);\n if (doc.errors.length === 0) for (const id of doc.globalStyles ?? []) ids.add(id);\n } catch {\n // One unreadable sibling costs its own styles and nothing else.\n }\n }\n return [...ids].sort();\n}\n\n/**\n * The bytes the views file must hold for this tree, and whether the map they\n * were computed from was read as a PROJECT.\n *\n * The content is always renderable — from the landscape file alone, or from\n * nothing — because a fleet with no generated file yet must still get one, and\n * the tree-changing verbs must still land theirs in the commit that changes the\n * tree. `known: false` is the caller's licence to leave an EXISTING file alone\n * instead (`../sync.ts`, `action: \"blocked\"`).\n */\nexport interface ExpectedViews {\n /**\n * The `architecture/` PROJECT was read. False means the content was rendered\n * from the landscape file alone — or from nothing at all — so its style census\n * is not one an existing file may be rewritten from.\n */\n known: boolean;\n /** The bytes the file must hold, or null for \"must be absent\". */\n content: string | null;\n}\n\nexport async function expectedViews(docsDir: DocsDir, tree: FleetTree): Promise<ExpectedViews> {\n const read = await landscapeFacts(docsDir);\n return { known: read.known, content: renderSubsystemViews(tree, read.facts ?? { elements: [] }) };\n}\n\n/**\n * The one line a person is owed when the map cannot be read — the same sentence\n * from `../sync.ts` (which then leaves an existing file alone) and from a\n * tree-changing verb (which renders the tolerant shape anyway, because its own\n * contract is that the file lands in the verb's commit). It names the gating\n * surface rather than repeating its findings: `loam validate --all` is where the\n * broken document is named. It lives here, beside the facts it is about, because\n * both callers are on this side of the `txn/` boundary and a constant in\n * `../sync.ts` would point the package edge back up.\n */\nexport const LANDSCAPE_UNREADABLE_NOTE =\n \"note: architecture/ does not parse as one project, so the element join is whatever \" +\n \"architecture/landscape.likec4 alone says and the style census is the union of the documents that parse on \" +\n \"their own — fix the map (`loam validate --all` names the file), then re-run `loam subsystem sync`.\";\n\n/**\n * The tree AS THE RENAMES LEAVE IT, computed in memory: every dir under a\n * moved root is remapped by prefix, and each entry's placement chain is\n * re-derived from its remapped dir. The views file is rendered from this, so\n * the commit lands file and renames agreeing — never from a re-walk that\n * would have to happen between two states. Beside `expectedViews` because the\n * two are one promise: what the commit renames and what the file says must\n * come from the same in-memory answer.\n */\nexport function movedTree(tree: FleetTree, renames: { from: string; to: string }[], servicesRoot: string): FleetTree {\n const remap = (dir: string): string => {\n for (const r of renames) {\n if (dir === r.from || dir.startsWith(r.from + sep)) return r.to + dir.slice(r.from.length);\n }\n return dir;\n };\n const chainOf = (dir: string): string[] => relative(servicesRoot, dir).split(sep);\n return {\n findings: tree.findings,\n services: tree.services.map((s) => {\n const dir = remap(s.dir);\n // The constructor call records provenance: a readdir-established\n // directory carried through a rename this same commit performs.\n return { ...s, dir: serviceDirOf(dir), subsystem: chainOf(dir).slice(0, -1) };\n }),\n subsystems: tree.subsystems.map((s) => {\n const dir = remap(s.dir);\n return { ...s, dir, path: chainOf(dir) };\n }),\n };\n}\n"]}
@@ -31,6 +31,11 @@ import { coversEntries, coversUnknownFindings } from "./checks/requirements.js";
31
31
  export async function deltaArchCoverage(delta) {
32
32
  const { docsDir, elements, relationships, taggedEls, taggedRels, archDeltas, featureServices, scenarioText, preloadedLand, fleet, } = delta;
33
33
  const findings = [];
34
+ // The read index the service models below are read through. A feature target
35
+ // may arrive without one (`validate --feature` builds no fleet), and it gets
36
+ // its own for `validateService`'s reason: `serviceModel` needs the enumeration
37
+ // and a document memo to answer at all.
38
+ const context = fleet ?? new FleetContext();
34
39
  // The delta's own element→service resolver, built once for the whole feature.
35
40
  // `serviceOf` is a one-shot wrapper that rebuilds its index on every call, and
36
41
  // the two loops below ask it up to five times per tagged edge — so a delta
@@ -171,16 +176,18 @@ export async function deltaArchCoverage(delta) {
171
176
  };
172
177
  const unresolved = coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable);
173
178
  if (unresolved.length > 0) {
174
- const modelPath = (await locateServicePaths(docsDir, svc, fleet)).model;
175
- const model = existsSync(modelPath)
176
- ? fleet === undefined
177
- ? await loadFile(modelPath)
178
- : await fleet.loadLikeC4(modelPath)
179
- : null;
180
- if (model !== null && model.errors.length === 0) {
179
+ // Through the ONE reader of `model.likec4`: a model that EXTENDS the map
180
+ // is not a file this could open, and reading it as one would answer with
181
+ // a pile of parse errors — which this arm reads as "no widening", so
182
+ // every `Covers:` line naming a container of a migrated service would be
183
+ // convicted as a typo. `mapUnreadable` is the same silence for the same
184
+ // reason: nothing may widen a scope out of a document nobody could read.
185
+ const svcPaths = await locateServicePaths(docsDir, svc, fleet);
186
+ const model = existsSync(svcPaths.model) ? await context.serviceModel(docsDir, svcPaths) : null;
187
+ if (model !== null && !model.mapUnreadable && model.doc.errors.length === 0) {
181
188
  scope = {
182
- elements: [...baseElements, ...model.elements],
183
- relationships: [...baseRels, ...model.relationships],
189
+ elements: [...baseElements, ...model.doc.elements],
190
+ relationships: [...baseRels, ...model.doc.relationships],
184
191
  health: health.ids,
185
192
  known,
186
193
  };