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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +947 -0
  2. package/MIGRATING-from-OpenSpec.md +7 -1
  3. package/QUICKSTART.md +8 -3
  4. package/README.md +20 -13
  5. package/ROADMAP.md +329 -22
  6. package/SCHEMA.md +472 -24
  7. package/WORKFLOW.md +23 -10
  8. package/dist/commands/adopt/render.js +47 -8
  9. package/dist/commands/adopt/render.js.map +1 -1
  10. package/dist/commands/archive/plan/landscape.js +45 -1
  11. package/dist/commands/archive/plan/landscape.js.map +1 -1
  12. package/dist/commands/archive/plan/model/extending.js +85 -0
  13. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  14. package/dist/commands/archive/run.js +5 -2
  15. package/dist/commands/archive/run.js.map +1 -1
  16. package/dist/commands/context/print.js +11 -1
  17. package/dist/commands/context/print.js.map +1 -1
  18. package/dist/commands/doctor.js +46 -0
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/explore.js +12 -2
  21. package/dist/commands/explore.js.map +1 -1
  22. package/dist/commands/list/views.js +5 -1
  23. package/dist/commands/list/views.js.map +1 -1
  24. package/dist/commands/seed/plan.js +97 -3
  25. package/dist/commands/seed/plan.js.map +1 -1
  26. package/dist/commands/seed/seed.js +7 -2
  27. package/dist/commands/seed/seed.js.map +1 -1
  28. package/dist/commands/show/feature.js +2 -1
  29. package/dist/commands/show/feature.js.map +1 -1
  30. package/dist/commands/show/marks.js +3 -5
  31. package/dist/commands/show/marks.js.map +1 -1
  32. package/dist/commands/show/service.js +39 -18
  33. package/dist/commands/show/service.js.map +1 -1
  34. package/dist/commands/subsystem/sync.js +184 -39
  35. package/dist/commands/subsystem/sync.js.map +1 -1
  36. package/dist/commands/subsystem/txn/exclude.js +132 -0
  37. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  38. package/dist/commands/subsystem/txn/report.js +124 -0
  39. package/dist/commands/subsystem/txn/report.js.map +1 -0
  40. package/dist/commands/subsystem/txn/txn.js +29 -3
  41. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  42. package/dist/commands/subsystem/txn/views.js +113 -16
  43. package/dist/commands/subsystem/txn/views.js.map +1 -1
  44. package/dist/commands/validate/arch-coverage.js +16 -9
  45. package/dist/commands/validate/arch-coverage.js.map +1 -1
  46. package/dist/commands/validate/checks/fleet-shape.js +30 -77
  47. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  48. package/dist/commands/validate/checks/vocabulary.js +4 -19
  49. package/dist/commands/validate/checks/vocabulary.js.map +1 -1
  50. package/dist/commands/validate/feature.js +1 -1
  51. package/dist/commands/validate/feature.js.map +1 -1
  52. package/dist/commands/validate/fleet/census.js +11 -10
  53. package/dist/commands/validate/fleet/census.js.map +1 -1
  54. package/dist/commands/validate/fleet/landscape.js +128 -114
  55. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  56. package/dist/commands/validate/fleet/load.js +66 -0
  57. package/dist/commands/validate/fleet/load.js.map +1 -1
  58. package/dist/commands/validate/fleet/map/attest.js +103 -0
  59. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  60. package/dist/commands/validate/fleet/map/bindings.js +96 -0
  61. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  62. package/dist/commands/validate/fleet/map/consumers.js +298 -0
  63. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  64. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  65. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  66. package/dist/commands/validate/fleet/map/usecases.js +56 -0
  67. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  68. package/dist/commands/validate/fleet/usecases/steps.js +49 -17
  69. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  70. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  71. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  72. package/dist/commands/validate/fleet/views/ids.js +13 -9
  73. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  74. package/dist/commands/validate/fleet/views/projects.js +278 -0
  75. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  76. package/dist/commands/validate/fleet/views/root-documents.js +97 -0
  77. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  78. package/dist/commands/validate/fleet/views/stale.js +2 -2
  79. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  80. package/dist/commands/validate/service/model/diverged.js +226 -0
  81. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  82. package/dist/commands/validate/service/model/grade.js +163 -0
  83. package/dist/commands/validate/service/model/grade.js.map +1 -0
  84. package/dist/commands/validate/service/service.js +110 -34
  85. package/dist/commands/validate/service/service.js.map +1 -1
  86. package/dist/commands/validate/service/specs.js +39 -5
  87. package/dist/commands/validate/service/specs.js.map +1 -1
  88. package/dist/commands/validate/service/spine.js +41 -3
  89. package/dist/commands/validate/service/spine.js.map +1 -1
  90. package/dist/commands/validate/service/usecases/tags.js +113 -0
  91. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  92. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  93. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  94. package/dist/commands/validate/validate.js +10 -24
  95. package/dist/commands/validate/validate.js.map +1 -1
  96. package/dist/core/agent/agents-md/artifacts.js +1 -0
  97. package/dist/core/agent/agents-md/artifacts.js.map +1 -1
  98. package/dist/core/agent/agents-md/command-map.js +18 -2
  99. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  100. package/dist/core/agent/agents-md/cycle.js +24 -9
  101. package/dist/core/agent/agents-md/cycle.js.map +1 -1
  102. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  103. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  104. package/dist/core/agent/agents-md/map/subsystem.js +51 -2
  105. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  106. package/dist/core/agent/agents-md/map/usecases.js +45 -3
  107. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  108. package/dist/core/agent/agents-md.js +14 -4
  109. package/dist/core/agent/agents-md.js.map +1 -1
  110. package/dist/core/agent/support/report.js +39 -5
  111. package/dist/core/agent/support/report.js.map +1 -1
  112. package/dist/core/agent/workflows/adopt.js +125 -6
  113. package/dist/core/agent/workflows/adopt.js.map +1 -1
  114. package/dist/core/agent/workflows/check.js +35 -19
  115. package/dist/core/agent/workflows/check.js.map +1 -1
  116. package/dist/core/agent/workflows/closing.js +3 -1
  117. package/dist/core/agent/workflows/closing.js.map +1 -1
  118. package/dist/core/brief/brief.js +63 -11
  119. package/dist/core/brief/brief.js.map +1 -1
  120. package/dist/core/brief/checks.js +138 -3
  121. package/dist/core/brief/checks.js.map +1 -1
  122. package/dist/core/brief/landscape.js +212 -129
  123. package/dist/core/brief/landscape.js.map +1 -1
  124. package/dist/core/brief/map/extend.js +81 -0
  125. package/dist/core/brief/map/extend.js.map +1 -0
  126. package/dist/core/brief/map/owed.js +284 -0
  127. package/dist/core/brief/map/owed.js.map +1 -0
  128. package/dist/core/brief/targets.js +30 -26
  129. package/dist/core/brief/targets.js.map +1 -1
  130. package/dist/core/brief/unchecked.js +12 -2
  131. package/dist/core/brief/unchecked.js.map +1 -1
  132. package/dist/core/c4/likec4.js +23 -1
  133. package/dist/core/c4/likec4.js.map +1 -1
  134. package/dist/core/c4/parsed/deployment.js +5 -3
  135. package/dist/core/c4/parsed/deployment.js.map +1 -1
  136. package/dist/core/c4/parsed/view-ids.js +39 -1
  137. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  138. package/dist/core/c4/prefetch/fleet.js +142 -0
  139. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  140. package/dist/core/c4/project/architecture.js +52 -4
  141. package/dist/core/c4/project/architecture.js.map +1 -1
  142. package/dist/core/c4/project/batch.js +140 -0
  143. package/dist/core/c4/project/batch.js.map +1 -0
  144. package/dist/core/c4/project/documents.js +9 -4
  145. package/dist/core/c4/project/documents.js.map +1 -1
  146. package/dist/core/c4/project/load.js +77 -38
  147. package/dist/core/c4/project/load.js.map +1 -1
  148. package/dist/core/c4/project/staged.js +46 -12
  149. package/dist/core/c4/project/staged.js.map +1 -1
  150. package/dist/core/c4/resolve/attested.js +0 -0
  151. package/dist/core/c4/resolve/attested.js.map +1 -0
  152. package/dist/core/c4/resolve/service.js +8 -17
  153. package/dist/core/c4/resolve/service.js.map +1 -1
  154. package/dist/core/c4/resolve/steps.js +18 -0
  155. package/dist/core/c4/resolve/steps.js.map +1 -1
  156. package/dist/core/c4/root-project/exclude.js +352 -0
  157. package/dist/core/c4/root-project/exclude.js.map +1 -0
  158. package/dist/core/c4/service-model/documents.js +47 -0
  159. package/dist/core/c4/service-model/documents.js.map +1 -0
  160. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  161. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  162. package/dist/core/c4/service-model/load.js +165 -0
  163. package/dist/core/c4/service-model/load.js.map +1 -0
  164. package/dist/core/c4/service-model/renderer.js +128 -0
  165. package/dist/core/c4/service-model/renderer.js.map +1 -0
  166. package/dist/core/c4/service-model/shape.js +126 -0
  167. package/dist/core/c4/service-model/shape.js.map +1 -0
  168. package/dist/core/c4/service-model/slice.js +188 -0
  169. package/dist/core/c4/service-model/slice.js.map +1 -0
  170. package/dist/core/c4/source-scan.js +26 -0
  171. package/dist/core/c4/source-scan.js.map +1 -1
  172. package/dist/core/c4/splice/authored-source.js +60 -13
  173. package/dist/core/c4/splice/authored-source.js.map +1 -1
  174. package/dist/core/c4/splice/contract.js.map +1 -1
  175. package/dist/core/c4/splice/identity/declared.js +87 -0
  176. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  177. package/dist/core/c4/splice/identity/edges.js +17 -0
  178. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  179. package/dist/core/c4/splice/identity/existing.js +80 -0
  180. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  181. package/dist/core/c4/splice/identity/interior.js +80 -0
  182. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  183. package/dist/core/c4/splice/identity/owner.js +50 -0
  184. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  185. package/dist/core/c4/splice/landscape-merge.js +154 -100
  186. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  187. package/dist/core/c4/splice/model/merge.js +140 -0
  188. package/dist/core/c4/splice/model/merge.js.map +1 -0
  189. package/dist/core/c4/splice/placement.js +14 -4
  190. package/dist/core/c4/splice/placement.js.map +1 -1
  191. package/dist/core/c4/workspace.js +2 -1
  192. package/dist/core/c4/workspace.js.map +1 -1
  193. package/dist/core/coherence/coherence.js +7 -0
  194. package/dist/core/coherence/coherence.js.map +1 -1
  195. package/dist/core/delta/delta.js +9 -0
  196. package/dist/core/delta/delta.js.map +1 -1
  197. package/dist/core/deployment/delta.js +4 -0
  198. package/dist/core/deployment/delta.js.map +1 -1
  199. package/dist/core/diff/base-state.js +21 -5
  200. package/dist/core/diff/base-state.js.map +1 -1
  201. package/dist/core/diff/victims.js +10 -1
  202. package/dist/core/diff/victims.js.map +1 -1
  203. package/dist/core/docs.js +31 -22
  204. package/dist/core/docs.js.map +1 -1
  205. package/dist/core/doctor/config.js +7 -3
  206. package/dist/core/doctor/config.js.map +1 -1
  207. package/dist/core/doctor/doctor.js +48 -9
  208. package/dist/core/doctor/doctor.js.map +1 -1
  209. package/dist/core/doctor/report.js.map +1 -1
  210. package/dist/core/doctor/reports/scan.js +242 -0
  211. package/dist/core/doctor/reports/scan.js.map +1 -0
  212. package/dist/core/explain/families.js +1 -1
  213. package/dist/core/explain/families.js.map +1 -1
  214. package/dist/core/explain/refusals.js +1 -1
  215. package/dist/core/explain/refusals.js.map +1 -1
  216. package/dist/core/explore/explore.js +44 -9
  217. package/dist/core/explore/explore.js.map +1 -1
  218. package/dist/core/fleet-context.js +47 -0
  219. package/dist/core/fleet-context.js.map +1 -1
  220. package/dist/core/gate/partners.js +5 -9
  221. package/dist/core/gate/partners.js.map +1 -1
  222. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  223. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  224. package/dist/core/pack/living.js +47 -13
  225. package/dist/core/pack/living.js.map +1 -1
  226. package/dist/core/pack/pack.js +9 -6
  227. package/dist/core/pack/pack.js.map +1 -1
  228. package/dist/core/projection/arch-slice.js +2 -2
  229. package/dist/core/projection/arch-slice.js.map +1 -1
  230. package/dist/core/repo/paths.js +40 -0
  231. package/dist/core/repo/paths.js.map +1 -1
  232. package/dist/core/repo/tree/render/projects.js +182 -0
  233. package/dist/core/repo/tree/render/projects.js.map +1 -0
  234. package/dist/core/repo/tree/render/stale.js +2 -3
  235. package/dist/core/repo/tree/render/stale.js.map +1 -1
  236. package/dist/core/repo/tree/render/views.js +51 -3
  237. package/dist/core/repo/tree/render/views.js.map +1 -1
  238. package/dist/core/repo/tree/walk.js +45 -2
  239. package/dist/core/repo/tree/walk.js.map +1 -1
  240. package/dist/core/scaffold/landscape.js +219 -9
  241. package/dist/core/scaffold/landscape.js.map +1 -1
  242. package/dist/core/staging/writes.js.map +1 -1
  243. package/dist/core/usecases/delta/claims.js +1 -0
  244. package/dist/core/usecases/delta/claims.js.map +1 -1
  245. package/dist/core/usecases/delta/overlay.js +4 -0
  246. package/dist/core/usecases/delta/overlay.js.map +1 -1
  247. package/dist/core/usecases/fleet.js +18 -6
  248. package/dist/core/usecases/fleet.js.map +1 -1
  249. package/dist/core/usecases/operations.js +7 -0
  250. package/dist/core/usecases/operations.js.map +1 -1
  251. package/dist/core/usecases/service/exclude.js +119 -0
  252. package/dist/core/usecases/service/exclude.js.map +1 -0
  253. package/dist/core/usecases/service/flows.js +161 -0
  254. package/dist/core/usecases/service/flows.js.map +1 -0
  255. package/dist/core/vocabulary/maturity.js +10 -0
  256. package/dist/core/vocabulary/maturity.js.map +1 -1
  257. package/examples/README.md +23 -0
  258. package/examples/docs/architecture/landscape.likec4 +41 -0
  259. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  260. package/examples/docs/likec4.config.json +0 -1
  261. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  262. package/examples/docs/services/order-service/arch.spec.md +13 -6
  263. package/examples/docs/services/order-service/model.likec4 +53 -67
  264. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  265. package/examples/docs/services/payment-service/model.likec4 +32 -33
  266. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  267. package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
  268. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  269. package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
  270. package/package.json +1 -1
@@ -1,10 +1,14 @@
1
- import { loadSource } from "../likec4.js";
1
+ import { parentIdOf } from "../../kernel/ids/fqn/ancestors.js";
2
+ import { errorText, loadSource } from "../likec4.js";
2
3
  import { elementService, serviceOf } from "../resolve/service.js";
3
4
  import { scanModel } from "../source-scan.js";
4
- import { spliceSource } from "./authored-source.js";
5
- import { LandscapeSpliceError } from "./contract.js";
5
+ import { matchLineEndings, spliceSource } from "./authored-source.js";
6
+ import { LandscapeSpliceError, } from "./contract.js";
6
7
  import { assertMergeableDelta } from "./delta-blocks.js";
7
- import { relKey, relSortKey } from "./identity/edges.js";
8
+ import { relSortKey, relStatementKey } from "./identity/edges.js";
9
+ import { newAdditions } from "./identity/existing.js";
10
+ import { planInteriors } from "./identity/interior.js";
11
+ import { ownerOf } from "./identity/owner.js";
8
12
  import { elementSpot, nestedInsert, relSpot, topStatements, } from "./placement.js";
9
13
  /* ------------------------------------------------------------------ */
10
14
  /* Merging */
@@ -33,20 +37,37 @@ import { elementSpot, nestedInsert, relSpot, topStatements, } from "./placement.
33
37
  * dotted id at top level, which LikeC4 rejects. The additions carry no marker
34
38
  * comment: which archive added what is the git history's to say.
35
39
  *
36
- * Existence is checked semantically against the parsed landscape (by element
37
- * id/title and by edge identity), so re-archiving is idempotent and title
38
- * strings appearing elsewhere in the source cause no false skips. Assumes the
39
- * delta reuses the landscape's element identifiers for existing services. A
40
- * title match whose two sides are BOTH bound to different services is not an
41
- * existence hit but a collision, and refuses the archive (see the guard).
40
+ * Existence is checked semantically against the parsed landscape AND against
41
+ * every extending model the request carries (by element id/title and by edge
42
+ * identity), so re-archiving is idempotent whichever document the additions
43
+ * landed in — `./identity/existing.ts` holds that phase and its two joins, and
44
+ * this function only turns the collision it reports into the refusal.
42
45
  *
43
46
  * Splicing is text surgery, so the computed result is PARSED before it is
44
47
  * returned: a merged landscape LikeC4 rejects refuses the archive at plan time
45
48
  * (merge-failed, nothing written — the unparseable-delta discipline) instead
46
49
  * of landing in the living docs.
50
+ *
51
+ * NOT EVERYTHING LANDS HERE. A service whose model EXTENDS this map owns its
52
+ * own interior, and an addition nested under such a service is ROUTED to that
53
+ * model instead (`./identity/owner.ts` decides, `./model/merge.ts` splices).
54
+ * Until it was, a delta's container landed in the map and the service was
55
+ * `c4.invalid` — a duplicate declaration — the moment its model wrote the same
56
+ * container, which is exactly what the adopt brief tells it to do
57
+ * (verification 2026-09-04, E1). With no extending models in the request this
58
+ * function behaves as it always did.
59
+ *
60
+ * Routing per ADDITION is not the whole of it, because a service the feature
61
+ * INTRODUCES carries its interior in one element's authored bytes and those
62
+ * children carry no feature tag of their own. `./identity/interior.ts` decides, once
63
+ * per such block, whether it hands that interior to the model — cut out of what
64
+ * lands on the map — or rides whole; the routing below DEFERS to that decision
65
+ * for everything written inside the block, which is what keeps the two from
66
+ * placing the same child twice.
47
67
  */
48
68
  export async function planLandscapeMerge(merge) {
49
69
  const { landscapeText: text, deltaText, deltaElements, newEls, newRels, featureId } = merge;
70
+ const models = merge.models ?? [];
50
71
  // First, and before the early return for "nothing to add": a block this merge
51
72
  // cannot carry is lost whether or not the delta also has tagged elements to
52
73
  // splice. See delta-blocks.ts.
@@ -55,65 +76,43 @@ export async function planLandscapeMerge(merge) {
55
76
  if (land.errors.length > 0) {
56
77
  throw new LandscapeSpliceError(`landscape.likec4 has ${land.errors.length} error(s) — fix it before archiving`);
57
78
  }
58
- const haveIds = new Set(land.elements.map((e) => e.id));
59
- // The title join needs the matched element back, not just membership: the
60
- // cross-service guard below compares service BINDINGS across the join.
61
- const byTitle = new Map();
62
- const seeTitle = (el) => {
63
- byTitle.set(el.title, [...(byTitle.get(el.title) ?? []), el]);
64
- };
65
- for (const el of land.elements)
66
- seeTitle(el);
67
- const addedEls = [];
68
- for (const e of newEls) {
69
- if (haveIds.has(e.id))
70
- continue;
71
- const sameTitle = byTitle.get(e.title);
72
- if (sameTitle !== undefined) {
73
- // A title match is the id-less fallback join, and skipping on it is only
74
- // safe when the two sides could be the same box. When both sides carry an
75
- // explicit `metadata { service }` binding and every binding disagrees,
76
- // they are provably DIFFERENT services' boxes sharing a title ('API',
77
- // 'Database') — the skip would silently drop the addition, and any delta
78
- // edge into it would then refuse the whole archive at the parse net
79
- // below with a message about nothing. Refuse here instead, at plan time,
80
- // naming both sides.
81
- if (e.service !== undefined && sameTitle.every((m) => m.service !== undefined && m.service !== e.service)) {
82
- const m = sameTitle[0];
83
- throw new LandscapeSpliceError(`the delta's '${e.id}' (bound to service '${e.service}') shares the title '${e.title}' with '${m.id}' (bound to service '${m.service}') — a title join across services would silently drop the addition; ` +
84
- `retitle one of them, or reuse the id '${m.id}' if they really are the same element`);
85
- }
86
- // KNOWN (narrowed by the guard above): with EITHER side unbound the title
87
- // join stays trusting — the unbound title-fallback is the legal legacy
88
- // pattern — so a cross-service collision hiding behind an unbound element
89
- // is still silently skipped here. Scoping titles per service is backlog.
90
- continue;
91
- }
92
- haveIds.add(e.id);
93
- seeTitle(e);
94
- addedEls.push(e);
79
+ // Service binding for the existence joins AND for routing and placement:
80
+ // living elements first (they win a shared id), the delta's after — a spliced
81
+ // statement's id resolves to its service the moment its bytes land in the scan.
82
+ const bindEls = [...land.elements, ...deltaElements.filter((d) => !land.elements.some((l) => l.id === d.id))];
83
+ // What the living fleet already has — the map AND every extending model, by id
84
+ // and by title. `./identity/existing.ts` holds both joins and their asymmetry.
85
+ const added = newAdditions({
86
+ livingEls: land.elements,
87
+ livingRels: land.relationships,
88
+ bindEls,
89
+ deltaElements,
90
+ candidates: { els: newEls, rels: newRels },
91
+ models,
92
+ });
93
+ if (added.collision !== undefined) {
94
+ const { addition: e, living: m } = added.collision;
95
+ throw new LandscapeSpliceError(`the delta's '${e.id}' (bound to service '${e.service}') shares the title '${e.title}' with '${m.id}' (bound to service '${m.service}') — a title join across services would silently drop the addition; ` +
96
+ `retitle one of them, or reuse the id '${m.id}' if they really are the same element`);
95
97
  }
96
- // Edges are matched by COUNT, not by membership: two edges the model cannot tell
97
- // apart are still two edges, and dropping the second one silently loses a call
98
- // the author drew. An edge already in the landscape consumes one delta edge of
99
- // the same identity, which is what keeps re-archiving idempotent.
100
- const have = new Map();
101
- for (const r of land.relationships) {
102
- const k = relKey(land.elements, r);
103
- have.set(k, (have.get(k) ?? 0) + 1);
98
+ const addedEls = added.els;
99
+ const addedRels = added.rels;
100
+ const owner = ownerOf({
101
+ bindEls,
102
+ living: new Set(land.elements.map((e) => e.id)),
103
+ declared: added.declaredIds,
104
+ added: new Set(addedEls.map((e) => e.id)),
105
+ models,
106
+ });
107
+ // Nothing to add: the same shape the merge returned before routing existed,
108
+ // and the reason it is decided HERE rather than after the loops below — a
109
+ // no-op archive must not have to scan the delta, nor answer the landscape
110
+ // guards further down, which refuse documents this merge is not touching.
111
+ // Equivalent to the old "nothing reached the map and nothing was routed":
112
+ // every addition ends up in one of those two, so an empty pair means both.
113
+ if (addedEls.length === 0 && addedRels.length === 0) {
114
+ return { content: null, addedEls: [], addedRels: [], models: [] };
104
115
  }
105
- const addedRels = [];
106
- for (const r of newRels) {
107
- const k = relKey(deltaElements, r);
108
- const n = have.get(k) ?? 0;
109
- if (n > 0) {
110
- have.set(k, n - 1);
111
- continue;
112
- }
113
- addedRels.push(r);
114
- }
115
- if (addedEls.length === 0 && addedRels.length === 0)
116
- return { content: null, addedEls, addedRels };
117
116
  const scan = scanModel(deltaText);
118
117
  if (scan === null) {
119
118
  throw new LandscapeSpliceError("delta.likec4 has no model block — nothing to splice the additions from");
@@ -123,6 +122,61 @@ export async function planLandscapeMerge(merge) {
123
122
  // parent is itself new is recognised as riding inside the parent's text and
124
123
  // is never inserted twice.
125
124
  const byId = new Map(scan.elements.map((e) => [e.id, e]));
125
+ // Relationships: match each parsed addition back to its statement in the
126
+ // delta source — full identity (`relStatementKey`), consumed one statement
127
+ // per addition so duplicates stay duplicates. Built before the routing
128
+ // because routing has to know WHERE an edge was written: one drawn inside a
129
+ // block that rides whole is already on the map.
130
+ const pool = new Map();
131
+ for (const s of scan.rels) {
132
+ const k = relStatementKey(s);
133
+ pool.set(k, [...(pool.get(k) ?? []), s]);
134
+ }
135
+ const interiors = planInteriors({ scan, deltaElements, owner, addedEls });
136
+ const routed = new Map();
137
+ const into = (model) => {
138
+ const existing = routed.get(model.path);
139
+ if (existing !== undefined)
140
+ return existing;
141
+ const fresh = { model, els: [], rels: [] };
142
+ routed.set(model.path, fresh);
143
+ return fresh;
144
+ };
145
+ for (const share of interiors.routed)
146
+ into(share.model).els.push(...share.els);
147
+ const mapEls = [];
148
+ for (const e of addedEls) {
149
+ const src = byId.get(e.id);
150
+ // Written inside a block that has already decided: a split block handed it
151
+ // to the model with its siblings, and a whole one carries it onto the map
152
+ // in the parent's own bytes, where the splice loop's `rides` finds it. Both
153
+ // are placed exactly once, which is the point of asking the block first.
154
+ const fate = src === undefined ? null : interiors.covering(src);
155
+ if (fate === "split")
156
+ continue;
157
+ const model = fate === "whole" ? undefined : owner(parentIdOf(e.id));
158
+ if (model === undefined)
159
+ mapEls.push(e);
160
+ else
161
+ into(model).els.push(e);
162
+ }
163
+ const mapRels = [];
164
+ for (const r of addedRels) {
165
+ // An edge written inside a block that rides whole names its endpoints by
166
+ // their LOCAL names and reads only there; it stays with its block.
167
+ const stmts = pool.get(relStatementKey(r)) ?? [];
168
+ const inBlock = stmts.length > 0 && stmts.every((s) => interiors.covering(s) !== null);
169
+ // Otherwise the SOURCE decides: a call is drawn where the caller draws its
170
+ // calls, and only when the source is not itself interior does the target's
171
+ // owner get the edge. A service-level edge belongs to neither and stays on
172
+ // the map.
173
+ const model = inBlock ? undefined : owner(parentIdOf(r.source)) ?? owner(parentIdOf(r.target));
174
+ if (model === undefined)
175
+ mapRels.push(r);
176
+ else
177
+ into(model).rels.push(r);
178
+ }
179
+ const touchesMap = mapEls.length > 0 || mapRels.length > 0;
126
180
  const spliced = [];
127
181
  const rides = (start, end) => spliced.some((r) => start >= r.start && end <= r.end);
128
182
  // The living model, scanned on MASKED source (scanModel) and RE-SCANNED
@@ -137,10 +191,6 @@ export async function planLandscapeMerge(merge) {
137
191
  // LikeC4, so the parse net below would pass a landscape containing none of
138
192
  // the architecture).
139
193
  let content = text;
140
- // Service binding for placement: living elements first (they win a shared
141
- // id), the delta's after — a spliced statement's id resolves to its service
142
- // the moment its bytes land in the scan.
143
- const bindEls = [...land.elements, ...deltaElements.filter((d) => !land.elements.some((l) => l.id === d.id))];
144
194
  let livingScan = scanModel(content);
145
195
  let stmts = livingScan === null ? [] : topStatements(livingScan, bindEls);
146
196
  const rescan = () => {
@@ -199,7 +249,7 @@ export async function planLandscapeMerge(merge) {
199
249
  };
200
250
  // Ancestors first: a spliced parent covers its children before they are seen.
201
251
  const depth = (id) => id.split(".").length;
202
- const sortedEls = [...addedEls].sort((a, b) => depth(a.id) - depth(b.id) || (byId.get(a.id)?.start ?? 0) - (byId.get(b.id)?.start ?? 0));
252
+ const sortedEls = [...mapEls].sort((a, b) => depth(a.id) - depth(b.id) || (byId.get(a.id)?.start ?? 0) - (byId.get(b.id)?.start ?? 0));
203
253
  for (const e of sortedEls) {
204
254
  const src = byId.get(e.id);
205
255
  if (src === undefined) {
@@ -207,10 +257,15 @@ export async function planLandscapeMerge(merge) {
207
257
  }
208
258
  if (rides(src.start, src.end))
209
259
  continue;
260
+ // Every split block's children, whichever block they belong to:
261
+ // `spliceSource` keeps only the ranges that fall inside the bytes it is
262
+ // carrying, so a block riding inside ANOTHER new block still loses its own
263
+ // interior on the way to the map.
264
+ const omit = interiors.omit;
210
265
  const dot = e.id.lastIndexOf(".");
211
266
  if (dot === -1) {
212
267
  const spot = elementSpot(modelRegion(), e.id, elementService(e));
213
- applyTop(spot, spliceSource(deltaText, src, featureId, " "));
268
+ applyTop(spot, spliceSource(deltaText, src, { featureId, indent: " ", omit }));
214
269
  spliced.push({ start: src.start, end: src.end });
215
270
  continue;
216
271
  }
@@ -219,24 +274,14 @@ export async function planLandscapeMerge(merge) {
219
274
  if (parent === null) {
220
275
  throw new LandscapeSpliceError(`'${e.id}' nests under '${parentId}', which is neither in the living landscape nor added by this delta — there is nowhere to insert it`);
221
276
  }
222
- const nested = nestedInsert(content, parent, spliceSource(deltaText, src, featureId, parent.indent + " "));
277
+ const nested = nestedInsert(content, parent, spliceSource(deltaText, src, { featureId, indent: parent.indent + " ", omit }));
223
278
  applyAt(nested.at, nested.insert);
224
279
  spliced.push({ start: src.start, end: src.end });
225
280
  }
226
- // Relationships: match each parsed addition back to its statement in the
227
- // delta source — full identity (endpoints, title, all three spine keys,
228
- // tags), consumed one statement per addition so duplicates stay duplicates.
229
- // The spine keys earn their place here the same way they do in `relKey`:
230
- // without them two additions differing only by `publishes` share one pool
231
- // entry, and `shift()` hands the second one the first one's authored bytes.
232
- const relKeyOf = (r) => JSON.stringify([r.source, r.target, r.title ?? "", r.op ?? "", r.publishes ?? "", r.consumes ?? "", [...r.tags].sort()]);
233
- const pool = new Map();
234
- for (const s of scan.rels) {
235
- const k = relKeyOf(s);
236
- pool.set(k, [...(pool.get(k) ?? []), s]);
237
- }
238
- for (const r of addedRels) {
239
- const s = pool.get(relKeyOf(r))?.shift();
281
+ // Each map-bound addition takes its statement out of the pool built above:
282
+ // one statement per addition, so duplicates stay duplicates.
283
+ for (const r of mapRels) {
284
+ const s = pool.get(relStatementKey(r))?.shift();
240
285
  if (s === undefined) {
241
286
  throw new LandscapeSpliceError(`cannot locate the '${r.source} -> ${r.target}' relationship in delta.likec4 — the landscape merge splices authored source, and no matching declaration was found`);
242
287
  }
@@ -248,22 +293,31 @@ export async function planLandscapeMerge(merge) {
248
293
  // safety-netted by the re-parse below), never which service a check
249
294
  // grades.
250
295
  const spot = relSpot(modelRegion(), serviceOf(deltaElements, r.source), key);
251
- applyTop(spot, spliceSource(deltaText, s, featureId, " "));
296
+ applyTop(spot, spliceSource(deltaText, s, { featureId, indent: " " }));
252
297
  }
253
298
  // The safety net: prove the computed landscape parses before anything is
254
299
  // written. Splice bugs — and legal inputs the living document cannot absorb,
255
300
  // like a kind its specification never declares — refuse here, at plan time,
256
- // instead of corrupting the one file the whole fleet reads.
257
- const check = await loadSource(content);
258
- if (check.errors.length > 0) {
259
- const detail = check.errors
260
- .slice(0, 3)
261
- .map((e) => (typeof e.line === "number" ? `L${e.line}: ${e.message}` : e.message))
262
- .join("; ");
263
- throw new LandscapeSpliceError(`the merged landscape would not parse (${check.errors.length} error(s): ${detail}) — nothing was written. ` +
264
- `The delta's additions do not fit the living landscape as authored — most often an element kind or tag ` +
265
- `its specification block does not declare; fix the landscape's specification or the delta, then re-run`);
301
+ // instead of corrupting the one file the whole fleet reads. Skipped when
302
+ // every addition was routed away: the text is the one already parsed above,
303
+ // and the models get their own net from the caller, which is the only side
304
+ // that can stage a model beside the map it extends.
305
+ // The splice composed bytes from two documents, and the delta's newlines are
306
+ // not necessarily the map's. Corrected before the net, so what is proven is
307
+ // what would be written.
308
+ content = matchLineEndings(text, content);
309
+ if (touchesMap) {
310
+ const check = await loadSource(content);
311
+ if (check.errors.length > 0) {
312
+ const detail = check.errors
313
+ .slice(0, 3)
314
+ .map(errorText)
315
+ .join("; ");
316
+ throw new LandscapeSpliceError(`the merged landscape would not parse (${check.errors.length} error(s): ${detail}) — nothing was written. ` +
317
+ `The delta's additions do not fit the living landscape as authored — most often an element kind or tag ` +
318
+ `its specification block does not declare; fix the landscape's specification or the delta, then re-run`);
319
+ }
266
320
  }
267
- return { content, addedEls, addedRels };
321
+ return { content: touchesMap ? content : null, addedEls: mapEls, addedRels: mapRels, models: [...routed.values()] };
268
322
  }
269
323
  //# sourceMappingURL=landscape-merge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"landscape-merge.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/landscape-merge.ts"],"names":[],"mappings":"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","sourcesContent":["\nimport { loadSource, type Elem, type Rel } from \"../likec4.js\";\nimport { elementService, serviceOf } from \"../resolve/service.js\";\nimport { scanModel, type ScannedElement, type ScannedModel, type ScannedRel } from \"../source-scan.js\";\nimport { spliceSource } from \"./authored-source.js\";\nimport { LandscapeSpliceError, type LandscapeMergeRequest, type LandscapePlan } from \"./contract.js\";\nimport { assertMergeableDelta } from \"./delta-blocks.js\";\nimport { relKey, relSortKey } from \"./identity/edges.js\";\nimport {\n elementSpot,\n nestedInsert,\n relSpot,\n topStatements,\n type ModelRegion,\n type Spot,\n} from \"./placement.js\";\n\n/* ------------------------------------------------------------------ */\n/* Merging */\n/* ------------------------------------------------------------------ */\n\n/**\n * Plan the merge of the feature's new elements + relationships into the living\n * landscape's `model { ... }` block (preserving the rest of the file).\n *\n * The additions are SPLICED from delta.likec4 as authored — byte for byte,\n * technology, style, icons, links, metadata, nested children and all — never\n * re-serialized from the parsed model: a rebuild keeps only the fields loam\n * models, and every field it forgets is authored detail destroyed. The one\n * edit on the way over is dropping the feature's own tag — the additions are\n * baseline now (SCHEMA.md documents the drop as why unarchive restores bytes\n * instead of recomputing) — and a construct the strip empties goes with it.\n *\n * Placement is deterministic and SERVICE-GROUPED (the spot* helpers in\n * placement.ts):\n * a top-level addition lands beside what already belongs to its service, so\n * two concurrent archives touching different services usually splice on\n * different lines and git merges their PRs — the single shared append point\n * every archive used to hit made two concurrent archives conflict BY\n * CONSTRUCTION. A NESTED element lands inside its parent's block — inside the\n * living parent when the landscape already has it, riding verbatim inside the\n * spliced parent when the parent is new from the same delta — never as a flat\n * dotted id at top level, which LikeC4 rejects. The additions carry no marker\n * comment: which archive added what is the git history's to say.\n *\n * Existence is checked semantically against the parsed landscape (by element\n * id/title and by edge identity), so re-archiving is idempotent and title\n * strings appearing elsewhere in the source cause no false skips. Assumes the\n * delta reuses the landscape's element identifiers for existing services. A\n * title match whose two sides are BOTH bound to different services is not an\n * existence hit but a collision, and refuses the archive (see the guard).\n *\n * Splicing is text surgery, so the computed result is PARSED before it is\n * returned: a merged landscape LikeC4 rejects refuses the archive at plan time\n * (merge-failed, nothing written — the unparseable-delta discipline) instead\n * of landing in the living docs.\n */\nexport async function planLandscapeMerge(merge: LandscapeMergeRequest): Promise<LandscapePlan> {\n const { landscapeText: text, deltaText, deltaElements, newEls, newRels, featureId } = merge;\n // First, and before the early return for \"nothing to add\": a block this merge\n // cannot carry is lost whether or not the delta also has tagged elements to\n // splice. See delta-blocks.ts.\n assertMergeableDelta(deltaText);\n const land = await loadSource(text);\n if (land.errors.length > 0) {\n throw new LandscapeSpliceError(`landscape.likec4 has ${land.errors.length} error(s) — fix it before archiving`);\n }\n const haveIds = new Set(land.elements.map((e) => e.id));\n // The title join needs the matched element back, not just membership: the\n // cross-service guard below compares service BINDINGS across the join.\n const byTitle = new Map<string, Elem[]>();\n const seeTitle = (el: Elem): void => {\n byTitle.set(el.title, [...(byTitle.get(el.title) ?? []), el]);\n };\n for (const el of land.elements) seeTitle(el);\n const addedEls: Elem[] = [];\n for (const e of newEls) {\n if (haveIds.has(e.id)) continue;\n const sameTitle = byTitle.get(e.title);\n if (sameTitle !== undefined) {\n // A title match is the id-less fallback join, and skipping on it is only\n // safe when the two sides could be the same box. When both sides carry an\n // explicit `metadata { service }` binding and every binding disagrees,\n // they are provably DIFFERENT services' boxes sharing a title ('API',\n // 'Database') — the skip would silently drop the addition, and any delta\n // edge into it would then refuse the whole archive at the parse net\n // below with a message about nothing. Refuse here instead, at plan time,\n // naming both sides.\n if (e.service !== undefined && sameTitle.every((m) => m.service !== undefined && m.service !== e.service)) {\n const m = sameTitle[0]!;\n throw new LandscapeSpliceError(\n `the delta's '${e.id}' (bound to service '${e.service}') shares the title '${e.title}' with '${m.id}' (bound to service '${m.service}') — a title join across services would silently drop the addition; ` +\n `retitle one of them, or reuse the id '${m.id}' if they really are the same element`,\n );\n }\n // KNOWN (narrowed by the guard above): with EITHER side unbound the title\n // join stays trusting — the unbound title-fallback is the legal legacy\n // pattern — so a cross-service collision hiding behind an unbound element\n // is still silently skipped here. Scoping titles per service is backlog.\n continue;\n }\n haveIds.add(e.id);\n seeTitle(e);\n addedEls.push(e);\n }\n\n // Edges are matched by COUNT, not by membership: two edges the model cannot tell\n // apart are still two edges, and dropping the second one silently loses a call\n // the author drew. An edge already in the landscape consumes one delta edge of\n // the same identity, which is what keeps re-archiving idempotent.\n const have = new Map<string, number>();\n for (const r of land.relationships) {\n const k = relKey(land.elements, r);\n have.set(k, (have.get(k) ?? 0) + 1);\n }\n const addedRels: Rel[] = [];\n for (const r of newRels) {\n const k = relKey(deltaElements, r);\n const n = have.get(k) ?? 0;\n if (n > 0) {\n have.set(k, n - 1);\n continue;\n }\n addedRels.push(r);\n }\n\n if (addedEls.length === 0 && addedRels.length === 0) return { content: null, addedEls, addedRels };\n\n const scan = scanModel(deltaText);\n if (scan === null) {\n throw new LandscapeSpliceError(\"delta.likec4 has no model block — nothing to splice the additions from\");\n }\n\n // Everything below either locates a declaration's authored bytes or refuses.\n // `spliced` remembers the delta ranges already carried over, so a child whose\n // parent is itself new is recognised as riding inside the parent's text and\n // is never inserted twice.\n const byId = new Map(scan.elements.map((e) => [e.id, e]));\n const spliced: Array<{ start: number; end: number }> = [];\n const rides = (start: number, end: number): boolean =>\n spliced.some((r) => start >= r.start && end <= r.end);\n\n // The living model, scanned on MASKED source (scanModel) and RE-SCANNED\n // after every splice: each addition lands in the text exactly as the\n // previous one left it, so an archive of N additions produces the same\n // bytes as N single-addition archives run back to back. Placement then\n // composes per statement — the unit the order-independence argument below\n // is stated in — instead of per archive, where a batch computed against a\n // stale layout could interleave with itself. Masking matters — a `model {`\n // spelled inside a comment or string above the real block must not capture\n // the match, or every top-level addition lands inside the comment (legal\n // LikeC4, so the parse net below would pass a landscape containing none of\n // the architecture).\n let content = text;\n // Service binding for placement: living elements first (they win a shared\n // id), the delta's after — a spliced statement's id resolves to its service\n // the moment its bytes land in the scan.\n const bindEls = [...land.elements, ...deltaElements.filter((d) => !land.elements.some((l) => l.id === d.id))];\n let livingScan = scanModel(content);\n let stmts = livingScan === null ? [] : topStatements(livingScan, bindEls);\n const rescan = (): void => {\n livingScan = scanModel(content);\n stmts = livingScan === null ? [] : topStatements(livingScan, bindEls);\n };\n const requireModel = (): ScannedModel => {\n if (livingScan === null) throw new LandscapeSpliceError(\"landscape.likec4 has no model block\");\n return livingScan;\n };\n // Built fresh at every use, never captured: `content` and `stmts` are rebound\n // by each rescan, and a region assembled before a splice describes a document\n // that no longer exists.\n const modelRegion = (): ModelRegion => ({ text: content, stmts, close: requireModel().close });\n const applyAt = (at: number, insert: string): void => {\n content = content.slice(0, at) + insert + content.slice(at);\n rescan();\n };\n const applyTop = (spot: Spot, block: string): void => {\n // A bare spot shares its line with other text (the closing brace, or a\n // statement the block displaces) — it brings its own newlines; a\n // line-start spot slots between lines.\n applyAt(spot.at, spot.bare ? `\\n${block}\\n` : `${block}\\n`);\n };\n\n // Placement walks the scanned statement layout, so a declaration the scan\n // cannot see is one placement would blindly splice around — LikeC4 accepts\n // two declarations on one line, but scanModel's statement head runs to the\n // end of the line, so the second rides invisibly inside the first: an\n // element bound to its service would miss its neighborhood, and a bodyless\n // parent gaining a body could wrap the wrong declaration. Refuse\n // mechanically (the parse-net discipline) instead of splicing blind.\n if (livingScan !== null) {\n const seen = new Set(livingScan.elements.map((e) => e.id));\n const invisible = land.elements.find((e) => !seen.has(e.id));\n if (invisible !== undefined) {\n throw new LandscapeSpliceError(\n `landscape.likec4 declares '${invisible.id}' in a form placement cannot locate — most often two declarations sharing one line; give each its own line, then re-run`,\n );\n }\n if (livingScan.rels.length < land.relationships.length) {\n throw new LandscapeSpliceError(\n `landscape.likec4 declares ${land.relationships.length} relationship(s) but placement can locate only ${livingScan.rels.length} — most often two statements sharing one line; give each its own line, then re-run`,\n );\n }\n }\n\n const livingParentOf = (parentId: string): ScannedElement | null => {\n if (livingScan === null) return null;\n const direct = livingScan.elements.find((e) => e.id === parentId);\n if (direct !== undefined) return direct;\n // The delta may spell an existing element under its own id; the title is\n // the stable cross-namespace name (the same rule the existence check uses).\n const title = deltaElements.find((e) => e.id === parentId)?.title;\n const livingId = title === undefined ? undefined : land.elements.find((e) => e.title === title)?.id;\n if (livingId === undefined) return null;\n return livingScan.elements.find((e) => e.id === livingId) ?? null;\n };\n\n // Ancestors first: a spliced parent covers its children before they are seen.\n const depth = (id: string): number => id.split(\".\").length;\n const sortedEls = [...addedEls].sort(\n (a, b) => depth(a.id) - depth(b.id) || (byId.get(a.id)?.start ?? 0) - (byId.get(b.id)?.start ?? 0),\n );\n for (const e of sortedEls) {\n const src = byId.get(e.id);\n if (src === undefined) {\n throw new LandscapeSpliceError(\n `cannot locate '${e.id}' in delta.likec4 — the landscape merge splices authored source, and this declaration was not found`,\n );\n }\n if (rides(src.start, src.end)) continue;\n const dot = e.id.lastIndexOf(\".\");\n if (dot === -1) {\n const spot = elementSpot(modelRegion(), e.id, elementService(e));\n applyTop(spot, spliceSource(deltaText, src, featureId, \" \"));\n spliced.push({ start: src.start, end: src.end });\n continue;\n }\n const parentId = e.id.slice(0, dot);\n const parent = livingParentOf(parentId);\n if (parent === null) {\n throw new LandscapeSpliceError(\n `'${e.id}' nests under '${parentId}', which is neither in the living landscape nor added by this delta — there is nowhere to insert it`,\n );\n }\n const nested = nestedInsert(content, parent, spliceSource(deltaText, src, featureId, parent.indent + \" \"));\n applyAt(nested.at, nested.insert);\n spliced.push({ start: src.start, end: src.end });\n }\n\n // Relationships: match each parsed addition back to its statement in the\n // delta source — full identity (endpoints, title, all three spine keys,\n // tags), consumed one statement per addition so duplicates stay duplicates.\n // The spine keys earn their place here the same way they do in `relKey`:\n // without them two additions differing only by `publishes` share one pool\n // entry, and `shift()` hands the second one the first one's authored bytes.\n const relKeyOf = (r: Rel | ScannedRel): string =>\n JSON.stringify([r.source, r.target, r.title ?? \"\", r.op ?? \"\", r.publishes ?? \"\", r.consumes ?? \"\", [...r.tags].sort()]);\n const pool = new Map<string, ScannedRel[]>();\n for (const s of scan.rels) {\n const k = relKeyOf(s);\n pool.set(k, [...(pool.get(k) ?? []), s]);\n }\n for (const r of addedRels) {\n const s = pool.get(relKeyOf(r))?.shift();\n if (s === undefined) {\n throw new LandscapeSpliceError(\n `cannot locate the '${r.source} -> ${r.target}' relationship in delta.likec4 — the landscape merge splices authored source, and no matching declaration was found`,\n );\n }\n if (rides(s.start, s.end)) continue;\n const key = relSortKey(deltaElements, r);\n // Deliberately no `known` fleet set: this resolves only the anchor a new\n // relationship lands after (placement.ts's reason — cosmetic grouping,\n // safety-netted by the re-parse below), never which service a check\n // grades.\n const spot = relSpot(modelRegion(), serviceOf(deltaElements, r.source), key);\n applyTop(spot, spliceSource(deltaText, s, featureId, \" \"));\n }\n\n // The safety net: prove the computed landscape parses before anything is\n // written. Splice bugs — and legal inputs the living document cannot absorb,\n // like a kind its specification never declares — refuse here, at plan time,\n // instead of corrupting the one file the whole fleet reads.\n const check = await loadSource(content);\n if (check.errors.length > 0) {\n const detail = check.errors\n .slice(0, 3)\n .map((e) => (typeof e.line === \"number\" ? `L${e.line}: ${e.message}` : e.message))\n .join(\"; \");\n throw new LandscapeSpliceError(\n `the merged landscape would not parse (${check.errors.length} error(s): ${detail}) — nothing was written. ` +\n `The delta's additions do not fit the living landscape as authored — most often an element kind or tag ` +\n `its specification block does not declare; fix the landscape's specification or the delta, then re-run`,\n );\n }\n\n return { content, addedEls, addedRels };\n}\n"]}
1
+ {"version":3,"file":"landscape-merge.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/landscape-merge.ts"],"names":[],"mappings":"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","sourcesContent":["\nimport { parentIdOf } from \"../../kernel/ids/fqn/ancestors.js\";\nimport { errorText, loadSource, type Elem, type Rel } from \"../likec4.js\";\nimport { elementService, serviceOf } from \"../resolve/service.js\";\nimport { scanModel, type ScannedElement, type ScannedModel, type ScannedRel } from \"../source-scan.js\";\nimport { matchLineEndings, spliceSource } from \"./authored-source.js\";\nimport {\n LandscapeSpliceError,\n type ExtendingModel,\n type LandscapeMergeRequest,\n type LandscapePlan,\n type ModelAdditions,\n} from \"./contract.js\";\nimport { assertMergeableDelta } from \"./delta-blocks.js\";\nimport { relSortKey, relStatementKey } from \"./identity/edges.js\";\nimport { newAdditions } from \"./identity/existing.js\";\nimport { planInteriors } from \"./identity/interior.js\";\nimport { ownerOf } from \"./identity/owner.js\";\nimport {\n elementSpot,\n nestedInsert,\n relSpot,\n topStatements,\n type ModelRegion,\n type Spot,\n} from \"./placement.js\";\n\n/* ------------------------------------------------------------------ */\n/* Merging */\n/* ------------------------------------------------------------------ */\n\n/**\n * Plan the merge of the feature's new elements + relationships into the living\n * landscape's `model { ... }` block (preserving the rest of the file).\n *\n * The additions are SPLICED from delta.likec4 as authored — byte for byte,\n * technology, style, icons, links, metadata, nested children and all — never\n * re-serialized from the parsed model: a rebuild keeps only the fields loam\n * models, and every field it forgets is authored detail destroyed. The one\n * edit on the way over is dropping the feature's own tag — the additions are\n * baseline now (SCHEMA.md documents the drop as why unarchive restores bytes\n * instead of recomputing) — and a construct the strip empties goes with it.\n *\n * Placement is deterministic and SERVICE-GROUPED (the spot* helpers in\n * placement.ts):\n * a top-level addition lands beside what already belongs to its service, so\n * two concurrent archives touching different services usually splice on\n * different lines and git merges their PRs — the single shared append point\n * every archive used to hit made two concurrent archives conflict BY\n * CONSTRUCTION. A NESTED element lands inside its parent's block — inside the\n * living parent when the landscape already has it, riding verbatim inside the\n * spliced parent when the parent is new from the same delta — never as a flat\n * dotted id at top level, which LikeC4 rejects. The additions carry no marker\n * comment: which archive added what is the git history's to say.\n *\n * Existence is checked semantically against the parsed landscape AND against\n * every extending model the request carries (by element id/title and by edge\n * identity), so re-archiving is idempotent whichever document the additions\n * landed in — `./identity/existing.ts` holds that phase and its two joins, and\n * this function only turns the collision it reports into the refusal.\n *\n * Splicing is text surgery, so the computed result is PARSED before it is\n * returned: a merged landscape LikeC4 rejects refuses the archive at plan time\n * (merge-failed, nothing written — the unparseable-delta discipline) instead\n * of landing in the living docs.\n *\n * NOT EVERYTHING LANDS HERE. A service whose model EXTENDS this map owns its\n * own interior, and an addition nested under such a service is ROUTED to that\n * model instead (`./identity/owner.ts` decides, `./model/merge.ts` splices).\n * Until it was, a delta's container landed in the map and the service was\n * `c4.invalid` — a duplicate declaration — the moment its model wrote the same\n * container, which is exactly what the adopt brief tells it to do\n * (verification 2026-09-04, E1). With no extending models in the request this\n * function behaves as it always did.\n *\n * Routing per ADDITION is not the whole of it, because a service the feature\n * INTRODUCES carries its interior in one element's authored bytes and those\n * children carry no feature tag of their own. `./identity/interior.ts` decides, once\n * per such block, whether it hands that interior to the model — cut out of what\n * lands on the map — or rides whole; the routing below DEFERS to that decision\n * for everything written inside the block, which is what keeps the two from\n * placing the same child twice.\n */\nexport async function planLandscapeMerge(merge: LandscapeMergeRequest): Promise<LandscapePlan> {\n const { landscapeText: text, deltaText, deltaElements, newEls, newRels, featureId } = merge;\n const models = merge.models ?? [];\n // First, and before the early return for \"nothing to add\": a block this merge\n // cannot carry is lost whether or not the delta also has tagged elements to\n // splice. See delta-blocks.ts.\n assertMergeableDelta(deltaText);\n const land = await loadSource(text);\n if (land.errors.length > 0) {\n throw new LandscapeSpliceError(`landscape.likec4 has ${land.errors.length} error(s) — fix it before archiving`);\n }\n // Service binding for the existence joins AND for routing and placement:\n // living elements first (they win a shared id), the delta's after — a spliced\n // statement's id resolves to its service the moment its bytes land in the scan.\n const bindEls = [...land.elements, ...deltaElements.filter((d) => !land.elements.some((l) => l.id === d.id))];\n // What the living fleet already has — the map AND every extending model, by id\n // and by title. `./identity/existing.ts` holds both joins and their asymmetry.\n const added = newAdditions({\n livingEls: land.elements,\n livingRels: land.relationships,\n bindEls,\n deltaElements,\n candidates: { els: newEls, rels: newRels },\n models,\n });\n if (added.collision !== undefined) {\n const { addition: e, living: m } = added.collision;\n throw new LandscapeSpliceError(\n `the delta's '${e.id}' (bound to service '${e.service}') shares the title '${e.title}' with '${m.id}' (bound to service '${m.service}') — a title join across services would silently drop the addition; ` +\n `retitle one of them, or reuse the id '${m.id}' if they really are the same element`,\n );\n }\n const addedEls = added.els;\n const addedRels = added.rels;\n\n const owner = ownerOf({\n bindEls,\n living: new Set(land.elements.map((e) => e.id)),\n declared: added.declaredIds,\n added: new Set(addedEls.map((e) => e.id)),\n models,\n });\n // Nothing to add: the same shape the merge returned before routing existed,\n // and the reason it is decided HERE rather than after the loops below — a\n // no-op archive must not have to scan the delta, nor answer the landscape\n // guards further down, which refuse documents this merge is not touching.\n // Equivalent to the old \"nothing reached the map and nothing was routed\":\n // every addition ends up in one of those two, so an empty pair means both.\n if (addedEls.length === 0 && addedRels.length === 0) {\n return { content: null, addedEls: [], addedRels: [], models: [] };\n }\n\n const scan = scanModel(deltaText);\n if (scan === null) {\n throw new LandscapeSpliceError(\"delta.likec4 has no model block — nothing to splice the additions from\");\n }\n\n // Everything below either locates a declaration's authored bytes or refuses.\n // `spliced` remembers the delta ranges already carried over, so a child whose\n // parent is itself new is recognised as riding inside the parent's text and\n // is never inserted twice.\n const byId = new Map(scan.elements.map((e) => [e.id, e]));\n // Relationships: match each parsed addition back to its statement in the\n // delta source — full identity (`relStatementKey`), consumed one statement\n // per addition so duplicates stay duplicates. Built before the routing\n // because routing has to know WHERE an edge was written: one drawn inside a\n // block that rides whole is already on the map.\n const pool = new Map<string, ScannedRel[]>();\n for (const s of scan.rels) {\n const k = relStatementKey(s);\n pool.set(k, [...(pool.get(k) ?? []), s]);\n }\n\n const interiors = planInteriors({ scan, deltaElements, owner, addedEls });\n const routed = new Map<string, ModelAdditions>();\n const into = (model: ExtendingModel): ModelAdditions => {\n const existing = routed.get(model.path);\n if (existing !== undefined) return existing;\n const fresh: ModelAdditions = { model, els: [], rels: [] };\n routed.set(model.path, fresh);\n return fresh;\n };\n for (const share of interiors.routed) into(share.model).els.push(...share.els);\n\n const mapEls: Elem[] = [];\n for (const e of addedEls) {\n const src = byId.get(e.id);\n // Written inside a block that has already decided: a split block handed it\n // to the model with its siblings, and a whole one carries it onto the map\n // in the parent's own bytes, where the splice loop's `rides` finds it. Both\n // are placed exactly once, which is the point of asking the block first.\n const fate = src === undefined ? null : interiors.covering(src);\n if (fate === \"split\") continue;\n const model = fate === \"whole\" ? undefined : owner(parentIdOf(e.id));\n if (model === undefined) mapEls.push(e);\n else into(model).els.push(e);\n }\n const mapRels: Rel[] = [];\n for (const r of addedRels) {\n // An edge written inside a block that rides whole names its endpoints by\n // their LOCAL names and reads only there; it stays with its block.\n const stmts = pool.get(relStatementKey(r)) ?? [];\n const inBlock = stmts.length > 0 && stmts.every((s) => interiors.covering(s) !== null);\n // Otherwise the SOURCE decides: a call is drawn where the caller draws its\n // calls, and only when the source is not itself interior does the target's\n // owner get the edge. A service-level edge belongs to neither and stays on\n // the map.\n const model = inBlock ? undefined : owner(parentIdOf(r.source)) ?? owner(parentIdOf(r.target));\n if (model === undefined) mapRels.push(r);\n else into(model).rels.push(r);\n }\n const touchesMap = mapEls.length > 0 || mapRels.length > 0;\n\n const spliced: Array<{ start: number; end: number }> = [];\n const rides = (start: number, end: number): boolean =>\n spliced.some((r) => start >= r.start && end <= r.end);\n\n // The living model, scanned on MASKED source (scanModel) and RE-SCANNED\n // after every splice: each addition lands in the text exactly as the\n // previous one left it, so an archive of N additions produces the same\n // bytes as N single-addition archives run back to back. Placement then\n // composes per statement — the unit the order-independence argument below\n // is stated in — instead of per archive, where a batch computed against a\n // stale layout could interleave with itself. Masking matters — a `model {`\n // spelled inside a comment or string above the real block must not capture\n // the match, or every top-level addition lands inside the comment (legal\n // LikeC4, so the parse net below would pass a landscape containing none of\n // the architecture).\n let content = text;\n let livingScan = scanModel(content);\n let stmts = livingScan === null ? [] : topStatements(livingScan, bindEls);\n const rescan = (): void => {\n livingScan = scanModel(content);\n stmts = livingScan === null ? [] : topStatements(livingScan, bindEls);\n };\n const requireModel = (): ScannedModel => {\n if (livingScan === null) throw new LandscapeSpliceError(\"landscape.likec4 has no model block\");\n return livingScan;\n };\n // Built fresh at every use, never captured: `content` and `stmts` are rebound\n // by each rescan, and a region assembled before a splice describes a document\n // that no longer exists.\n const modelRegion = (): ModelRegion => ({ text: content, stmts, close: requireModel().close });\n const applyAt = (at: number, insert: string): void => {\n content = content.slice(0, at) + insert + content.slice(at);\n rescan();\n };\n const applyTop = (spot: Spot, block: string): void => {\n // A bare spot shares its line with other text (the closing brace, or a\n // statement the block displaces) — it brings its own newlines; a\n // line-start spot slots between lines.\n applyAt(spot.at, spot.bare ? `\\n${block}\\n` : `${block}\\n`);\n };\n\n // Placement walks the scanned statement layout, so a declaration the scan\n // cannot see is one placement would blindly splice around — LikeC4 accepts\n // two declarations on one line, but scanModel's statement head runs to the\n // end of the line, so the second rides invisibly inside the first: an\n // element bound to its service would miss its neighborhood, and a bodyless\n // parent gaining a body could wrap the wrong declaration. Refuse\n // mechanically (the parse-net discipline) instead of splicing blind.\n if (livingScan !== null) {\n const seen = new Set(livingScan.elements.map((e) => e.id));\n const invisible = land.elements.find((e) => !seen.has(e.id));\n if (invisible !== undefined) {\n throw new LandscapeSpliceError(\n `landscape.likec4 declares '${invisible.id}' in a form placement cannot locate — most often two declarations sharing one line; give each its own line, then re-run`,\n );\n }\n if (livingScan.rels.length < land.relationships.length) {\n throw new LandscapeSpliceError(\n `landscape.likec4 declares ${land.relationships.length} relationship(s) but placement can locate only ${livingScan.rels.length} — most often two statements sharing one line; give each its own line, then re-run`,\n );\n }\n }\n\n const livingParentOf = (parentId: string): ScannedElement | null => {\n if (livingScan === null) return null;\n const direct = livingScan.elements.find((e) => e.id === parentId);\n if (direct !== undefined) return direct;\n // The delta may spell an existing element under its own id; the title is\n // the stable cross-namespace name (the same rule the existence check uses).\n const title = deltaElements.find((e) => e.id === parentId)?.title;\n const livingId = title === undefined ? undefined : land.elements.find((e) => e.title === title)?.id;\n if (livingId === undefined) return null;\n return livingScan.elements.find((e) => e.id === livingId) ?? null;\n };\n\n // Ancestors first: a spliced parent covers its children before they are seen.\n const depth = (id: string): number => id.split(\".\").length;\n const sortedEls = [...mapEls].sort(\n (a, b) => depth(a.id) - depth(b.id) || (byId.get(a.id)?.start ?? 0) - (byId.get(b.id)?.start ?? 0),\n );\n for (const e of sortedEls) {\n const src = byId.get(e.id);\n if (src === undefined) {\n throw new LandscapeSpliceError(\n `cannot locate '${e.id}' in delta.likec4 — the landscape merge splices authored source, and this declaration was not found`,\n );\n }\n if (rides(src.start, src.end)) continue;\n // Every split block's children, whichever block they belong to:\n // `spliceSource` keeps only the ranges that fall inside the bytes it is\n // carrying, so a block riding inside ANOTHER new block still loses its own\n // interior on the way to the map.\n const omit = interiors.omit;\n const dot = e.id.lastIndexOf(\".\");\n if (dot === -1) {\n const spot = elementSpot(modelRegion(), e.id, elementService(e));\n applyTop(spot, spliceSource(deltaText, src, { featureId, indent: \" \", omit }));\n spliced.push({ start: src.start, end: src.end });\n continue;\n }\n const parentId = e.id.slice(0, dot);\n const parent = livingParentOf(parentId);\n if (parent === null) {\n throw new LandscapeSpliceError(\n `'${e.id}' nests under '${parentId}', which is neither in the living landscape nor added by this delta — there is nowhere to insert it`,\n );\n }\n const nested = nestedInsert(\n content,\n parent,\n spliceSource(deltaText, src, { featureId, indent: parent.indent + \" \", omit }),\n );\n applyAt(nested.at, nested.insert);\n spliced.push({ start: src.start, end: src.end });\n }\n\n // Each map-bound addition takes its statement out of the pool built above:\n // one statement per addition, so duplicates stay duplicates.\n for (const r of mapRels) {\n const s = pool.get(relStatementKey(r))?.shift();\n if (s === undefined) {\n throw new LandscapeSpliceError(\n `cannot locate the '${r.source} -> ${r.target}' relationship in delta.likec4 — the landscape merge splices authored source, and no matching declaration was found`,\n );\n }\n if (rides(s.start, s.end)) continue;\n const key = relSortKey(deltaElements, r);\n // Deliberately no `known` fleet set: this resolves only the anchor a new\n // relationship lands after (placement.ts's reason — cosmetic grouping,\n // safety-netted by the re-parse below), never which service a check\n // grades.\n const spot = relSpot(modelRegion(), serviceOf(deltaElements, r.source), key);\n applyTop(spot, spliceSource(deltaText, s, { featureId, indent: \" \" }));\n }\n\n // The safety net: prove the computed landscape parses before anything is\n // written. Splice bugs — and legal inputs the living document cannot absorb,\n // like a kind its specification never declares — refuse here, at plan time,\n // instead of corrupting the one file the whole fleet reads. Skipped when\n // every addition was routed away: the text is the one already parsed above,\n // and the models get their own net from the caller, which is the only side\n // that can stage a model beside the map it extends.\n // The splice composed bytes from two documents, and the delta's newlines are\n // not necessarily the map's. Corrected before the net, so what is proven is\n // what would be written.\n content = matchLineEndings(text, content);\n if (touchesMap) {\n const check = await loadSource(content);\n if (check.errors.length > 0) {\n const detail = check.errors\n .slice(0, 3)\n .map(errorText)\n .join(\"; \");\n throw new LandscapeSpliceError(\n `the merged landscape would not parse (${check.errors.length} error(s): ${detail}) — nothing was written. ` +\n `The delta's additions do not fit the living landscape as authored — most often an element kind or tag ` +\n `its specification block does not declare; fix the landscape's specification or the delta, then re-run`,\n );\n }\n }\n\n return { content: touchesMap ? content : null, addedEls: mapEls, addedRels: mapRels, models: [...routed.values()] };\n}\n"]}
@@ -0,0 +1,140 @@
1
+ import { scanModel } from "../../source-scan.js";
2
+ import { matchLineEndings, spliceSource } from "../authored-source.js";
3
+ import { LandscapeSpliceError } from "../contract.js";
4
+ import { relStatementKey } from "../identity/edges.js";
5
+ import { closeSpot, nestedInsert, spotAfter } from "../placement.js";
6
+ /** The merged model source. Refuses (never guesses) when the delta or the model cannot be read. */
7
+ export function spliceModel(req) {
8
+ const { additions, deltaText, featureId } = req;
9
+ const { model } = additions;
10
+ const scan = scanModel(deltaText);
11
+ if (scan === null) {
12
+ throw new LandscapeSpliceError("delta.likec4 has no model block — nothing to splice the additions from");
13
+ }
14
+ const byId = new Map(scan.elements.map((e) => [e.id, e]));
15
+ const pool = new Map();
16
+ for (const s of scan.rels) {
17
+ const k = relStatementKey(s);
18
+ pool.set(k, [...(pool.get(k) ?? []), s]);
19
+ }
20
+ let content = withModelBlock(model.text);
21
+ let live = read(content, model.path);
22
+ // The delta ranges already carried over, so a child whose parent is itself
23
+ // new is recognised as riding inside the parent's text — the same rule the
24
+ // landscape merge states, and the reason `node` inside a new `cache` is never
25
+ // spliced twice.
26
+ const spliced = [];
27
+ const rides = (start, end) => spliced.some((r) => start >= r.start && end <= r.end);
28
+ const applyAt = (at, insert) => {
29
+ content = content.slice(0, at) + insert + content.slice(at);
30
+ live = read(content, model.path);
31
+ };
32
+ // Ancestors first, exactly as the landscape merge orders them: a spliced
33
+ // parent covers its children before they are seen.
34
+ const depth = (id) => id.split(".").length;
35
+ const ordered = [...additions.els].sort((a, b) => depth(a.id) - depth(b.id) || (byId.get(a.id)?.start ?? 0) - (byId.get(b.id)?.start ?? 0));
36
+ for (const e of ordered) {
37
+ const src = byId.get(e.id);
38
+ if (src === undefined) {
39
+ throw new LandscapeSpliceError(`cannot locate '${e.id}' in delta.likec4 — the model merge splices authored source, and this declaration was not found`);
40
+ }
41
+ if (rides(src.start, src.end))
42
+ continue;
43
+ const [at, insert] = elementInsert({
44
+ content,
45
+ live,
46
+ block: (indent) => spliceSource(deltaText, src, { featureId, indent }),
47
+ parent: parentIdOf(e),
48
+ });
49
+ applyAt(at, insert);
50
+ spliced.push({ start: src.start, end: src.end });
51
+ }
52
+ for (const r of additions.rels) {
53
+ const s = pool.get(relStatementKey(r))?.shift();
54
+ if (s === undefined) {
55
+ throw new LandscapeSpliceError(`cannot locate the '${r.source} -> ${r.target}' relationship in delta.likec4 — the model merge splices authored source, and no matching declaration was found`);
56
+ }
57
+ if (rides(s.start, s.end))
58
+ continue;
59
+ const base = blockIndent(content, live.close);
60
+ // Top-level in the model block: an edge written inside an `extend` block is
61
+ // that block's, and anchoring after it would splice into someone's body.
62
+ const top = live.rels.filter((x) => !live.elements.some((el) => x.start > el.start && x.start < el.end));
63
+ const anchor = top.at(-1);
64
+ const spot = anchor === undefined ? closeSpot(content, live.close) : spotAfter(content, live.close, anchor.end);
65
+ const block = spliceSource(deltaText, s, { featureId, indent: `${base} ` });
66
+ applyAt(spot.at, spot.bare ? `\n${block}\n` : `${block}\n`);
67
+ }
68
+ // The delta's newlines are not necessarily this model's, and a merge that left
69
+ // both conventions in one file is noise in every editor and every diff — the
70
+ // same correction the landscape merge makes, from the same helper
71
+ // (verification 2026-09-04, W-CRLF).
72
+ return matchLineEndings(model.text, content);
73
+ }
74
+ /** The parent an addition nests under. Routing only ever sends nested ids here. */
75
+ function parentIdOf(e) {
76
+ return e.id.slice(0, e.id.lastIndexOf("."));
77
+ }
78
+ /**
79
+ * Where one element's block goes, as `(offset, text)`.
80
+ *
81
+ * Two arms, and the second is the one that makes a freshly adopted service
82
+ * work: a model that has never named this container's parent has no `extend`
83
+ * block for it, so the merge writes one rather than refusing. A parent the
84
+ * merged project will not resolve is caught by the caller's parse net, which is
85
+ * the only reader that can see the map and the model together.
86
+ */
87
+ function elementInsert(req) {
88
+ const { content, live, block, parent } = req;
89
+ const declared = live.elements.find((x) => x.id === parent);
90
+ if (declared !== undefined) {
91
+ const nested = nestedInsert(content, declared, block(`${declared.indent} `));
92
+ return [nested.at, nested.insert];
93
+ }
94
+ const base = blockIndent(content, live.close);
95
+ const body = `${base} extend ${parent} {\n${block(`${base} `)}\n${base} }`;
96
+ const spot = closeSpot(content, live.close);
97
+ return [spot.at, spot.bare ? `\n${body}\n` : `${body}\n`];
98
+ }
99
+ /**
100
+ * The model, with a `model { }` block appended when the file has none.
101
+ *
102
+ * `../../service-model/shape.ts` answers `extending` for an empty file, a
103
+ * views-only file and a tags-only `specification { }` — three shapes it calls
104
+ * legal on purpose — so routing hands those models a nested addition, and
105
+ * refusing there took a whole legal shape away from `loam archive` for a
106
+ * document the merge is perfectly able to author (review of E1: before the
107
+ * routing existed, the same delta merged onto the map). It is the call
108
+ * `elementInsert` already makes one level down — a model that has never named
109
+ * the parent gains the `extend` block rather than refusing — and the caller's
110
+ * parse net proves the result either way.
111
+ */
112
+ function withModelBlock(text) {
113
+ if (scanModel(text) !== null)
114
+ return text;
115
+ const pad = text === "" || text.endsWith("\n") ? "" : "\n";
116
+ return `${text}${pad}model {\n}\n`;
117
+ }
118
+ /**
119
+ * The model's `model { }` block, re-scanned after every splice so each addition
120
+ * lands in the text the previous one left — the landscape merge's rule, for the
121
+ * same reason: placement composes per statement, not per archive.
122
+ *
123
+ * `withModelBlock` guarantees the FIRST read finds one, so a null here means a
124
+ * splice destroyed the block it was writing into: mechanical, so it refuses at
125
+ * plan time with nothing written, exactly as the parse nets do.
126
+ */
127
+ function read(content, path) {
128
+ const scan = scanModel(content);
129
+ if (scan === null) {
130
+ throw new LandscapeSpliceError(`merging this feature's additions left ${path} without a model block — nothing was written; re-run after reporting it`);
131
+ }
132
+ return scan;
133
+ }
134
+ /** The indentation of the line the model block's closing brace sits on, or "" when it shares one. */
135
+ function blockIndent(text, close) {
136
+ const lineStart = text.lastIndexOf("\n", close - 1) + 1;
137
+ const before = text.slice(lineStart, close);
138
+ return /^[ \t]*$/.test(before) ? before : "";
139
+ }
140
+ //# sourceMappingURL=merge.js.map