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

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 (648) hide show
  1. package/CHANGELOG.md +1315 -1
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +40 -22
  5. package/QUICKSTART.md +181 -9
  6. package/README.md +256 -57
  7. package/ROADMAP.md +83 -7
  8. package/SCHEMA.md +569 -98
  9. package/WORKFLOW.md +272 -62
  10. package/dist/cli.js +26 -8
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt/adopt.js +5 -10
  13. package/dist/commands/adopt/adopt.js.map +1 -1
  14. package/dist/commands/archive/archive.js +15 -18
  15. package/dist/commands/archive/archive.js.map +1 -1
  16. package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
  17. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  18. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  19. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/openapi.js +25 -42
  21. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
  23. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  24. package/dist/commands/archive/plan/gate.js +3 -7
  25. package/dist/commands/archive/plan/gate.js.map +1 -1
  26. package/dist/commands/archive/plan/landscape.js +4 -4
  27. package/dist/commands/archive/plan/landscape.js.map +1 -1
  28. package/dist/commands/archive/plan/refusal.js +8 -18
  29. package/dist/commands/archive/plan/refusal.js.map +1 -1
  30. package/dist/commands/archive/plan/specs.js +134 -25
  31. package/dist/commands/archive/plan/specs.js.map +1 -1
  32. package/dist/commands/archive/plan/state.js +1 -0
  33. package/dist/commands/archive/plan/state.js.map +1 -1
  34. package/dist/commands/archive/run.js +33 -11
  35. package/dist/commands/archive/run.js.map +1 -1
  36. package/dist/commands/archive/shipped.js +49 -0
  37. package/dist/commands/archive/shipped.js.map +1 -0
  38. package/dist/commands/context/context.js +4 -7
  39. package/dist/commands/context/context.js.map +1 -1
  40. package/dist/commands/context/print.js +2 -2
  41. package/dist/commands/context/print.js.map +1 -1
  42. package/dist/commands/delta/delta.js +44 -12
  43. package/dist/commands/delta/delta.js.map +1 -1
  44. package/dist/commands/delta/print.js +1 -1
  45. package/dist/commands/delta/print.js.map +1 -1
  46. package/dist/commands/dependencies.js +3 -7
  47. package/dist/commands/dependencies.js.map +1 -1
  48. package/dist/commands/diff/diff.js +20 -27
  49. package/dist/commands/diff/diff.js.map +1 -1
  50. package/dist/commands/explore.js +4 -8
  51. package/dist/commands/explore.js.map +1 -1
  52. package/dist/commands/gate/gate.js +45 -8
  53. package/dist/commands/gate/gate.js.map +1 -1
  54. package/dist/commands/gate/print.js +15 -3
  55. package/dist/commands/gate/print.js.map +1 -1
  56. package/dist/commands/gherkin/commit.js +13 -54
  57. package/dist/commands/gherkin/commit.js.map +1 -1
  58. package/dist/commands/gherkin/gherkin.js +36 -20
  59. package/dist/commands/gherkin/gherkin.js.map +1 -1
  60. package/dist/commands/gherkin/reconcile.js +10 -13
  61. package/dist/commands/gherkin/reconcile.js.map +1 -1
  62. package/dist/commands/init/first-hour.js +44 -0
  63. package/dist/commands/init/first-hour.js.map +1 -1
  64. package/dist/commands/init/init.js +18 -22
  65. package/dist/commands/init/init.js.map +1 -1
  66. package/dist/commands/list/campaign/campaign.js +4 -6
  67. package/dist/commands/list/campaign/campaign.js.map +1 -1
  68. package/dist/commands/list/json.js +0 -4
  69. package/dist/commands/list/json.js.map +1 -1
  70. package/dist/commands/list/list.js +38 -23
  71. package/dist/commands/list/list.js.map +1 -1
  72. package/dist/commands/list/print.js +38 -6
  73. package/dist/commands/list/print.js.map +1 -1
  74. package/dist/commands/list/suites/suites.js +140 -0
  75. package/dist/commands/list/suites/suites.js.map +1 -0
  76. package/dist/commands/list/views.js +28 -33
  77. package/dist/commands/list/views.js.map +1 -1
  78. package/dist/commands/mcp/dispatch.js +91 -36
  79. package/dist/commands/mcp/dispatch.js.map +1 -1
  80. package/dist/commands/mcp/mcp.js +56 -15
  81. package/dist/commands/mcp/mcp.js.map +1 -1
  82. package/dist/commands/mcp/serve.js.map +1 -1
  83. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  84. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  85. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  86. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  87. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  88. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  89. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  90. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  91. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  92. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  93. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  94. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  95. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  96. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  97. package/dist/commands/new/capability-template.js +118 -0
  98. package/dist/commands/new/capability-template.js.map +1 -0
  99. package/dist/commands/new/commit.js +8 -22
  100. package/dist/commands/new/commit.js.map +1 -1
  101. package/dist/commands/new/modifies.js +206 -0
  102. package/dist/commands/new/modifies.js.map +1 -0
  103. package/dist/commands/new/new.js +59 -14
  104. package/dist/commands/new/new.js.map +1 -1
  105. package/dist/commands/new/templates.js +48 -124
  106. package/dist/commands/new/templates.js.map +1 -1
  107. package/dist/commands/open.js +3 -7
  108. package/dist/commands/open.js.map +1 -1
  109. package/dist/commands/policy/gate.js +27 -0
  110. package/dist/commands/policy/gate.js.map +1 -1
  111. package/dist/commands/rebase/plan.js +2 -2
  112. package/dist/commands/rebase/plan.js.map +1 -1
  113. package/dist/commands/rebase/rebase.js +60 -35
  114. package/dist/commands/rebase/rebase.js.map +1 -1
  115. package/dist/commands/seed/commit.js +21 -25
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/seed.js +6 -9
  118. package/dist/commands/seed/seed.js.map +1 -1
  119. package/dist/commands/show/show.js +3 -7
  120. package/dist/commands/show/show.js.map +1 -1
  121. package/dist/commands/status/print.js +13 -4
  122. package/dist/commands/status/print.js.map +1 -1
  123. package/dist/commands/status/status.js +3 -7
  124. package/dist/commands/status/status.js.map +1 -1
  125. package/dist/commands/steps/steps.js +3 -7
  126. package/dist/commands/steps/steps.js.map +1 -1
  127. package/dist/commands/subsystem/edit.js +3 -5
  128. package/dist/commands/subsystem/edit.js.map +1 -1
  129. package/dist/commands/subsystem/move.js +5 -8
  130. package/dist/commands/subsystem/move.js.map +1 -1
  131. package/dist/commands/subsystem/query.js +2 -4
  132. package/dist/commands/subsystem/query.js.map +1 -1
  133. package/dist/commands/subsystem/subsystem.js +4 -7
  134. package/dist/commands/subsystem/subsystem.js.map +1 -1
  135. package/dist/commands/subsystem/sync.js +14 -41
  136. package/dist/commands/subsystem/sync.js.map +1 -1
  137. package/dist/commands/subsystem/txn/txn.js +17 -38
  138. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  139. package/dist/commands/subsystem/txn/views.js +32 -5
  140. package/dist/commands/subsystem/txn/views.js.map +1 -1
  141. package/dist/commands/tasks/render.js +116 -0
  142. package/dist/commands/tasks/render.js.map +1 -0
  143. package/dist/commands/tasks/tasks.js +113 -0
  144. package/dist/commands/tasks/tasks.js.map +1 -0
  145. package/dist/commands/unarchive/manifest.js +6 -9
  146. package/dist/commands/unarchive/manifest.js.map +1 -1
  147. package/dist/commands/unarchive/restore.js +17 -28
  148. package/dist/commands/unarchive/restore.js.map +1 -1
  149. package/dist/commands/unarchive/unarchive.js +22 -30
  150. package/dist/commands/unarchive/unarchive.js.map +1 -1
  151. package/dist/commands/validate/arch-coverage.js +6 -15
  152. package/dist/commands/validate/arch-coverage.js.map +1 -1
  153. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  154. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  155. package/dist/commands/validate/checks/requirements.js +18 -8
  156. package/dist/commands/validate/checks/requirements.js.map +1 -1
  157. package/dist/commands/validate/checks/sources.js +6 -2
  158. package/dist/commands/validate/checks/sources.js.map +1 -1
  159. package/dist/commands/validate/feature.js +23 -19
  160. package/dist/commands/validate/feature.js.map +1 -1
  161. package/dist/commands/validate/fleet/landscape.js +23 -23
  162. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  163. package/dist/commands/validate/fleet/map/attest.js +6 -10
  164. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  165. package/dist/commands/validate/fleet/map/bindings.js +1 -3
  166. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  167. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  168. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  169. package/dist/commands/validate/fleet/obligations.js +1 -5
  170. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  171. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  172. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  173. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  174. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  175. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  176. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  177. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  178. package/dist/commands/validate/fleet/views/root-documents.js +8 -28
  179. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  180. package/dist/commands/validate/links/corpus.js +1 -4
  181. package/dist/commands/validate/links/corpus.js.map +1 -1
  182. package/dist/commands/validate/links/glossary.js.map +1 -1
  183. package/dist/commands/validate/report.js +5 -4
  184. package/dist/commands/validate/report.js.map +1 -1
  185. package/dist/commands/validate/scope.js +113 -0
  186. package/dist/commands/validate/scope.js.map +1 -0
  187. package/dist/commands/validate/service/api.js +16 -104
  188. package/dist/commands/validate/service/api.js.map +1 -1
  189. package/dist/commands/validate/service/contracts/generated.js +134 -0
  190. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  191. package/dist/commands/validate/service/events/events.js +1 -1
  192. package/dist/commands/validate/service/events/events.js.map +1 -1
  193. package/dist/commands/validate/service/events/message-producers.js +1 -1
  194. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  195. package/dist/commands/validate/service/evidence-pins.js +56 -10
  196. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  197. package/dist/commands/validate/service/service.js +16 -26
  198. package/dist/commands/validate/service/service.js.map +1 -1
  199. package/dist/commands/validate/service/specs.js +31 -38
  200. package/dist/commands/validate/service/specs.js.map +1 -1
  201. package/dist/commands/validate/service/spine.js +8 -11
  202. package/dist/commands/validate/service/spine.js.map +1 -1
  203. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  204. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  205. package/dist/commands/validate/validate.js +44 -26
  206. package/dist/commands/validate/validate.js.map +1 -1
  207. package/dist/commands/verify/cross/diff.js +2 -6
  208. package/dist/commands/verify/cross/diff.js.map +1 -1
  209. package/dist/commands/verify/evidence/contract.js +1 -1
  210. package/dist/commands/verify/evidence/contract.js.map +1 -1
  211. package/dist/commands/verify/evidence/read.js +12 -3
  212. package/dist/commands/verify/evidence/read.js.map +1 -1
  213. package/dist/commands/verify/frozen.js +45 -23
  214. package/dist/commands/verify/frozen.js.map +1 -1
  215. package/dist/commands/verify/record.js +69 -17
  216. package/dist/commands/verify/record.js.map +1 -1
  217. package/dist/commands/verify/report.js +27 -44
  218. package/dist/commands/verify/report.js.map +1 -1
  219. package/dist/commands/verify/results.js +112 -30
  220. package/dist/commands/verify/results.js.map +1 -1
  221. package/dist/commands/verify/verify.js +54 -51
  222. package/dist/commands/verify/verify.js.map +1 -1
  223. package/dist/commands/vouch/pack/pack.js +2 -2
  224. package/dist/commands/vouch/pack/pack.js.map +1 -1
  225. package/dist/commands/vouch/pack/print.js +1 -6
  226. package/dist/commands/vouch/pack/print.js.map +1 -1
  227. package/dist/commands/vouch/pack/sections.js +1 -2
  228. package/dist/commands/vouch/pack/sections.js.map +1 -1
  229. package/dist/commands/vouch/run.js +26 -81
  230. package/dist/commands/vouch/run.js.map +1 -1
  231. package/dist/commands/vouch/sample/plan.js +20 -34
  232. package/dist/commands/vouch/sample/plan.js.map +1 -1
  233. package/dist/commands/vouch/vet/verify.js +39 -7
  234. package/dist/commands/vouch/vet/verify.js.map +1 -1
  235. package/dist/commands/vouch/vouch.js +10 -17
  236. package/dist/commands/vouch/vouch.js.map +1 -1
  237. package/dist/core/agent/agents-md/command-map.js +56 -101
  238. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  239. package/dist/core/agent/agents-md/map/explain.js +4 -3
  240. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  241. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  242. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  243. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  244. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  245. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  246. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  247. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  248. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  249. package/dist/core/agent/agents-md/map/status.js +9 -3
  250. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  251. package/dist/core/agent/agents-md/map/subsystem.js +4 -3
  252. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  253. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  254. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  255. package/dist/core/agent/agents-md/refusals.js +32 -18
  256. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  257. package/dist/core/agent/agents-md.js +9 -3
  258. package/dist/core/agent/agents-md.js.map +1 -1
  259. package/dist/core/agent/agents-stamp.js +10 -0
  260. package/dist/core/agent/agents-stamp.js.map +1 -1
  261. package/dist/core/agent/scaffold.js +10 -15
  262. package/dist/core/agent/scaffold.js.map +1 -1
  263. package/dist/core/agent/tools/dialects.js +1 -0
  264. package/dist/core/agent/tools/dialects.js.map +1 -1
  265. package/dist/core/agent/tools/registry.js +41 -76
  266. package/dist/core/agent/tools/registry.js.map +1 -1
  267. package/dist/core/agent/workflows/adopt.js +5 -3
  268. package/dist/core/agent/workflows/adopt.js.map +1 -1
  269. package/dist/core/agent/workflows/check.js +20 -16
  270. package/dist/core/agent/workflows/check.js.map +1 -1
  271. package/dist/core/agent/workflows/closing.js +54 -21
  272. package/dist/core/agent/workflows/closing.js.map +1 -1
  273. package/dist/core/agent/workflows/feature.js +79 -11
  274. package/dist/core/agent/workflows/feature.js.map +1 -1
  275. package/dist/core/agent/workflows/implement.js +91 -13
  276. package/dist/core/agent/workflows/implement.js.map +1 -1
  277. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  278. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  279. package/dist/core/agent/workflows/reference/codes.js +8 -6
  280. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  281. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  282. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  283. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  284. package/dist/core/agent/workflows/reference/spine.js +50 -18
  285. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  286. package/dist/core/asyncapi/baseline/plan.js +14 -81
  287. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  288. package/dist/core/asyncapi/digest.js +30 -25
  289. package/dist/core/asyncapi/digest.js.map +1 -1
  290. package/dist/core/asyncapi/merge/components.js +5 -4
  291. package/dist/core/asyncapi/merge/components.js.map +1 -1
  292. package/dist/core/asyncapi/merge/error.js +7 -2
  293. package/dist/core/asyncapi/merge/error.js.map +1 -1
  294. package/dist/core/asyncapi/merge/markers.js +30 -50
  295. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  296. package/dist/core/asyncapi/merge/merge.js +31 -53
  297. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  298. package/dist/core/asyncapi/merge/pin.js +10 -36
  299. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  300. package/dist/core/asyncapi/model.js.map +1 -1
  301. package/dist/core/asyncapi/read.js +36 -12
  302. package/dist/core/asyncapi/read.js.map +1 -1
  303. package/dist/core/brief/brief.js +6 -18
  304. package/dist/core/brief/brief.js.map +1 -1
  305. package/dist/core/brief/landscape.js +8 -12
  306. package/dist/core/brief/landscape.js.map +1 -1
  307. package/dist/core/brief/map/extend.js +8 -5
  308. package/dist/core/brief/map/extend.js.map +1 -1
  309. package/dist/core/brief/map/owed.js +8 -26
  310. package/dist/core/brief/map/owed.js.map +1 -1
  311. package/dist/core/brief/targets.js +15 -5
  312. package/dist/core/brief/targets.js.map +1 -1
  313. package/dist/core/c4/likec4.js +147 -16
  314. package/dist/core/c4/likec4.js.map +1 -1
  315. package/dist/core/c4/locate/unlocated.js +150 -0
  316. package/dist/core/c4/locate/unlocated.js.map +1 -0
  317. package/dist/core/c4/parsed/values.js +54 -1
  318. package/dist/core/c4/parsed/values.js.map +1 -1
  319. package/dist/core/c4/prefetch/fleet.js +1 -9
  320. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  321. package/dist/core/c4/project/documents.js +31 -6
  322. package/dist/core/c4/project/documents.js.map +1 -1
  323. package/dist/core/c4/project/load.js +6 -17
  324. package/dist/core/c4/project/load.js.map +1 -1
  325. package/dist/core/c4/resolve/external.js +51 -0
  326. package/dist/core/c4/resolve/external.js.map +1 -0
  327. package/dist/core/c4/resolve/service.js +25 -1
  328. package/dist/core/c4/resolve/service.js.map +1 -1
  329. package/dist/core/c4/seed/template.js +15 -1
  330. package/dist/core/c4/seed/template.js.map +1 -1
  331. package/dist/core/c4/service-model/shape.js +2 -22
  332. package/dist/core/c4/service-model/shape.js.map +1 -1
  333. package/dist/core/c4/source-mask.js +288 -4
  334. package/dist/core/c4/source-mask.js.map +1 -1
  335. package/dist/core/c4/source-scan.js +203 -147
  336. package/dist/core/c4/source-scan.js.map +1 -1
  337. package/dist/core/c4/splice/authored-source.js +19 -0
  338. package/dist/core/c4/splice/authored-source.js.map +1 -1
  339. package/dist/core/c4/splice/delta-blocks.js +5 -18
  340. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  341. package/dist/core/c4/splice/landscape-merge.js +13 -8
  342. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  343. package/dist/core/c4/splice/model/merge.js +10 -6
  344. package/dist/core/c4/splice/model/merge.js.map +1 -1
  345. package/dist/core/c4/splice/placement.js +108 -18
  346. package/dist/core/c4/splice/placement.js.map +1 -1
  347. package/dist/core/c4/workspace.js +13 -22
  348. package/dist/core/c4/workspace.js.map +1 -1
  349. package/dist/core/capabilities/capabilities.js +6 -6
  350. package/dist/core/capabilities/capabilities.js.map +1 -1
  351. package/dist/core/capabilities/delta/doc.js +7 -10
  352. package/dist/core/capabilities/delta/doc.js.map +1 -1
  353. package/dist/core/capabilities/findings.js +15 -20
  354. package/dist/core/capabilities/findings.js.map +1 -1
  355. package/dist/core/capabilities/realizes/findings.js +12 -19
  356. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  357. package/dist/core/capabilities/realizes/join.js +0 -20
  358. package/dist/core/capabilities/realizes/join.js.map +1 -1
  359. package/dist/core/capabilities/rollup.js +6 -0
  360. package/dist/core/capabilities/rollup.js.map +1 -1
  361. package/dist/core/capabilities/tree.js +6 -28
  362. package/dist/core/capabilities/tree.js.map +1 -1
  363. package/dist/core/capabilities/usecase-join.js +3 -13
  364. package/dist/core/capabilities/usecase-join.js.map +1 -1
  365. package/dist/core/coherence/authoring/scaffold.js +11 -3
  366. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  367. package/dist/core/coherence/authoring/sentinels.js +2 -1
  368. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  369. package/dist/core/coherence/coherence.js +30 -16
  370. package/dist/core/coherence/coherence.js.map +1 -1
  371. package/dist/core/coherence/consumed/operations.js +70 -0
  372. package/dist/core/coherence/consumed/operations.js.map +1 -0
  373. package/dist/core/coherence/consumed/refusal.js +38 -0
  374. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  375. package/dist/core/coherence/declared.js +60 -34
  376. package/dist/core/coherence/declared.js.map +1 -1
  377. package/dist/core/coherence/events/declared.js +44 -59
  378. package/dist/core/coherence/events/declared.js.map +1 -1
  379. package/dist/core/coherence/events/events.js +119 -8
  380. package/dist/core/coherence/events/events.js.map +1 -1
  381. package/dist/core/coherence/events/grades.js +31 -8
  382. package/dist/core/coherence/events/grades.js.map +1 -1
  383. package/dist/core/coherence/events/lookups.js +14 -82
  384. package/dist/core/coherence/events/lookups.js.map +1 -1
  385. package/dist/core/coherence/lookups.js +18 -78
  386. package/dist/core/coherence/lookups.js.map +1 -1
  387. package/dist/core/coherence/retired/archived.js +285 -0
  388. package/dist/core/coherence/retired/archived.js.map +1 -0
  389. package/dist/core/coherence/retired/event-pair.js +62 -0
  390. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  391. package/dist/core/coherence/retired/merged.js +79 -0
  392. package/dist/core/coherence/retired/merged.js.map +1 -0
  393. package/dist/core/coherence/retired/retirement.js +49 -0
  394. package/dist/core/coherence/retired/retirement.js.map +1 -0
  395. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  396. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  397. package/dist/core/coherence/retired/revival/revived.js +297 -0
  398. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  399. package/dist/core/consumers/after.js +113 -0
  400. package/dist/core/consumers/after.js.map +1 -0
  401. package/dist/core/consumers/consumers.js +296 -0
  402. package/dist/core/consumers/consumers.js.map +1 -0
  403. package/dist/core/consumers/current.js +211 -0
  404. package/dist/core/consumers/current.js.map +1 -0
  405. package/dist/core/consumers/scan.js +46 -0
  406. package/dist/core/consumers/scan.js.map +1 -0
  407. package/dist/core/consumers/state.js +27 -0
  408. package/dist/core/consumers/state.js.map +1 -0
  409. package/dist/core/delta/delta.js +1 -1
  410. package/dist/core/delta/delta.js.map +1 -1
  411. package/dist/core/deployment/delta.js +3 -6
  412. package/dist/core/deployment/delta.js.map +1 -1
  413. package/dist/core/diff/base-git.js +94 -12
  414. package/dist/core/diff/base-git.js.map +1 -1
  415. package/dist/core/diff/base-state.js +28 -16
  416. package/dist/core/diff/base-state.js.map +1 -1
  417. package/dist/core/diff/joins.js +136 -0
  418. package/dist/core/diff/joins.js.map +1 -0
  419. package/dist/core/diff/scope/changed-paths.js +22 -6
  420. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  421. package/dist/core/diff/scope/edges.js +82 -0
  422. package/dist/core/diff/scope/edges.js.map +1 -0
  423. package/dist/core/diff/semantic.js +100 -31
  424. package/dist/core/diff/semantic.js.map +1 -1
  425. package/dist/core/doctor/agents.js +34 -18
  426. package/dist/core/doctor/agents.js.map +1 -1
  427. package/dist/core/doctor/report.js.map +1 -1
  428. package/dist/core/document/apply.js +10 -2
  429. package/dist/core/document/apply.js.map +1 -1
  430. package/dist/core/document/frontmatter.js +55 -0
  431. package/dist/core/document/frontmatter.js.map +1 -1
  432. package/dist/core/document/parse.js +29 -6
  433. package/dist/core/document/parse.js.map +1 -1
  434. package/dist/core/document/scenarios.js +17 -0
  435. package/dist/core/document/scenarios.js.map +1 -1
  436. package/dist/core/document/spec.js +4 -11
  437. package/dist/core/document/spec.js.map +1 -1
  438. package/dist/core/envelope/config.js +10 -0
  439. package/dist/core/envelope/config.js.map +1 -1
  440. package/dist/core/envelope/json.js +34 -0
  441. package/dist/core/envelope/json.js.map +1 -1
  442. package/dist/core/explain/families.js +17 -12
  443. package/dist/core/explain/families.js.map +1 -1
  444. package/dist/core/explain/refusals.js +3 -3
  445. package/dist/core/explain/refusals.js.map +1 -1
  446. package/dist/core/explain/terms.js +18 -9
  447. package/dist/core/explain/terms.js.map +1 -1
  448. package/dist/core/explore/explore.js +2 -7
  449. package/dist/core/explore/explore.js.map +1 -1
  450. package/dist/core/fleet-context.js +34 -95
  451. package/dist/core/fleet-context.js.map +1 -1
  452. package/dist/core/gate/checks.js +5 -2
  453. package/dist/core/gate/checks.js.map +1 -1
  454. package/dist/core/gate/partners.js +4 -2
  455. package/dist/core/gate/partners.js.map +1 -1
  456. package/dist/core/gate/report.js.map +1 -1
  457. package/dist/core/gate/verification.js +75 -12
  458. package/dist/core/gate/verification.js.map +1 -1
  459. package/dist/core/gherkin/stale.js +13 -37
  460. package/dist/core/gherkin/stale.js.map +1 -1
  461. package/dist/core/glossary/tree.js +6 -19
  462. package/dist/core/glossary/tree.js.map +1 -1
  463. package/dist/core/kernel/concurrency.js +19 -0
  464. package/dist/core/kernel/concurrency.js.map +1 -1
  465. package/dist/core/kernel/records.js +11 -0
  466. package/dist/core/kernel/records.js.map +1 -1
  467. package/dist/core/links/backlinks.js +1 -3
  468. package/dist/core/links/backlinks.js.map +1 -1
  469. package/dist/core/links/corpus.js +5 -36
  470. package/dist/core/links/corpus.js.map +1 -1
  471. package/dist/core/mcp/argv.js +101 -0
  472. package/dist/core/mcp/argv.js.map +1 -1
  473. package/dist/core/mcp/author-tools.js +2 -1
  474. package/dist/core/mcp/author-tools.js.map +1 -1
  475. package/dist/core/mcp/protocol.js +2 -2
  476. package/dist/core/mcp/protocol.js.map +1 -1
  477. package/dist/core/mcp/tools.js +43 -33
  478. package/dist/core/mcp/tools.js.map +1 -1
  479. package/dist/core/openapi/baseline/gate.js +53 -12
  480. package/dist/core/openapi/baseline/gate.js.map +1 -1
  481. package/dist/core/openapi/baseline/plan.js +22 -8
  482. package/dist/core/openapi/baseline/plan.js.map +1 -1
  483. package/dist/core/openapi/baseline/record.js +0 -1
  484. package/dist/core/openapi/baseline/record.js.map +1 -1
  485. package/dist/core/openapi/contract-text.js +33 -0
  486. package/dist/core/openapi/contract-text.js.map +1 -0
  487. package/dist/core/openapi/contract-tree.js +56 -0
  488. package/dist/core/openapi/contract-tree.js.map +1 -0
  489. package/dist/core/openapi/depth.js +38 -24
  490. package/dist/core/openapi/depth.js.map +1 -1
  491. package/dist/core/openapi/digest.js +52 -2
  492. package/dist/core/openapi/digest.js.map +1 -1
  493. package/dist/core/openapi/doc.js +12 -4
  494. package/dist/core/openapi/doc.js.map +1 -1
  495. package/dist/core/openapi/merge/components.js +25 -60
  496. package/dist/core/openapi/merge/components.js.map +1 -1
  497. package/dist/core/openapi/merge/error.js +3 -13
  498. package/dist/core/openapi/merge/error.js.map +1 -1
  499. package/dist/core/openapi/merge/markers.js +26 -30
  500. package/dist/core/openapi/merge/markers.js.map +1 -1
  501. package/dist/core/openapi/merge/merge.js +26 -52
  502. package/dist/core/openapi/merge/merge.js.map +1 -1
  503. package/dist/core/openapi/merge/pin.js +15 -35
  504. package/dist/core/openapi/merge/pin.js.map +1 -1
  505. package/dist/core/openspec/decide/verdict.js.map +1 -1
  506. package/dist/core/openspec/inventory.js +4 -5
  507. package/dist/core/openspec/inventory.js.map +1 -1
  508. package/dist/core/openspec/model/mapping.js +30 -28
  509. package/dist/core/openspec/model/mapping.js.map +1 -1
  510. package/dist/core/openspec/model/model.js +12 -7
  511. package/dist/core/openspec/model/model.js.map +1 -1
  512. package/dist/core/openspec/scan/changes.js +10 -4
  513. package/dist/core/openspec/scan/changes.js.map +1 -1
  514. package/dist/core/openspec/scan/config.js +7 -16
  515. package/dist/core/openspec/scan/config.js.map +1 -1
  516. package/dist/core/openspec/scan/shape.js +63 -19
  517. package/dist/core/openspec/scan/shape.js.map +1 -1
  518. package/dist/core/openspec/scan/walk.js +21 -38
  519. package/dist/core/openspec/scan/walk.js.map +1 -1
  520. package/dist/core/pack/living.js +2 -16
  521. package/dist/core/pack/living.js.map +1 -1
  522. package/dist/core/provenance/findings.js +48 -12
  523. package/dist/core/provenance/findings.js.map +1 -1
  524. package/dist/core/provenance/git.js +33 -57
  525. package/dist/core/provenance/git.js.map +1 -1
  526. package/dist/core/provenance/gitq/moves.js +71 -114
  527. package/dist/core/provenance/gitq/moves.js.map +1 -1
  528. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  529. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  530. package/dist/core/provenance/sources.js +23 -6
  531. package/dist/core/provenance/sources.js.map +1 -1
  532. package/dist/core/provenance/stamp.js +5 -17
  533. package/dist/core/provenance/stamp.js.map +1 -1
  534. package/dist/core/repo/entries.js.map +1 -1
  535. package/dist/core/repo/repo.js +24 -14
  536. package/dist/core/repo/repo.js.map +1 -1
  537. package/dist/core/repo/tree/find.js +12 -3
  538. package/dist/core/repo/tree/find.js.map +1 -1
  539. package/dist/core/repo/tree/fs.js +27 -1
  540. package/dist/core/repo/tree/fs.js.map +1 -1
  541. package/dist/core/repo/tree/walk.js +8 -30
  542. package/dist/core/repo/tree/walk.js.map +1 -1
  543. package/dist/core/results.js +47 -12
  544. package/dist/core/results.js.map +1 -1
  545. package/dist/core/staging/commit.js +10 -12
  546. package/dist/core/staging/commit.js.map +1 -1
  547. package/dist/core/staging/lock.js +74 -12
  548. package/dist/core/staging/lock.js.map +1 -1
  549. package/dist/core/staging/recovery/intent.js +2 -5
  550. package/dist/core/staging/recovery/intent.js.map +1 -1
  551. package/dist/core/staging/recovery/recover.js +60 -1
  552. package/dist/core/staging/recovery/recover.js.map +1 -1
  553. package/dist/core/staging/snapshot/manifest.js +152 -0
  554. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  555. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  556. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  557. package/dist/core/status/actions/execution.js +11 -0
  558. package/dist/core/status/actions/execution.js.map +1 -1
  559. package/dist/core/status/feature/artifacts.js +2 -2
  560. package/dist/core/status/feature/artifacts.js.map +1 -1
  561. package/dist/core/status/feature/feature.js +5 -11
  562. package/dist/core/status/feature/feature.js.map +1 -1
  563. package/dist/core/status/fleet/fleet.js +6 -2
  564. package/dist/core/status/fleet/fleet.js.map +1 -1
  565. package/dist/core/status/fleet/next.js +26 -3
  566. package/dist/core/status/fleet/next.js.map +1 -1
  567. package/dist/core/status/report.js.map +1 -1
  568. package/dist/core/status/verification.js +30 -15
  569. package/dist/core/status/verification.js.map +1 -1
  570. package/dist/core/tasks/packet.js +174 -0
  571. package/dist/core/tasks/packet.js.map +1 -0
  572. package/dist/core/tasks/record.js +43 -0
  573. package/dist/core/tasks/record.js.map +1 -0
  574. package/dist/core/tasks/sources.js +181 -0
  575. package/dist/core/tasks/sources.js.map +1 -0
  576. package/dist/core/tasks/stamps.js +76 -0
  577. package/dist/core/tasks/stamps.js.map +1 -0
  578. package/dist/core/tasks/unit.js +0 -0
  579. package/dist/core/tasks/unit.js.map +1 -0
  580. package/dist/core/usecases/capability.js +11 -14
  581. package/dist/core/usecases/capability.js.map +1 -1
  582. package/dist/core/usecases/delta/flows.js +5 -7
  583. package/dist/core/usecases/delta/flows.js.map +1 -1
  584. package/dist/core/usecases/fleet.js +178 -31
  585. package/dist/core/usecases/fleet.js.map +1 -1
  586. package/dist/core/usecases/operations.js +5 -4
  587. package/dist/core/usecases/operations.js.map +1 -1
  588. package/dist/core/usecases/service/flows.js +9 -6
  589. package/dist/core/usecases/service/flows.js.map +1 -1
  590. package/dist/core/usecases/suites/participants.js +83 -0
  591. package/dist/core/usecases/suites/participants.js.map +1 -0
  592. package/dist/core/usecases/suites/suites.js +45 -0
  593. package/dist/core/usecases/suites/suites.js.map +1 -0
  594. package/dist/core/verify/answers.js +12 -5
  595. package/dist/core/verify/answers.js.map +1 -1
  596. package/dist/core/verify/build.js +102 -82
  597. package/dist/core/verify/build.js.map +1 -1
  598. package/dist/core/verify/checklist.js +3 -0
  599. package/dist/core/verify/checklist.js.map +1 -1
  600. package/dist/core/verify/location.js +67 -0
  601. package/dist/core/verify/location.js.map +1 -0
  602. package/dist/core/verify/pins/lint.js +3 -2
  603. package/dist/core/verify/pins/lint.js.map +1 -1
  604. package/dist/core/verify/pins/pin.js +1 -1
  605. package/dist/core/verify/pins/pin.js.map +1 -1
  606. package/dist/core/verify/read/file.js +224 -0
  607. package/dist/core/verify/read/file.js.map +1 -0
  608. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  609. package/dist/core/verify/read/shape.js.map +1 -0
  610. package/dist/core/verify/read/union.js +105 -0
  611. package/dist/core/verify/read/union.js.map +1 -0
  612. package/dist/core/verify/record.js +35 -12
  613. package/dist/core/verify/record.js.map +1 -1
  614. package/dist/core/verify/store/commit.js +40 -30
  615. package/dist/core/verify/store/commit.js.map +1 -1
  616. package/dist/core/verify/store/render.js +8 -4
  617. package/dist/core/verify/store/render.js.map +1 -1
  618. package/dist/core/vocabulary/issue.js +1 -0
  619. package/dist/core/vocabulary/issue.js.map +1 -1
  620. package/dist/core/vocabulary/maturity.js +12 -2
  621. package/dist/core/vocabulary/maturity.js.map +1 -1
  622. package/examples/README.md +21 -6
  623. package/examples/docs/architecture/landscape.likec4 +8 -0
  624. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  625. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  626. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  627. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  628. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  629. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  630. package/package.json +1 -1
  631. package/dist/commands/validate/checks/vocabulary.js +0 -16
  632. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  633. package/dist/core/agent/agents-md/artifacts.js +0 -221
  634. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  635. package/dist/core/agent/agents-md/cycle.js +0 -180
  636. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  637. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  638. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  639. package/dist/core/agent/agents-md/spine.js +0 -40
  640. package/dist/core/agent/agents-md/spine.js.map +0 -1
  641. package/dist/core/coherence/events/merged.js +0 -40
  642. package/dist/core/coherence/events/merged.js.map +0 -1
  643. package/dist/core/diff/current-state.js +0 -96
  644. package/dist/core/diff/current-state.js.map +0 -1
  645. package/dist/core/diff/victims.js +0 -256
  646. package/dist/core/diff/victims.js.map +0 -1
  647. package/dist/core/staging/snapshot.js.map +0 -1
  648. package/dist/core/verify/file.js.map +0 -1
@@ -1,11 +1,12 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { relative } from "node:path";
2
3
  import { LikeC4 } from "likec4";
3
4
  import { declaredService } from "../kernel/ids/service.js";
4
5
  import { readDynamicViews } from "./parsed/dynamic-views.js";
5
6
  import { readGlobalStyleIds, readViewIds } from "./parsed/view-ids.js";
6
7
  import { readSpecification } from "./parsed/specification.js";
7
8
  import { readDeployment } from "./parsed/deployment.js";
8
- import { descText, metaKey } from "./parsed/values.js";
9
+ import { compareRels, descText, metaKey } from "./parsed/values.js";
9
10
  /**
10
11
  * The one spelling of a diagnostic loam prints — `L<n>: <message>`, with the
11
12
  * line 1-BASED: the number an editor's gutter shows, so an agent told to open
@@ -23,9 +24,115 @@ import { descText, metaKey } from "./parsed/values.js";
23
24
  *
24
25
  * Four modules used to hand-roll this ternary; they all read this one now, so
25
26
  * the base cannot drift between them again.
27
+ *
28
+ * The message itself is BOUNDED here too, and for the same reason it lives in
29
+ * one place: every surface that prints a LikeC4 diagnostic — validate, context,
30
+ * show, tasks, doctor, gate, archive — prints it through this function, so this
31
+ * is the one spelling that can keep all of them small. See `boundedMessage`.
26
32
  */
27
33
  export function errorText(e) {
28
- return typeof e.line === "number" ? `L${e.line + 1}: ${e.message}` : e.message;
34
+ const message = boundedMessage(e.message);
35
+ return typeof e.line === "number" ? `L${e.line + 1}: ${message}` : message;
36
+ }
37
+ /**
38
+ * The documents a project's diagnostics name, docs-relative POSIX, deduped and
39
+ * sorted — the one spelling of "which files broke" behind `landscape.invalid`,
40
+ * `spine.landscape-invalid`, the brief's unparseable arm, the work packet and
41
+ * `loam explore`. Those surfaces are read one after another on the same tree,
42
+ * and an agent has to be able to tell that they name the same file. Given ONE
43
+ * error it spells that error's document: validate's detail-line prefix.
44
+ */
45
+ export function brokenDocumentPaths(docsDir, errors) {
46
+ return [...new Set(errors.map((e) => e.sourceFsPath).filter((p) => p !== undefined))]
47
+ .map((abs) => relative(docsDir, abs).split(/[\\/]/).join("/"))
48
+ .sort();
49
+ }
50
+ /**
51
+ * How many alternatives of a parser's "one of these possible Token sequences"
52
+ * list a printed diagnostic keeps — the number `likec4 validate` itself prints
53
+ * before its `...`.
54
+ */
55
+ const TOKEN_ALTERNATIVES = 4;
56
+ /** The heading line of Chevrotain's no-viable-alternative message, whose numbered list follows it. */
57
+ const TOKEN_SEQUENCES = /possible Token sequences:$/;
58
+ /** One numbered alternative in that list: ` 12. [IdTerminal, Dot]`. */
59
+ const ALTERNATIVE = /^\s+\d+\. /;
60
+ /**
61
+ * A LikeC4 message, with Chevrotain's list of expected token sequences cut to
62
+ * its first few alternatives and a count of the rest.
63
+ *
64
+ * When the parser meets a token no rule can take, Chevrotain lists EVERY token
65
+ * sequence any rule could have accepted there — and inside a LikeC4 `model { }`
66
+ * block that is the whole grammar. Measured at the 1.59.2 pin: deleting the
67
+ * closing quote of one `technology '…'` makes three such messages of about
68
+ * 6,160 lines each, and `loam validate --service` printed 18,503 lines
69
+ * (434 KB; 458 KB as `--json`) for one keystroke. `capDetails` bounds how MANY
70
+ * details a finding prints, never how big one is, and `loam context` has no cap
71
+ * at all — so a broken feature delta turned a 24 KB briefing into 626 KB for
72
+ * every agent working on any service the feature touches. An agent in the
73
+ * check-fix loop gets nothing from alternative 4,000: what it acts on is the
74
+ * line number and the `but found: '…'` line, which is kept.
75
+ *
76
+ * Upstream `likec4 validate` keeps the first five lines of any message and
77
+ * prints `...`; this cuts the LIST only, so the `but found:` line after it
78
+ * survives, and says how many alternatives were dropped so nothing reads as
79
+ * complete when it is not (the convention `capDetails` uses). Any other
80
+ * message is returned exactly as LikeC4 wrote it.
81
+ */
82
+ function boundedMessage(message) {
83
+ const lines = message.split("\n");
84
+ if (!TOKEN_SEQUENCES.test(lines[0] ?? ""))
85
+ return message;
86
+ const alternatives = lines.filter((line) => ALTERNATIVE.test(line)).length;
87
+ if (alternatives <= TOKEN_ALTERNATIVES)
88
+ return message;
89
+ const kept = [];
90
+ let seen = 0;
91
+ for (const line of lines) {
92
+ if (!ALTERNATIVE.test(line))
93
+ kept.push(line);
94
+ else if (++seen <= TOKEN_ALTERNATIVES)
95
+ kept.push(line);
96
+ else if (seen === TOKEN_ALTERNATIVES + 1)
97
+ kept.push(` … (+${alternatives - TOKEN_ALTERNATIVES} more)`);
98
+ }
99
+ return kept.join("\n");
100
+ }
101
+ /**
102
+ * Diagnostics as loam keeps them: each one ONCE, in line order.
103
+ *
104
+ * LikeC4 can report one fault more than once: measured at the 1.59.2 pin, one
105
+ * dropped quote in a service model came back as two byte-identical messages at
106
+ * the same line — each of them the full token list `boundedMessage` cuts — and
107
+ * every `details` list printed both. The same message at the same line of the
108
+ * same file is one fault; the identity includes the file because LikeC4 blames
109
+ * a duplicate declaration on BOTH documents of a project, and those are two
110
+ * lines an author has to look at.
111
+ *
112
+ * In line order because LikeC4 hands its lexer's errors over before its
113
+ * parser's: the first location printed for that dropped quote was an
114
+ * "unexpected character" twelve lines BELOW it, where the runaway string ended,
115
+ * while the parser's errors two lines below it came after. The sort is stable,
116
+ * so diagnostics on one line keep LikeC4's order, and a list mixing documents
117
+ * stays in line order within each of them — every loader groups by document
118
+ * after this, which is why sorting by line alone is enough. A diagnostic with
119
+ * no line is about the document as a whole and leads.
120
+ *
121
+ * Applied where `getErrors()` is read (`loadSource`, `./workspace.ts`,
122
+ * `./project/load.ts`), so every loader hands every reader the same list and
123
+ * test/likec4-batch-parity.test.ts can keep asserting the two modes equal.
124
+ */
125
+ export function distinctErrors(errors) {
126
+ const seen = new Set();
127
+ return errors
128
+ .filter((e) => {
129
+ const key = `${e.sourceFsPath ?? ""}\0${e.line ?? ""}\0${e.message}`;
130
+ if (seen.has(key))
131
+ return false;
132
+ seen.add(key);
133
+ return true;
134
+ })
135
+ .sort((a, b) => (a.line ?? -1) - (b.line ?? -1));
29
136
  }
30
137
  /** Flatten a LikeC4 model into loam's neutral `Elem`/`Rel` view. */
31
138
  export function flattenModel(model) {
@@ -43,7 +150,8 @@ export function flattenModel(model) {
43
150
  tags: [...(e.tags ?? [])],
44
151
  };
45
152
  });
46
- const relationships = [...model.relationships()].map((r) => ({
153
+ const relationships = [...model.relationships()]
154
+ .map((r) => ({
47
155
  source: r.source.id,
48
156
  target: r.target.id,
49
157
  // LikeC4 reports an untitled edge as title: null — normalize to the declared `title?: string`.
@@ -52,7 +160,9 @@ export function flattenModel(model) {
52
160
  publishes: metaKey(r.metadata, "publishes"),
53
161
  consumes: metaKey(r.metadata, "consumes"),
54
162
  tags: [...(r.tags ?? [])],
55
- }));
163
+ }))
164
+ // One order for every run and machine: `compareRels` (./parsed/values.ts) says why LikeC4 gives none.
165
+ .sort(compareRels);
56
166
  return { elements, relationships };
57
167
  }
58
168
  /**
@@ -79,8 +189,9 @@ export async function loadSource(src) {
79
189
  // whose element type is structurally wider than ours, so the compiler can
80
190
  // check the shape here. The cast was the only thing standing between a
81
191
  // dependency renaming `message` and a build that still succeeded — and the
82
- // `?? []` it carried could never fire.
83
- const errors = likec4.getErrors();
192
+ // `?? []` it carried could never fire. Through `distinctErrors`, as every
193
+ // loader's diagnostics are, so this one agrees with the batch loader.
194
+ const errors = distinctErrors(likec4.getErrors());
84
195
  if (errors.length > 0) {
85
196
  return { errors, elements: [], relationships: [] };
86
197
  }
@@ -94,19 +205,39 @@ export async function loadSource(src) {
94
205
  // a second into minutes. The elements and relationships below are identical
95
206
  // either way. The `await` is load-bearing for anything reading `$data`: it is
96
207
  // undefined on the unresolved promise.
97
- const model = (await likec4.parsedModel());
98
- return {
99
- errors,
100
- specification: readSpecification(model.specification),
101
- views: readDynamicViews(model),
102
- viewIds: readViewIds(model),
103
- globalStyles: readGlobalStyleIds(model),
104
- deployment: readDeployment(model),
105
- ...flattenModel(model),
106
- };
208
+ return cleanDoc((await likec4.parsedModel()));
107
209
  }
108
210
  finally {
109
211
  await likec4.dispose();
110
212
  }
111
213
  }
214
+ /**
215
+ * Everything loam reads out of one document that parsed clean, from LikeC4's
216
+ * parsed stage. Shared with `./workspace.ts`'s batch loader, which used to
217
+ * spell the same six reads out again: an adapter wired into one loader and not
218
+ * the other reads as an absence rather than an error, the failure
219
+ * test/likec4-batch-parity.test.ts exists to catch. `./project/load.ts`'s
220
+ * `cleanProjectDoc` wraps the same reads in a project's record, whose error
221
+ * shape differs and which requires the fields `LoadedDoc` leaves optional.
222
+ */
223
+ export function cleanDoc(model) {
224
+ return { errors: [], ...readParsedModel(model) };
225
+ }
226
+ /**
227
+ * The six reads themselves — one spelling under every loader (`cleanDoc`
228
+ * above, `./project/load.ts`'s `cleanProjectDoc`), so the parity test guards
229
+ * one function rather than a convention. The return type is inferred on
230
+ * purpose: it is `LoadedDoc` minus `errors` with every read PRESENT, which is
231
+ * what lets a project's record, whose fields are required, spread it.
232
+ */
233
+ export function readParsedModel(model) {
234
+ return {
235
+ specification: readSpecification(model.specification),
236
+ views: readDynamicViews(model),
237
+ viewIds: readViewIds(model),
238
+ globalStyles: readGlobalStyleIds(model),
239
+ deployment: readDeployment(model),
240
+ ...flattenModel(model),
241
+ };
242
+ }
112
243
  //# sourceMappingURL=likec4.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"likec4.js","sourceRoot":"","sources":["../../../src/core/c4/likec4.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { LikeC4 } from \"likec4\";\nimport { declaredService, type DeclaredService } from \"../kernel/ids/service.js\";\nimport { readDynamicViews, type ParsedView } from \"./parsed/dynamic-views.js\";\nimport { readGlobalStyleIds, readViewIds, type ViewIdClaim } from \"./parsed/view-ids.js\";\nimport { readSpecification, type DocSpecification } from \"./parsed/specification.js\";\nimport { readDeployment, type DeploymentModel } from \"./parsed/deployment.js\";\nimport { descText, metaKey } from \"./parsed/values.js\";\n\n/** A parse/validation issue reported by LikeC4. */\nexport interface LikeC4Error {\n message: string;\n /** As LikeC4 hands it over: an LSP range line, which starts at 0. Print it through `errorText`. */\n line?: number;\n sourceFsPath?: string;\n}\n\n/**\n * The one spelling of a diagnostic loam prints — `L<n>: <message>`, with the\n * line 1-BASED: the number an editor's gutter shows, so an agent told to open\n * it opens the line the fault is on. LikeC4's `getErrors()` copies the LSP\n * range's start line, which counts from 0, and every message used to print that\n * raw number: the named line landed one above the fault every time\n * (verification 2026-09-04, W6).\n *\n * `likec4 validate` DOES NOT AGREE, and that is deliberate rather than\n * unnoticed. Measured at the 1.59.2 pin on a document whose fault is on file\n * line 10: the CLI prints `Line 9:` and loam prints `L10:` for the same\n * diagnostic — it renders the LSP index raw. Copying that would make loam's\n * output wrong in the way the fix above repaired, so the two spellings differ\n * by exactly one and a reader comparing them should expect it.\n *\n * Four modules used to hand-roll this ternary; they all read this one now, so\n * the base cannot drift between them again.\n */\nexport function errorText(e: LikeC4Error): string {\n return typeof e.line === \"number\" ? `L${e.line + 1}: ${e.message}` : e.message;\n}\n\n/** loam-neutral element view (flattened from the LikeC4 parsed model). */\nexport interface Elem {\n id: string;\n kind: string;\n title: string;\n description?: string;\n /**\n * The `services/<id>` directory this element stands for, from the element's\n * `metadata { service '...' }`. Absent on the elements nobody has bound —\n * see `elementService` for the fallback.\n */\n service?: DeclaredService;\n tags: string[];\n}\n\n/** loam-neutral relationship view. */\nexport interface Rel {\n source: string;\n target: string;\n title?: string;\n /** OpenAPI operationId this call uses, from the relationship's `metadata { op '...' }`. */\n op?: string;\n /**\n * AsyncAPI message this edge PRODUCES, from `metadata { publishes '...' }`.\n * Resolved against the edge SOURCE's own contract: a service publishes what it\n * declares an `action: send` for.\n */\n publishes?: string;\n /**\n * AsyncAPI message this edge CONSUMES, from `metadata { consumes '...' }`.\n * Two keys rather than one directional `message`, because the async spine is\n * not symmetric with the HTTP one: there the PROVIDER owns the contract and\n * every consumer is checked against it, while here the PRODUCER owns the\n * message and the consumer lives in another repository entirely. Which side of\n * an edge owes the declaration has to be readable from the edge itself — the\n * arrow cannot say it, because a Kafka edge points at the broker from both\n * ends.\n */\n consumes?: string;\n tags: string[];\n}\n\nexport interface LoadedDoc {\n errors: LikeC4Error[];\n elements: Elem[];\n relationships: Rel[];\n /**\n * The document's `specification { }` block — what its KINDS declare, as\n * opposed to what its elements do. Optional because a document that did not\n * parse has none, and because the several `LoadedDoc` literals standing in for\n * an absent file (`show`'s missing model.likec4, `unreadableLandscape`) have\n * nothing to put here: a reader must treat \"absent\" and \"declares nothing\" the\n * same way. See `./parsed/specification.ts` for why loam reads it at all —\n * short version: since LikeC4 1.59.0 a kind can carry tags, and only the\n * specification can tell an inherited tag from an authored one.\n */\n specification?: DocSpecification;\n /**\n * The `dynamic view`s the document DECLARES — the ordered steps of a use\n * case, never a rendering (docs/DESIGN.md rule 26). Optional for exactly the\n * reason `specification` is: a document that did not parse has none, and the\n * several `LoadedDoc` literals standing in for an absent file have nothing to\n * put here. Absent and empty mean the same thing to every reader — no views\n * were declared, or none could be read — and neither is a finding: a fleet\n * that draws no diagrams owes loam no views block.\n */\n views?: ParsedView[];\n /**\n * The ids of every view the document AUTHORS, static ones included — a census,\n * not a read of what any of them shows. Separate from `views` above because\n * the two answer different questions off different filters (see\n * `./parsed/view-ids.ts`), and because a static view contributes an id and\n * nothing else loam may look at.\n */\n viewIds?: ViewIdClaim[];\n /**\n * The ids of every GLOBAL STYLE the document declares — `global { styleGroup\n * <id> { … } }` and the single-rule `global { style <id> … }` alike, since\n * LikeC4 files both under one id table (hence not `styleGroups`). A\n * declaration census exactly as `viewIds` is: the ids, sorted, and never a\n * rule inside one (docs/DESIGN.md rule 26 says why a style is a rendering\n * instruction loam may not read). One consumer — the generated subsystem\n * views, which reference the group named `subsystems` when it is declared\n * and nothing otherwise, because a reference to an undeclared id is a parse\n * error that blanks the whole `architecture/` project in the renderer.\n * Optional for the reason `views` is: a document that did not parse has\n * none, and the `LoadedDoc` literals standing in for an absent file have\n * nothing to put here; absent and empty mean the same thing to every reader.\n */\n globalStyles?: string[];\n /**\n * The `deployment { }` model the document declares — nodes, instances and the\n * edges between them (`./parsed/deployment.ts`). Optional for the reason\n * `views` is, and read the same way by every consumer: absent and empty mean\n * \"no topology\", and neither is a finding. A fleet that draws no deployment\n * owes loam none, which is also the whole opt-in the axis has.\n */\n deployment?: DeploymentModel;\n}\n\n/**\n * The slice of a LikeC4 model loam reads: elements and relationships, and on\n * each of those only id/kind/title/description/tags/metadata.\n *\n * It is a type of its own, rather than an inline cast at the one call site,\n * because BOTH of LikeC4's model stages expose exactly this slice — which is\n * what makes the cheap stage a safe substitute for the expensive one. The\n * substitution is pinned in test/likec4-model-parity.test.ts, which flattens\n * both stages through `flattenModel` below and compares the results.\n *\n * Views are deliberately NOT in this interface, even though loam does read them.\n * What this interface pins is the two-stage PARITY that makes the cheap stage a\n * safe substitute for the expensive one — and the stages are not claimed to\n * agree about views, nor need to: the parsed stage is the only one rule 26\n * permits. That read has its own adapter, `./parsed/dynamic-views.ts`.\n */\nexport interface ReadableModel {\n elements: () => Iterable<{\n id: string;\n kind: string;\n title: string;\n description?: unknown;\n tags?: readonly string[];\n metadata?: unknown;\n }>;\n relationships: () => Iterable<{\n source: { id: string };\n target: { id: string };\n title?: string | null;\n tags?: readonly string[];\n metadata?: unknown;\n }>;\n /**\n * The parsed `specification { }` block. Typed `unknown` and normalized by\n * `readSpecification` rather than described here, because it is NOT part of\n * the two-stage parity this interface pins: the shape is LikeC4's internal\n * record (1.59.2 spells element-kind tags and relationship-kind tags\n * differently), and writing it out here would be a structural claim about a\n * dependency's internals that the compiler would then enforce against us.\n */\n specification?: unknown;\n}\n\n/** Flatten a LikeC4 model into loam's neutral `Elem`/`Rel` view. */\nexport function flattenModel(model: ReadableModel): { elements: Elem[]; relationships: Rel[] } {\n const elements: Elem[] = [...model.elements()].map((e) => {\n // The one crossing between LikeC4's parse output and loam's type system:\n // a `metadata { service '...' }` binding is text somebody wrote, and this\n // is where it acquires a type that says so.\n const bound = metaKey(e.metadata, \"service\");\n return {\n id: e.id,\n kind: e.kind,\n title: e.title,\n description: descText(e.description),\n ...(bound === undefined ? {} : { service: declaredService(bound) }),\n tags: [...(e.tags ?? [])],\n };\n });\n const relationships: Rel[] = [...model.relationships()].map((r) => ({\n source: r.source.id,\n target: r.target.id,\n // LikeC4 reports an untitled edge as title: null — normalize to the declared `title?: string`.\n title: r.title ?? undefined,\n op: metaKey(r.metadata, \"op\"),\n publishes: metaKey(r.metadata, \"publishes\"),\n consumes: metaKey(r.metadata, \"consumes\"),\n tags: [...(r.tags ?? [])],\n }));\n return { elements, relationships };\n}\n\n/**\n * Load and validate a single self-contained `.likec4` document, in-process\n * (no external tool, no JVM). Returns validation errors and, if clean, the\n * flattened elements + relationships.\n */\nexport async function loadFile(path: string): Promise<LoadedDoc> {\n return loadSource(await readFile(path, \"utf8\"));\n}\n\n/**\n * `loadFile` for text that is not (yet) on disk — what the archive merge uses\n * to prove a computed landscape parses BEFORE anything is written.\n */\nexport async function loadSource(src: string): Promise<LoadedDoc> {\n // LikeC4 owns a Langium workspace and registers a process-exit listener for\n // it. Validation creates many short-lived instances, so every acquired\n // instance must be disposed even on an invalid document or a failed model\n // computation. Its logger is disabled because diagnostics are returned in\n // LoadedDoc instead of being written out-of-band to stderr.\n const likec4 = await LikeC4.fromSource(src, { logger: false });\n try {\n // Assigned, not cast: LikeC4 declares `getErrors()` as a non-nullable array\n // whose element type is structurally wider than ours, so the compiler can\n // check the shape here. The cast was the only thing standing between a\n // dependency renaming `message` and a build that still succeeded — and the\n // `?? []` it carried could never fire.\n const errors: LikeC4Error[] = likec4.getErrors();\n if (errors.length > 0) {\n return { errors, elements: [], relationships: [] };\n }\n\n // `parsedModel`, not `computedModel`: the computed stage RESOLVES every\n // view — expanding its predicates against the model and deriving the\n // ancestor edges a diagram needs (and computing a default view when the\n // document declares none). loam reads what a view DECLARES and never what a\n // view SHOWS (docs/DESIGN.md rule 26), so none of that resolution is ever\n // wanted; it renders nothing. That work is superlinear in the number of\n // RELATIONSHIPS, so a landscape at fleet shape turned `loam list` from under\n // a second into minutes. The elements and relationships below are identical\n // either way. The `await` is load-bearing for anything reading `$data`: it is\n // undefined on the unresolved promise.\n const model = (await likec4.parsedModel()) as ReadableModel;\n return {\n errors,\n specification: readSpecification(model.specification),\n views: readDynamicViews(model),\n viewIds: readViewIds(model),\n globalStyles: readGlobalStyleIds(model),\n deployment: readDeployment(model),\n ...flattenModel(model),\n };\n } finally {\n await likec4.dispose();\n }\n}\n"]}
1
+ {"version":3,"file":"likec4.js","sourceRoot":"","sources":["../../../src/core/c4/likec4.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { relative } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { declaredService, type DeclaredService } from \"../kernel/ids/service.js\";\nimport { readDynamicViews, type ParsedView } from \"./parsed/dynamic-views.js\";\nimport { readGlobalStyleIds, readViewIds, type ViewIdClaim } from \"./parsed/view-ids.js\";\nimport { readSpecification, type DocSpecification } from \"./parsed/specification.js\";\nimport { readDeployment, type DeploymentModel } from \"./parsed/deployment.js\";\nimport { compareRels, descText, metaKey } from \"./parsed/values.js\";\n\n/** A parse/validation issue reported by LikeC4. */\nexport interface LikeC4Error {\n message: string;\n /** As LikeC4 hands it over: an LSP range line, which starts at 0. Print it through `errorText`. */\n line?: number;\n sourceFsPath?: string;\n}\n\n/**\n * The one spelling of a diagnostic loam prints — `L<n>: <message>`, with the\n * line 1-BASED: the number an editor's gutter shows, so an agent told to open\n * it opens the line the fault is on. LikeC4's `getErrors()` copies the LSP\n * range's start line, which counts from 0, and every message used to print that\n * raw number: the named line landed one above the fault every time\n * (verification 2026-09-04, W6).\n *\n * `likec4 validate` DOES NOT AGREE, and that is deliberate rather than\n * unnoticed. Measured at the 1.59.2 pin on a document whose fault is on file\n * line 10: the CLI prints `Line 9:` and loam prints `L10:` for the same\n * diagnostic — it renders the LSP index raw. Copying that would make loam's\n * output wrong in the way the fix above repaired, so the two spellings differ\n * by exactly one and a reader comparing them should expect it.\n *\n * Four modules used to hand-roll this ternary; they all read this one now, so\n * the base cannot drift between them again.\n *\n * The message itself is BOUNDED here too, and for the same reason it lives in\n * one place: every surface that prints a LikeC4 diagnostic — validate, context,\n * show, tasks, doctor, gate, archive — prints it through this function, so this\n * is the one spelling that can keep all of them small. See `boundedMessage`.\n */\nexport function errorText(e: LikeC4Error): string {\n const message = boundedMessage(e.message);\n return typeof e.line === \"number\" ? `L${e.line + 1}: ${message}` : message;\n}\n\n/**\n * The documents a project's diagnostics name, docs-relative POSIX, deduped and\n * sorted — the one spelling of \"which files broke\" behind `landscape.invalid`,\n * `spine.landscape-invalid`, the brief's unparseable arm, the work packet and\n * `loam explore`. Those surfaces are read one after another on the same tree,\n * and an agent has to be able to tell that they name the same file. Given ONE\n * error it spells that error's document: validate's detail-line prefix.\n */\nexport function brokenDocumentPaths(docsDir: string, errors: readonly LikeC4Error[]): string[] {\n return [...new Set(errors.map((e) => e.sourceFsPath).filter((p): p is string => p !== undefined))]\n .map((abs) => relative(docsDir, abs).split(/[\\\\/]/).join(\"/\"))\n .sort();\n}\n\n/**\n * How many alternatives of a parser's \"one of these possible Token sequences\"\n * list a printed diagnostic keeps — the number `likec4 validate` itself prints\n * before its `...`.\n */\nconst TOKEN_ALTERNATIVES = 4;\n\n/** The heading line of Chevrotain's no-viable-alternative message, whose numbered list follows it. */\nconst TOKEN_SEQUENCES = /possible Token sequences:$/;\n\n/** One numbered alternative in that list: ` 12. [IdTerminal, Dot]`. */\nconst ALTERNATIVE = /^\\s+\\d+\\. /;\n\n/**\n * A LikeC4 message, with Chevrotain's list of expected token sequences cut to\n * its first few alternatives and a count of the rest.\n *\n * When the parser meets a token no rule can take, Chevrotain lists EVERY token\n * sequence any rule could have accepted there — and inside a LikeC4 `model { }`\n * block that is the whole grammar. Measured at the 1.59.2 pin: deleting the\n * closing quote of one `technology '…'` makes three such messages of about\n * 6,160 lines each, and `loam validate --service` printed 18,503 lines\n * (434 KB; 458 KB as `--json`) for one keystroke. `capDetails` bounds how MANY\n * details a finding prints, never how big one is, and `loam context` has no cap\n * at all — so a broken feature delta turned a 24 KB briefing into 626 KB for\n * every agent working on any service the feature touches. An agent in the\n * check-fix loop gets nothing from alternative 4,000: what it acts on is the\n * line number and the `but found: '…'` line, which is kept.\n *\n * Upstream `likec4 validate` keeps the first five lines of any message and\n * prints `...`; this cuts the LIST only, so the `but found:` line after it\n * survives, and says how many alternatives were dropped so nothing reads as\n * complete when it is not (the convention `capDetails` uses). Any other\n * message is returned exactly as LikeC4 wrote it.\n */\nfunction boundedMessage(message: string): string {\n const lines = message.split(\"\\n\");\n if (!TOKEN_SEQUENCES.test(lines[0] ?? \"\")) return message;\n const alternatives = lines.filter((line) => ALTERNATIVE.test(line)).length;\n if (alternatives <= TOKEN_ALTERNATIVES) return message;\n const kept: string[] = [];\n let seen = 0;\n for (const line of lines) {\n if (!ALTERNATIVE.test(line)) kept.push(line);\n else if (++seen <= TOKEN_ALTERNATIVES) kept.push(line);\n else if (seen === TOKEN_ALTERNATIVES + 1) kept.push(` … (+${alternatives - TOKEN_ALTERNATIVES} more)`);\n }\n return kept.join(\"\\n\");\n}\n\n/**\n * Diagnostics as loam keeps them: each one ONCE, in line order.\n *\n * LikeC4 can report one fault more than once: measured at the 1.59.2 pin, one\n * dropped quote in a service model came back as two byte-identical messages at\n * the same line — each of them the full token list `boundedMessage` cuts — and\n * every `details` list printed both. The same message at the same line of the\n * same file is one fault; the identity includes the file because LikeC4 blames\n * a duplicate declaration on BOTH documents of a project, and those are two\n * lines an author has to look at.\n *\n * In line order because LikeC4 hands its lexer's errors over before its\n * parser's: the first location printed for that dropped quote was an\n * \"unexpected character\" twelve lines BELOW it, where the runaway string ended,\n * while the parser's errors two lines below it came after. The sort is stable,\n * so diagnostics on one line keep LikeC4's order, and a list mixing documents\n * stays in line order within each of them — every loader groups by document\n * after this, which is why sorting by line alone is enough. A diagnostic with\n * no line is about the document as a whole and leads.\n *\n * Applied where `getErrors()` is read (`loadSource`, `./workspace.ts`,\n * `./project/load.ts`), so every loader hands every reader the same list and\n * test/likec4-batch-parity.test.ts can keep asserting the two modes equal.\n */\nexport function distinctErrors<E extends LikeC4Error>(errors: readonly E[]): E[] {\n const seen = new Set<string>();\n return errors\n .filter((e) => {\n const key = `${e.sourceFsPath ?? \"\"}\\0${e.line ?? \"\"}\\0${e.message}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n })\n .sort((a, b) => (a.line ?? -1) - (b.line ?? -1));\n}\n\n/** loam-neutral element view (flattened from the LikeC4 parsed model). */\nexport interface Elem {\n id: string;\n kind: string;\n title: string;\n description?: string;\n /**\n * The `services/<id>` directory this element stands for, from the element's\n * `metadata { service '...' }`. Absent on the elements nobody has bound —\n * see `elementService` for the fallback.\n */\n service?: DeclaredService;\n tags: string[];\n}\n\n/** loam-neutral relationship view. */\nexport interface Rel {\n source: string;\n target: string;\n title?: string;\n /** OpenAPI operationId this call uses, from the relationship's `metadata { op '...' }`. */\n op?: string;\n /**\n * AsyncAPI message this edge PRODUCES, from `metadata { publishes '...' }`.\n * Resolved against the edge SOURCE's own contract: a service publishes what it\n * declares an `action: send` for.\n */\n publishes?: string;\n /**\n * AsyncAPI message this edge CONSUMES, from `metadata { consumes '...' }`.\n * Two keys rather than one directional `message`, because the async spine is\n * not symmetric with the HTTP one: there the PROVIDER owns the contract and\n * every consumer is checked against it, while here the PRODUCER owns the\n * message and the consumer lives in another repository entirely. Which side of\n * an edge owes the declaration has to be readable from the edge itself — the\n * arrow cannot say it, because a Kafka edge points at the broker from both\n * ends.\n */\n consumes?: string;\n tags: string[];\n}\n\nexport interface LoadedDoc {\n errors: LikeC4Error[];\n elements: Elem[];\n relationships: Rel[];\n /**\n * The document's `specification { }` block — what its KINDS declare, as\n * opposed to what its elements do. Optional because a document that did not\n * parse has none, and because the several `LoadedDoc` literals standing in for\n * an absent file (`show`'s missing model.likec4, `unreadableLandscape`) have\n * nothing to put here: a reader must treat \"absent\" and \"declares nothing\" the\n * same way. See `./parsed/specification.ts` for why loam reads it at all —\n * short version: since LikeC4 1.59.0 a kind can carry tags, and only the\n * specification can tell an inherited tag from an authored one.\n */\n specification?: DocSpecification;\n /**\n * The `dynamic view`s the document DECLARES — the ordered steps of a use\n * case, never a rendering (docs/DESIGN.md rule 26). Optional for exactly the\n * reason `specification` is: a document that did not parse has none, and the\n * several `LoadedDoc` literals standing in for an absent file have nothing to\n * put here. Absent and empty mean the same thing to every reader — no views\n * were declared, or none could be read — and neither is a finding: a fleet\n * that draws no diagrams owes loam no views block.\n */\n views?: ParsedView[];\n /**\n * The ids of every view the document AUTHORS, static ones included — a census,\n * not a read of what any of them shows. Separate from `views` above because\n * the two answer different questions off different filters (see\n * `./parsed/view-ids.ts`), and because a static view contributes an id and\n * nothing else loam may look at.\n */\n viewIds?: ViewIdClaim[];\n /**\n * The ids of every GLOBAL STYLE the document declares — `global { styleGroup\n * <id> { … } }` and the single-rule `global { style <id> … }` alike, since\n * LikeC4 files both under one id table (hence not `styleGroups`). A\n * declaration census exactly as `viewIds` is: the ids, sorted, and never a\n * rule inside one (docs/DESIGN.md rule 26 says why a style is a rendering\n * instruction loam may not read). One consumer — the generated subsystem\n * views, which reference the group named `subsystems` when it is declared\n * and nothing otherwise, because a reference to an undeclared id is a parse\n * error that blanks the whole `architecture/` project in the renderer.\n * Optional for the reason `views` is: a document that did not parse has\n * none, and the `LoadedDoc` literals standing in for an absent file have\n * nothing to put here; absent and empty mean the same thing to every reader.\n */\n globalStyles?: string[];\n /**\n * The `deployment { }` model the document declares — nodes, instances and the\n * edges between them (`./parsed/deployment.ts`). Optional for the reason\n * `views` is, and read the same way by every consumer: absent and empty mean\n * \"no topology\", and neither is a finding. A fleet that draws no deployment\n * owes loam none, which is also the whole opt-in the axis has.\n */\n deployment?: DeploymentModel;\n}\n\n/**\n * The slice of a LikeC4 model loam reads: elements and relationships, and on\n * each of those only id/kind/title/description/tags/metadata.\n *\n * It is a type of its own, rather than an inline cast at the one call site,\n * because BOTH of LikeC4's model stages expose exactly this slice — which is\n * what makes the cheap stage a safe substitute for the expensive one. The\n * substitution is pinned in test/likec4-model-parity.test.ts, which flattens\n * both stages through `flattenModel` below and compares the results.\n *\n * Views are deliberately NOT in this interface, even though loam does read them.\n * What this interface pins is the two-stage PARITY that makes the cheap stage a\n * safe substitute for the expensive one — and the stages are not claimed to\n * agree about views, nor need to: the parsed stage is the only one rule 26\n * permits. That read has its own adapter, `./parsed/dynamic-views.ts`.\n */\nexport interface ReadableModel {\n elements: () => Iterable<{\n id: string;\n kind: string;\n title: string;\n description?: unknown;\n tags?: readonly string[];\n metadata?: unknown;\n }>;\n relationships: () => Iterable<{\n source: { id: string };\n target: { id: string };\n title?: string | null;\n tags?: readonly string[];\n metadata?: unknown;\n }>;\n /**\n * The parsed `specification { }` block. Typed `unknown` and normalized by\n * `readSpecification` rather than described here, because it is NOT part of\n * the two-stage parity this interface pins: the shape is LikeC4's internal\n * record (1.59.2 spells element-kind tags and relationship-kind tags\n * differently), and writing it out here would be a structural claim about a\n * dependency's internals that the compiler would then enforce against us.\n */\n specification?: unknown;\n}\n\n/** Flatten a LikeC4 model into loam's neutral `Elem`/`Rel` view. */\nexport function flattenModel(model: ReadableModel): { elements: Elem[]; relationships: Rel[] } {\n const elements: Elem[] = [...model.elements()].map((e) => {\n // The one crossing between LikeC4's parse output and loam's type system:\n // a `metadata { service '...' }` binding is text somebody wrote, and this\n // is where it acquires a type that says so.\n const bound = metaKey(e.metadata, \"service\");\n return {\n id: e.id,\n kind: e.kind,\n title: e.title,\n description: descText(e.description),\n ...(bound === undefined ? {} : { service: declaredService(bound) }),\n tags: [...(e.tags ?? [])],\n };\n });\n const relationships: Rel[] = [...model.relationships()]\n .map((r) => ({\n source: r.source.id,\n target: r.target.id,\n // LikeC4 reports an untitled edge as title: null — normalize to the declared `title?: string`.\n title: r.title ?? undefined,\n op: metaKey(r.metadata, \"op\"),\n publishes: metaKey(r.metadata, \"publishes\"),\n consumes: metaKey(r.metadata, \"consumes\"),\n tags: [...(r.tags ?? [])],\n }))\n // One order for every run and machine: `compareRels` (./parsed/values.ts) says why LikeC4 gives none.\n .sort(compareRels);\n return { elements, relationships };\n}\n\n/**\n * Load and validate a single self-contained `.likec4` document, in-process\n * (no external tool, no JVM). Returns validation errors and, if clean, the\n * flattened elements + relationships.\n */\nexport async function loadFile(path: string): Promise<LoadedDoc> {\n return loadSource(await readFile(path, \"utf8\"));\n}\n\n/**\n * `loadFile` for text that is not (yet) on disk — what the archive merge uses\n * to prove a computed landscape parses BEFORE anything is written.\n */\nexport async function loadSource(src: string): Promise<LoadedDoc> {\n // LikeC4 owns a Langium workspace and registers a process-exit listener for\n // it. Validation creates many short-lived instances, so every acquired\n // instance must be disposed even on an invalid document or a failed model\n // computation. Its logger is disabled because diagnostics are returned in\n // LoadedDoc instead of being written out-of-band to stderr.\n const likec4 = await LikeC4.fromSource(src, { logger: false });\n try {\n // Assigned, not cast: LikeC4 declares `getErrors()` as a non-nullable array\n // whose element type is structurally wider than ours, so the compiler can\n // check the shape here. The cast was the only thing standing between a\n // dependency renaming `message` and a build that still succeeded — and the\n // `?? []` it carried could never fire. Through `distinctErrors`, as every\n // loader's diagnostics are, so this one agrees with the batch loader.\n const errors: LikeC4Error[] = distinctErrors(likec4.getErrors());\n if (errors.length > 0) {\n return { errors, elements: [], relationships: [] };\n }\n\n // `parsedModel`, not `computedModel`: the computed stage RESOLVES every\n // view — expanding its predicates against the model and deriving the\n // ancestor edges a diagram needs (and computing a default view when the\n // document declares none). loam reads what a view DECLARES and never what a\n // view SHOWS (docs/DESIGN.md rule 26), so none of that resolution is ever\n // wanted; it renders nothing. That work is superlinear in the number of\n // RELATIONSHIPS, so a landscape at fleet shape turned `loam list` from under\n // a second into minutes. The elements and relationships below are identical\n // either way. The `await` is load-bearing for anything reading `$data`: it is\n // undefined on the unresolved promise.\n return cleanDoc((await likec4.parsedModel()) as ReadableModel);\n } finally {\n await likec4.dispose();\n }\n}\n\n/**\n * Everything loam reads out of one document that parsed clean, from LikeC4's\n * parsed stage. Shared with `./workspace.ts`'s batch loader, which used to\n * spell the same six reads out again: an adapter wired into one loader and not\n * the other reads as an absence rather than an error, the failure\n * test/likec4-batch-parity.test.ts exists to catch. `./project/load.ts`'s\n * `cleanProjectDoc` wraps the same reads in a project's record, whose error\n * shape differs and which requires the fields `LoadedDoc` leaves optional.\n */\nexport function cleanDoc(model: ReadableModel): LoadedDoc {\n return { errors: [], ...readParsedModel(model) };\n}\n\n/**\n * The six reads themselves — one spelling under every loader (`cleanDoc`\n * above, `./project/load.ts`'s `cleanProjectDoc`), so the parity test guards\n * one function rather than a convention. The return type is inferred on\n * purpose: it is `LoadedDoc` minus `errors` with every read PRESENT, which is\n * what lets a project's record, whose fields are required, spread it.\n */\nexport function readParsedModel(model: ReadableModel) {\n return {\n specification: readSpecification(model.specification),\n views: readDynamicViews(model),\n viewIds: readViewIds(model),\n globalStyles: readGlobalStyleIds(model),\n deployment: readDeployment(model),\n ...flattenModel(model),\n };\n}\n"]}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * What the parsed model declares and the splice map cannot locate — named the
3
+ * way an author can act on it.
4
+ *
5
+ * The archive splices the landscape by its text scan (`../source-scan.ts`) and
6
+ * refuses rather than splice around a declaration the scan cannot see. That
7
+ * refusal used to guess its cause: "most often two declarations sharing one
8
+ * line", whatever the map held. A fleet on 0.2.0-alpha.7 read it over a
9
+ * standalone declaration on a line of its own — the scan was blind to a second
10
+ * `model { }` block — and was asked to rewrite a valid map to satisfy a message
11
+ * that described something else. So the cause is no longer guessed where it can
12
+ * be read: LikeC4's own locator says on which line the declaration is, the
13
+ * line itself says whether anything but the declaration's own lead (`Lead`)
14
+ * precedes it there, and the scan says what it read in that place instead.
15
+ *
16
+ * A second parse of the landscape, and it is paid only on the refusal path —
17
+ * the merge decides THAT something is unlocated from the parse it already has,
18
+ * and asks this module only for the words.
19
+ */
20
+ import { LikeC4 } from "likec4";
21
+ import { maskSource } from "../source-mask.js";
22
+ /**
23
+ * The first declaration of `text` that `scan` does not locate — elements in
24
+ * the parser's order, then relationships in document order; null when every
25
+ * one is located.
26
+ *
27
+ * An element is located when the scan holds its id. A relationship has no id
28
+ * the text can carry, so it is located by POSITION: LikeC4 places each edge at
29
+ * its arrow, and each scanned statement may stand for one edge — the second
30
+ * arrow inside one scanned span is the edge the scan swallowed.
31
+ */
32
+ export async function firstUnlocated(text, scan) {
33
+ const likec4 = await LikeC4.fromSource(text, { logger: false });
34
+ try {
35
+ const model = await likec4.parsedModel();
36
+ const where = placer(text, scan);
37
+ const scanned = new Set(scan.elements.map((e) => e.id));
38
+ for (const e of model.elements()) {
39
+ if (scanned.has(e.id))
40
+ continue;
41
+ return { kind: "element", name: e.id, ...where.place(likec4.languageServices.locate({ element: e.id })?.range.start, { kind: e.kind }) };
42
+ }
43
+ const edges = [...model.relationships()].map((r) => {
44
+ const start = likec4.languageServices.locate({ relation: r.id })?.range.start;
45
+ return { name: `${r.source.id} -> ${r.target.id}`, start, offset: start === undefined ? -1 : where.offset(start) };
46
+ });
47
+ const claimed = new Set();
48
+ for (const edge of edges.sort((a, b) => a.offset - b.offset)) {
49
+ if (edge.offset === -1)
50
+ continue;
51
+ const span = scan.rels.findIndex((s) => edge.offset >= s.start && edge.offset < s.end);
52
+ if (span !== -1 && !claimed.has(span)) {
53
+ claimed.add(span);
54
+ continue;
55
+ }
56
+ return { kind: "relationship", name: edge.name, ...where.place(edge.start, "edge") };
57
+ }
58
+ // Every located edge has a statement of its own: an unlocated one, if the
59
+ // count says there is one, is among those the locator could not place.
60
+ const lost = edges.find((edge) => edge.offset === -1);
61
+ return lost === undefined ? null : { kind: "relationship", name: lost.name };
62
+ }
63
+ finally {
64
+ await likec4.dispose();
65
+ }
66
+ }
67
+ /** A whole token — not the tail of a longer id — directly before the anchor, the kind written there. */
68
+ const ELEMENT_LEAD = /(?<![\w.-])([A-Za-z_][\w-]*)\s*$/;
69
+ /** An optional source reference and an optional `-[`, directly before the anchor; always matches, possibly empty. */
70
+ const EDGE_LEAD = /(?<![\w.-])(?:[A-Za-z_][\w.-]*\s*)?(?:-\[\s*)?$/;
71
+ /**
72
+ * Where the declaration anchored at `at` starts, in the masked `code` of the
73
+ * line starting at `from`: before its lead when the code there ends in one,
74
+ * else `at`. Masked, a literal or a URI is blank, so the word before one reads
75
+ * as an edge's source here exactly as `relationHead` reads it in the scan
76
+ * (`link https://… -> zulu` is `link -> zulu` to both). Such a line is a
77
+ * scanned edge of its own, so its edge is unlocated only when an earlier
78
+ * statement's span ran on over it — and then the line-break explanation is
79
+ * the one to give, which is what dropping that word leads to.
80
+ */
81
+ function leadStart(code, span, lead) {
82
+ const before = code.slice(span.from, span.at);
83
+ const m = lead === "edge" ? EDGE_LEAD.exec(before) : ELEMENT_LEAD.exec(before);
84
+ return m === null || (lead !== "edge" && m[1] !== lead.kind) ? span.at : span.from + m.index;
85
+ }
86
+ /**
87
+ * Reads a position against the text and the scan. Lines are split the way LSP
88
+ * counts them — `\r\n`, `\r` or `\n` — because that is the count LikeC4's
89
+ * positions are in. A bare carriage return used to be the one break the scan
90
+ * did not make, and so the cause the "read it as part of" words were written
91
+ * for; `statementHead` breaks there too now, and the words stay for whatever
92
+ * run-on the scan has next.
93
+ */
94
+ function placer(text, scan) {
95
+ const starts = [0];
96
+ for (const m of text.matchAll(/\r\n|\r|\n/g))
97
+ starts.push(m.index + m[0].length);
98
+ const lineOf = (offset) => starts.findLastIndex((s) => s <= offset) + 1;
99
+ const { code } = maskSource(text);
100
+ const offset = (start) => (starts[start.line] ?? text.length) + start.character;
101
+ const heads = [
102
+ ...scan.elements
103
+ .filter((e) => e.extend !== true)
104
+ .map((e) => ({ name: `'${e.id}'`, start: e.start, end: e.bodyOpen === -1 ? e.end : e.bodyOpen })),
105
+ ...scan.rels.map((r) => ({ name: `the edge '${r.source} -> ${r.target}'`, start: r.start, end: r.end })),
106
+ ];
107
+ const place = (start, lead) => {
108
+ if (start === undefined)
109
+ return {};
110
+ const at = offset(start);
111
+ const from = starts[start.line] ?? at;
112
+ // Measured from where the declaration starts, not from the anchor: its own
113
+ // kind keyword or source is no other statement (`Lead`).
114
+ const before = code.slice(from, leadStart(code, { from, at }, lead));
115
+ // Innermost first: a nested declaration's span lies inside its parent's.
116
+ const cover = heads.filter((h) => at >= h.start && at < h.end).sort((a, b) => b.start - a.start)[0];
117
+ return {
118
+ line: start.line + 1,
119
+ ...(/\S/.test(before) ? { sharesLine: true } : {}),
120
+ ...(cover === undefined ? {} : { readAs: { name: cover.name, line: lineOf(cover.start) } }),
121
+ };
122
+ };
123
+ return { place, offset };
124
+ }
125
+ /**
126
+ * The refusal's words for one unlocated declaration of `file`. A shared line
127
+ * is named only when the line shows one, and the statement the scan read
128
+ * instead only when it read one: a guess at the cause is the message this
129
+ * replaced.
130
+ */
131
+ export function unlocatedReason(file, gap) {
132
+ const remedy = "nothing was written; rewrite it on a line of its own as `name = kind 'title' {` (an edge as `a -> b 'title'`) and re-run — " +
133
+ "and report the original shape: LikeC4 parses it, so it is a form the splice map should read";
134
+ if (gap === null)
135
+ return `${file} declares more than placement can locate, and LikeC4's locator could not say where — ${remedy}`;
136
+ const what = `${file} declares ${gap.kind === "element" ? `'${gap.name}'` : `the relationship '${gap.name}'`}`;
137
+ const where = gap.line === undefined ? "" : ` at line ${gap.line}`;
138
+ const read = gap.readAs;
139
+ if (gap.sharesLine === true) {
140
+ const other = read === undefined ? "the statement before it" : read.name;
141
+ return (`${what}${where} in a form placement cannot locate: it shares that line with ${other}, and the splice map reads ` +
142
+ `one statement per line where LikeC4 accepts two — give each its own line, then re-run`);
143
+ }
144
+ if (read !== undefined) {
145
+ return (`${what}${where} in a form placement cannot locate: the splice map read it as part of ${read.name}, which starts at ` +
146
+ `line ${read.line} — it ran that statement on past a line break LikeC4 makes — ${remedy}`);
147
+ }
148
+ return `${what}${where} in a form placement cannot locate, and the splice map read no statement there — ${remedy}`;
149
+ }
150
+ //# sourceMappingURL=unlocated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unlocated.js","sourceRoot":"","sources":["../../../../src/core/c4/locate/unlocated.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the parsed model declares and the splice map cannot locate — named the\n * way an author can act on it.\n *\n * The archive splices the landscape by its text scan (`../source-scan.ts`) and\n * refuses rather than splice around a declaration the scan cannot see. That\n * refusal used to guess its cause: \"most often two declarations sharing one\n * line\", whatever the map held. A fleet on 0.2.0-alpha.7 read it over a\n * standalone declaration on a line of its own — the scan was blind to a second\n * `model { }` block — and was asked to rewrite a valid map to satisfy a message\n * that described something else. So the cause is no longer guessed where it can\n * be read: LikeC4's own locator says on which line the declaration is, the\n * line itself says whether anything but the declaration's own lead (`Lead`)\n * precedes it there, and the scan says what it read in that place instead.\n *\n * A second parse of the landscape, and it is paid only on the refusal path —\n * the merge decides THAT something is unlocated from the parse it already has,\n * and asks this module only for the words.\n */\nimport { LikeC4 } from \"likec4\";\nimport { maskSource } from \"../source-mask.js\";\nimport type { ScannedModel } from \"../source-scan.js\";\n\n/** One declaration the parser has and the scan cannot place. */\nexport interface Unlocated {\n kind: \"element\" | \"relationship\";\n /** The element's id, or the relationship as `source -> target`, as the parser resolved them. */\n name: string;\n /** 1-based line LikeC4's locator puts the declaration on; absent when it has no answer. */\n line?: number;\n /**\n * Code other than the declaration's own lead precedes it on its own line —\n * the shared line is then a FACT, not a guess.\n */\n sharesLine?: true;\n /** The scanned statement that covers where the declaration sits, and the line that statement starts on. */\n readAs?: { name: string; line: number };\n}\n\n/**\n * The first declaration of `text` that `scan` does not locate — elements in\n * the parser's order, then relationships in document order; null when every\n * one is located.\n *\n * An element is located when the scan holds its id. A relationship has no id\n * the text can carry, so it is located by POSITION: LikeC4 places each edge at\n * its arrow, and each scanned statement may stand for one edge — the second\n * arrow inside one scanned span is the edge the scan swallowed.\n */\nexport async function firstUnlocated(text: string, scan: ScannedModel): Promise<Unlocated | null> {\n const likec4 = await LikeC4.fromSource(text, { logger: false });\n try {\n const model = await likec4.parsedModel();\n const where = placer(text, scan);\n const scanned = new Set(scan.elements.map((e) => e.id));\n for (const e of model.elements()) {\n if (scanned.has(e.id)) continue;\n return { kind: \"element\", name: e.id, ...where.place(likec4.languageServices.locate({ element: e.id })?.range.start, { kind: e.kind }) };\n }\n const edges = [...model.relationships()].map((r) => {\n const start = likec4.languageServices.locate({ relation: r.id })?.range.start;\n return { name: `${r.source.id} -> ${r.target.id}`, start, offset: start === undefined ? -1 : where.offset(start) };\n });\n const claimed = new Set<number>();\n for (const edge of edges.sort((a, b) => a.offset - b.offset)) {\n if (edge.offset === -1) continue;\n const span = scan.rels.findIndex((s) => edge.offset >= s.start && edge.offset < s.end);\n if (span !== -1 && !claimed.has(span)) {\n claimed.add(span);\n continue;\n }\n return { kind: \"relationship\", name: edge.name, ...where.place(edge.start, \"edge\") };\n }\n // Every located edge has a statement of its own: an unlocated one, if the\n // count says there is one, is among those the locator could not place.\n const lost = edges.find((edge) => edge.offset === -1);\n return lost === undefined ? null : { kind: \"relationship\", name: lost.name };\n } finally {\n await likec4.dispose();\n }\n}\n\n/** An LSP position: a 0-based line and a character within it. */\ninterface Position {\n line: number;\n character: number;\n}\n\n/**\n * The part of a declaration LikeC4's locator leaves BEFORE its anchor. It\n * points an element at its NAME and a relationship at its ARROW — inside\n * `-[…]->`, at the kind — so `container api 'API'` has its kind keyword in\n * front of the anchor and `zulu -> apple` its source. Read as \"code before\n * it\", every unlocated edge and every `kind name` element shared its line with\n * itself, and the line-break explanation `unlocatedReason` holds below the\n * shared-line one was never reached. An element's lead is a token equal to\n * its parsed kind; an edge's is its source reference (`it`, a dotted path or\n * none) and the `-[` opening a bracketed kind.\n */\ntype Lead = { kind: string } | \"edge\";\n\n/** A whole token — not the tail of a longer id — directly before the anchor, the kind written there. */\nconst ELEMENT_LEAD = /(?<![\\w.-])([A-Za-z_][\\w-]*)\\s*$/;\n/** An optional source reference and an optional `-[`, directly before the anchor; always matches, possibly empty. */\nconst EDGE_LEAD = /(?<![\\w.-])(?:[A-Za-z_][\\w.-]*\\s*)?(?:-\\[\\s*)?$/;\n\n/**\n * Where the declaration anchored at `at` starts, in the masked `code` of the\n * line starting at `from`: before its lead when the code there ends in one,\n * else `at`. Masked, a literal or a URI is blank, so the word before one reads\n * as an edge's source here exactly as `relationHead` reads it in the scan\n * (`link https://… -> zulu` is `link -> zulu` to both). Such a line is a\n * scanned edge of its own, so its edge is unlocated only when an earlier\n * statement's span ran on over it — and then the line-break explanation is\n * the one to give, which is what dropping that word leads to.\n */\nfunction leadStart(code: string, span: { from: number; at: number }, lead: Lead): number {\n const before = code.slice(span.from, span.at);\n const m = lead === \"edge\" ? EDGE_LEAD.exec(before) : ELEMENT_LEAD.exec(before);\n return m === null || (lead !== \"edge\" && m[1] !== lead.kind) ? span.at : span.from + m.index;\n}\n\n/** Everything a position tells about a declaration, and the offset arithmetic that is built on. */\ninterface Placer {\n place: (start: Position | undefined, lead: Lead) => Pick<Unlocated, \"line\" | \"sharesLine\" | \"readAs\">;\n offset: (start: Position) => number;\n}\n\n/**\n * Reads a position against the text and the scan. Lines are split the way LSP\n * counts them — `\\r\\n`, `\\r` or `\\n` — because that is the count LikeC4's\n * positions are in. A bare carriage return used to be the one break the scan\n * did not make, and so the cause the \"read it as part of\" words were written\n * for; `statementHead` breaks there too now, and the words stay for whatever\n * run-on the scan has next.\n */\nfunction placer(text: string, scan: ScannedModel): Placer {\n const starts = [0];\n for (const m of text.matchAll(/\\r\\n|\\r|\\n/g)) starts.push(m.index + m[0].length);\n const lineOf = (offset: number): number => starts.findLastIndex((s) => s <= offset) + 1;\n const { code } = maskSource(text);\n const offset = (start: Position): number => (starts[start.line] ?? text.length) + start.character;\n const heads = [\n ...scan.elements\n .filter((e) => e.extend !== true)\n .map((e) => ({ name: `'${e.id}'`, start: e.start, end: e.bodyOpen === -1 ? e.end : e.bodyOpen })),\n ...scan.rels.map((r) => ({ name: `the edge '${r.source} -> ${r.target}'`, start: r.start, end: r.end })),\n ];\n const place = (start: Position | undefined, lead: Lead): Pick<Unlocated, \"line\" | \"sharesLine\" | \"readAs\"> => {\n if (start === undefined) return {};\n const at = offset(start);\n const from = starts[start.line] ?? at;\n // Measured from where the declaration starts, not from the anchor: its own\n // kind keyword or source is no other statement (`Lead`).\n const before = code.slice(from, leadStart(code, { from, at }, lead));\n // Innermost first: a nested declaration's span lies inside its parent's.\n const cover = heads.filter((h) => at >= h.start && at < h.end).sort((a, b) => b.start - a.start)[0];\n return {\n line: start.line + 1,\n ...(/\\S/.test(before) ? { sharesLine: true as const } : {}),\n ...(cover === undefined ? {} : { readAs: { name: cover.name, line: lineOf(cover.start) } }),\n };\n };\n return { place, offset };\n}\n\n/**\n * The refusal's words for one unlocated declaration of `file`. A shared line\n * is named only when the line shows one, and the statement the scan read\n * instead only when it read one: a guess at the cause is the message this\n * replaced.\n */\nexport function unlocatedReason(file: string, gap: Unlocated | null): string {\n const remedy =\n \"nothing was written; rewrite it on a line of its own as `name = kind 'title' {` (an edge as `a -> b 'title'`) and re-run — \" +\n \"and report the original shape: LikeC4 parses it, so it is a form the splice map should read\";\n if (gap === null) return `${file} declares more than placement can locate, and LikeC4's locator could not say where — ${remedy}`;\n const what = `${file} declares ${gap.kind === \"element\" ? `'${gap.name}'` : `the relationship '${gap.name}'`}`;\n const where = gap.line === undefined ? \"\" : ` at line ${gap.line}`;\n const read = gap.readAs;\n if (gap.sharesLine === true) {\n const other = read === undefined ? \"the statement before it\" : read.name;\n return (\n `${what}${where} in a form placement cannot locate: it shares that line with ${other}, and the splice map reads ` +\n `one statement per line where LikeC4 accepts two — give each its own line, then re-run`\n );\n }\n if (read !== undefined) {\n return (\n `${what}${where} in a form placement cannot locate: the splice map read it as part of ${read.name}, which starts at ` +\n `line ${read.line} — it ran that statement on past a line break LikeC4 makes — ${remedy}`\n );\n }\n return `${what}${where} in a form placement cannot locate, and the splice map read no statement there — ${remedy}`;\n}\n"]}
@@ -9,7 +9,9 @@
9
9
  * (`../likec4.ts`) that calls them, because the adapter is at its line limit and
10
10
  * because "decode a value LikeC4 left untyped" is a subject of its own: the
11
11
  * specification reader beside this file is the same subject on a different
12
- * corner of the same object.
12
+ * corner of the same object. The ORDER relationships come back in is one more
13
+ * such corner — LikeC4 promises none, and the one it happens to give changes
14
+ * between runs — so `compareRels` lives here too.
13
15
  */
14
16
  /**
15
17
  * Read one string key out of a LikeC4 `metadata { ... }` block. Four keys carry
@@ -54,4 +56,55 @@ export function descText(d) {
54
56
  }
55
57
  return undefined;
56
58
  }
59
+ /**
60
+ * The ONE order every loader hands relationships back in: by source, then
61
+ * target, title, `op`, `publishes`, `consumes` and tags — plain code-unit
62
+ * compares, so the order is a function of the documents' text alone and never
63
+ * of the locale, the machine or the run. Two edges equal on every key are equal
64
+ * in every field `Rel` (`../likec4.ts`) carries, so which of them comes first
65
+ * cannot be seen. `flattenModel` there sorts with it; it lives here, beside the
66
+ * other reads of LikeC4's parse output that cannot be taken at face value,
67
+ * because the adapter is at its line limit.
68
+ *
69
+ * LikeC4 does not give one, for two measured reasons (1.59.2). Its parsed
70
+ * model indexes relations in a plain object keyed by an id HASHED FROM THE
71
+ * DOCUMENT'S URI — and every loader stages documents under a fresh
72
+ * crypto-random folder per invocation (`../workspace.ts` says why), so the ids
73
+ * change on every run. A JavaScript object enumerates integer-like keys
74
+ * numerically ahead of every other key, so whenever a hash comes out all
75
+ * digits (about one hash in 2,200) that edge jumps to the front of the list,
76
+ * on that run only. And inside a project of several documents the base order
77
+ * is whichever file Langium's concurrent folder walk finished reading first.
78
+ * The first reason is what failed a scheduled CI run on 2026-09-28:
79
+ * `loadProjectBatch` and `loadProject` returned one service's two edges in
80
+ * opposite orders on an unchanged commit — and on a fleet map of a few hundred
81
+ * edges it would reorder some run's findings far more often than that.
82
+ *
83
+ * Declaration order is therefore NOT what a reader gets, and nothing may read
84
+ * meaning into the position of an edge: what the `Rel` list promises is the
85
+ * same order for the same documents, which is what the text report and the
86
+ * `--json` envelope being diffed between runs and machines needs.
87
+ */
88
+ export function compareRels(a, b) {
89
+ // An absent field sorts ahead of every present one, the empty string
90
+ // included, so "no title" and "an empty title" cannot tie. Tags are LikeC4
91
+ // identifiers, which cannot contain the comma that joins them.
92
+ const opt = (value) => (value === undefined ? "" : `=${value}`);
93
+ const keys = (r) => [
94
+ r.source,
95
+ r.target,
96
+ opt(r.title),
97
+ opt(r.op),
98
+ opt(r.publishes),
99
+ opt(r.consumes),
100
+ r.tags.join(","),
101
+ ];
102
+ const ka = keys(a);
103
+ const kb = keys(b);
104
+ for (let i = 0; i < ka.length; i++) {
105
+ if (ka[i] !== kb[i])
106
+ return ka[i] < kb[i] ? -1 : 1;
107
+ }
108
+ return 0;
109
+ }
57
110
  //# sourceMappingURL=values.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"values.js","sourceRoot":"","sources":["../../../../src/core/c4/parsed/values.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO,CAAC,CAAU,EAAE,GAAW;IAC7C,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAI,CAA6B,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC;QACpC,4EAA4E;QAC5E,2EAA2E;QAC3E,8EAA8E;QAC9E,4EAA4E;QAC5E,2EAA2E;QAC3E,wEAAwE;QACxE,kDAAkD;QAClD,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,QAAQ,CAAC,CAAU;IACjC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,CAA4B,CAAC;QACvC,KAAK,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;YACjD,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YACjB,IAAI,OAAO,CAAC,KAAK,QAAQ;gBAAE,OAAO,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["/**\n * The corners of LikeC4's parse output that are typed `unknown`, decoded.\n *\n * LikeC4 models `metadata { ... }` values and a `description` as open shapes —\n * a value may be a string, an array, or a rich-text object, and the declared\n * type says none of that. Every one of those crossings needs a defensive read\n * that answers \"not present\" rather than throwing, and each read encodes a\n * defect somebody hit. They live together here, one module below the adapter\n * (`../likec4.ts`) that calls them, because the adapter is at its line limit and\n * because \"decode a value LikeC4 left untyped\" is a subject of its own: the\n * specification reader beside this file is the same subject on a different\n * corner of the same object.\n */\n\n/**\n * Read one string key out of a LikeC4 `metadata { ... }` block. Four keys carry\n * loam's spines: `op` on a relationship (the OpenAPI operationId it calls),\n * `publishes`/`consumes` on a relationship (the AsyncAPI message it produces or\n * receives), and `service` on an element (the services/<id> directory it stands\n * for). Every one of them is read by BOTH model readers — the parsed one\n * (`flattenModel` in `../likec4.ts`, which calls this) and the text scanner\n * `scanModel` uses for archive's splice map — because a key only one of them\n * sees is a key the merge silently drops. Elements with no metadata come back as\n * `{}`, so a missing key is indistinguishable from a missing block — both mean\n * \"not bound\".\n */\nexport function metaKey(m: unknown, key: string): string | undefined {\n if (m && typeof m === \"object\") {\n const v = (m as Record<string, unknown>)[key];\n if (typeof v === \"string\") return v;\n // A key written TWICE in one block comes back as an array, accepted with no\n // error, and reading only the string form dropped every value — so an edge\n // naming two operations reported as naming none, `c4.op-link-missing` telling\n // the author the opposite of what they wrote. First wins, matching the text\n // scanner's `keyedLiteral`: the two readers disagreeing about a binding is\n // exactly what the paragraph above exists to prevent. Later values stay\n // dropped — one id per key per edge is the model.\n if (Array.isArray(v) && typeof v[0] === \"string\") return v[0];\n }\n return undefined;\n}\n\n/** LikeC4 descriptions can be a string or a rich-text object ({ txt } / { text } / { md }). */\nexport function descText(d: unknown): string | undefined {\n if (typeof d === \"string\") return d;\n if (d && typeof d === \"object\") {\n const o = d as Record<string, unknown>;\n for (const key of [\"txt\", \"text\", \"md\", \"value\"]) {\n const v = o[key];\n if (typeof v === \"string\") return v;\n }\n }\n return undefined;\n}\n"]}
1
+ {"version":3,"file":"values.js","sourceRoot":"","sources":["../../../../src/core/c4/parsed/values.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO,CAAC,CAAU,EAAE,GAAW;IAC7C,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAI,CAA6B,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC;QACpC,4EAA4E;QAC5E,2EAA2E;QAC3E,8EAA8E;QAC9E,4EAA4E;QAC5E,2EAA2E;QAC3E,wEAAwE;QACxE,kDAAkD;QAClD,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,QAAQ,CAAC,CAAU;IACjC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,CAA4B,CAAC;QACvC,KAAK,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;YACjD,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YACjB,IAAI,OAAO,CAAC,KAAK,QAAQ;gBAAE,OAAO,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,WAAW,CAAC,CAAU,EAAE,CAAU;IAChD,qEAAqE;IACrE,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAM,GAAG,GAAG,CAAC,KAAyB,EAAU,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IAC5F,MAAM,IAAI,GAAG,CAAC,CAAU,EAAY,EAAE,CAAC;QACrC,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,MAAM;QACR,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;QACZ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACT,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;QAChB,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;QACf,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;KACjB,CAAC;IACF,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACnB,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAAE,OAAO,EAAE,CAAC,CAAC,CAAE,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC","sourcesContent":["/**\n * The corners of LikeC4's parse output that are typed `unknown`, decoded.\n *\n * LikeC4 models `metadata { ... }` values and a `description` as open shapes —\n * a value may be a string, an array, or a rich-text object, and the declared\n * type says none of that. Every one of those crossings needs a defensive read\n * that answers \"not present\" rather than throwing, and each read encodes a\n * defect somebody hit. They live together here, one module below the adapter\n * (`../likec4.ts`) that calls them, because the adapter is at its line limit and\n * because \"decode a value LikeC4 left untyped\" is a subject of its own: the\n * specification reader beside this file is the same subject on a different\n * corner of the same object. The ORDER relationships come back in is one more\n * such corner — LikeC4 promises none, and the one it happens to give changes\n * between runs — so `compareRels` lives here too.\n */\n\n/**\n * Read one string key out of a LikeC4 `metadata { ... }` block. Four keys carry\n * loam's spines: `op` on a relationship (the OpenAPI operationId it calls),\n * `publishes`/`consumes` on a relationship (the AsyncAPI message it produces or\n * receives), and `service` on an element (the services/<id> directory it stands\n * for). Every one of them is read by BOTH model readers — the parsed one\n * (`flattenModel` in `../likec4.ts`, which calls this) and the text scanner\n * `scanModel` uses for archive's splice map — because a key only one of them\n * sees is a key the merge silently drops. Elements with no metadata come back as\n * `{}`, so a missing key is indistinguishable from a missing block — both mean\n * \"not bound\".\n */\nexport function metaKey(m: unknown, key: string): string | undefined {\n if (m && typeof m === \"object\") {\n const v = (m as Record<string, unknown>)[key];\n if (typeof v === \"string\") return v;\n // A key written TWICE in one block comes back as an array, accepted with no\n // error, and reading only the string form dropped every value — so an edge\n // naming two operations reported as naming none, `c4.op-link-missing` telling\n // the author the opposite of what they wrote. First wins, matching the text\n // scanner's `keyedLiteral`: the two readers disagreeing about a binding is\n // exactly what the paragraph above exists to prevent. Later values stay\n // dropped — one id per key per edge is the model.\n if (Array.isArray(v) && typeof v[0] === \"string\") return v[0];\n }\n return undefined;\n}\n\n/** LikeC4 descriptions can be a string or a rich-text object ({ txt } / { text } / { md }). */\nexport function descText(d: unknown): string | undefined {\n if (typeof d === \"string\") return d;\n if (d && typeof d === \"object\") {\n const o = d as Record<string, unknown>;\n for (const key of [\"txt\", \"text\", \"md\", \"value\"]) {\n const v = o[key];\n if (typeof v === \"string\") return v;\n }\n }\n return undefined;\n}\n\n/**\n * The fields of a relationship its order is decided on — `Rel` in\n * `../likec4.ts`, stated structurally because that module imports this one and\n * a type imported back would close a cycle.\n */\nexport interface RelKeys {\n readonly source: string;\n readonly target: string;\n readonly title?: string;\n readonly op?: string;\n readonly publishes?: string;\n readonly consumes?: string;\n readonly tags: readonly string[];\n}\n\n/**\n * The ONE order every loader hands relationships back in: by source, then\n * target, title, `op`, `publishes`, `consumes` and tags — plain code-unit\n * compares, so the order is a function of the documents' text alone and never\n * of the locale, the machine or the run. Two edges equal on every key are equal\n * in every field `Rel` (`../likec4.ts`) carries, so which of them comes first\n * cannot be seen. `flattenModel` there sorts with it; it lives here, beside the\n * other reads of LikeC4's parse output that cannot be taken at face value,\n * because the adapter is at its line limit.\n *\n * LikeC4 does not give one, for two measured reasons (1.59.2). Its parsed\n * model indexes relations in a plain object keyed by an id HASHED FROM THE\n * DOCUMENT'S URI — and every loader stages documents under a fresh\n * crypto-random folder per invocation (`../workspace.ts` says why), so the ids\n * change on every run. A JavaScript object enumerates integer-like keys\n * numerically ahead of every other key, so whenever a hash comes out all\n * digits (about one hash in 2,200) that edge jumps to the front of the list,\n * on that run only. And inside a project of several documents the base order\n * is whichever file Langium's concurrent folder walk finished reading first.\n * The first reason is what failed a scheduled CI run on 2026-09-28:\n * `loadProjectBatch` and `loadProject` returned one service's two edges in\n * opposite orders on an unchanged commit — and on a fleet map of a few hundred\n * edges it would reorder some run's findings far more often than that.\n *\n * Declaration order is therefore NOT what a reader gets, and nothing may read\n * meaning into the position of an edge: what the `Rel` list promises is the\n * same order for the same documents, which is what the text report and the\n * `--json` envelope being diffed between runs and machines needs.\n */\nexport function compareRels(a: RelKeys, b: RelKeys): number {\n // An absent field sorts ahead of every present one, the empty string\n // included, so \"no title\" and \"an empty title\" cannot tie. Tags are LikeC4\n // identifiers, which cannot contain the comma that joins them.\n const opt = (value: string | undefined): string => (value === undefined ? \"\" : `=${value}`);\n const keys = (r: RelKeys): string[] => [\n r.source,\n r.target,\n opt(r.title),\n opt(r.op),\n opt(r.publishes),\n opt(r.consumes),\n r.tags.join(\",\"),\n ];\n const ka = keys(a);\n const kb = keys(b);\n for (let i = 0; i < ka.length; i++) {\n if (ka[i] !== kb[i]) return ka[i]! < kb[i]! ? -1 : 1;\n }\n return 0;\n}\n"]}
@@ -24,20 +24,12 @@
24
24
  * cannot leak one docs repo into the next.
25
25
  */
26
26
  import { resolve } from "node:path";
27
+ import { memo } from "../../kernel/concurrency.js";
27
28
  import { loadArchitecture } from "../project/architecture.js";
28
29
  import { asLoadedDoc, loadProject } from "../project/load.js";
29
30
  import { modelProjectDocuments } from "../service-model/documents.js";
30
31
  import { batchServiceProjects, serviceModelAt, } from "../service-model/load.js";
31
32
  import { readModelShape, readModelShapes } from "../service-model/shape.js";
32
- /** One in-flight promise per key, created on first ask — the memo idiom, once. */
33
- function memo(store, k, make) {
34
- let pending = store.get(k);
35
- if (pending === undefined) {
36
- pending = make();
37
- store.set(k, pending);
38
- }
39
- return pending;
40
- }
41
33
  export class ServiceModels {
42
34
  deps;
43
35
  architectures = new Map();