@ybotok/loam 0.2.0-alpha.5 → 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 (273) hide show
  1. package/CHANGELOG.md +901 -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 +264 -11
  6. package/SCHEMA.md +439 -38
  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 +128 -74
  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 +10 -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 +127 -119
  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 +258 -70
  75. package/dist/commands/validate/fleet/views/projects.js.map +1 -1
  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 -4
  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 -11
  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 +123 -14
  113. package/dist/core/agent/workflows/adopt.js.map +1 -1
  114. package/dist/core/agent/workflows/check.js +35 -20
  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 +133 -7
  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 -29
  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 +29 -24
  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 +36 -8
  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/refusals.js +1 -1
  213. package/dist/core/explain/refusals.js.map +1 -1
  214. package/dist/core/explore/explore.js +44 -9
  215. package/dist/core/explore/explore.js.map +1 -1
  216. package/dist/core/fleet-context.js +47 -0
  217. package/dist/core/fleet-context.js.map +1 -1
  218. package/dist/core/gate/partners.js +5 -9
  219. package/dist/core/gate/partners.js.map +1 -1
  220. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  221. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  222. package/dist/core/pack/living.js +47 -13
  223. package/dist/core/pack/living.js.map +1 -1
  224. package/dist/core/pack/pack.js +9 -6
  225. package/dist/core/pack/pack.js.map +1 -1
  226. package/dist/core/projection/arch-slice.js +2 -2
  227. package/dist/core/projection/arch-slice.js.map +1 -1
  228. package/dist/core/repo/paths.js +11 -9
  229. package/dist/core/repo/paths.js.map +1 -1
  230. package/dist/core/repo/tree/render/projects.js +93 -35
  231. package/dist/core/repo/tree/render/projects.js.map +1 -1
  232. package/dist/core/repo/tree/render/stale.js +2 -3
  233. package/dist/core/repo/tree/render/stale.js.map +1 -1
  234. package/dist/core/repo/tree/render/views.js +51 -3
  235. package/dist/core/repo/tree/render/views.js.map +1 -1
  236. package/dist/core/repo/tree/walk.js +45 -2
  237. package/dist/core/repo/tree/walk.js.map +1 -1
  238. package/dist/core/scaffold/landscape.js +219 -9
  239. package/dist/core/scaffold/landscape.js.map +1 -1
  240. package/dist/core/staging/writes.js.map +1 -1
  241. package/dist/core/usecases/delta/claims.js +1 -0
  242. package/dist/core/usecases/delta/claims.js.map +1 -1
  243. package/dist/core/usecases/delta/overlay.js +4 -0
  244. package/dist/core/usecases/delta/overlay.js.map +1 -1
  245. package/dist/core/usecases/fleet.js +18 -6
  246. package/dist/core/usecases/fleet.js.map +1 -1
  247. package/dist/core/usecases/operations.js +7 -0
  248. package/dist/core/usecases/operations.js.map +1 -1
  249. package/dist/core/usecases/service/exclude.js +119 -0
  250. package/dist/core/usecases/service/exclude.js.map +1 -0
  251. package/dist/core/usecases/service/flows.js +161 -0
  252. package/dist/core/usecases/service/flows.js.map +1 -0
  253. package/dist/core/vocabulary/maturity.js +10 -0
  254. package/dist/core/vocabulary/maturity.js.map +1 -1
  255. package/examples/README.md +24 -6
  256. package/examples/docs/architecture/landscape.likec4 +41 -0
  257. package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
  258. package/examples/docs/likec4.config.json +0 -1
  259. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  260. package/examples/docs/services/order-service/arch.spec.md +13 -6
  261. package/examples/docs/services/order-service/model.likec4 +53 -67
  262. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  263. package/examples/docs/services/payment-service/model.likec4 +32 -35
  264. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  265. package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
  266. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  267. package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
  268. package/package.json +1 -1
  269. package/examples/docs/services/checkout-web/likec4.config.json +0 -4
  270. package/examples/docs/services/order-service/likec4.config.json +0 -4
  271. package/examples/docs/services/payment-service/likec4.config.json +0 -4
  272. package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
  273. package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The service's OWN part of a per-service project — derived, because LikeC4
3
+ * does not record it.
4
+ *
5
+ * Measured at the 1.59.2 pin: `$data.views[id]` carries a `sourcePath` and
6
+ * `$data.elements[fqn]` carries NO source-document field at all. So after the
7
+ * map and one extending model have been parsed together there is nothing in the
8
+ * parse output that says which of the two files declared a given element. The
9
+ * answer has to be reconstructed, and this module is the reconstruction: DIFF
10
+ * the project against the architecture-alone load, and resolve what is left
11
+ * through the same element→service resolver every fleet check already joins on.
12
+ *
13
+ * Why it matters that this is exact rather than approximate: every model-
14
+ * dependent check reads the slice as if it were the standalone file it used to
15
+ * read — `c4.no-relationships`, `health.dependency-unmodelled`, `Covers:`
16
+ * resolution, the attested-call scan. Too WIDE and a service is graded against
17
+ * the whole fleet map (every service would suddenly have relationships, and
18
+ * every dependency would resolve). Too NARROW and a service that models its
19
+ * partners correctly is told it models nothing.
20
+ */
21
+ import { properAncestorIds } from "../../kernel/ids/fqn/ancestors.js";
22
+ import { serviceResolver } from "../resolve/service.js";
23
+ /**
24
+ * A relationship's identity for the DIFF, and every field of it is one loam
25
+ * already reads somewhere.
26
+ *
27
+ * There is no id to join on: LikeC4 mints relationship ids per parse, so the
28
+ * same edge in two loads of overlapping document sets is two different ids.
29
+ * Endpoints alone are not enough either — a fleet may draw two edges between
30
+ * the same pair (`api -> kafka 'publishes'` and `api -> kafka 'consumes'`), and
31
+ * collapsing them would let a model silently inherit one of the map's. So the
32
+ * key is everything a check can branch on: the endpoints, the label, and the
33
+ * three metadata keys plus the tag set, sorted because tag order is authoring
34
+ * order and means nothing.
35
+ */
36
+ function relKey(rel) {
37
+ return JSON.stringify([
38
+ rel.source,
39
+ rel.target,
40
+ rel.title ?? "",
41
+ rel.op ?? "",
42
+ rel.publishes ?? "",
43
+ rel.consumes ?? "",
44
+ [...rel.tags].sort(),
45
+ ]);
46
+ }
47
+ /**
48
+ * The service's own slice of a per-service project.
49
+ *
50
+ * OWN is resolution PLUS NESTING, and the second half is not a widening for
51
+ * convenience — it is the only thing that keeps a model's own file from being
52
+ * filtered against it.
53
+ *
54
+ * Resolution first: every project element whose id resolves to this service,
55
+ * whether the model added it or the map already declared it. The bound element
56
+ * itself has to be in — a standalone model declared it, and every check that
57
+ * reads "the service's elements" expects it.
58
+ *
59
+ * Then nesting, because resolution alone ejects a child the author wrote INSIDE
60
+ * their own `extend` block. `serviceResolver`'s second rung answers with the
61
+ * nearest ancestor whose TITLE names a real `services/<id>/` directory, and that
62
+ * rung sees the child before it sees the parent: a container written as
63
+ * `store = database 'db'` under `extend svcA { … }` resolves to a service called
64
+ * `db` the moment some other team has adopted one, and svcA's own element is
65
+ * bound by title only. The model then reported one element fewer than it
66
+ * declares, a `Covers: svcA.store` line was convicted as a typo, and
67
+ * `c4.element-unowned` told the author their own container belonged in somebody
68
+ * else's model. A standalone model was never filtered against its own file at
69
+ * all, so every one of those is a regression the second shape introduced.
70
+ *
71
+ * So: an OWN ROOT is an own element none of whose proper ancestors is own — the
72
+ * element the author actually extended — and everything nested under one is own
73
+ * too. Id nesting under the extended element is evidence nobody had to guess at,
74
+ * and it outranks a title guess. It does NOT outrank an explicit
75
+ * `metadata { service '…' }` binding, which is a claim somebody wrote down: a
76
+ * map that nests one service's element inside another's keeps both on the right
77
+ * side of the line.
78
+ *
79
+ * OWN RELATIONSHIPS are the diff: the project's edges minus the ones the
80
+ * architecture documents already have. Resolution cannot decide this one, and
81
+ * the reason is the whole point of the fleet map: the map is where cross-service
82
+ * edges live, so `checkoutWeb -> paymentService` resolves to this service at one
83
+ * end and must NOT become part of the model's own relationships — otherwise
84
+ * every service would be graded as declaring calls it never wrote, and
85
+ * `landscape.service-isolated` (which asks whether the map's edges and the
86
+ * model's agree) would be answering about itself.
87
+ *
88
+ * The diff is a MULTISET difference, not a set one, and that distinction is the
89
+ * whole grade for a model that agrees with the map. A model may legitimately
90
+ * draw an edge the map also draws, spelled identically —
91
+ * `marketplace.paymentService -> stripe 'Authorizes cards'` — and under a set
92
+ * difference every such edge would vanish from the model's own list. Then
93
+ * `attestedCalls` finds no attested call, `landscape.service-isolated` reports a
94
+ * service that talks to nobody, the adopt brief's `landscape.attested` list
95
+ * comes back empty and `c4.no-relationships` fires on a model full of edges. So
96
+ * each structural key drops `min(architecture, project)` occurrences and the
97
+ * rest are the model's: one copy in the map cancels exactly one copy here.
98
+ *
99
+ * PARTNERS are the endpoints of the model's own edges that lie outside the
100
+ * service, AND every ancestor of such an endpoint that the project holds. The
101
+ * ancestors are not tidiness: a standalone model had to declare `kafka` in order
102
+ * to draw an edge at `kafka.orderEvents`, and health.yaml names the dependency
103
+ * `kafka`. Dropping the ancestor would turn a fleet's dependency declarations
104
+ * into `health.dependency-unmodelled` findings the day it migrated its models,
105
+ * with nothing in the docs having changed.
106
+ *
107
+ * UNOWNED is what the model added outside its own element — present in the
108
+ * project, absent from the architecture load, and resolving to somebody else. A
109
+ * new top-level element (`ledger` beside the map's systems) and a child added
110
+ * under another service's element both land here, and both are things the map
111
+ * or the other service should be declaring instead.
112
+ */
113
+ export function sliceForService(input) {
114
+ const resolve = serviceResolver(input.project.elements, input.known);
115
+ const byId = new Map(input.project.elements.map((e) => [e.id, e]));
116
+ const ownIds = new Set(input.project.elements.filter((e) => resolve(e.id) === input.service).map((e) => e.id));
117
+ // The elements the author extended: own, with nothing own above them. Frozen
118
+ // before the loop below adds anything, so a child pulled in by nesting cannot
119
+ // itself become a root and widen the set a second time.
120
+ const roots = [...ownIds].filter((id) => !properAncestorIds(id).some((up) => ownIds.has(up)));
121
+ for (const element of input.project.elements) {
122
+ if (ownIds.has(element.id))
123
+ continue;
124
+ if (!roots.some((root) => element.id.startsWith(`${root}.`)))
125
+ continue;
126
+ // The one thing nesting does not outrank. An explicit binding anywhere at or
127
+ // above this element names its service outright, and a map that files one
128
+ // service's element under another's would otherwise hand the outer service
129
+ // every element of the inner one.
130
+ if (boundServiceOf(element.id, byId) !== undefined)
131
+ continue;
132
+ ownIds.add(element.id);
133
+ }
134
+ // One budget per structural key: the map's copies cancel the project's, one
135
+ // for one, and what the budget does not cover is the model's own.
136
+ const budget = new Map();
137
+ for (const rel of input.architecture.relationships) {
138
+ const k = relKey(rel);
139
+ budget.set(k, (budget.get(k) ?? 0) + 1);
140
+ }
141
+ const relationships = input.project.relationships.filter((rel) => {
142
+ const k = relKey(rel);
143
+ const left = budget.get(k) ?? 0;
144
+ if (left === 0)
145
+ return true;
146
+ budget.set(k, left - 1);
147
+ return false;
148
+ });
149
+ const partnerIds = new Set();
150
+ for (const rel of relationships) {
151
+ for (const endpoint of [rel.source, rel.target]) {
152
+ if (ownIds.has(endpoint))
153
+ continue;
154
+ for (const id of [endpoint, ...properAncestorIds(endpoint)]) {
155
+ if (byId.has(id) && !ownIds.has(id))
156
+ partnerIds.add(id);
157
+ }
158
+ }
159
+ }
160
+ // Project order for both lists, never the order ids happened to be
161
+ // discovered in: the elements of a service are printed, counted and diffed,
162
+ // and a set that reorders itself between two runs of the same command is a
163
+ // diff nobody can read.
164
+ const archElementIds = new Set(input.architecture.elements.map((e) => e.id));
165
+ return {
166
+ elements: input.project.elements.filter((e) => ownIds.has(e.id) || partnerIds.has(e.id)),
167
+ relationships,
168
+ unowned: input.project.elements.filter((e) => !archElementIds.has(e.id) && !ownIds.has(e.id)),
169
+ };
170
+ }
171
+ /**
172
+ * The nearest explicit `metadata { service '…' }` binding at or above an id, or
173
+ * undefined when nothing on the chain declares one.
174
+ *
175
+ * `serviceResolver`'s first rung, asked on its own — because the caller above
176
+ * needs to tell that rung's answer apart from the two guesses below it, and the
177
+ * resolver returns one `DeclaredService` for all three. A binding is a claim
178
+ * somebody wrote down; a title match is evidence, and a bare title is a guess.
179
+ */
180
+ function boundServiceOf(id, byId) {
181
+ for (const candidate of [id, ...properAncestorIds(id)]) {
182
+ const bound = byId.get(candidate)?.service;
183
+ if (bound !== undefined)
184
+ return bound;
185
+ }
186
+ return undefined;
187
+ }
188
+ //# sourceMappingURL=slice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slice.js","sourceRoot":"","sources":["../../../../src/core/c4/service-model/slice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAEtE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAuBxD;;;;;;;;;;;;GAYG;AACH,SAAS,MAAM,CAAC,GAAQ;IACtB,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,GAAG,CAAC,MAAM;QACV,GAAG,CAAC,MAAM;QACV,GAAG,CAAC,KAAK,IAAI,EAAE;QACf,GAAG,CAAC,EAAE,IAAI,EAAE;QACZ,GAAG,CAAC,SAAS,IAAI,EAAE;QACnB,GAAG,CAAC,QAAQ,IAAI,EAAE;QAClB,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE;KACrB,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,MAAM,UAAU,eAAe,CAAC,KAAmB;IACjD,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACrE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,IAAI,GAAG,CACpB,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACvF,CAAC;IACF,6EAA6E;IAC7E,8EAA8E;IAC9E,wDAAwD;IACxD,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9F,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC7C,IAAI,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAAE,SAAS;QACrC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;YAAE,SAAS;QACvE,6EAA6E;QAC7E,0EAA0E;QAC1E,2EAA2E;QAC3E,kCAAkC;QAClC,IAAI,cAAc,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,SAAS;YAAE,SAAS;QAC7D,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACzB,CAAC;IAED,4EAA4E;IAC5E,kEAAkE;IAClE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC;QACnD,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,MAAM,aAAa,GAAG,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE;QAC/D,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC5B,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;QACxB,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAChC,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAChD,IAAI,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACnC,KAAK,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;gBAC5D,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;oBAAE,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;IACH,CAAC;IAED,mEAAmE;IACnE,4EAA4E;IAC5E,2EAA2E;IAC3E,wBAAwB;IACxB,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7E,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACxF,aAAa;QACb,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;KAC9F,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,cAAc,CAAC,EAAU,EAAE,IAA+B;IACjE,KAAK,MAAM,SAAS,IAAI,CAAC,EAAE,EAAE,GAAG,iBAAiB,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QACvD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAC3C,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;IACxC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["/**\n * The service's OWN part of a per-service project — derived, because LikeC4\n * does not record it.\n *\n * Measured at the 1.59.2 pin: `$data.views[id]` carries a `sourcePath` and\n * `$data.elements[fqn]` carries NO source-document field at all. So after the\n * map and one extending model have been parsed together there is nothing in the\n * parse output that says which of the two files declared a given element. The\n * answer has to be reconstructed, and this module is the reconstruction: DIFF\n * the project against the architecture-alone load, and resolve what is left\n * through the same element→service resolver every fleet check already joins on.\n *\n * Why it matters that this is exact rather than approximate: every model-\n * dependent check reads the slice as if it were the standalone file it used to\n * read — `c4.no-relationships`, `health.dependency-unmodelled`, `Covers:`\n * resolution, the attested-call scan. Too WIDE and a service is graded against\n * the whole fleet map (every service would suddenly have relationships, and\n * every dependency would resolve). Too NARROW and a service that models its\n * partners correctly is told it models nothing.\n */\nimport { properAncestorIds } from \"../../kernel/ids/fqn/ancestors.js\";\nimport type { Elem, LoadedDoc, Rel } from \"../likec4.js\";\nimport { serviceResolver } from \"../resolve/service.js\";\n\n/** What one service owns inside a project that also holds the fleet map. */\nexport interface OwnSlice {\n /** The service's own elements, plus the partner elements its own edges point at. */\n elements: Elem[];\n /** Only the relationships this model added — the project's, minus the map's. */\n relationships: Rel[];\n /** Elements this model added OUTSIDE its own element: the `c4.element-unowned` set. */\n unowned: Elem[];\n}\n\nexport interface SliceRequest {\n /** The per-service project: the architecture documents and this model, parsed together. */\n project: LoadedDoc;\n /** The same architecture documents parsed WITHOUT the model — the diff's other half. */\n architecture: LoadedDoc;\n /** The `services/<…>/<id>` directory name this model belongs to. */\n service: string;\n /** Every service directory that exists, so an edge into a modelled container resolves to its owner. */\n known: ReadonlySet<string>;\n}\n\n/**\n * A relationship's identity for the DIFF, and every field of it is one loam\n * already reads somewhere.\n *\n * There is no id to join on: LikeC4 mints relationship ids per parse, so the\n * same edge in two loads of overlapping document sets is two different ids.\n * Endpoints alone are not enough either — a fleet may draw two edges between\n * the same pair (`api -> kafka 'publishes'` and `api -> kafka 'consumes'`), and\n * collapsing them would let a model silently inherit one of the map's. So the\n * key is everything a check can branch on: the endpoints, the label, and the\n * three metadata keys plus the tag set, sorted because tag order is authoring\n * order and means nothing.\n */\nfunction relKey(rel: Rel): string {\n return JSON.stringify([\n rel.source,\n rel.target,\n rel.title ?? \"\",\n rel.op ?? \"\",\n rel.publishes ?? \"\",\n rel.consumes ?? \"\",\n [...rel.tags].sort(),\n ]);\n}\n\n/**\n * The service's own slice of a per-service project.\n *\n * OWN is resolution PLUS NESTING, and the second half is not a widening for\n * convenience — it is the only thing that keeps a model's own file from being\n * filtered against it.\n *\n * Resolution first: every project element whose id resolves to this service,\n * whether the model added it or the map already declared it. The bound element\n * itself has to be in — a standalone model declared it, and every check that\n * reads \"the service's elements\" expects it.\n *\n * Then nesting, because resolution alone ejects a child the author wrote INSIDE\n * their own `extend` block. `serviceResolver`'s second rung answers with the\n * nearest ancestor whose TITLE names a real `services/<id>/` directory, and that\n * rung sees the child before it sees the parent: a container written as\n * `store = database 'db'` under `extend svcA { … }` resolves to a service called\n * `db` the moment some other team has adopted one, and svcA's own element is\n * bound by title only. The model then reported one element fewer than it\n * declares, a `Covers: svcA.store` line was convicted as a typo, and\n * `c4.element-unowned` told the author their own container belonged in somebody\n * else's model. A standalone model was never filtered against its own file at\n * all, so every one of those is a regression the second shape introduced.\n *\n * So: an OWN ROOT is an own element none of whose proper ancestors is own — the\n * element the author actually extended — and everything nested under one is own\n * too. Id nesting under the extended element is evidence nobody had to guess at,\n * and it outranks a title guess. It does NOT outrank an explicit\n * `metadata { service '…' }` binding, which is a claim somebody wrote down: a\n * map that nests one service's element inside another's keeps both on the right\n * side of the line.\n *\n * OWN RELATIONSHIPS are the diff: the project's edges minus the ones the\n * architecture documents already have. Resolution cannot decide this one, and\n * the reason is the whole point of the fleet map: the map is where cross-service\n * edges live, so `checkoutWeb -> paymentService` resolves to this service at one\n * end and must NOT become part of the model's own relationships — otherwise\n * every service would be graded as declaring calls it never wrote, and\n * `landscape.service-isolated` (which asks whether the map's edges and the\n * model's agree) would be answering about itself.\n *\n * The diff is a MULTISET difference, not a set one, and that distinction is the\n * whole grade for a model that agrees with the map. A model may legitimately\n * draw an edge the map also draws, spelled identically —\n * `marketplace.paymentService -> stripe 'Authorizes cards'` — and under a set\n * difference every such edge would vanish from the model's own list. Then\n * `attestedCalls` finds no attested call, `landscape.service-isolated` reports a\n * service that talks to nobody, the adopt brief's `landscape.attested` list\n * comes back empty and `c4.no-relationships` fires on a model full of edges. So\n * each structural key drops `min(architecture, project)` occurrences and the\n * rest are the model's: one copy in the map cancels exactly one copy here.\n *\n * PARTNERS are the endpoints of the model's own edges that lie outside the\n * service, AND every ancestor of such an endpoint that the project holds. The\n * ancestors are not tidiness: a standalone model had to declare `kafka` in order\n * to draw an edge at `kafka.orderEvents`, and health.yaml names the dependency\n * `kafka`. Dropping the ancestor would turn a fleet's dependency declarations\n * into `health.dependency-unmodelled` findings the day it migrated its models,\n * with nothing in the docs having changed.\n *\n * UNOWNED is what the model added outside its own element — present in the\n * project, absent from the architecture load, and resolving to somebody else. A\n * new top-level element (`ledger` beside the map's systems) and a child added\n * under another service's element both land here, and both are things the map\n * or the other service should be declaring instead.\n */\nexport function sliceForService(input: SliceRequest): OwnSlice {\n const resolve = serviceResolver(input.project.elements, input.known);\n const byId = new Map(input.project.elements.map((e) => [e.id, e]));\n const ownIds = new Set(\n input.project.elements.filter((e) => resolve(e.id) === input.service).map((e) => e.id),\n );\n // The elements the author extended: own, with nothing own above them. Frozen\n // before the loop below adds anything, so a child pulled in by nesting cannot\n // itself become a root and widen the set a second time.\n const roots = [...ownIds].filter((id) => !properAncestorIds(id).some((up) => ownIds.has(up)));\n for (const element of input.project.elements) {\n if (ownIds.has(element.id)) continue;\n if (!roots.some((root) => element.id.startsWith(`${root}.`))) continue;\n // The one thing nesting does not outrank. An explicit binding anywhere at or\n // above this element names its service outright, and a map that files one\n // service's element under another's would otherwise hand the outer service\n // every element of the inner one.\n if (boundServiceOf(element.id, byId) !== undefined) continue;\n ownIds.add(element.id);\n }\n\n // One budget per structural key: the map's copies cancel the project's, one\n // for one, and what the budget does not cover is the model's own.\n const budget = new Map<string, number>();\n for (const rel of input.architecture.relationships) {\n const k = relKey(rel);\n budget.set(k, (budget.get(k) ?? 0) + 1);\n }\n const relationships = input.project.relationships.filter((rel) => {\n const k = relKey(rel);\n const left = budget.get(k) ?? 0;\n if (left === 0) return true;\n budget.set(k, left - 1);\n return false;\n });\n\n const partnerIds = new Set<string>();\n for (const rel of relationships) {\n for (const endpoint of [rel.source, rel.target]) {\n if (ownIds.has(endpoint)) continue;\n for (const id of [endpoint, ...properAncestorIds(endpoint)]) {\n if (byId.has(id) && !ownIds.has(id)) partnerIds.add(id);\n }\n }\n }\n\n // Project order for both lists, never the order ids happened to be\n // discovered in: the elements of a service are printed, counted and diffed,\n // and a set that reorders itself between two runs of the same command is a\n // diff nobody can read.\n const archElementIds = new Set(input.architecture.elements.map((e) => e.id));\n return {\n elements: input.project.elements.filter((e) => ownIds.has(e.id) || partnerIds.has(e.id)),\n relationships,\n unowned: input.project.elements.filter((e) => !archElementIds.has(e.id) && !ownIds.has(e.id)),\n };\n}\n\n/**\n * The nearest explicit `metadata { service '…' }` binding at or above an id, or\n * undefined when nothing on the chain declares one.\n *\n * `serviceResolver`'s first rung, asked on its own — because the caller above\n * needs to tell that rung's answer apart from the two guesses below it, and the\n * resolver returns one `DeclaredService` for all three. A binding is a claim\n * somebody wrote down; a title match is evidence, and a bare title is a guess.\n */\nfunction boundServiceOf(id: string, byId: ReadonlyMap<string, Elem>): string | undefined {\n for (const candidate of [id, ...properAncestorIds(id)]) {\n const bound = byId.get(candidate)?.service;\n if (bound !== undefined) return bound;\n }\n return undefined;\n}\n"]}
@@ -109,6 +109,29 @@ export function scanModel(text) {
109
109
  }
110
110
  const head = code.slice(stmtStart, headEnd);
111
111
  const parentId = stack.length > 0 ? stack[stack.length - 1].id : "";
112
+ // `extend <fqn> { … }` — the extending model's one construct, and the only
113
+ // frame here whose id is not minted from the enclosing scope: it names an
114
+ // element the fleet map already declares, so its children are `<fqn>.<name>`
115
+ // and a reference inside it resolves against that scope. Without this the
116
+ // block fell through to the property-line arm and everything in it was
117
+ // invisible, which is why the archive spliced a service's interior into the
118
+ // map instead (verification 2026-09-04, E1).
119
+ const extended = opener === -1 ? null : /^extend\s+([A-Za-z_][\w.-]*)\s*$/.exec(head);
120
+ if (extended !== null) {
121
+ const id = parentId === "" ? extended[1] : `${parentId}.${extended[1]}`;
122
+ const el = {
123
+ id,
124
+ start: stmtStart,
125
+ end: headEnd,
126
+ bodyOpen: opener,
127
+ bodyClose: -1,
128
+ indent: indentOf(stmtStart),
129
+ extend: true,
130
+ };
131
+ stack.push({ id, el });
132
+ i = opener + 1;
133
+ continue;
134
+ }
112
135
  // Classify: `name = kind` first, then a relationship (the arrow cannot hide
113
136
  // in a string — the head is masked), then the `kind name` form, else a
114
137
  // property/tag line that is consumed whole, nested block included.
@@ -133,6 +156,9 @@ export function scanModel(text) {
133
156
  bodyClose: -1,
134
157
  indent: indentOf(stmtStart),
135
158
  };
159
+ const titled = literalIn(stmtStart, headEnd)?.value;
160
+ if (titled !== undefined)
161
+ el.title = titled;
136
162
  if (opener !== -1) {
137
163
  stack.push({ id, el });
138
164
  i = opener + 1;
@@ -1 +1 @@
1
- {"version":3,"file":"source-scan.js","sourceRoot":"","sources":["../../../src/core/c4/source-scan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The text-level view of a LikeC4 document: which bytes each declaration owns.\n *\n * loam's archive merges the landscape by SPLICING authored text, not by\n * re-rendering a parsed model — a re-render would reformat every line somebody\n * wrote. So the merge needs both views of the same file: the parsed one\n * (`likec4.ts`) for what the model MEANS, and this one for where each\n * declaration physically is. A key only one of them reads is a key the merge\n * silently drops, which is why `metadata` handling is spelled out in both.\n */\nimport { maskSource, matchBrace, type SourceLiteral } from \"./source-mask.js\";\n\nexport interface ScannedElement {\n /** Full dotted id — `parent.child` for a nested declaration. */\n id: string;\n /** Span of the whole declaration: first head character to one past its end. */\n start: number;\n end: number;\n /** Its own body braces; -1 when the declaration has no body. */\n bodyOpen: number;\n bodyClose: number;\n /** Leading whitespace of the declaration's first line. */\n indent: string;\n}\n\n/** A relationship statement located in source, endpoints resolved to full ids (best effort). */\nexport interface ScannedRel {\n source: string;\n target: string;\n title?: string;\n op?: string;\n /** `metadata { publishes '...' }` — see `Rel.publishes`. */\n publishes?: string;\n /** `metadata { consumes '...' }` — see `Rel.consumes`. */\n consumes?: string;\n tags: string[];\n start: number;\n end: number;\n indent: string;\n}\n\nexport interface ScannedModel {\n /** The `model` block's own braces. */\n open: number;\n close: number;\n /** In document order. A nested declaration's span lies inside its parent's. */\n elements: ScannedElement[];\n rels: ScannedRel[];\n}\n\n/**\n * Locate every element and relationship declaration inside the top-level\n * `model { ... }` block of a LikeC4 source, with the byte range each one\n * occupies. This is the splice map `loam archive` merges the landscape with:\n * the parsed model says WHAT exists, this says WHERE its authored text is —\n * verbatim, so nothing the parser does not model (technology, style, links)\n * can be lost by rebuilding it.\n *\n * Both declaration forms are recognised — `name = kind ...` always, and\n * `kind name ...` for any kind the document's own `specification` block\n * declares. Relationship endpoints are resolved the way LikeC4 reads them:\n * innermost enclosing scope outward, then the top level, then a unique leaf\n * name anywhere; `it`/`this` (and an omitted source) mean the enclosing\n * element. A name that resolves nowhere is left as written — the caller's\n * match against the parsed model then fails loudly instead of guessing.\n *\n * Returns null when there is no model block. Callers only hand in documents\n * LikeC4 has already parsed, so the scan can trust balanced braces.\n */\nexport function scanModel(text: string): ScannedModel | null {\n const { code, literals } = maskSource(text);\n const m = /\\bmodel\\s*\\{/.exec(code);\n if (!m) return null;\n const open = m.index + m[0].length - 1;\n const close = matchBrace(code, open);\n if (close === -1) return null;\n\n // Element kinds from the specification block(s): the `<kind> <name>`\n // declaration form is recognisable only by its first token being a kind.\n const kinds = new Set<string>();\n for (const sm of code.matchAll(/\\bspecification\\s*\\{/g)) {\n const sClose = matchBrace(code, sm.index + sm[0].length - 1);\n if (sClose === -1) continue;\n for (const em of code.slice(sm.index, sClose).matchAll(/\\belement\\s+([A-Za-z_][\\w-]*)/g)) {\n kinds.add(em[1]!);\n }\n }\n\n const indentOf = (at: number): string => {\n const nl = text.lastIndexOf(\"\\n\", at - 1);\n const ws = text.slice(nl + 1, at);\n return /^[ \\t]*$/.test(ws) ? ws : \"\";\n };\n const literalIn = (from: number, to: number): SourceLiteral | undefined =>\n literals.find((l) => l.start >= from && l.end <= to);\n const keyedLiteral = (key: string, from: number, to: number): string | undefined => {\n if (from === -1) return undefined;\n const re = new RegExp(`\\\\b${key}\\\\b`, \"g\");\n re.lastIndex = from;\n const km = re.exec(code);\n if (km === null || km.index >= to) return undefined;\n return literalIn(km.index, to)?.value;\n };\n const trimmedEnd = (from: number, to: number): number => {\n let e = to;\n while (e > from && /\\s/.test(text[e - 1]!)) e -= 1;\n return e;\n };\n\n const elements: ScannedElement[] = [];\n interface RawRel {\n srcLit?: string;\n tgtLit: string;\n parent: string;\n start: number;\n end: number;\n indent: string;\n title?: string;\n op?: string;\n publishes?: string;\n consumes?: string;\n tags: string[];\n }\n const raw: RawRel[] = [];\n const stack: Array<{ id: string; el: ScannedElement }> = [];\n\n let i = open + 1;\n while (i < close) {\n const ch = code[i]!;\n if (/\\s/.test(ch)) {\n i += 1;\n continue;\n }\n if (ch === \"}\") {\n const frame = stack.pop();\n if (frame !== undefined) {\n frame.el.bodyClose = i;\n frame.el.end = i + 1;\n elements.push(frame.el);\n }\n i += 1;\n continue;\n }\n\n // A statement's head: up to its own `{`, the end of the line, or the\n // enclosing block's `}` on the same line — whichever comes first.\n const stmtStart = i;\n let headEnd = close;\n let opener = -1;\n for (let j = i; j < close; j += 1) {\n const c = code[j]!;\n if (c === \"\\n\" || c === \"}\") {\n headEnd = j;\n break;\n }\n if (c === \"{\") {\n opener = j;\n headEnd = j;\n break;\n }\n }\n const head = code.slice(stmtStart, headEnd);\n const parentId = stack.length > 0 ? stack[stack.length - 1]!.id : \"\";\n\n // Classify: `name = kind` first, then a relationship (the arrow cannot hide\n // in a string — the head is masked), then the `kind name` form, else a\n // property/tag line that is consumed whole, nested block included.\n let localName: string | null = null;\n const assign = /^([A-Za-z_]\\w*)\\s*=\\s*[A-Za-z_]/.exec(head);\n if (assign !== null) {\n localName = assign[1]!;\n } else if (!head.includes(\"->\")) {\n const toks = head.trim().split(/\\s+/);\n if (toks.length >= 2 && kinds.has(toks[0]!) && /^[A-Za-z_]\\w*$/.test(toks[1]!)) {\n localName = toks[1]!;\n }\n }\n\n if (localName !== null) {\n const id = parentId === \"\" ? localName : `${parentId}.${localName}`;\n const el: ScannedElement = {\n id,\n start: stmtStart,\n end: headEnd,\n bodyOpen: opener,\n bodyClose: -1,\n indent: indentOf(stmtStart),\n };\n if (opener !== -1) {\n stack.push({ id, el });\n i = opener + 1;\n } else {\n el.end = trimmedEnd(stmtStart, headEnd);\n elements.push(el);\n i = headEnd;\n }\n continue;\n }\n\n if (head.includes(\"->\")) {\n const bClose = opener === -1 ? -1 : matchBrace(code, opener);\n const stmtEnd = opener === -1 ? trimmedEnd(stmtStart, headEnd) : bClose === -1 ? close : bClose + 1;\n const am = /^([A-Za-z_][\\w.]*)?\\s*(?:-\\[[^\\]]*\\])?->\\s*([A-Za-z_][\\w.]*)/.exec(head);\n if (am !== null) {\n const arrowEnd = stmtStart + am[0].length;\n const bodyFrom = opener === -1 ? -1 : opener + 1;\n const bodyTo = opener === -1 ? -1 : stmtEnd - 1;\n const title = literalIn(arrowEnd, headEnd)?.value ?? keyedLiteral(\"title\", bodyFrom, bodyTo);\n const tags: string[] = [];\n for (const tm of code.slice(stmtStart, stmtEnd).matchAll(/#([\\w-]+)/g)) tags.push(tm[1]!);\n raw.push({\n srcLit: am[1],\n tgtLit: am[2]!,\n parent: parentId,\n start: stmtStart,\n end: stmtEnd,\n indent: indentOf(stmtStart),\n title,\n op: keyedLiteral(\"op\", bodyFrom, bodyTo),\n publishes: keyedLiteral(\"publishes\", bodyFrom, bodyTo),\n consumes: keyedLiteral(\"consumes\", bodyFrom, bodyTo),\n tags,\n });\n }\n i = Math.max(stmtEnd, headEnd);\n continue;\n }\n\n if (opener !== -1) {\n const bClose = matchBrace(code, opener);\n i = bClose === -1 ? close : bClose + 1;\n } else {\n i = headEnd;\n }\n }\n\n // Resolve relationship endpoints against everything declared anywhere in the\n // model — LikeC4 resolution is not order-dependent, so neither is this.\n const ids = new Set(elements.map((e) => e.id));\n const byLeaf = new Map<string, string[]>();\n for (const e of elements) {\n const leaf = e.id.slice(e.id.lastIndexOf(\".\") + 1);\n byLeaf.set(leaf, [...(byLeaf.get(leaf) ?? []), e.id]);\n }\n const resolve = (lit: string | undefined, enclosing: string): string => {\n if (lit === undefined || lit === \"it\" || lit === \"this\") return enclosing;\n const segs = lit.split(\".\");\n let base: string | undefined;\n for (let scope = enclosing; scope !== \"\" && base === undefined; ) {\n if (ids.has(`${scope}.${segs[0]!}`)) base = `${scope}.${segs[0]!}`;\n const dot = scope.lastIndexOf(\".\");\n scope = dot === -1 ? \"\" : scope.slice(0, dot);\n }\n if (base === undefined && ids.has(segs[0]!)) base = segs[0]!;\n if (base === undefined) {\n const leafed = byLeaf.get(segs[0]!);\n if (leafed !== undefined && leafed.length === 1) base = leafed[0]!;\n }\n return [base ?? segs[0]!, ...segs.slice(1)].join(\".\");\n };\n\n const rels: ScannedRel[] = raw.map((r) => ({\n source: resolve(r.srcLit, r.parent),\n target: resolve(r.tgtLit, r.parent),\n title: r.title,\n op: r.op,\n publishes: r.publishes,\n consumes: r.consumes,\n tags: r.tags,\n start: r.start,\n end: r.end,\n indent: r.indent,\n }));\n\n elements.sort((a, b) => a.start - b.start);\n return { open, close, elements, rels };\n}\n"]}
1
+ {"version":3,"file":"source-scan.js","sourceRoot":"","sources":["../../../src/core/c4/source-scan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The text-level view of a LikeC4 document: which bytes each declaration owns.\n *\n * loam's archive merges the landscape by SPLICING authored text, not by\n * re-rendering a parsed model — a re-render would reformat every line somebody\n * wrote. So the merge needs both views of the same file: the parsed one\n * (`likec4.ts`) for what the model MEANS, and this one for where each\n * declaration physically is. A key only one of them reads is a key the merge\n * silently drops, which is why `metadata` handling is spelled out in both.\n */\nimport { maskSource, matchBrace, type SourceLiteral } from \"./source-mask.js\";\n\nexport interface ScannedElement {\n /** Full dotted id — `parent.child` for a nested declaration. */\n id: string;\n /** Span of the whole declaration: first head character to one past its end. */\n start: number;\n end: number;\n /** Its own body braces; -1 when the declaration has no body. */\n bodyOpen: number;\n bodyClose: number;\n /** Leading whitespace of the declaration's first line. */\n indent: string;\n /**\n * The title the declaration's HEAD names — the first string literal before\n * its body, which is where both declaration forms write it (pinned against\n * the parser by `test/likec4-scan-parity.test.ts`).\n *\n * Absent when the head names none: LikeC4 then titles the element after its\n * own name, and a default title is one the archive's title join must not\n * match on — `splice/identity/declared.ts` says why, and why losing it costs\n * that join nothing.\n */\n title?: string;\n /**\n * True for an `extend <fqn> { … }` frame — a block that DECLARES nothing and\n * reopens an element another document owns. Its id is the fqn as written, and\n * its children hang off that, which is what lets the archive splice a\n * service's interior into the model that owns it. Placement must skip it: it\n * is not a top-level declaration of this document, so anchoring beside it\n * would group additions around a box that lives somewhere else.\n */\n extend?: true;\n}\n\n/** A relationship statement located in source, endpoints resolved to full ids (best effort). */\nexport interface ScannedRel {\n source: string;\n target: string;\n title?: string;\n op?: string;\n /** `metadata { publishes '...' }` — see `Rel.publishes`. */\n publishes?: string;\n /** `metadata { consumes '...' }` — see `Rel.consumes`. */\n consumes?: string;\n tags: string[];\n start: number;\n end: number;\n indent: string;\n}\n\nexport interface ScannedModel {\n /** The `model` block's own braces. */\n open: number;\n close: number;\n /** In document order. A nested declaration's span lies inside its parent's. */\n elements: ScannedElement[];\n rels: ScannedRel[];\n}\n\n/**\n * Locate every element and relationship declaration inside the top-level\n * `model { ... }` block of a LikeC4 source, with the byte range each one\n * occupies. This is the splice map `loam archive` merges the landscape with:\n * the parsed model says WHAT exists, this says WHERE its authored text is —\n * verbatim, so nothing the parser does not model (technology, style, links)\n * can be lost by rebuilding it.\n *\n * Both declaration forms are recognised — `name = kind ...` always, and\n * `kind name ...` for any kind the document's own `specification` block\n * declares. Relationship endpoints are resolved the way LikeC4 reads them:\n * innermost enclosing scope outward, then the top level, then a unique leaf\n * name anywhere; `it`/`this` (and an omitted source) mean the enclosing\n * element. A name that resolves nowhere is left as written — the caller's\n * match against the parsed model then fails loudly instead of guessing.\n *\n * Returns null when there is no model block. Callers only hand in documents\n * LikeC4 has already parsed, so the scan can trust balanced braces.\n */\nexport function scanModel(text: string): ScannedModel | null {\n const { code, literals } = maskSource(text);\n const m = /\\bmodel\\s*\\{/.exec(code);\n if (!m) return null;\n const open = m.index + m[0].length - 1;\n const close = matchBrace(code, open);\n if (close === -1) return null;\n\n // Element kinds from the specification block(s): the `<kind> <name>`\n // declaration form is recognisable only by its first token being a kind.\n const kinds = new Set<string>();\n for (const sm of code.matchAll(/\\bspecification\\s*\\{/g)) {\n const sClose = matchBrace(code, sm.index + sm[0].length - 1);\n if (sClose === -1) continue;\n for (const em of code.slice(sm.index, sClose).matchAll(/\\belement\\s+([A-Za-z_][\\w-]*)/g)) {\n kinds.add(em[1]!);\n }\n }\n\n const indentOf = (at: number): string => {\n const nl = text.lastIndexOf(\"\\n\", at - 1);\n const ws = text.slice(nl + 1, at);\n return /^[ \\t]*$/.test(ws) ? ws : \"\";\n };\n const literalIn = (from: number, to: number): SourceLiteral | undefined =>\n literals.find((l) => l.start >= from && l.end <= to);\n const keyedLiteral = (key: string, from: number, to: number): string | undefined => {\n if (from === -1) return undefined;\n const re = new RegExp(`\\\\b${key}\\\\b`, \"g\");\n re.lastIndex = from;\n const km = re.exec(code);\n if (km === null || km.index >= to) return undefined;\n return literalIn(km.index, to)?.value;\n };\n const trimmedEnd = (from: number, to: number): number => {\n let e = to;\n while (e > from && /\\s/.test(text[e - 1]!)) e -= 1;\n return e;\n };\n\n const elements: ScannedElement[] = [];\n interface RawRel {\n srcLit?: string;\n tgtLit: string;\n parent: string;\n start: number;\n end: number;\n indent: string;\n title?: string;\n op?: string;\n publishes?: string;\n consumes?: string;\n tags: string[];\n }\n const raw: RawRel[] = [];\n const stack: Array<{ id: string; el: ScannedElement }> = [];\n\n let i = open + 1;\n while (i < close) {\n const ch = code[i]!;\n if (/\\s/.test(ch)) {\n i += 1;\n continue;\n }\n if (ch === \"}\") {\n const frame = stack.pop();\n if (frame !== undefined) {\n frame.el.bodyClose = i;\n frame.el.end = i + 1;\n elements.push(frame.el);\n }\n i += 1;\n continue;\n }\n\n // A statement's head: up to its own `{`, the end of the line, or the\n // enclosing block's `}` on the same line — whichever comes first.\n const stmtStart = i;\n let headEnd = close;\n let opener = -1;\n for (let j = i; j < close; j += 1) {\n const c = code[j]!;\n if (c === \"\\n\" || c === \"}\") {\n headEnd = j;\n break;\n }\n if (c === \"{\") {\n opener = j;\n headEnd = j;\n break;\n }\n }\n const head = code.slice(stmtStart, headEnd);\n const parentId = stack.length > 0 ? stack[stack.length - 1]!.id : \"\";\n\n // `extend <fqn> { … }` — the extending model's one construct, and the only\n // frame here whose id is not minted from the enclosing scope: it names an\n // element the fleet map already declares, so its children are `<fqn>.<name>`\n // and a reference inside it resolves against that scope. Without this the\n // block fell through to the property-line arm and everything in it was\n // invisible, which is why the archive spliced a service's interior into the\n // map instead (verification 2026-09-04, E1).\n const extended = opener === -1 ? null : /^extend\\s+([A-Za-z_][\\w.-]*)\\s*$/.exec(head);\n if (extended !== null) {\n const id = parentId === \"\" ? extended[1]! : `${parentId}.${extended[1]!}`;\n const el: ScannedElement = {\n id,\n start: stmtStart,\n end: headEnd,\n bodyOpen: opener,\n bodyClose: -1,\n indent: indentOf(stmtStart),\n extend: true,\n };\n stack.push({ id, el });\n i = opener + 1;\n continue;\n }\n\n // Classify: `name = kind` first, then a relationship (the arrow cannot hide\n // in a string — the head is masked), then the `kind name` form, else a\n // property/tag line that is consumed whole, nested block included.\n let localName: string | null = null;\n const assign = /^([A-Za-z_]\\w*)\\s*=\\s*[A-Za-z_]/.exec(head);\n if (assign !== null) {\n localName = assign[1]!;\n } else if (!head.includes(\"->\")) {\n const toks = head.trim().split(/\\s+/);\n if (toks.length >= 2 && kinds.has(toks[0]!) && /^[A-Za-z_]\\w*$/.test(toks[1]!)) {\n localName = toks[1]!;\n }\n }\n\n if (localName !== null) {\n const id = parentId === \"\" ? localName : `${parentId}.${localName}`;\n const el: ScannedElement = {\n id,\n start: stmtStart,\n end: headEnd,\n bodyOpen: opener,\n bodyClose: -1,\n indent: indentOf(stmtStart),\n };\n const titled = literalIn(stmtStart, headEnd)?.value;\n if (titled !== undefined) el.title = titled;\n if (opener !== -1) {\n stack.push({ id, el });\n i = opener + 1;\n } else {\n el.end = trimmedEnd(stmtStart, headEnd);\n elements.push(el);\n i = headEnd;\n }\n continue;\n }\n\n if (head.includes(\"->\")) {\n const bClose = opener === -1 ? -1 : matchBrace(code, opener);\n const stmtEnd = opener === -1 ? trimmedEnd(stmtStart, headEnd) : bClose === -1 ? close : bClose + 1;\n const am = /^([A-Za-z_][\\w.]*)?\\s*(?:-\\[[^\\]]*\\])?->\\s*([A-Za-z_][\\w.]*)/.exec(head);\n if (am !== null) {\n const arrowEnd = stmtStart + am[0].length;\n const bodyFrom = opener === -1 ? -1 : opener + 1;\n const bodyTo = opener === -1 ? -1 : stmtEnd - 1;\n const title = literalIn(arrowEnd, headEnd)?.value ?? keyedLiteral(\"title\", bodyFrom, bodyTo);\n const tags: string[] = [];\n for (const tm of code.slice(stmtStart, stmtEnd).matchAll(/#([\\w-]+)/g)) tags.push(tm[1]!);\n raw.push({\n srcLit: am[1],\n tgtLit: am[2]!,\n parent: parentId,\n start: stmtStart,\n end: stmtEnd,\n indent: indentOf(stmtStart),\n title,\n op: keyedLiteral(\"op\", bodyFrom, bodyTo),\n publishes: keyedLiteral(\"publishes\", bodyFrom, bodyTo),\n consumes: keyedLiteral(\"consumes\", bodyFrom, bodyTo),\n tags,\n });\n }\n i = Math.max(stmtEnd, headEnd);\n continue;\n }\n\n if (opener !== -1) {\n const bClose = matchBrace(code, opener);\n i = bClose === -1 ? close : bClose + 1;\n } else {\n i = headEnd;\n }\n }\n\n // Resolve relationship endpoints against everything declared anywhere in the\n // model — LikeC4 resolution is not order-dependent, so neither is this.\n const ids = new Set(elements.map((e) => e.id));\n const byLeaf = new Map<string, string[]>();\n for (const e of elements) {\n const leaf = e.id.slice(e.id.lastIndexOf(\".\") + 1);\n byLeaf.set(leaf, [...(byLeaf.get(leaf) ?? []), e.id]);\n }\n const resolve = (lit: string | undefined, enclosing: string): string => {\n if (lit === undefined || lit === \"it\" || lit === \"this\") return enclosing;\n const segs = lit.split(\".\");\n let base: string | undefined;\n for (let scope = enclosing; scope !== \"\" && base === undefined; ) {\n if (ids.has(`${scope}.${segs[0]!}`)) base = `${scope}.${segs[0]!}`;\n const dot = scope.lastIndexOf(\".\");\n scope = dot === -1 ? \"\" : scope.slice(0, dot);\n }\n if (base === undefined && ids.has(segs[0]!)) base = segs[0]!;\n if (base === undefined) {\n const leafed = byLeaf.get(segs[0]!);\n if (leafed !== undefined && leafed.length === 1) base = leafed[0]!;\n }\n return [base ?? segs[0]!, ...segs.slice(1)].join(\".\");\n };\n\n const rels: ScannedRel[] = raw.map((r) => ({\n source: resolve(r.srcLit, r.parent),\n target: resolve(r.tgtLit, r.parent),\n title: r.title,\n op: r.op,\n publishes: r.publishes,\n consumes: r.consumes,\n tags: r.tags,\n start: r.start,\n end: r.end,\n indent: r.indent,\n }));\n\n elements.sort((a, b) => a.start - b.start);\n return { open, close, elements, rels };\n}\n"]}
@@ -1,20 +1,59 @@
1
1
  import { maskSource, matchBrace } from "../source-mask.js";
2
+ export function spliceSource(deltaText, decl, carry) {
3
+ const { featureId, indent: targetIndent, omit = [] } = carry;
4
+ const block = deltaText.slice(decl.start, decl.end);
5
+ const cuts = omit
6
+ .filter((r) => r.start >= decl.start && r.end <= decl.end)
7
+ .map((r) => wholeLines(block, r.start - decl.start, r.end - decl.start));
8
+ return reindent(stripFeatureTag(block, featureId, cuts), decl.indent, targetIndent);
9
+ }
2
10
  /**
3
- * A declaration's authored source, ready to land in the living landscape:
4
- * byte-verbatim except that the feature's own tag is stripped and the
5
- * indentation is rebased from where the block sat in the delta to where it
6
- * lands. A construct the strip leaves empty goes too — a line that held only
7
- * the tag disappears whole, and `x = kind 'y' { #FEAT-1 }` lands as
8
- * `x = kind 'y'`, not as a pair of empty braces.
11
+ * Widen a cut to the whole lines it sits on, when it has them to itself.
12
+ *
13
+ * A scanned declaration's span starts at its first character and ends one past
14
+ * its last brace, so cutting it verbatim would leave the leading indent and the
15
+ * trailing newline behind as a blank line — and the emptied-line rule below only
16
+ * fires on a removal a line contains. A cut sharing its line with other code is
17
+ * left exactly as asked.
9
18
  */
10
- export function spliceSource(deltaText, decl, featureId, targetIndent) {
11
- const block = deltaText.slice(decl.start, decl.end);
12
- return reindent(stripFeatureTag(block, featureId), decl.indent, targetIndent);
19
+ function wholeLines(block, start, end) {
20
+ const lineStart = block.lastIndexOf("\n", start - 1) + 1;
21
+ if (!/^[ \t]*$/.test(block.slice(lineStart, start)))
22
+ return [start, end];
23
+ const nl = block.indexOf("\n", end);
24
+ const to = nl === -1 ? block.length : nl;
25
+ if (!/^[ \t\r]*$/.test(block.slice(end, to)))
26
+ return [start, end];
27
+ return [lineStart, nl === -1 ? block.length : nl + 1];
28
+ }
29
+ /**
30
+ * Rewrite the line endings a merge INSERTED to the ones the document already
31
+ * uses.
32
+ *
33
+ * Splicing composes bytes from two files, and the delta's newlines are not
34
+ * necessarily the living document's: on a repository without `core.autocrlf`
35
+ * normalisation a CRLF landscape or model came back with a handful of bare-LF
36
+ * lines in the spliced region — two conventions in one file, noisy in an editor
37
+ * and in every diff (verification 2026-09-04, W-CRLF). One helper for both
38
+ * merges, because both had it.
39
+ *
40
+ * Only a document with ONE convention is corrected, and then in whichever
41
+ * direction it uses: a file that is already mixed has no convention to match, and
42
+ * guessing one would rewrite lines nobody touched.
43
+ */
44
+ export function matchLineEndings(document, merged) {
45
+ const crlf = (document.match(/\r\n/g) ?? []).length;
46
+ const bare = (document.match(/(?<!\r)\n/g) ?? []).length;
47
+ if (crlf > 0 && bare === 0)
48
+ return merged.replace(/(?<!\r)\n/g, "\r\n");
49
+ if (bare > 0 && crlf === 0)
50
+ return merged.replace(/\r\n/g, "\n");
51
+ return merged;
13
52
  }
14
- function stripFeatureTag(block, featureId) {
53
+ function stripFeatureTag(block, featureId, cuts) {
15
54
  const { code } = maskSource(block);
16
55
  const esc = featureId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
- const removals = [];
56
+ const removals = cuts.map(([s, e]) => [s, e]);
18
57
  for (const m of block.matchAll(new RegExp(`#${esc}(?![\\w-])`, "g"))) {
19
58
  // Only tags in CODE count — a description quoting "#FEAT-1" is content.
20
59
  if (code.slice(m.index, m.index + m[0].length) !== m[0])
@@ -88,13 +127,21 @@ function applyRemovals(text, removals) {
88
127
  * Rebase a block's indentation: the `base` its lines carried in the delta
89
128
  * becomes `target`. The first line arrives with no leading whitespace (the
90
129
  * scanner's span starts at the declaration itself), so it only gains `target`.
130
+ *
131
+ * A CRLF delta's blank line is `"\r"`, not `""`, so splitting on `\n` alone used
132
+ * to hand a non-empty body to the rule below and every blank line inside a
133
+ * spliced block came out carrying the target indent as trailing whitespace. The
134
+ * carriage return is set aside and put back, which leaves the line's own ending
135
+ * exactly as authored for `matchLineEndings` to reconcile.
91
136
  */
92
137
  function reindent(block, base, target) {
93
138
  return block
94
139
  .split("\n")
95
140
  .map((line, k) => {
96
- const body = k === 0 ? line : line.startsWith(base) ? line.slice(base.length) : line;
97
- return body.length === 0 ? "" : target + body;
141
+ const cr = line.endsWith("\r");
142
+ const raw = cr ? line.slice(0, -1) : line;
143
+ const body = k === 0 ? raw : raw.startsWith(base) ? raw.slice(base.length) : raw;
144
+ return (body.length === 0 ? "" : target + body) + (cr ? "\r" : "");
98
145
  })
99
146
  .join("\n");
100
147
  }
@@ -1 +1 @@
1
- {"version":3,"file":"authored-source.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/authored-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { maskSource, matchBrace } from \"../source-mask.js\";\n\n/**\n * A declaration's authored source, ready to land in the living landscape:\n * byte-verbatim except that the feature's own tag is stripped and the\n * indentation is rebased from where the block sat in the delta to where it\n * lands. A construct the strip leaves empty goes too — a line that held only\n * the tag disappears whole, and `x = kind 'y' { #FEAT-1 }` lands as\n * `x = kind 'y'`, not as a pair of empty braces.\n */\nexport function spliceSource(\n deltaText: string,\n decl: { start: number; end: number; indent: string },\n featureId: string,\n targetIndent: string,\n): string {\n const block = deltaText.slice(decl.start, decl.end);\n return reindent(stripFeatureTag(block, featureId), decl.indent, targetIndent);\n}\n\nfunction stripFeatureTag(block: string, featureId: string): string {\n const { code } = maskSource(block);\n const esc = featureId.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const removals: Array<[number, number]> = [];\n for (const m of block.matchAll(new RegExp(`#${esc}(?![\\\\w-])`, \"g\"))) {\n // Only tags in CODE count — a description quoting \"#FEAT-1\" is content.\n if (code.slice(m.index, m.index + m[0].length) !== m[0]) continue;\n let s = m.index;\n let e = s + m[0].length;\n // Take the whitespace on ONE side with the token — trailing first, so\n // `#FEAT-1 #critical` leaves `#critical` at its own indent, and a token at\n // the end of a line does not leave a trailing space behind.\n if (block[e] === \" \" || block[e] === \"\\t\") {\n while (block[e] === \" \" || block[e] === \"\\t\") e += 1;\n } else {\n while (s > 0 && (block[s - 1] === \" \" || block[s - 1] === \"\\t\")) s -= 1;\n }\n removals.push([s, e]);\n }\n if (removals.length === 0) return block;\n\n // A line the strip emptied disappears whole, newline included.\n for (let lineStart = 0; lineStart < block.length; ) {\n const nl = block.indexOf(\"\\n\", lineStart);\n const lineEnd = nl === -1 ? block.length : nl + 1;\n if (\n removals.some(([s, e]) => s >= lineStart && e <= lineEnd) &&\n /^\\s*$/.test(residual(block, lineStart, nl === -1 ? block.length : nl, removals))\n ) {\n removals.push([lineStart, lineEnd]);\n }\n lineStart = lineEnd;\n }\n\n // A body the strip emptied goes too, braces and all — but only one the strip\n // emptied: braces that were authored empty are authored bytes and survive.\n for (let at = code.indexOf(\"{\"); at !== -1; at = code.indexOf(\"{\", at + 1)) {\n const closeAt = matchBrace(code, at);\n if (closeAt === -1) continue;\n if (!removals.some(([s, e]) => s > at && e <= closeAt)) continue;\n if (!/^\\s*$/.test(residual(block, at + 1, closeAt, removals))) continue;\n let s = at;\n while (s > 0 && /\\s/.test(block[s - 1]!)) s -= 1;\n removals.push([s, closeAt + 1]);\n }\n return applyRemovals(block, removals);\n}\n\n/** The text of [from, to) with the removal ranges cut out. */\nfunction residual(text: string, from: number, to: number, removals: Array<[number, number]>): string {\n let out = \"\";\n for (let i = from; i < to; i += 1) {\n if (!removals.some(([s, e]) => i >= s && i < e)) out += text[i];\n }\n return out;\n}\n\nfunction applyRemovals(text: string, removals: Array<[number, number]>): string {\n const sorted = [...removals].sort((a, b) => a[0] - b[0] || b[1] - a[1]);\n let out = \"\";\n let i = 0;\n for (const [s, e] of sorted) {\n if (s > i) out += text.slice(i, s);\n if (e > i) i = e;\n }\n return out + text.slice(i);\n}\n\n/**\n * Rebase a block's indentation: the `base` its lines carried in the delta\n * becomes `target`. The first line arrives with no leading whitespace (the\n * scanner's span starts at the declaration itself), so it only gains `target`.\n */\nfunction reindent(block: string, base: string, target: string): string {\n return block\n .split(\"\\n\")\n .map((line, k) => {\n const body = k === 0 ? line : line.startsWith(base) ? line.slice(base.length) : line;\n return body.length === 0 ? \"\" : target + body;\n })\n .join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"authored-source.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/authored-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { maskSource, matchBrace } from \"../source-mask.js\";\n\n/** A half-open byte range of `deltaText`, as the source scan reports one. */\nexport interface SourceRange {\n start: number;\n end: number;\n}\n\n/**\n * A declaration's authored source, ready to land in the living landscape:\n * byte-verbatim except that the feature's own tag is stripped and the\n * indentation is rebased from where the block sat in the delta to where it\n * lands. A construct the strip leaves empty goes too — a line that held only\n * the tag disappears whole, and `x = kind 'y' { #FEAT-1 }` lands as\n * `x = kind 'y'`, not as a pair of empty braces.\n *\n * `omit` cuts named ranges of the delta out on the way over, and it exists for\n * one caller: a NEW service element whose interior belongs to that service's\n * extending model. The children come along in the parent's bytes — they carry no\n * feature tag of their own, because LikeC4 does not inherit tags, so nothing in\n * the addition set can see them — and the map must take the box without them or\n * the model can never declare them (verification 2026-09-04, refutation of E1).\n * The ranges join the tag's removals, so a body they empty collapses the same\n * way `#FEAT-1`-only braces do.\n */\n/** How a declaration is carried over: whose tag to drop, where it lands, and what to leave behind. */\nexport interface Carry {\n featureId: string;\n /** The indent of the position the block lands at. */\n indent: string;\n /** Delta ranges cut out on the way over — see the banner; `[]` when nothing rides separately. */\n omit?: readonly SourceRange[];\n}\n\nexport function spliceSource(\n deltaText: string,\n decl: { start: number; end: number; indent: string },\n carry: Carry,\n): string {\n const { featureId, indent: targetIndent, omit = [] } = carry;\n const block = deltaText.slice(decl.start, decl.end);\n const cuts = omit\n .filter((r) => r.start >= decl.start && r.end <= decl.end)\n .map((r) => wholeLines(block, r.start - decl.start, r.end - decl.start));\n return reindent(stripFeatureTag(block, featureId, cuts), decl.indent, targetIndent);\n}\n\n/**\n * Widen a cut to the whole lines it sits on, when it has them to itself.\n *\n * A scanned declaration's span starts at its first character and ends one past\n * its last brace, so cutting it verbatim would leave the leading indent and the\n * trailing newline behind as a blank line — and the emptied-line rule below only\n * fires on a removal a line contains. A cut sharing its line with other code is\n * left exactly as asked.\n */\nfunction wholeLines(block: string, start: number, end: number): [number, number] {\n const lineStart = block.lastIndexOf(\"\\n\", start - 1) + 1;\n if (!/^[ \\t]*$/.test(block.slice(lineStart, start))) return [start, end];\n const nl = block.indexOf(\"\\n\", end);\n const to = nl === -1 ? block.length : nl;\n if (!/^[ \\t\\r]*$/.test(block.slice(end, to))) return [start, end];\n return [lineStart, nl === -1 ? block.length : nl + 1];\n}\n\n/**\n * Rewrite the line endings a merge INSERTED to the ones the document already\n * uses.\n *\n * Splicing composes bytes from two files, and the delta's newlines are not\n * necessarily the living document's: on a repository without `core.autocrlf`\n * normalisation a CRLF landscape or model came back with a handful of bare-LF\n * lines in the spliced region — two conventions in one file, noisy in an editor\n * and in every diff (verification 2026-09-04, W-CRLF). One helper for both\n * merges, because both had it.\n *\n * Only a document with ONE convention is corrected, and then in whichever\n * direction it uses: a file that is already mixed has no convention to match, and\n * guessing one would rewrite lines nobody touched.\n */\nexport function matchLineEndings(document: string, merged: string): string {\n const crlf = (document.match(/\\r\\n/g) ?? []).length;\n const bare = (document.match(/(?<!\\r)\\n/g) ?? []).length;\n if (crlf > 0 && bare === 0) return merged.replace(/(?<!\\r)\\n/g, \"\\r\\n\");\n if (bare > 0 && crlf === 0) return merged.replace(/\\r\\n/g, \"\\n\");\n return merged;\n}\n\nfunction stripFeatureTag(block: string, featureId: string, cuts: ReadonlyArray<[number, number]>): string {\n const { code } = maskSource(block);\n const esc = featureId.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const removals: Array<[number, number]> = cuts.map(([s, e]) => [s, e]);\n for (const m of block.matchAll(new RegExp(`#${esc}(?![\\\\w-])`, \"g\"))) {\n // Only tags in CODE count — a description quoting \"#FEAT-1\" is content.\n if (code.slice(m.index, m.index + m[0].length) !== m[0]) continue;\n let s = m.index;\n let e = s + m[0].length;\n // Take the whitespace on ONE side with the token — trailing first, so\n // `#FEAT-1 #critical` leaves `#critical` at its own indent, and a token at\n // the end of a line does not leave a trailing space behind.\n if (block[e] === \" \" || block[e] === \"\\t\") {\n while (block[e] === \" \" || block[e] === \"\\t\") e += 1;\n } else {\n while (s > 0 && (block[s - 1] === \" \" || block[s - 1] === \"\\t\")) s -= 1;\n }\n removals.push([s, e]);\n }\n if (removals.length === 0) return block;\n\n // A line the strip emptied disappears whole, newline included.\n for (let lineStart = 0; lineStart < block.length; ) {\n const nl = block.indexOf(\"\\n\", lineStart);\n const lineEnd = nl === -1 ? block.length : nl + 1;\n if (\n removals.some(([s, e]) => s >= lineStart && e <= lineEnd) &&\n /^\\s*$/.test(residual(block, lineStart, nl === -1 ? block.length : nl, removals))\n ) {\n removals.push([lineStart, lineEnd]);\n }\n lineStart = lineEnd;\n }\n\n // A body the strip emptied goes too, braces and all — but only one the strip\n // emptied: braces that were authored empty are authored bytes and survive.\n for (let at = code.indexOf(\"{\"); at !== -1; at = code.indexOf(\"{\", at + 1)) {\n const closeAt = matchBrace(code, at);\n if (closeAt === -1) continue;\n if (!removals.some(([s, e]) => s > at && e <= closeAt)) continue;\n if (!/^\\s*$/.test(residual(block, at + 1, closeAt, removals))) continue;\n let s = at;\n while (s > 0 && /\\s/.test(block[s - 1]!)) s -= 1;\n removals.push([s, closeAt + 1]);\n }\n return applyRemovals(block, removals);\n}\n\n/** The text of [from, to) with the removal ranges cut out. */\nfunction residual(text: string, from: number, to: number, removals: Array<[number, number]>): string {\n let out = \"\";\n for (let i = from; i < to; i += 1) {\n if (!removals.some(([s, e]) => i >= s && i < e)) out += text[i];\n }\n return out;\n}\n\nfunction applyRemovals(text: string, removals: Array<[number, number]>): string {\n const sorted = [...removals].sort((a, b) => a[0] - b[0] || b[1] - a[1]);\n let out = \"\";\n let i = 0;\n for (const [s, e] of sorted) {\n if (s > i) out += text.slice(i, s);\n if (e > i) i = e;\n }\n return out + text.slice(i);\n}\n\n/**\n * Rebase a block's indentation: the `base` its lines carried in the delta\n * becomes `target`. The first line arrives with no leading whitespace (the\n * scanner's span starts at the declaration itself), so it only gains `target`.\n *\n * A CRLF delta's blank line is `\"\\r\"`, not `\"\"`, so splitting on `\\n` alone used\n * to hand a non-empty body to the rule below and every blank line inside a\n * spliced block came out carrying the target indent as trailing whitespace. The\n * carriage return is set aside and put back, which leaves the line's own ending\n * exactly as authored for `matchLineEndings` to reconcile.\n */\nfunction reindent(block: string, base: string, target: string): string {\n return block\n .split(\"\\n\")\n .map((line, k) => {\n const cr = line.endsWith(\"\\r\");\n const raw = cr ? line.slice(0, -1) : line;\n const body = k === 0 ? raw : raw.startsWith(base) ? raw.slice(base.length) : raw;\n return (body.length === 0 ? \"\" : target + body) + (cr ? \"\\r\" : \"\");\n })\n .join(\"\\n\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/contract.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;CAAG","sourcesContent":["import type { Elem, Rel } from \"../likec4.js\";\n\n/**\n * A landscape merge the splicer could not compute. Every one is mechanical —\n * the input cannot be spliced as authored, never a judgment `--approve` may\n * override — and it is thrown at plan time, so nothing has been written.\n * `archiveErrorCode` (commands/archive/archive.ts) owns the mapping to `merge-failed`;\n * the splicer itself never names a CLI code.\n */\nexport class LandscapeSpliceError extends Error {}\n\nexport interface LandscapePlan {\n /** The merged landscape source, or null when everything was already there. */\n content: string | null;\n addedEls: Elem[];\n addedRels: Rel[];\n}\n\n/**\n * Everything the landscape splicer reads, as text and parsed views already in\n * hand. The merge is a pure text-to-text computation — reading the two files\n * (and deciding to write the result) stays with the caller, which is also what\n * keeps the splicer free of the staging layer.\n */\nexport interface LandscapeMergeRequest {\n /** The living landscape.likec4, decoded by the caller's readUtf8. */\n landscapeText: string;\n /** delta.likec4 as authored — the bytes the additions are spliced from. */\n deltaText: string;\n /** EVERY element the delta declares — the title join and the parent lookup read past the tagged ones. */\n deltaElements: Elem[];\n /** The delta's elements tagged with the feature: the candidate additions. */\n newEls: Elem[];\n /** The delta's relationships tagged with the feature. */\n newRels: Rel[];\n featureId: string;\n}\n"]}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/contract.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;CAAG","sourcesContent":["import type { Elem, Rel } from \"../likec4.js\";\n\n/**\n * A landscape merge the splicer could not compute. Every one is mechanical —\n * the input cannot be spliced as authored, never a judgment `--approve` may\n * override — and it is thrown at plan time, so nothing has been written.\n * `archiveErrorCode` (commands/archive/archive.ts) owns the mapping to `merge-failed`;\n * the splicer itself never names a CLI code.\n */\nexport class LandscapeSpliceError extends Error {}\n\n/**\n * A service whose `model.likec4` EXTENDS the fleet map — the document that owns\n * that service's interior, and therefore the one a nested addition belongs in.\n *\n * The merge takes these as TEXT already in hand, exactly as it takes the\n * landscape: reading the fleet and writing the result stay with the caller.\n * A service with a standalone model, or none, is simply absent from the list,\n * and the merge then behaves as it did before the extending shape existed.\n */\nexport interface ExtendingModel {\n /** The `services/<…>/<svc>` directory name — what an element's binding resolves to. */\n service: string;\n /** Repo-relative and `/`-separated: what a message names and where the caller writes. */\n path: string;\n /** The living model source, as authored. */\n text: string;\n}\n\n/** What one extending model owes this feature — routed, not yet spliced. */\nexport interface ModelAdditions {\n model: ExtendingModel;\n els: Elem[];\n rels: Rel[];\n}\n\nexport interface LandscapePlan {\n /** The merged landscape source, or null when everything was already there. */\n content: string | null;\n addedEls: Elem[];\n addedRels: Rel[];\n /**\n * The additions this merge routed AWAY from the map, per model that owns\n * them. Empty when the request named no extending models — the shape every\n * caller had before the routing existed. `./model/merge.ts` turns each entry\n * into that model's merged text.\n */\n models: ModelAdditions[];\n}\n\n/**\n * Everything the landscape splicer reads, as text and parsed views already in\n * hand. The merge is a pure text-to-text computation — reading the two files\n * (and deciding to write the result) stays with the caller, which is also what\n * keeps the splicer free of the staging layer.\n */\nexport interface LandscapeMergeRequest {\n /** The living landscape.likec4, decoded by the caller's readUtf8. */\n landscapeText: string;\n /** delta.likec4 as authored — the bytes the additions are spliced from. */\n deltaText: string;\n /** EVERY element the delta declares — the title join and the parent lookup read past the tagged ones. */\n deltaElements: Elem[];\n /** The delta's elements tagged with the feature: the candidate additions. */\n newEls: Elem[];\n /** The delta's relationships tagged with the feature. */\n newRels: Rel[];\n featureId: string;\n /**\n * The fleet's extending models. Omitted (or empty) is the pre-extending\n * behaviour byte for byte: every addition lands on the map.\n */\n models?: readonly ExtendingModel[];\n}\n"]}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * What the fleet's extending models ALREADY declare — the half of the archive's
3
+ * existence check that does not live on the map.
4
+ *
5
+ * The check has two joins: by id, and by TITLE for a delta that spells an
6
+ * element under its own identifier rather than the map's. Only the id half ever
7
+ * learned about the models the merge now writes into, so a container a service
8
+ * already draws under a different id was spliced in beside itself and the
9
+ * service silently grew two boxes with one name — while the SAME delta against
10
+ * the same title ON THE MAP adds nothing (review of E1). One rule, two
11
+ * documents, two answers.
12
+ *
13
+ * THE MODEL HALF IS SCOPED TO ITS SERVICE, and the map's is deliberately not.
14
+ * The map's global title index is the id-less fallback for documents written
15
+ * before `metadata { service }` existed, and it carries a hazard its own
16
+ * comment states: two services' boxes sharing a title join to each other unless
17
+ * both sides are bound. A service's interior is exactly where that would bite —
18
+ * every extending model in a fleet names a container 'api' — so a model's
19
+ * declaration joins by title only against an addition resolving to the SAME
20
+ * explicitly bound service. Either side unbound means no title join at all: an
21
+ * addition landing beside a namesake is a duplicate the author can see, and a
22
+ * wrongly skipped one is a container that quietly never arrived.
23
+ *
24
+ * The reader is the SCAN and not a parse, for the reason the caller states: ids
25
+ * are exact inside the fqn namespace the map and its models share, and parsing
26
+ * every model here would cost one LikeC4 workspace per service to answer a
27
+ * question about text. Two kinds of declaration therefore carry no title, and
28
+ * neither loses anything: one whose head names no title (LikeC4 titles it after
29
+ * its own name, and a model's default and a delta's differ exactly when their
30
+ * ids do — which the id join already covers) and one writing its title as a
31
+ * body property.
32
+ */
33
+ import { ancestorIds } from "../../../kernel/ids/fqn/ancestors.js";
34
+ /**
35
+ * Index the scanned models.
36
+ *
37
+ * `bindEls` is the corpus the service binding resolves against — the living
38
+ * map's elements plus the delta's, the same one routing uses, because a model's
39
+ * declaration is bound by an ancestor the MAP owns and a delta's addition by an
40
+ * ancestor either document may.
41
+ */
42
+ export function modelDeclarations(scans, bindEls) {
43
+ const bound = new Map();
44
+ for (const e of bindEls)
45
+ if (e.service !== undefined)
46
+ bound.set(e.id, e.service);
47
+ // An EXPLICIT binding on the id or an ancestor, and nothing else.
48
+ // `serviceResolver`'s last rung answers with the element's own title or its
49
+ // raw id — the rung its own comment calls the one that lies — and here that
50
+ // would file two unbound services' containers under invented names and then
51
+ // let those names join.
52
+ const serviceOf = (id) => {
53
+ for (const candidate of ancestorIds(id)) {
54
+ const service = bound.get(candidate);
55
+ if (service !== undefined)
56
+ return service;
57
+ }
58
+ return undefined;
59
+ };
60
+ const ids = [];
61
+ const titles = new Set();
62
+ for (const { scan } of scans) {
63
+ for (const e of scan?.elements ?? []) {
64
+ // An `extend` frame declares nothing: it reopens an element the map owns.
65
+ if (e.extend === true)
66
+ continue;
67
+ ids.push(e.id);
68
+ if (e.title === undefined)
69
+ continue;
70
+ const service = serviceOf(e.id);
71
+ if (service !== undefined)
72
+ titles.add(titleKey(service, e.title));
73
+ }
74
+ }
75
+ return {
76
+ ids,
77
+ declaresTitle: (id, title) => {
78
+ const service = serviceOf(id);
79
+ return service !== undefined && titles.has(titleKey(service, title));
80
+ },
81
+ };
82
+ }
83
+ /** Length-prefixed: a service id and a title cannot spell another pair's key. */
84
+ function titleKey(service, title) {
85
+ return `${service.length}:${service}${title}`;
86
+ }
87
+ //# sourceMappingURL=declared.js.map