@ybotok/loam 0.2.0-alpha.5 → 0.2.0-alpha.7

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 (303) hide show
  1. package/CHANGELOG.md +1063 -0
  2. package/MIGRATING-from-OpenSpec.md +7 -4
  3. package/QUICKSTART.md +8 -3
  4. package/README.md +13 -7
  5. package/ROADMAP.md +285 -11
  6. package/SCHEMA.md +566 -45
  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 +53 -23
  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/commit.js +34 -1
  25. package/dist/commands/seed/commit.js.map +1 -1
  26. package/dist/commands/seed/plan.js +158 -13
  27. package/dist/commands/seed/plan.js.map +1 -1
  28. package/dist/commands/seed/seed.js +7 -2
  29. package/dist/commands/seed/seed.js.map +1 -1
  30. package/dist/commands/show/feature.js +2 -1
  31. package/dist/commands/show/feature.js.map +1 -1
  32. package/dist/commands/show/marks.js +3 -5
  33. package/dist/commands/show/marks.js.map +1 -1
  34. package/dist/commands/show/service.js +39 -18
  35. package/dist/commands/show/service.js.map +1 -1
  36. package/dist/commands/subsystem/edit.js +4 -2
  37. package/dist/commands/subsystem/edit.js.map +1 -1
  38. package/dist/commands/subsystem/move.js +55 -17
  39. package/dist/commands/subsystem/move.js.map +1 -1
  40. package/dist/commands/subsystem/sync.js +238 -87
  41. package/dist/commands/subsystem/sync.js.map +1 -1
  42. package/dist/commands/subsystem/txn/exclude.js +132 -0
  43. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  44. package/dist/commands/subsystem/txn/report.js +174 -0
  45. package/dist/commands/subsystem/txn/report.js.map +1 -0
  46. package/dist/commands/subsystem/txn/txn.js +126 -22
  47. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  48. package/dist/commands/subsystem/txn/views.js +159 -19
  49. package/dist/commands/subsystem/txn/views.js.map +1 -1
  50. package/dist/commands/validate/arch-coverage.js +16 -9
  51. package/dist/commands/validate/arch-coverage.js.map +1 -1
  52. package/dist/commands/validate/checks/fleet-shape.js +30 -77
  53. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  54. package/dist/commands/validate/checks/health-deps.js +9 -6
  55. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  56. package/dist/commands/validate/checks/vocabulary.js +4 -19
  57. package/dist/commands/validate/checks/vocabulary.js.map +1 -1
  58. package/dist/commands/validate/feature.js +1 -1
  59. package/dist/commands/validate/feature.js.map +1 -1
  60. package/dist/commands/validate/fleet/census.js +37 -13
  61. package/dist/commands/validate/fleet/census.js.map +1 -1
  62. package/dist/commands/validate/fleet/landscape.js +131 -120
  63. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  64. package/dist/commands/validate/fleet/load.js +66 -0
  65. package/dist/commands/validate/fleet/load.js.map +1 -1
  66. package/dist/commands/validate/fleet/map/attest.js +102 -0
  67. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  68. package/dist/commands/validate/fleet/map/bindings.js +109 -0
  69. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  70. package/dist/commands/validate/fleet/map/consumers.js +219 -0
  71. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  72. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  73. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  74. package/dist/commands/validate/fleet/map/usecases.js +56 -0
  75. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  76. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  77. package/dist/commands/validate/fleet/usecases/steps.js +49 -17
  78. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  79. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  80. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  81. package/dist/commands/validate/fleet/views/ids.js +45 -15
  82. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  83. package/dist/commands/validate/fleet/views/projects.js +258 -70
  84. package/dist/commands/validate/fleet/views/projects.js.map +1 -1
  85. package/dist/commands/validate/fleet/views/root-documents.js +99 -0
  86. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  87. package/dist/commands/validate/fleet/views/stale.js +32 -15
  88. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  89. package/dist/commands/validate/service/model/diverged.js +226 -0
  90. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  91. package/dist/commands/validate/service/model/grade.js +163 -0
  92. package/dist/commands/validate/service/model/grade.js.map +1 -0
  93. package/dist/commands/validate/service/service.js +110 -34
  94. package/dist/commands/validate/service/service.js.map +1 -1
  95. package/dist/commands/validate/service/specs.js +39 -5
  96. package/dist/commands/validate/service/specs.js.map +1 -1
  97. package/dist/commands/validate/service/spine.js +41 -3
  98. package/dist/commands/validate/service/spine.js.map +1 -1
  99. package/dist/commands/validate/service/usecases/tags.js +113 -0
  100. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  101. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  102. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  103. package/dist/commands/validate/validate.js +10 -24
  104. package/dist/commands/validate/validate.js.map +1 -1
  105. package/dist/core/agent/agents-md/artifacts.js +1 -0
  106. package/dist/core/agent/agents-md/artifacts.js.map +1 -1
  107. package/dist/core/agent/agents-md/command-map.js +19 -5
  108. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  109. package/dist/core/agent/agents-md/cycle.js +24 -9
  110. package/dist/core/agent/agents-md/cycle.js.map +1 -1
  111. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  112. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  113. package/dist/core/agent/agents-md/map/subsystem.js +70 -17
  114. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  115. package/dist/core/agent/agents-md/map/usecases.js +45 -3
  116. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  117. package/dist/core/agent/agents-md/refusals.js +5 -3
  118. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  119. package/dist/core/agent/agents-md.js +26 -7
  120. package/dist/core/agent/agents-md.js.map +1 -1
  121. package/dist/core/agent/support/report.js +39 -5
  122. package/dist/core/agent/support/report.js.map +1 -1
  123. package/dist/core/agent/workflows/adopt.js +123 -14
  124. package/dist/core/agent/workflows/adopt.js.map +1 -1
  125. package/dist/core/agent/workflows/check.js +34 -20
  126. package/dist/core/agent/workflows/check.js.map +1 -1
  127. package/dist/core/agent/workflows/closing.js +3 -1
  128. package/dist/core/agent/workflows/closing.js.map +1 -1
  129. package/dist/core/agent/workflows/reference/spine.js +12 -6
  130. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  131. package/dist/core/brief/brief.js +88 -19
  132. package/dist/core/brief/brief.js.map +1 -1
  133. package/dist/core/brief/checks.js +133 -7
  134. package/dist/core/brief/checks.js.map +1 -1
  135. package/dist/core/brief/landscape.js +212 -129
  136. package/dist/core/brief/landscape.js.map +1 -1
  137. package/dist/core/brief/map/extend.js +81 -0
  138. package/dist/core/brief/map/extend.js.map +1 -0
  139. package/dist/core/brief/map/owed.js +284 -0
  140. package/dist/core/brief/map/owed.js.map +1 -0
  141. package/dist/core/brief/targets.js +94 -29
  142. package/dist/core/brief/targets.js.map +1 -1
  143. package/dist/core/brief/unchecked.js +13 -3
  144. package/dist/core/brief/unchecked.js.map +1 -1
  145. package/dist/core/c4/likec4.js +23 -1
  146. package/dist/core/c4/likec4.js.map +1 -1
  147. package/dist/core/c4/parsed/deployment.js +5 -3
  148. package/dist/core/c4/parsed/deployment.js.map +1 -1
  149. package/dist/core/c4/parsed/view-ids.js +39 -1
  150. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  151. package/dist/core/c4/prefetch/fleet.js +142 -0
  152. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  153. package/dist/core/c4/project/architecture.js +77 -7
  154. package/dist/core/c4/project/architecture.js.map +1 -1
  155. package/dist/core/c4/project/batch.js +140 -0
  156. package/dist/core/c4/project/batch.js.map +1 -0
  157. package/dist/core/c4/project/documents.js +12 -6
  158. package/dist/core/c4/project/documents.js.map +1 -1
  159. package/dist/core/c4/project/load.js +85 -42
  160. package/dist/core/c4/project/load.js.map +1 -1
  161. package/dist/core/c4/project/staged.js +53 -15
  162. package/dist/core/c4/project/staged.js.map +1 -1
  163. package/dist/core/c4/resolve/attested.js +0 -0
  164. package/dist/core/c4/resolve/attested.js.map +1 -0
  165. package/dist/core/c4/resolve/service.js +8 -17
  166. package/dist/core/c4/resolve/service.js.map +1 -1
  167. package/dist/core/c4/resolve/steps.js +18 -0
  168. package/dist/core/c4/resolve/steps.js.map +1 -1
  169. package/dist/core/c4/root-project/exclude.js +352 -0
  170. package/dist/core/c4/root-project/exclude.js.map +1 -0
  171. package/dist/core/c4/service-model/documents.js +47 -0
  172. package/dist/core/c4/service-model/documents.js.map +1 -0
  173. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  174. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  175. package/dist/core/c4/service-model/load.js +165 -0
  176. package/dist/core/c4/service-model/load.js.map +1 -0
  177. package/dist/core/c4/service-model/renderer.js +128 -0
  178. package/dist/core/c4/service-model/renderer.js.map +1 -0
  179. package/dist/core/c4/service-model/shape.js +126 -0
  180. package/dist/core/c4/service-model/shape.js.map +1 -0
  181. package/dist/core/c4/service-model/slice.js +188 -0
  182. package/dist/core/c4/service-model/slice.js.map +1 -0
  183. package/dist/core/c4/source-scan.js +26 -0
  184. package/dist/core/c4/source-scan.js.map +1 -1
  185. package/dist/core/c4/splice/authored-source.js +60 -13
  186. package/dist/core/c4/splice/authored-source.js.map +1 -1
  187. package/dist/core/c4/splice/contract.js.map +1 -1
  188. package/dist/core/c4/splice/identity/declared.js +87 -0
  189. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  190. package/dist/core/c4/splice/identity/edges.js +17 -0
  191. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  192. package/dist/core/c4/splice/identity/existing.js +80 -0
  193. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  194. package/dist/core/c4/splice/identity/interior.js +80 -0
  195. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  196. package/dist/core/c4/splice/identity/owner.js +50 -0
  197. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  198. package/dist/core/c4/splice/landscape-merge.js +154 -100
  199. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  200. package/dist/core/c4/splice/model/merge.js +140 -0
  201. package/dist/core/c4/splice/model/merge.js.map +1 -0
  202. package/dist/core/c4/splice/placement.js +14 -4
  203. package/dist/core/c4/splice/placement.js.map +1 -1
  204. package/dist/core/c4/workspace.js +2 -1
  205. package/dist/core/c4/workspace.js.map +1 -1
  206. package/dist/core/coherence/coherence.js +7 -0
  207. package/dist/core/coherence/coherence.js.map +1 -1
  208. package/dist/core/delta/delta.js +9 -0
  209. package/dist/core/delta/delta.js.map +1 -1
  210. package/dist/core/deployment/delta.js +16 -10
  211. package/dist/core/deployment/delta.js.map +1 -1
  212. package/dist/core/diff/base-state.js +21 -5
  213. package/dist/core/diff/base-state.js.map +1 -1
  214. package/dist/core/diff/victims.js +10 -1
  215. package/dist/core/diff/victims.js.map +1 -1
  216. package/dist/core/docs.js +29 -24
  217. package/dist/core/docs.js.map +1 -1
  218. package/dist/core/doctor/config.js +7 -3
  219. package/dist/core/doctor/config.js.map +1 -1
  220. package/dist/core/doctor/doctor.js +36 -8
  221. package/dist/core/doctor/doctor.js.map +1 -1
  222. package/dist/core/doctor/report.js.map +1 -1
  223. package/dist/core/doctor/reports/scan.js +242 -0
  224. package/dist/core/doctor/reports/scan.js.map +1 -0
  225. package/dist/core/explain/refusals.js +1 -1
  226. package/dist/core/explain/refusals.js.map +1 -1
  227. package/dist/core/explore/explore.js +44 -9
  228. package/dist/core/explore/explore.js.map +1 -1
  229. package/dist/core/fleet-context.js +47 -0
  230. package/dist/core/fleet-context.js.map +1 -1
  231. package/dist/core/gate/checks.js +5 -1
  232. package/dist/core/gate/checks.js.map +1 -1
  233. package/dist/core/gate/partners.js +65 -26
  234. package/dist/core/gate/partners.js.map +1 -1
  235. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  236. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  237. package/dist/core/pack/living.js +47 -13
  238. package/dist/core/pack/living.js.map +1 -1
  239. package/dist/core/pack/pack.js +9 -6
  240. package/dist/core/pack/pack.js.map +1 -1
  241. package/dist/core/projection/arch-slice.js +2 -2
  242. package/dist/core/projection/arch-slice.js.map +1 -1
  243. package/dist/core/repo/paths.js +45 -38
  244. package/dist/core/repo/paths.js.map +1 -1
  245. package/dist/core/repo/tree/render/body.js +45 -6
  246. package/dist/core/repo/tree/render/body.js.map +1 -1
  247. package/dist/core/repo/tree/render/projects.js +93 -35
  248. package/dist/core/repo/tree/render/projects.js.map +1 -1
  249. package/dist/core/repo/tree/render/service/header.js +125 -0
  250. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  251. package/dist/core/repo/tree/render/service/peers.js +28 -0
  252. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  253. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  254. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  255. package/dist/core/repo/tree/render/service/views.js +78 -0
  256. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  257. package/dist/core/repo/tree/render/stale.js +20 -4
  258. package/dist/core/repo/tree/render/stale.js.map +1 -1
  259. package/dist/core/repo/tree/render/views.js +149 -32
  260. package/dist/core/repo/tree/render/views.js.map +1 -1
  261. package/dist/core/repo/tree/walk.js +45 -2
  262. package/dist/core/repo/tree/walk.js.map +1 -1
  263. package/dist/core/scaffold/landscape.js +219 -9
  264. package/dist/core/scaffold/landscape.js.map +1 -1
  265. package/dist/core/staging/writes.js.map +1 -1
  266. package/dist/core/usecases/delta/claims.js +1 -0
  267. package/dist/core/usecases/delta/claims.js.map +1 -1
  268. package/dist/core/usecases/delta/overlay.js +4 -0
  269. package/dist/core/usecases/delta/overlay.js.map +1 -1
  270. package/dist/core/usecases/fleet.js +18 -6
  271. package/dist/core/usecases/fleet.js.map +1 -1
  272. package/dist/core/usecases/operations.js +7 -0
  273. package/dist/core/usecases/operations.js.map +1 -1
  274. package/dist/core/usecases/service/exclude.js +119 -0
  275. package/dist/core/usecases/service/exclude.js.map +1 -0
  276. package/dist/core/usecases/service/flows.js +161 -0
  277. package/dist/core/usecases/service/flows.js.map +1 -0
  278. package/dist/core/verify/checklist.js +42 -10
  279. package/dist/core/verify/checklist.js.map +1 -1
  280. package/dist/core/vocabulary/issue.js.map +1 -1
  281. package/dist/core/vocabulary/maturity.js +51 -1
  282. package/dist/core/vocabulary/maturity.js.map +1 -1
  283. package/examples/README.md +30 -8
  284. package/examples/docs/architecture/landscape.likec4 +47 -0
  285. package/examples/docs/architecture/services.likec4 +70 -0
  286. package/examples/docs/architecture/subsystems.likec4 +3 -0
  287. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  288. package/examples/docs/likec4.config.json +0 -1
  289. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  290. package/examples/docs/services/order-service/arch.spec.md +13 -6
  291. package/examples/docs/services/order-service/model.likec4 +58 -70
  292. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  293. package/examples/docs/services/payment-service/model.likec4 +38 -39
  294. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  295. package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
  296. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  297. package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
  298. package/package.json +1 -1
  299. package/examples/docs/services/checkout-web/likec4.config.json +0 -4
  300. package/examples/docs/services/order-service/likec4.config.json +0 -4
  301. package/examples/docs/services/payment-service/likec4.config.json +0 -4
  302. package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
  303. package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
@@ -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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"writes.js","sourceRoot":"","sources":["../../../src/core/staging/writes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA8B5C,mDAAmD;AACnD,MAAM,UAAU,OAAO,CAAC,OAA+B;IACrD,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9E,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,OAA+B;IACrE,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACrF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAsBD;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAI1B,IAAI;IAHG,IAAI,GAAG,cAAc,CAAC;IAExC,YACW,IAAY,EACrB,MAAc;QAEd,KAAK,CAAC,GAAG,IAAI,IAAI,MAAM,yDAAyD,CAAC,CAAC;oBAHzE,IAAI;IAIf,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,UAAU,CAAC,KAAa,EAAE,IAAY;IAC7C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,YAAY,CACpB,IAAI,EACJ,mGAAmG;YACjG,gFAAgF,CACnF,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,YAAY,CACpB,IAAI,EACJ,2GAA2G;YACzG,iGAAiG;YACjG,yGAAyG;YACzG,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,OAAO,UAAU,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,SAAS,CAAC,CAAgB,EAAE,CAAgB;IAC1D,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,OAAwB;IAC7C,OAAO,OAAO,OAAO,KAAK,QAAQ;QAChC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAC5D,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC","sourcesContent":["/**\n * What a write IS before anything touches disk: the planned record, the staged\n * record, and the bytes both are made of.\n *\n * This is the package's floor. Everything else here — the lock, the swap, the\n * snapshot, the recovery journal — is a rule about WHEN bytes may land; only\n * this module decides what they are. Decoding lives here for the same reason:\n * text is produced only where a parser needs it and `readUtf8` is the one door,\n * so no later addition can reach a docs file through `readFile(…, \"utf8\")` and\n * silently substitute U+FFFD for bytes loam did not understand.\n */\nimport { isUtf8 } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\n\n/**\n * A planned file write — the merge is computed fully before anything touches\n * disk. `content: null` means \"delete this file\"; only `unarchive` plans those,\n * to take back a file the archive created.\n *\n * BYTES, not text. The whole write path — pre-image, staged content, snapshot,\n * the compare-and-set before each swap — moves Buffers, because a docs repo is\n * not guaranteed to hold text loam can decode: `Buffer.toString(\"utf8\")` never\n * fails, it substitutes U+FFFD, so reading a file as a string and writing the\n * string back is a silent, permanent rewrite of every byte loam did not\n * understand. It reached the snapshot too, so `unarchive` restored the damage\n * and reported success. Text is now produced only where a PARSER needs it, by\n * `decodeUtf8`, which refuses instead of substituting.\n */\nexport interface PlannedWrite {\n path: string;\n /**\n * A `string` is accepted for the callers whose new version IS text they\n * composed from nothing (a re-pinned delta, a restamped spec) and is encoded\n * as UTF-8 once, at staging. A caller holding BYTES — anything derived from a\n * file loam read — must hand over the Buffer: the round trip through a string\n * is the corruption above.\n */\n content: string | Buffer | null;\n /** Create the target atomically without replacing a concurrently created file. */\n exclusive?: boolean;\n}\n\n/** The one place a planned write becomes bytes. */\nexport function toBytes(content: string | Buffer | null): Buffer | null {\n return typeof content === \"string\" ? Buffer.from(content, \"utf8\") : content;\n}\n\n/**\n * The one place that decides whether a planned write is a CREATE *from the\n * filesystem*. (`core/verify/store/commit.ts` is the deliberate second rule:\n * it decides from the caller's held pre-image instead, because a CAS must\n * follow the read it promised, not what `existsSync` says now.)\n *\n * `exclusive` is a no-clobber create (link(2)), so it can only be asked of a\n * path that does not exist yet: asking it of an overwrite would fail EEXIST on\n * every single run. Deciding here — rather than at each of the dozen\n * `writes.push` sites in archive and unarchive — is what keeps the two commands\n * from disagreeing about which of their writes may safely lose a race. An\n * OVERWRITE is not left unguarded: `swapStaged` compares the target against the\n * pre-image immediately before the rename, so a file another writer changed\n * under us stops the commit instead of silently burying their bytes.\n */\nexport function planWrite(path: string, content: string | Buffer | null): PlannedWrite {\n if (content !== null && !existsSync(path)) return { path, content, exclusive: true };\n return { path, content };\n}\n\n/** A planned write with its new bytes parked next to the target, ready to swap in. */\nexport interface StagedWrite {\n write: PlannedWrite;\n /** What this write puts on disk — `write.content` encoded once. Null for a delete. */\n content: Buffer | null;\n /** Temp file holding the new bytes, in the target's OWN directory. Null for a delete. */\n tmp: string | null;\n /** The target's bytes before the swap; null when it did not exist. */\n before: Buffer | null;\n /**\n * Topmost directory this write had to create, if any — a rollback owes the repo\n * its directories back too. An empty `services/<svc>/` left standing is the\n * fleet claiming a service that was never merged.\n */\n createdDir: string | null;\n swapped: boolean;\n}\n\n\n\n/**\n * A file loam must PARSE that does not decode as the UTF-8 text loam owns.\n *\n * Commands map this to `merge-failed`: nothing was written, the file is named,\n * and re-saving it as UTF-8 makes the command work. The read-only migration\n * ingest already refuses non-UTF-8 input outright (core/openspec/) —\n * the destructive path has strictly more to lose, so it refuses the same way\n * rather than substituting U+FFFD and rewriting the file over the author.\n *\n * `reason` says what the bytes actually ARE, because the two ways a document\n * fails this test destroy different things: one substitutes bytes, the other\n * erases the whole document's meaning. A reader told the wrong one goes looking\n * in the wrong place. The advice that follows is the same either way, so it is\n * spelled once, here.\n */\nexport class NotUtf8Error extends Error {\n override readonly name = \"NotUtf8Error\";\n\n constructor(\n readonly path: string,\n reason: string,\n ) {\n super(`${path} ${reason} Nothing was written. Re-save it as UTF-8, then re-run.`);\n }\n}\n\n/**\n * Decode bytes a parser is about to be handed, or refuse naming the file.\n *\n * Two tests, the same two the read path makes (document-bytes.ts's\n * `decodeDocument`), because this side has strictly more to lose by missing\n * either. `isUtf8` catches the byte sequences that cannot be UTF-8 at all —\n * which is what a byte-order mark makes UTF-16 into. Without a BOM, UTF-16LE of\n * ASCII is a sequence of perfectly VALID UTF-8 bytes: every other byte is NUL,\n * so `isUtf8` says yes, the file decodes with a U+0000 between every character,\n * and every parser loam owns reads its requirements, frontmatter and headings\n * as absent rather than as unreadable. On the read path that is a document\n * graded green while nothing in it was checked; here it is worse, because\n * `archive` merges against that empty baseline and writes the result back — the\n * author's requirements are not mis-read, they are gone, and the undo snapshot\n * records the same emptiness as the pre-image.\n */\nfunction decodeUtf8(bytes: Buffer, path: string): string {\n if (!isUtf8(bytes)) {\n throw new NotUtf8Error(\n path,\n `is not valid UTF-8 — loam parses this file as text and writes it back, which would replace every ` +\n `undecodable byte with U+FFFD, in the living document AND in the undo snapshot.`,\n );\n }\n const text = bytes.toString(\"utf8\");\n if (text.includes(\"\\u0000\")) {\n throw new NotUtf8Error(\n path,\n `holds NUL characters, which no document loam owns does — almost always UTF-16 saved without a byte-order ` +\n `mark. Its bytes are valid UTF-8, so it decodes without complaint into text whose requirements, ` +\n `frontmatter and headings all parse as ABSENT: the merge would compute over an empty baseline and write ` +\n `that back, taking every requirement in the file with it.`,\n );\n }\n return text;\n}\n\n/** Read a file loam is going to parse. The only supported way to turn a docs file into a string. */\nexport async function readUtf8(path: string): Promise<string> {\n return decodeUtf8(await readFile(path), path);\n}\n\n/** Same bytes? Absence is its own value: a file that is gone is not a file that is empty. */\nexport function sameBytes(a: Buffer | null, b: Buffer | null): boolean {\n return a === null || b === null ? a === b : a.equals(b);\n}\n\n/* ------------------------------------------------------------------ */\n/* Reading loam's own records back off the disk */\n/* ------------------------------------------------------------------ */\n\n/** The one spelling of a sha256 loam wrote, for reading one back out of a record. */\nexport function isDigest(value: unknown): value is string {\n return typeof value === \"string\" && /^[0-9a-f]{64}$/.test(value);\n}\n\nexport function sha256(content: Buffer | string): string {\n return typeof content === \"string\"\n ? createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")\n : createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n"]}
1
+ {"version":3,"file":"writes.js","sourceRoot":"","sources":["../../../src/core/staging/writes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA+B5C,mDAAmD;AACnD,MAAM,UAAU,OAAO,CAAC,OAA+B;IACrD,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9E,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,OAA+B;IACrE,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACrF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAsBD;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAI1B,IAAI;IAHG,IAAI,GAAG,cAAc,CAAC;IAExC,YACW,IAAY,EACrB,MAAc;QAEd,KAAK,CAAC,GAAG,IAAI,IAAI,MAAM,yDAAyD,CAAC,CAAC;oBAHzE,IAAI;IAIf,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,UAAU,CAAC,KAAa,EAAE,IAAY;IAC7C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,YAAY,CACpB,IAAI,EACJ,mGAAmG;YACjG,gFAAgF,CACnF,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,YAAY,CACpB,IAAI,EACJ,2GAA2G;YACzG,iGAAiG;YACjG,yGAAyG;YACzG,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,OAAO,UAAU,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,SAAS,CAAC,CAAgB,EAAE,CAAgB;IAC1D,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,OAAwB;IAC7C,OAAO,OAAO,OAAO,KAAK,QAAQ;QAChC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAC5D,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC","sourcesContent":["/**\n * What a write IS before anything touches disk: the planned record, the staged\n * record, and the bytes both are made of.\n *\n * This is the package's floor. Everything else here — the lock, the swap, the\n * snapshot, the recovery journal — is a rule about WHEN bytes may land; only\n * this module decides what they are. Decoding lives here for the same reason:\n * text is produced only where a parser needs it and `readUtf8` is the one door,\n * so no later addition can reach a docs file through `readFile(…, \"utf8\")` and\n * silently substitute U+FFFD for bytes loam did not understand.\n */\nimport { isUtf8 } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\n\n/**\n * A planned file write — the merge is computed fully before anything touches\n * disk. `content: null` means \"delete this file\": `unarchive` takes back a file the\n * archive created, and `subsystem sync` removes the generated views file and a\n * stray per-service project file beside an extending model.\n *\n * BYTES, not text. The whole write path — pre-image, staged content, snapshot,\n * the compare-and-set before each swap — moves Buffers, because a docs repo is\n * not guaranteed to hold text loam can decode: `Buffer.toString(\"utf8\")` never\n * fails, it substitutes U+FFFD, so reading a file as a string and writing the\n * string back is a silent, permanent rewrite of every byte loam did not\n * understand. It reached the snapshot too, so `unarchive` restored the damage\n * and reported success. Text is now produced only where a PARSER needs it, by\n * `decodeUtf8`, which refuses instead of substituting.\n */\nexport interface PlannedWrite {\n path: string;\n /**\n * A `string` is accepted for the callers whose new version IS text they\n * composed from nothing (a re-pinned delta, a restamped spec) and is encoded\n * as UTF-8 once, at staging. A caller holding BYTES — anything derived from a\n * file loam read — must hand over the Buffer: the round trip through a string\n * is the corruption above.\n */\n content: string | Buffer | null;\n /** Create the target atomically without replacing a concurrently created file. */\n exclusive?: boolean;\n}\n\n/** The one place a planned write becomes bytes. */\nexport function toBytes(content: string | Buffer | null): Buffer | null {\n return typeof content === \"string\" ? Buffer.from(content, \"utf8\") : content;\n}\n\n/**\n * The one place that decides whether a planned write is a CREATE *from the\n * filesystem*. (`core/verify/store/commit.ts` is the deliberate second rule:\n * it decides from the caller's held pre-image instead, because a CAS must\n * follow the read it promised, not what `existsSync` says now.)\n *\n * `exclusive` is a no-clobber create (link(2)), so it can only be asked of a\n * path that does not exist yet: asking it of an overwrite would fail EEXIST on\n * every single run. Deciding here — rather than at each of the dozen\n * `writes.push` sites in archive and unarchive — is what keeps the two commands\n * from disagreeing about which of their writes may safely lose a race. An\n * OVERWRITE is not left unguarded: `swapStaged` compares the target against the\n * pre-image immediately before the rename, so a file another writer changed\n * under us stops the commit instead of silently burying their bytes.\n */\nexport function planWrite(path: string, content: string | Buffer | null): PlannedWrite {\n if (content !== null && !existsSync(path)) return { path, content, exclusive: true };\n return { path, content };\n}\n\n/** A planned write with its new bytes parked next to the target, ready to swap in. */\nexport interface StagedWrite {\n write: PlannedWrite;\n /** What this write puts on disk — `write.content` encoded once. Null for a delete. */\n content: Buffer | null;\n /** Temp file holding the new bytes, in the target's OWN directory. Null for a delete. */\n tmp: string | null;\n /** The target's bytes before the swap; null when it did not exist. */\n before: Buffer | null;\n /**\n * Topmost directory this write had to create, if any — a rollback owes the repo\n * its directories back too. An empty `services/<svc>/` left standing is the\n * fleet claiming a service that was never merged.\n */\n createdDir: string | null;\n swapped: boolean;\n}\n\n\n\n/**\n * A file loam must PARSE that does not decode as the UTF-8 text loam owns.\n *\n * Commands map this to `merge-failed`: nothing was written, the file is named,\n * and re-saving it as UTF-8 makes the command work. The read-only migration\n * ingest already refuses non-UTF-8 input outright (core/openspec/) —\n * the destructive path has strictly more to lose, so it refuses the same way\n * rather than substituting U+FFFD and rewriting the file over the author.\n *\n * `reason` says what the bytes actually ARE, because the two ways a document\n * fails this test destroy different things: one substitutes bytes, the other\n * erases the whole document's meaning. A reader told the wrong one goes looking\n * in the wrong place. The advice that follows is the same either way, so it is\n * spelled once, here.\n */\nexport class NotUtf8Error extends Error {\n override readonly name = \"NotUtf8Error\";\n\n constructor(\n readonly path: string,\n reason: string,\n ) {\n super(`${path} ${reason} Nothing was written. Re-save it as UTF-8, then re-run.`);\n }\n}\n\n/**\n * Decode bytes a parser is about to be handed, or refuse naming the file.\n *\n * Two tests, the same two the read path makes (document-bytes.ts's\n * `decodeDocument`), because this side has strictly more to lose by missing\n * either. `isUtf8` catches the byte sequences that cannot be UTF-8 at all —\n * which is what a byte-order mark makes UTF-16 into. Without a BOM, UTF-16LE of\n * ASCII is a sequence of perfectly VALID UTF-8 bytes: every other byte is NUL,\n * so `isUtf8` says yes, the file decodes with a U+0000 between every character,\n * and every parser loam owns reads its requirements, frontmatter and headings\n * as absent rather than as unreadable. On the read path that is a document\n * graded green while nothing in it was checked; here it is worse, because\n * `archive` merges against that empty baseline and writes the result back — the\n * author's requirements are not mis-read, they are gone, and the undo snapshot\n * records the same emptiness as the pre-image.\n */\nfunction decodeUtf8(bytes: Buffer, path: string): string {\n if (!isUtf8(bytes)) {\n throw new NotUtf8Error(\n path,\n `is not valid UTF-8 — loam parses this file as text and writes it back, which would replace every ` +\n `undecodable byte with U+FFFD, in the living document AND in the undo snapshot.`,\n );\n }\n const text = bytes.toString(\"utf8\");\n if (text.includes(\"\\u0000\")) {\n throw new NotUtf8Error(\n path,\n `holds NUL characters, which no document loam owns does — almost always UTF-16 saved without a byte-order ` +\n `mark. Its bytes are valid UTF-8, so it decodes without complaint into text whose requirements, ` +\n `frontmatter and headings all parse as ABSENT: the merge would compute over an empty baseline and write ` +\n `that back, taking every requirement in the file with it.`,\n );\n }\n return text;\n}\n\n/** Read a file loam is going to parse. The only supported way to turn a docs file into a string. */\nexport async function readUtf8(path: string): Promise<string> {\n return decodeUtf8(await readFile(path), path);\n}\n\n/** Same bytes? Absence is its own value: a file that is gone is not a file that is empty. */\nexport function sameBytes(a: Buffer | null, b: Buffer | null): boolean {\n return a === null || b === null ? a === b : a.equals(b);\n}\n\n/* ------------------------------------------------------------------ */\n/* Reading loam's own records back off the disk */\n/* ------------------------------------------------------------------ */\n\n/** The one spelling of a sha256 loam wrote, for reading one back out of a record. */\nexport function isDigest(value: unknown): value is string {\n return typeof value === \"string\" && /^[0-9a-f]{64}$/.test(value);\n}\n\nexport function sha256(content: Buffer | string): string {\n return typeof content === \"string\"\n ? createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")\n : createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n"]}
@@ -111,6 +111,7 @@ export async function flowCoverage(req) {
111
111
  declared: gradableCapabilityIds(vocab),
112
112
  requirementsOf: (capability) => byCapability.get(capability),
113
113
  ...(req.load === undefined ? {} : { load: req.load }),
114
+ ...(req.models === undefined ? {} : { models: req.models }),
114
115
  });
115
116
  }
116
117
  //# sourceMappingURL=claims.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"claims.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/claims.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which of this feature's own promises its own flows keep.\n *\n * A cross-service criterion — \"I enter a login and a password and I am in\" —\n * belongs to no single service's spec, because each service promises only its\n * own part. A `dynamic view` can carry it, because it IS the hop sequence, and\n * that is the whole reason `#req-` exists. Until `./flows.ts` gave a feature a\n * slot for one, this answer could only ever be empty inside a feature window:\n * the flow was a fleet document, so a tag naming a requirement the same change\n * was adding named something that was not living yet.\n *\n * WHAT THIS FEEDS. `capability.uncovered` — the archive gate that refuses a\n * promise nothing keeps. Its message used to end by telling authors to archive\n * with `--approve` and tag the flow AFTERWARDS, which was honest advice about a\n * hole rather than a workflow anybody wanted: it lands the promise first, kept\n * by nothing, and relies on somebody coming back.\n *\n * THE RULE IS `../capability.ts`'s AND MUST STAY SO. Only a RESOLVED claim\n * counts, and both halves must resolve — the `#cap-` tag to exactly one declared\n * capability, the `#req-` tag to exactly one of its requirements. A broken tag\n * of either kind is already an error (`usecase.capability-unresolved`,\n * `usecase.requirement-unresolved`) and must never also mark a promise kept: a\n * typo that silenced the gate would turn a mistake into a green archive. The two\n * resolvers are called directly rather than through\n * `useCaseRequirementClaims` for one reason, and it is not a difference of\n * opinion about the rule — that function flattens its answer to a `<cap>#<id>`\n * string, and `capabilities/delta/uncovered.ts` keys this same join on NUL\n * precisely because a capability id has no grammar forbidding `#`. Handing it a\n * key it has written down a reason not to use would be the wrong kind of reuse.\n *\n * THE LADDER TRAVELS. `declared === null` — an absent or unreadable\n * `architecture/capabilities.yaml` — is an empty answer, not a suspended one, and\n * the caller is already suppressing the whole family behind it. An `unreadable`\n * overlay is different and must NOT be flattened to \"keeps nothing\": loam did not\n * look, which is never the same answer as \"there is nothing there\", and a gate\n * that refused on it would blame the author for a map it could not read.\n */\nimport type { LoadedDoc } from \"../../c4/likec4.js\";\nimport type { CapabilityVocabulary } from \"../../capabilities/capabilities.js\";\nimport { withFeatureCapabilities } from \"../../capabilities/delta/overlay.js\";\nimport type { CapabilityDeltaDoc } from \"../../capabilities/delta/uncovered.js\";\nimport { gradableCapabilityIds } from \"../../capabilities/findings.js\";\nimport { resolveCapabilityTags, resolveRequirementTags } from \"../../capabilities/usecase-join.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { compareIds } from \"../../repo/entries.js\";\nimport { readFeatureFlows, type FlowOverlayRequest } from \"./overlay.js\";\n\n/** One promise this feature's flows keep, with the flows that keep it. */\nexport interface FlowPromise {\n /** The capability the keeping view's `#cap-` tag resolved to. */\n capability: string;\n /** The `Requirement-ID` its `#req-` tag resolved to, inside that capability. */\n requirement: string;\n /** The view ids keeping it, deduplicated and sorted — message text, never identity. */\n flows: readonly string[];\n}\n\n/**\n * What the feature's flows keep, or one of the two honest refusals to say.\n *\n * THREE ARMS, because the two failures are not the same failure and a caller\n * that merged them would say the wrong thing. `unreadable` is loam unable to\n * READ the flows — a parse error, a hop naming an element the merge does not\n * land — and it earns `usecase.flow-invalid` and a refusal. `ungraded` is loam\n * able to read them and unable to RESOLVE their tags, because the capability\n * vocabulary this feature's merge would leave behind does not parse; the flows\n * are fine, the file they point into is not, and refusing over the flows would\n * name the wrong document.\n *\n * `read` with an empty `kept` is the ordinary answer and a real one: loam\n * looked, and no flow of this feature keeps a promise it adds.\n */\nexport type FlowPromises =\n | { kind: \"read\"; kept: readonly FlowPromise[] }\n | { kind: \"ungraded\"; why: string }\n | { kind: \"unreadable\"; errors: string[] };\n\n/** Everything the join needs, on top of what the overlay already asks for. */\nexport interface FlowPromisesRequest extends FlowOverlayRequest {\n /**\n * The capability ids to resolve `#cap-` against — the LIVING vocabulary\n * widened with the ids this feature's own capability deltas introduce, which\n * is the same both-corpora rule `Realizes:` follows\n * (`core/capabilities/delta/overlay.ts`). `null` suspends the axis.\n */\n declared: readonly string[] | null;\n /**\n * The requirement ids per capability, widened the same way\n * (`withFeatureRequirements`). `undefined` for a capability with no document\n * at all is a different answer from an empty set, and both are real here.\n */\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined;\n}\n\n/**\n * The promises this feature's own flows keep, read over the post-merge map.\n *\n * A feature with no flow is `read` with nothing kept, which is the ordinary\n * answer and costs one walk over a directory that is not there.\n */\nexport async function featureFlowPromises(req: FlowPromisesRequest): Promise<FlowPromises> {\n const scan = await readFeatureFlows(req);\n if (scan.kind === \"unreadable\") return { kind: \"unreadable\", errors: scan.errors };\n // The ladder, and it is `ungraded` rather than an empty answer. Once the\n // feature's own capability deltas are unioned in, `declared` is null for\n // exactly one reason — `architecture/capabilities.yaml` exists and does not\n // read as a vocabulary — so no `#cap-` tag can resolve and no claim can be\n // made either way. Returning \"keeps nothing\" here is what let\n // `capability.uncovered` convict a flow that does resolve, over a file the\n // feature gate reports nothing else about.\n if (req.declared === null) {\n return {\n kind: \"ungraded\",\n why: \"architecture/capabilities.yaml does not read as a capability vocabulary (`loam validate --all` reports it as capability.invalid)\",\n };\n }\n // A real, empty answer: loam read the flows and none of them claims anything.\n if (scan.views.length === 0) return { kind: \"read\", kept: [] };\n\n const byPromise = new Map<string, { promise: FlowPromise; flows: Set<string> }>();\n for (const view of scan.views) {\n const scope = resolveCapabilityTags(view.tags, req.declared).flatMap((claim) =>\n claim.kind === \"resolved\" ? [claim.id] : [],\n );\n for (const claim of resolveRequirementTags(view.tags, scope, req.requirementsOf)) {\n if (claim.kind !== \"resolved\") continue;\n // Keyed on the pair, never on a joined string: see the header. The Map is\n // only here to collapse two views keeping one promise into one row.\n const key = JSON.stringify([claim.capability, claim.id]);\n const found = byPromise.get(key);\n if (found === undefined) {\n byPromise.set(key, {\n promise: { capability: claim.capability, requirement: claim.id, flows: [] },\n flows: new Set([view.id]),\n });\n } else {\n found.flows.add(view.id);\n }\n }\n }\n // Sorted, and the view ids inside each row sorted too: nothing in loam has\n // measured that LikeC4 preserves view declaration order, so a message built\n // from it would reorder under a dependency bump.\n const kept = [...byPromise.values()]\n .map(({ promise, flows }) => ({ ...promise, flows: [...flows].sort(compareIds) }))\n .sort((a, b) => compareIds(a.capability, b.capability) || compareIds(a.requirement, b.requirement));\n return { kind: \"read\", kept };\n}\n\n/** Everything the coverage join needs, and nothing it would have to read twice. */\nexport interface FlowCoverageRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n featureId: string;\n /**\n * The feature's capability deltas, ALREADY PARSED by the caller's walk. They\n * arrive rather than being read because the one caller has just produced them,\n * and re-reading a document to answer a question about it is how two answers\n * about one file appear.\n */\n capabilities: readonly CapabilityDeltaDoc[];\n /** The LIVING capability vocabulary, read by the caller through its own index. */\n vocabulary: CapabilityVocabulary;\n /** The enumerated fleet, for `readFeatureFlows`' resolver and for its reason. */\n known: ReadonlySet<string>;\n /** The caller's memoised LikeC4 read; see `./overlay.ts`. */\n load?: (path: string) => Promise<LoadedDoc>;\n}\n\n/**\n * What this feature's own flows keep, resolved against the vocabulary and the\n * requirement ids its own merge would leave behind.\n *\n * THE WIDENING IS THE POINT, and it is `capabilities/delta/overlay.ts`'s\n * judgement applied to the other carrier: an analyst adds `NOTIFY-ONCE` to a\n * capability delta and an architect answers it with a flow in the same change,\n * so a `#req-` tag resolved against the LIVING tree alone names a promise that\n * does not exist yet — and the same archive that would land the flow lands the\n * promise it points at.\n *\n * `requirementsOf` KNOWS ONLY THE CAPABILITIES THIS FEATURE HAS A DELTA FOR, and\n * within those it carries living ∪ ADDED ∪ MODIFIED ids. The narrowing changes\n * no answer, for a reason stronger than \"it is probably fine\":\n * `uncoveredIssues` grades `r.kind === \"ADDED\"` and nothing else, and every\n * ADDED requirement it grades lives in one of these documents by construction —\n * so no id outside this map can reach a verdict. The LIVING half is in there\n * because a widening of the graded set must not silently need a second edit\n * here; today it decides nothing. A flow keeping a promise of a capability this\n * feature does NOT touch resolves to `undocumented` and counts for nothing,\n * which is correct: the fleet-scope `capability.requirement-unrealized` grades\n * that join over the whole corpus, and answering it here from a partial index is\n * how two surfaces start disagreeing.\n *\n * `declared` IS NOT NARROWED, and that asymmetry is load-bearing. It is what\n * `resolveCapabilityTags` compares a `#cap-` slug against, and a slug is not\n * injective — `payments/refunds` and `payments-refunds` flatten to one. Handing\n * it a short list would resolve a tag that is genuinely ambiguous fleet-wide and\n * attribute a whole flow to the wrong capability, silently. So the vocabulary is\n * the fleet's whole one, read once by the caller.\n */\nexport async function flowCoverage(req: FlowCoverageRequest): Promise<FlowPromises> {\n const vocab = withFeatureCapabilities(req.vocabulary, req.capabilities.map((doc) => doc.id));\n const byCapability = new Map<string, ReadonlySet<string>>();\n for (const doc of req.capabilities) {\n const ids = new Set<string>();\n // REMOVED is excluded on both sides — a promise on its way out keeps nothing.\n for (const r of doc.living) if (r.kind !== \"REMOVED\" && r.id !== undefined) ids.add(r.id);\n for (const r of doc.reqs) if ((r.kind === \"ADDED\" || r.kind === \"MODIFIED\") && r.id !== undefined) ids.add(r.id);\n byCapability.set(doc.id, ids);\n }\n return featureFlowPromises({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n known: req.known,\n declared: gradableCapabilityIds(vocab),\n requirementsOf: (capability) => byCapability.get(capability),\n ...(req.load === undefined ? {} : { load: req.load }),\n });\n}\n"]}
1
+ {"version":3,"file":"claims.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/claims.ts"],"names":[],"mappings":"AAwCA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AAE9E,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAA2B,MAAM,cAAc,CAAC;AAiDzE;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,GAAwB;IAChE,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IACnF,yEAAyE;IACzE,yEAAyE;IACzE,4EAA4E;IAC5E,2EAA2E;IAC3E,8DAA8D;IAC9D,2EAA2E;IAC3E,2CAA2C;IAC3C,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC1B,OAAO;YACL,IAAI,EAAE,UAAU;YAChB,GAAG,EAAE,kIAAkI;SACxI,CAAC;IACJ,CAAC;IACD,8EAA8E;IAC9E,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAE/D,MAAM,SAAS,GAAG,IAAI,GAAG,EAAwD,CAAC;IAClF,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAC7E,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAC5C,CAAC;QACF,KAAK,MAAM,KAAK,IAAI,sBAAsB,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;YACjF,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;gBAAE,SAAS;YACxC,0EAA0E;YAC1E,oEAAoE;YACpE,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;YACzD,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE;oBACjB,OAAO,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;oBAC3E,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;iBAC1B,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;IACH,CAAC;IACD,2EAA2E;IAC3E,4EAA4E;IAC5E,iDAAiD;IACjD,MAAM,IAAI,GAAG,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC;SACjC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;SACjF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;IACtG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAChC,CAAC;AA4BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAwB;IACzD,MAAM,KAAK,GAAG,uBAAuB,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7F,MAAM,YAAY,GAAG,IAAI,GAAG,EAA+B,CAAC;IAC5D,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,YAAY,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;QAC9B,8EAA8E;QAC9E,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,MAAM;YAAE,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,EAAE,KAAK,SAAS;gBAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1F,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI;YAAE,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,SAAS;gBAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACjH,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,mBAAmB,CAAC;QACzB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,QAAQ,EAAE,qBAAqB,CAAC,KAAK,CAAC;QACtC,cAAc,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC;QAC5D,GAAG,CAAC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;QACrD,GAAG,CAAC,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;KAC5D,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Which of this feature's own promises its own flows keep.\n *\n * A cross-service criterion — \"I enter a login and a password and I am in\" —\n * belongs to no single service's spec, because each service promises only its\n * own part. A `dynamic view` can carry it, because it IS the hop sequence, and\n * that is the whole reason `#req-` exists. Until `./flows.ts` gave a feature a\n * slot for one, this answer could only ever be empty inside a feature window:\n * the flow was a fleet document, so a tag naming a requirement the same change\n * was adding named something that was not living yet.\n *\n * WHAT THIS FEEDS. `capability.uncovered` — the archive gate that refuses a\n * promise nothing keeps. Its message used to end by telling authors to archive\n * with `--approve` and tag the flow AFTERWARDS, which was honest advice about a\n * hole rather than a workflow anybody wanted: it lands the promise first, kept\n * by nothing, and relies on somebody coming back.\n *\n * THE RULE IS `../capability.ts`'s AND MUST STAY SO. Only a RESOLVED claim\n * counts, and both halves must resolve — the `#cap-` tag to exactly one declared\n * capability, the `#req-` tag to exactly one of its requirements. A broken tag\n * of either kind is already an error (`usecase.capability-unresolved`,\n * `usecase.requirement-unresolved`) and must never also mark a promise kept: a\n * typo that silenced the gate would turn a mistake into a green archive. The two\n * resolvers are called directly rather than through\n * `useCaseRequirementClaims` for one reason, and it is not a difference of\n * opinion about the rule — that function flattens its answer to a `<cap>#<id>`\n * string, and `capabilities/delta/uncovered.ts` keys this same join on NUL\n * precisely because a capability id has no grammar forbidding `#`. Handing it a\n * key it has written down a reason not to use would be the wrong kind of reuse.\n *\n * THE LADDER TRAVELS. `declared === null` — an absent or unreadable\n * `architecture/capabilities.yaml` — is an empty answer, not a suspended one, and\n * the caller is already suppressing the whole family behind it. An `unreadable`\n * overlay is different and must NOT be flattened to \"keeps nothing\": loam did not\n * look, which is never the same answer as \"there is nothing there\", and a gate\n * that refused on it would blame the author for a map it could not read.\n */\nimport type { LoadedDoc } from \"../../c4/likec4.js\";\nimport type { ExtendingModel } from \"../../c4/splice/contract.js\";\nimport type { CapabilityVocabulary } from \"../../capabilities/capabilities.js\";\nimport { withFeatureCapabilities } from \"../../capabilities/delta/overlay.js\";\nimport type { CapabilityDeltaDoc } from \"../../capabilities/delta/uncovered.js\";\nimport { gradableCapabilityIds } from \"../../capabilities/findings.js\";\nimport { resolveCapabilityTags, resolveRequirementTags } from \"../../capabilities/usecase-join.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { compareIds } from \"../../repo/entries.js\";\nimport { readFeatureFlows, type FlowOverlayRequest } from \"./overlay.js\";\n\n/** One promise this feature's flows keep, with the flows that keep it. */\nexport interface FlowPromise {\n /** The capability the keeping view's `#cap-` tag resolved to. */\n capability: string;\n /** The `Requirement-ID` its `#req-` tag resolved to, inside that capability. */\n requirement: string;\n /** The view ids keeping it, deduplicated and sorted — message text, never identity. */\n flows: readonly string[];\n}\n\n/**\n * What the feature's flows keep, or one of the two honest refusals to say.\n *\n * THREE ARMS, because the two failures are not the same failure and a caller\n * that merged them would say the wrong thing. `unreadable` is loam unable to\n * READ the flows — a parse error, a hop naming an element the merge does not\n * land — and it earns `usecase.flow-invalid` and a refusal. `ungraded` is loam\n * able to read them and unable to RESOLVE their tags, because the capability\n * vocabulary this feature's merge would leave behind does not parse; the flows\n * are fine, the file they point into is not, and refusing over the flows would\n * name the wrong document.\n *\n * `read` with an empty `kept` is the ordinary answer and a real one: loam\n * looked, and no flow of this feature keeps a promise it adds.\n */\nexport type FlowPromises =\n | { kind: \"read\"; kept: readonly FlowPromise[] }\n | { kind: \"ungraded\"; why: string }\n | { kind: \"unreadable\"; errors: string[] };\n\n/** Everything the join needs, on top of what the overlay already asks for. */\nexport interface FlowPromisesRequest extends FlowOverlayRequest {\n /**\n * The capability ids to resolve `#cap-` against — the LIVING vocabulary\n * widened with the ids this feature's own capability deltas introduce, which\n * is the same both-corpora rule `Realizes:` follows\n * (`core/capabilities/delta/overlay.ts`). `null` suspends the axis.\n */\n declared: readonly string[] | null;\n /**\n * The requirement ids per capability, widened the same way\n * (`withFeatureRequirements`). `undefined` for a capability with no document\n * at all is a different answer from an empty set, and both are real here.\n */\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined;\n}\n\n/**\n * The promises this feature's own flows keep, read over the post-merge map.\n *\n * A feature with no flow is `read` with nothing kept, which is the ordinary\n * answer and costs one walk over a directory that is not there.\n */\nexport async function featureFlowPromises(req: FlowPromisesRequest): Promise<FlowPromises> {\n const scan = await readFeatureFlows(req);\n if (scan.kind === \"unreadable\") return { kind: \"unreadable\", errors: scan.errors };\n // The ladder, and it is `ungraded` rather than an empty answer. Once the\n // feature's own capability deltas are unioned in, `declared` is null for\n // exactly one reason — `architecture/capabilities.yaml` exists and does not\n // read as a vocabulary — so no `#cap-` tag can resolve and no claim can be\n // made either way. Returning \"keeps nothing\" here is what let\n // `capability.uncovered` convict a flow that does resolve, over a file the\n // feature gate reports nothing else about.\n if (req.declared === null) {\n return {\n kind: \"ungraded\",\n why: \"architecture/capabilities.yaml does not read as a capability vocabulary (`loam validate --all` reports it as capability.invalid)\",\n };\n }\n // A real, empty answer: loam read the flows and none of them claims anything.\n if (scan.views.length === 0) return { kind: \"read\", kept: [] };\n\n const byPromise = new Map<string, { promise: FlowPromise; flows: Set<string> }>();\n for (const view of scan.views) {\n const scope = resolveCapabilityTags(view.tags, req.declared).flatMap((claim) =>\n claim.kind === \"resolved\" ? [claim.id] : [],\n );\n for (const claim of resolveRequirementTags(view.tags, scope, req.requirementsOf)) {\n if (claim.kind !== \"resolved\") continue;\n // Keyed on the pair, never on a joined string: see the header. The Map is\n // only here to collapse two views keeping one promise into one row.\n const key = JSON.stringify([claim.capability, claim.id]);\n const found = byPromise.get(key);\n if (found === undefined) {\n byPromise.set(key, {\n promise: { capability: claim.capability, requirement: claim.id, flows: [] },\n flows: new Set([view.id]),\n });\n } else {\n found.flows.add(view.id);\n }\n }\n }\n // Sorted, and the view ids inside each row sorted too: nothing in loam has\n // measured that LikeC4 preserves view declaration order, so a message built\n // from it would reorder under a dependency bump.\n const kept = [...byPromise.values()]\n .map(({ promise, flows }) => ({ ...promise, flows: [...flows].sort(compareIds) }))\n .sort((a, b) => compareIds(a.capability, b.capability) || compareIds(a.requirement, b.requirement));\n return { kind: \"read\", kept };\n}\n\n/** Everything the coverage join needs, and nothing it would have to read twice. */\nexport interface FlowCoverageRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n featureId: string;\n /**\n * The feature's capability deltas, ALREADY PARSED by the caller's walk. They\n * arrive rather than being read because the one caller has just produced them,\n * and re-reading a document to answer a question about it is how two answers\n * about one file appear.\n */\n capabilities: readonly CapabilityDeltaDoc[];\n /** The LIVING capability vocabulary, read by the caller through its own index. */\n vocabulary: CapabilityVocabulary;\n /** The enumerated fleet, for `readFeatureFlows`' resolver and for its reason. */\n known: ReadonlySet<string>;\n /** The caller's memoised LikeC4 read; see `./overlay.ts`. */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models for the merge preview, as a thunk the overlay\n * calls only when this feature really brings a flow; see `./overlay.ts` for\n * why the preview is wrong without them and why it arrives as a function.\n */\n models?: () => Promise<readonly ExtendingModel[]>;\n}\n\n/**\n * What this feature's own flows keep, resolved against the vocabulary and the\n * requirement ids its own merge would leave behind.\n *\n * THE WIDENING IS THE POINT, and it is `capabilities/delta/overlay.ts`'s\n * judgement applied to the other carrier: an analyst adds `NOTIFY-ONCE` to a\n * capability delta and an architect answers it with a flow in the same change,\n * so a `#req-` tag resolved against the LIVING tree alone names a promise that\n * does not exist yet — and the same archive that would land the flow lands the\n * promise it points at.\n *\n * `requirementsOf` KNOWS ONLY THE CAPABILITIES THIS FEATURE HAS A DELTA FOR, and\n * within those it carries living ∪ ADDED ∪ MODIFIED ids. The narrowing changes\n * no answer, for a reason stronger than \"it is probably fine\":\n * `uncoveredIssues` grades `r.kind === \"ADDED\"` and nothing else, and every\n * ADDED requirement it grades lives in one of these documents by construction —\n * so no id outside this map can reach a verdict. The LIVING half is in there\n * because a widening of the graded set must not silently need a second edit\n * here; today it decides nothing. A flow keeping a promise of a capability this\n * feature does NOT touch resolves to `undocumented` and counts for nothing,\n * which is correct: the fleet-scope `capability.requirement-unrealized` grades\n * that join over the whole corpus, and answering it here from a partial index is\n * how two surfaces start disagreeing.\n *\n * `declared` IS NOT NARROWED, and that asymmetry is load-bearing. It is what\n * `resolveCapabilityTags` compares a `#cap-` slug against, and a slug is not\n * injective — `payments/refunds` and `payments-refunds` flatten to one. Handing\n * it a short list would resolve a tag that is genuinely ambiguous fleet-wide and\n * attribute a whole flow to the wrong capability, silently. So the vocabulary is\n * the fleet's whole one, read once by the caller.\n */\nexport async function flowCoverage(req: FlowCoverageRequest): Promise<FlowPromises> {\n const vocab = withFeatureCapabilities(req.vocabulary, req.capabilities.map((doc) => doc.id));\n const byCapability = new Map<string, ReadonlySet<string>>();\n for (const doc of req.capabilities) {\n const ids = new Set<string>();\n // REMOVED is excluded on both sides — a promise on its way out keeps nothing.\n for (const r of doc.living) if (r.kind !== \"REMOVED\" && r.id !== undefined) ids.add(r.id);\n for (const r of doc.reqs) if ((r.kind === \"ADDED\" || r.kind === \"MODIFIED\") && r.id !== undefined) ids.add(r.id);\n byCapability.set(doc.id, ids);\n }\n return featureFlowPromises({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n known: req.known,\n declared: gradableCapabilityIds(vocab),\n requirementsOf: (capability) => byCapability.get(capability),\n ...(req.load === undefined ? {} : { load: req.load }),\n ...(req.models === undefined ? {} : { models: req.models }),\n });\n}\n"]}
@@ -33,12 +33,16 @@ export async function readFeatureFlows(req) {
33
33
  if (flows.length === 0) {
34
34
  return { kind: "read", views: [], model: { elements: [], relationships: [], known: req.known }, resolve: (id) => id };
35
35
  }
36
+ // AFTER the early return, which is what makes the thunk worth its shape: the
37
+ // fleet read is paid only by a feature that actually brings a flow.
38
+ const models = req.models === undefined ? undefined : await req.models();
36
39
  const staged = await stageMergedProject({
37
40
  docsDir: req.docsDir,
38
41
  featureDir: req.featureDir,
39
42
  featureId: req.featureId,
40
43
  documents: flows.map((flow) => ({ rel: `${USECASE_SUBDIR}/${flow.rel}`, path: flow.path })),
41
44
  ...(req.load === undefined ? {} : { load: req.load }),
45
+ ...(models === undefined ? {} : { models }),
42
46
  });
43
47
  return interpret(staged, req.known);
44
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/overlay.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A feature's flows, read against the fleet map AS THIS FEATURE'S MERGE WOULD\n * LEAVE IT.\n *\n * The staging and the merge preview that make that possible are no longer here:\n * they moved to `core/c4/project/staged.ts` when the deployment slot became the\n * second feature-local `.likec4` document, because a second copy of that\n * machinery is the copy that drifts, and the two would disagree first about the\n * merge preview — the one thing both axes' refusals rest on. That module's\n * header carries the WHY of the post-merge corpus; this one keeps what is\n * particular to use cases, which is the interpretation: which of the staged\n * views are this feature's, and what they resolve against.\n *\n * FAIL CLOSED, and not silently. Every failure is the `unreadable` arm carrying\n * LikeC4's own messages: a flow this cannot read is never a flow that grades\n * clean, which is the asymmetry `../fleet.ts` states for the fleet-scope scan.\n */\nimport { type LoadedDoc } from \"../../c4/likec4.js\";\nimport { stageMergedProject, type StagedProject } from \"../../c4/project/staged.js\";\nimport { serviceResolver } from \"../../c4/resolve/service.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { isUseCase, type UseCaseScan } from \"../fleet.js\";\nimport { featureFlows, USECASE_SUBDIR } from \"./flows.js\";\n\n/** Everything the overlay needs, and nothing it would have to read twice. */\nexport interface FlowOverlayRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /**\n * The enumerated fleet, for the element→service resolver. It rides in for the\n * reason every other edge join carries it: without it a hop drawn into a\n * modelled container `payment.api` resolves to a service called \"api\" that\n * has never existed.\n */\n known: ReadonlySet<string>;\n /**\n * The caller's memoised LikeC4 read, when it has one. Only `delta.likec4` goes\n * through it, and only to compute the merge preview — but the caller\n * (`featureCoherence`) has already loaded that exact document, and loading one\n * spins a fresh Langium workspace, which `core/coherence/coherence.ts` calls\n * the dominant per-feature cost.\n *\n * A FUNCTION rather than the `FleetContext` itself, deliberately: this package\n * has no edge to `core/fleet-context.ts`, one memoised read does not justify\n * adding one, and the edge would push `core/usecases/` up a DAG level for\n * nothing. Absent, the load happens directly, which is right for a caller\n * running outside an invocation index.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n}\n\n/**\n * The flows this feature carries, parsed over the post-merge map — or the\n * honest refusal to say.\n *\n * A feature with no `usecases/` directory is `read` with no views, at the cost\n * of one walk over a directory that is not there. That is the axis's\n * per-feature opt-in and the whole price a fleet that has not adopted it pays.\n */\nexport async function readFeatureFlows(req: FlowOverlayRequest): Promise<UseCaseScan> {\n const flows = await featureFlows(req.featureDir);\n if (flows.length === 0) {\n return { kind: \"read\", views: [], model: { elements: [], relationships: [], known: req.known }, resolve: (id) => id };\n }\n const staged = await stageMergedProject({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n documents: flows.map((flow) => ({ rel: `${USECASE_SUBDIR}/${flow.rel}`, path: flow.path })),\n ...(req.load === undefined ? {} : { load: req.load }),\n });\n return interpret(staged, req.known);\n}\n\n/**\n * The staged parse as the use-case axis reads it.\n *\n * THE FEATURE'S OWN FLOWS ONLY. A living use case is the fleet scan's to report,\n * and repeating it here would file somebody else's finding against this\n * feature's gate — the same scoping every other feature-side check keeps.\n */\nfunction interpret(staged: StagedProject, known: ReadonlySet<string>): UseCaseScan {\n if (staged.kind === \"unreadable\") return { kind: \"unreadable\", errors: staged.errors };\n const { doc, real, mine } = staged;\n // Through `isUseCase`, because `UseCaseScan.views` is documented as the\n // reserved-tag views ONLY and this is the type's second producer. The\n // resolvers below filter on the prefix anyway, so no answer changes today —\n // but `flowsClaiming` and `servicesInFlowsClaiming` take a `UseCaseScan` and\n // read `scan.views` straight, and the whole point of exporting one predicate\n // was that a second spelling of the opt-in drifts.\n const views = (doc.views ?? [])\n .filter((view) => view.sourcePath !== undefined && mine.has(view.sourcePath))\n .filter(isUseCase);\n return {\n kind: \"read\",\n views: views.map((view) => ({ ...view, sourcePath: view.sourcePath === undefined ? view.sourcePath : real(view.sourcePath) })),\n model: { elements: doc.elements, relationships: doc.relationships, known },\n resolve: serviceResolver(doc.elements, known),\n };\n}\n"]}
1
+ {"version":3,"file":"overlay.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAkB,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAsB,MAAM,4BAA4B,CAAC;AAEpF,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,SAAS,EAAoB,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA+C1D;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAuB;IAC5D,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACjD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;IACxH,CAAC;IACD,6EAA6E;IAC7E,oEAAoE;IACpE,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC;IACzE,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC;QACtC,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,cAAc,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3F,GAAG,CAAC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;QACrD,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;KAC5C,CAAC,CAAC;IACH,OAAO,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;GAMG;AACH,SAAS,SAAS,CAAC,MAAqB,EAAE,KAA0B;IAClE,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;IACvF,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;IACnC,wEAAwE;IACxE,sEAAsE;IACtE,4EAA4E;IAC5E,6EAA6E;IAC7E,6EAA6E;IAC7E,mDAAmD;IACnD,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;SAC5E,MAAM,CAAC,SAAS,CAAC,CAAC;IACrB,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC9H,KAAK,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE;QAC1E,OAAO,EAAE,eAAe,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC;KAC9C,CAAC;AACJ,CAAC","sourcesContent":["/**\n * A feature's flows, read against the fleet map AS THIS FEATURE'S MERGE WOULD\n * LEAVE IT.\n *\n * The staging and the merge preview that make that possible are no longer here:\n * they moved to `core/c4/project/staged.ts` when the deployment slot became the\n * second feature-local `.likec4` document, because a second copy of that\n * machinery is the copy that drifts, and the two would disagree first about the\n * merge preview — the one thing both axes' refusals rest on. That module's\n * header carries the WHY of the post-merge corpus; this one keeps what is\n * particular to use cases, which is the interpretation: which of the staged\n * views are this feature's, and what they resolve against.\n *\n * FAIL CLOSED, and not silently. Every failure is the `unreadable` arm carrying\n * LikeC4's own messages: a flow this cannot read is never a flow that grades\n * clean, which is the asymmetry `../fleet.ts` states for the fleet-scope scan.\n */\nimport { type LoadedDoc } from \"../../c4/likec4.js\";\nimport { stageMergedProject, type StagedProject } from \"../../c4/project/staged.js\";\nimport type { ExtendingModel } from \"../../c4/splice/contract.js\";\nimport { serviceResolver } from \"../../c4/resolve/service.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { isUseCase, type UseCaseScan } from \"../fleet.js\";\nimport { featureFlows, USECASE_SUBDIR } from \"./flows.js\";\n\n/** Everything the overlay needs, and nothing it would have to read twice. */\nexport interface FlowOverlayRequest {\n docsDir: DocsDir;\n featureDir: FeatureDir;\n /** The feature's id — the tag that selects the delta's own additions. */\n featureId: string;\n /**\n * The enumerated fleet, for the element→service resolver. It rides in for the\n * reason every other edge join carries it: without it a hop drawn into a\n * modelled container `payment.api` resolves to a service called \"api\" that\n * has never existed.\n */\n known: ReadonlySet<string>;\n /**\n * The caller's memoised LikeC4 read, when it has one. Only `delta.likec4` goes\n * through it, and only to compute the merge preview — but the caller\n * (`featureCoherence`) has already loaded that exact document, and loading one\n * spins a fresh Langium workspace, which `core/coherence/coherence.ts` calls\n * the dominant per-feature cost.\n *\n * A FUNCTION rather than the `FleetContext` itself, deliberately: this package\n * has no edge to `core/fleet-context.ts`, one memoised read does not justify\n * adding one, and the edge would push `core/usecases/` up a DAG level for\n * nothing. Absent, the load happens directly, which is right for a caller\n * running outside an invocation index.\n */\n load?: (path: string) => Promise<LoadedDoc>;\n /**\n * The fleet's extending models, for the merge preview — the SAME list\n * `loam archive` routes with. Without it the preview holds interior the\n * archive will put in `services/<…>/model.likec4`, so a hop naming\n * `<service-fqn>.<container>` resolves here and against nothing once the flow\n * is living: `usecase.flow-invalid` fails open, and it is the gate\n * `--approve` cannot override (verification 2026-09-04, review F8).\n *\n * A THUNK, and both halves of that matter. A function for the reason `load`\n * is one: the reader takes a `FleetContext` and this package has no edge to\n * it, so the caller that owns the fleet read supplies the answer. LAZY\n * because the early return above is the axis's per-feature opt-in — a feature\n * with no `usecases/` directory must not pay for a fleet enumeration and one\n * read per extending model to be told there is nothing to grade.\n */\n models?: () => Promise<readonly ExtendingModel[]>;\n}\n\n/**\n * The flows this feature carries, parsed over the post-merge map — or the\n * honest refusal to say.\n *\n * A feature with no `usecases/` directory is `read` with no views, at the cost\n * of one walk over a directory that is not there. That is the axis's\n * per-feature opt-in and the whole price a fleet that has not adopted it pays.\n */\nexport async function readFeatureFlows(req: FlowOverlayRequest): Promise<UseCaseScan> {\n const flows = await featureFlows(req.featureDir);\n if (flows.length === 0) {\n return { kind: \"read\", views: [], model: { elements: [], relationships: [], known: req.known }, resolve: (id) => id };\n }\n // AFTER the early return, which is what makes the thunk worth its shape: the\n // fleet read is paid only by a feature that actually brings a flow.\n const models = req.models === undefined ? undefined : await req.models();\n const staged = await stageMergedProject({\n docsDir: req.docsDir,\n featureDir: req.featureDir,\n featureId: req.featureId,\n documents: flows.map((flow) => ({ rel: `${USECASE_SUBDIR}/${flow.rel}`, path: flow.path })),\n ...(req.load === undefined ? {} : { load: req.load }),\n ...(models === undefined ? {} : { models }),\n });\n return interpret(staged, req.known);\n}\n\n/**\n * The staged parse as the use-case axis reads it.\n *\n * THE FEATURE'S OWN FLOWS ONLY. A living use case is the fleet scan's to report,\n * and repeating it here would file somebody else's finding against this\n * feature's gate — the same scoping every other feature-side check keeps.\n */\nfunction interpret(staged: StagedProject, known: ReadonlySet<string>): UseCaseScan {\n if (staged.kind === \"unreadable\") return { kind: \"unreadable\", errors: staged.errors };\n const { doc, real, mine } = staged;\n // Through `isUseCase`, because `UseCaseScan.views` is documented as the\n // reserved-tag views ONLY and this is the type's second producer. The\n // resolvers below filter on the prefix anyway, so no answer changes today —\n // but `flowsClaiming` and `servicesInFlowsClaiming` take a `UseCaseScan` and\n // read `scan.views` straight, and the whole point of exporting one predicate\n // was that a second spelling of the opt-in drifts.\n const views = (doc.views ?? [])\n .filter((view) => view.sourcePath !== undefined && mine.has(view.sourcePath))\n .filter(isUseCase);\n return {\n kind: \"read\",\n views: views.map((view) => ({ ...view, sourcePath: view.sourcePath === undefined ? view.sourcePath : real(view.sourcePath) })),\n model: { elements: doc.elements, relationships: doc.relationships, known },\n resolve: serviceResolver(doc.elements, known),\n };\n}\n"]}
@@ -38,10 +38,8 @@
38
38
  */
39
39
  import { readFile } from "node:fs/promises";
40
40
  import { CAP_TAG_PREFIX, REQ_TAG_PREFIX } from "../capabilities/usecase-join.js";
41
- import { architectureDir, loadArchitecture } from "../c4/project/architecture.js";
42
- import { architectureDocuments } from "../c4/project/documents.js";
41
+ import { architectureProjectDocuments, loadArchitecture } from "../c4/project/architecture.js";
43
42
  import { serviceResolver } from "../c4/resolve/service.js";
44
- import { subsystemViewsPath } from "../repo/paths.js";
45
43
  /**
46
44
  * Does any document in the set even mention a reserved tag prefix?
47
45
  *
@@ -53,8 +51,15 @@ import { subsystemViewsPath } from "../repo/paths.js";
53
51
  * `req-` is the looser of the two substrings and that is accepted knowingly: a
54
52
  * document mentioning it in prose costs one project load loam would otherwise
55
53
  * have skipped, which is a slower right answer. Dropping it costs a wrong one.
54
+ *
55
+ * Exported for the service-scope reader (`./service/flows.ts`), which asks the
56
+ * same question of the `.likec4` files beside one `model.likec4`: a sibling set
57
+ * that mentions neither prefix declares no use case, so a hand-drawn
58
+ * `views.likec4` costs that service no LikeC4 workspace. One gate, two callers,
59
+ * because a second byte scan spelled in the service package would be free to
60
+ * disagree with this one about which prefixes opt a view in.
56
61
  */
57
- async function mentionsTagPrefix(paths) {
62
+ export async function mentionsTagPrefix(paths) {
58
63
  for (const path of paths) {
59
64
  try {
60
65
  const bytes = await readFile(path);
@@ -116,8 +121,15 @@ export async function readUseCases(req) {
116
121
  model: { elements: [], relationships: [], known: req.known },
117
122
  resolve: (id) => id,
118
123
  };
119
- const dir = architectureDir(req.docsDir);
120
- const documents = await architectureDocuments(dir, [subsystemViewsPath(req.docsDir)]);
124
+ // EXACTLY the documents `loadArchitecture` will load two lines down, root
125
+ // `exclude` and all — `architectureProjectDocuments` is that one answer
126
+ // (`c4/project/architecture.ts`). The gate used to walk `architecture/` for
127
+ // itself and skip the exclude, which could only over-scan: the byte gate
128
+ // reading a document the loader then drops decides "there might be a use
129
+ // case here" and pays for a project load that finds none. Never a wrong
130
+ // answer, but two lists that disagree about what the project is, and the
131
+ // next reader to take the wider one for the project's would be wrong.
132
+ const documents = await architectureProjectDocuments(req.docsDir);
121
133
  if (!(await mentionsTagPrefix(documents)))
122
134
  return none;
123
135
  const doc = await loadArchitecture(req.docsDir);