@ybotok/loam 0.2.0-alpha.6 → 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 (691) hide show
  1. package/CHANGELOG.md +1477 -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 +108 -11
  8. package/SCHEMA.md +721 -130
  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 +82 -33
  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 +55 -26
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/plan.js +64 -13
  118. package/dist/commands/seed/plan.js.map +1 -1
  119. package/dist/commands/seed/seed.js +6 -9
  120. package/dist/commands/seed/seed.js.map +1 -1
  121. package/dist/commands/show/show.js +3 -7
  122. package/dist/commands/show/show.js.map +1 -1
  123. package/dist/commands/status/print.js +13 -4
  124. package/dist/commands/status/print.js.map +1 -1
  125. package/dist/commands/status/status.js +3 -7
  126. package/dist/commands/status/status.js.map +1 -1
  127. package/dist/commands/steps/steps.js +3 -7
  128. package/dist/commands/steps/steps.js.map +1 -1
  129. package/dist/commands/subsystem/edit.js +7 -7
  130. package/dist/commands/subsystem/edit.js.map +1 -1
  131. package/dist/commands/subsystem/move.js +60 -25
  132. package/dist/commands/subsystem/move.js.map +1 -1
  133. package/dist/commands/subsystem/query.js +2 -4
  134. package/dist/commands/subsystem/query.js.map +1 -1
  135. package/dist/commands/subsystem/subsystem.js +4 -7
  136. package/dist/commands/subsystem/subsystem.js.map +1 -1
  137. package/dist/commands/subsystem/sync.js +141 -71
  138. package/dist/commands/subsystem/sync.js.map +1 -1
  139. package/dist/commands/subsystem/txn/report.js +55 -5
  140. package/dist/commands/subsystem/txn/report.js.map +1 -1
  141. package/dist/commands/subsystem/txn/txn.js +121 -45
  142. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  143. package/dist/commands/subsystem/txn/views.js +82 -12
  144. package/dist/commands/subsystem/txn/views.js.map +1 -1
  145. package/dist/commands/tasks/render.js +116 -0
  146. package/dist/commands/tasks/render.js.map +1 -0
  147. package/dist/commands/tasks/tasks.js +113 -0
  148. package/dist/commands/tasks/tasks.js.map +1 -0
  149. package/dist/commands/unarchive/manifest.js +6 -9
  150. package/dist/commands/unarchive/manifest.js.map +1 -1
  151. package/dist/commands/unarchive/restore.js +17 -28
  152. package/dist/commands/unarchive/restore.js.map +1 -1
  153. package/dist/commands/unarchive/unarchive.js +22 -30
  154. package/dist/commands/unarchive/unarchive.js.map +1 -1
  155. package/dist/commands/validate/arch-coverage.js +6 -15
  156. package/dist/commands/validate/arch-coverage.js.map +1 -1
  157. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  158. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  159. package/dist/commands/validate/checks/health-deps.js +9 -6
  160. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  161. package/dist/commands/validate/checks/requirements.js +18 -8
  162. package/dist/commands/validate/checks/requirements.js.map +1 -1
  163. package/dist/commands/validate/checks/sources.js +6 -2
  164. package/dist/commands/validate/checks/sources.js.map +1 -1
  165. package/dist/commands/validate/feature.js +23 -19
  166. package/dist/commands/validate/feature.js.map +1 -1
  167. package/dist/commands/validate/fleet/census.js +26 -3
  168. package/dist/commands/validate/fleet/census.js.map +1 -1
  169. package/dist/commands/validate/fleet/landscape.js +35 -32
  170. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  171. package/dist/commands/validate/fleet/map/attest.js +7 -12
  172. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  173. package/dist/commands/validate/fleet/map/bindings.js +16 -5
  174. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  175. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  176. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  177. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  178. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  179. package/dist/commands/validate/fleet/obligations.js +1 -5
  180. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  181. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  182. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  183. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  184. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  185. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  186. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  187. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  188. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  189. package/dist/commands/validate/fleet/views/ids.js +33 -7
  190. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  191. package/dist/commands/validate/fleet/views/root-documents.js +17 -35
  192. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  193. package/dist/commands/validate/fleet/views/stale.js +31 -14
  194. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  195. package/dist/commands/validate/links/corpus.js +1 -4
  196. package/dist/commands/validate/links/corpus.js.map +1 -1
  197. package/dist/commands/validate/links/glossary.js.map +1 -1
  198. package/dist/commands/validate/report.js +5 -4
  199. package/dist/commands/validate/report.js.map +1 -1
  200. package/dist/commands/validate/scope.js +113 -0
  201. package/dist/commands/validate/scope.js.map +1 -0
  202. package/dist/commands/validate/service/api.js +16 -104
  203. package/dist/commands/validate/service/api.js.map +1 -1
  204. package/dist/commands/validate/service/contracts/generated.js +134 -0
  205. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  206. package/dist/commands/validate/service/events/events.js +1 -1
  207. package/dist/commands/validate/service/events/events.js.map +1 -1
  208. package/dist/commands/validate/service/events/message-producers.js +1 -1
  209. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  210. package/dist/commands/validate/service/evidence-pins.js +56 -10
  211. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  212. package/dist/commands/validate/service/service.js +16 -26
  213. package/dist/commands/validate/service/service.js.map +1 -1
  214. package/dist/commands/validate/service/specs.js +31 -38
  215. package/dist/commands/validate/service/specs.js.map +1 -1
  216. package/dist/commands/validate/service/spine.js +8 -11
  217. package/dist/commands/validate/service/spine.js.map +1 -1
  218. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  219. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  220. package/dist/commands/validate/validate.js +44 -26
  221. package/dist/commands/validate/validate.js.map +1 -1
  222. package/dist/commands/verify/cross/diff.js +2 -6
  223. package/dist/commands/verify/cross/diff.js.map +1 -1
  224. package/dist/commands/verify/evidence/contract.js +1 -1
  225. package/dist/commands/verify/evidence/contract.js.map +1 -1
  226. package/dist/commands/verify/evidence/read.js +12 -3
  227. package/dist/commands/verify/evidence/read.js.map +1 -1
  228. package/dist/commands/verify/frozen.js +45 -23
  229. package/dist/commands/verify/frozen.js.map +1 -1
  230. package/dist/commands/verify/record.js +69 -17
  231. package/dist/commands/verify/record.js.map +1 -1
  232. package/dist/commands/verify/report.js +27 -44
  233. package/dist/commands/verify/report.js.map +1 -1
  234. package/dist/commands/verify/results.js +112 -30
  235. package/dist/commands/verify/results.js.map +1 -1
  236. package/dist/commands/verify/verify.js +54 -51
  237. package/dist/commands/verify/verify.js.map +1 -1
  238. package/dist/commands/vouch/pack/pack.js +2 -2
  239. package/dist/commands/vouch/pack/pack.js.map +1 -1
  240. package/dist/commands/vouch/pack/print.js +1 -6
  241. package/dist/commands/vouch/pack/print.js.map +1 -1
  242. package/dist/commands/vouch/pack/sections.js +1 -2
  243. package/dist/commands/vouch/pack/sections.js.map +1 -1
  244. package/dist/commands/vouch/run.js +26 -81
  245. package/dist/commands/vouch/run.js.map +1 -1
  246. package/dist/commands/vouch/sample/plan.js +20 -34
  247. package/dist/commands/vouch/sample/plan.js.map +1 -1
  248. package/dist/commands/vouch/vet/verify.js +39 -7
  249. package/dist/commands/vouch/vet/verify.js.map +1 -1
  250. package/dist/commands/vouch/vouch.js +10 -17
  251. package/dist/commands/vouch/vouch.js.map +1 -1
  252. package/dist/core/agent/agents-md/command-map.js +57 -102
  253. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  254. package/dist/core/agent/agents-md/map/explain.js +4 -3
  255. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  256. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  257. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  258. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  259. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  260. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  261. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  262. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  263. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  264. package/dist/core/agent/agents-md/map/status.js +9 -3
  265. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  266. package/dist/core/agent/agents-md/map/subsystem.js +31 -17
  267. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  268. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  269. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  270. package/dist/core/agent/agents-md/refusals.js +37 -21
  271. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  272. package/dist/core/agent/agents-md.js +25 -10
  273. package/dist/core/agent/agents-md.js.map +1 -1
  274. package/dist/core/agent/agents-stamp.js +10 -0
  275. package/dist/core/agent/agents-stamp.js.map +1 -1
  276. package/dist/core/agent/scaffold.js +10 -15
  277. package/dist/core/agent/scaffold.js.map +1 -1
  278. package/dist/core/agent/tools/dialects.js +1 -0
  279. package/dist/core/agent/tools/dialects.js.map +1 -1
  280. package/dist/core/agent/tools/registry.js +41 -76
  281. package/dist/core/agent/tools/registry.js.map +1 -1
  282. package/dist/core/agent/workflows/adopt.js +5 -3
  283. package/dist/core/agent/workflows/adopt.js.map +1 -1
  284. package/dist/core/agent/workflows/check.js +25 -22
  285. package/dist/core/agent/workflows/check.js.map +1 -1
  286. package/dist/core/agent/workflows/closing.js +54 -21
  287. package/dist/core/agent/workflows/closing.js.map +1 -1
  288. package/dist/core/agent/workflows/feature.js +79 -11
  289. package/dist/core/agent/workflows/feature.js.map +1 -1
  290. package/dist/core/agent/workflows/implement.js +91 -13
  291. package/dist/core/agent/workflows/implement.js.map +1 -1
  292. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  293. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  294. package/dist/core/agent/workflows/reference/codes.js +8 -6
  295. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  296. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  297. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  298. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  299. package/dist/core/agent/workflows/reference/spine.js +62 -24
  300. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  301. package/dist/core/asyncapi/baseline/plan.js +14 -81
  302. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  303. package/dist/core/asyncapi/digest.js +30 -25
  304. package/dist/core/asyncapi/digest.js.map +1 -1
  305. package/dist/core/asyncapi/merge/components.js +5 -4
  306. package/dist/core/asyncapi/merge/components.js.map +1 -1
  307. package/dist/core/asyncapi/merge/error.js +7 -2
  308. package/dist/core/asyncapi/merge/error.js.map +1 -1
  309. package/dist/core/asyncapi/merge/markers.js +30 -50
  310. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  311. package/dist/core/asyncapi/merge/merge.js +31 -53
  312. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  313. package/dist/core/asyncapi/merge/pin.js +10 -36
  314. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  315. package/dist/core/asyncapi/model.js.map +1 -1
  316. package/dist/core/asyncapi/read.js +36 -12
  317. package/dist/core/asyncapi/read.js.map +1 -1
  318. package/dist/core/brief/brief.js +30 -25
  319. package/dist/core/brief/brief.js.map +1 -1
  320. package/dist/core/brief/landscape.js +8 -12
  321. package/dist/core/brief/landscape.js.map +1 -1
  322. package/dist/core/brief/map/extend.js +8 -5
  323. package/dist/core/brief/map/extend.js.map +1 -1
  324. package/dist/core/brief/map/owed.js +8 -26
  325. package/dist/core/brief/map/owed.js.map +1 -1
  326. package/dist/core/brief/targets.js +79 -5
  327. package/dist/core/brief/targets.js.map +1 -1
  328. package/dist/core/brief/unchecked.js +1 -1
  329. package/dist/core/brief/unchecked.js.map +1 -1
  330. package/dist/core/c4/likec4.js +147 -16
  331. package/dist/core/c4/likec4.js.map +1 -1
  332. package/dist/core/c4/locate/unlocated.js +150 -0
  333. package/dist/core/c4/locate/unlocated.js.map +1 -0
  334. package/dist/core/c4/parsed/values.js +54 -1
  335. package/dist/core/c4/parsed/values.js.map +1 -1
  336. package/dist/core/c4/prefetch/fleet.js +1 -9
  337. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  338. package/dist/core/c4/project/architecture.js +31 -9
  339. package/dist/core/c4/project/architecture.js.map +1 -1
  340. package/dist/core/c4/project/documents.js +36 -10
  341. package/dist/core/c4/project/documents.js.map +1 -1
  342. package/dist/core/c4/project/load.js +14 -21
  343. package/dist/core/c4/project/load.js.map +1 -1
  344. package/dist/core/c4/project/staged.js +7 -3
  345. package/dist/core/c4/project/staged.js.map +1 -1
  346. package/dist/core/c4/resolve/external.js +51 -0
  347. package/dist/core/c4/resolve/external.js.map +1 -0
  348. package/dist/core/c4/resolve/service.js +25 -1
  349. package/dist/core/c4/resolve/service.js.map +1 -1
  350. package/dist/core/c4/seed/template.js +15 -1
  351. package/dist/core/c4/seed/template.js.map +1 -1
  352. package/dist/core/c4/service-model/shape.js +2 -22
  353. package/dist/core/c4/service-model/shape.js.map +1 -1
  354. package/dist/core/c4/source-mask.js +288 -4
  355. package/dist/core/c4/source-mask.js.map +1 -1
  356. package/dist/core/c4/source-scan.js +203 -147
  357. package/dist/core/c4/source-scan.js.map +1 -1
  358. package/dist/core/c4/splice/authored-source.js +19 -0
  359. package/dist/core/c4/splice/authored-source.js.map +1 -1
  360. package/dist/core/c4/splice/delta-blocks.js +5 -18
  361. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  362. package/dist/core/c4/splice/landscape-merge.js +13 -8
  363. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  364. package/dist/core/c4/splice/model/merge.js +10 -6
  365. package/dist/core/c4/splice/model/merge.js.map +1 -1
  366. package/dist/core/c4/splice/placement.js +108 -18
  367. package/dist/core/c4/splice/placement.js.map +1 -1
  368. package/dist/core/c4/workspace.js +13 -22
  369. package/dist/core/c4/workspace.js.map +1 -1
  370. package/dist/core/capabilities/capabilities.js +6 -6
  371. package/dist/core/capabilities/capabilities.js.map +1 -1
  372. package/dist/core/capabilities/delta/doc.js +7 -10
  373. package/dist/core/capabilities/delta/doc.js.map +1 -1
  374. package/dist/core/capabilities/findings.js +15 -20
  375. package/dist/core/capabilities/findings.js.map +1 -1
  376. package/dist/core/capabilities/realizes/findings.js +12 -19
  377. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  378. package/dist/core/capabilities/realizes/join.js +0 -20
  379. package/dist/core/capabilities/realizes/join.js.map +1 -1
  380. package/dist/core/capabilities/rollup.js +6 -0
  381. package/dist/core/capabilities/rollup.js.map +1 -1
  382. package/dist/core/capabilities/tree.js +6 -28
  383. package/dist/core/capabilities/tree.js.map +1 -1
  384. package/dist/core/capabilities/usecase-join.js +3 -13
  385. package/dist/core/capabilities/usecase-join.js.map +1 -1
  386. package/dist/core/coherence/authoring/scaffold.js +11 -3
  387. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  388. package/dist/core/coherence/authoring/sentinels.js +2 -1
  389. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  390. package/dist/core/coherence/coherence.js +30 -16
  391. package/dist/core/coherence/coherence.js.map +1 -1
  392. package/dist/core/coherence/consumed/operations.js +70 -0
  393. package/dist/core/coherence/consumed/operations.js.map +1 -0
  394. package/dist/core/coherence/consumed/refusal.js +38 -0
  395. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  396. package/dist/core/coherence/declared.js +60 -34
  397. package/dist/core/coherence/declared.js.map +1 -1
  398. package/dist/core/coherence/events/declared.js +44 -59
  399. package/dist/core/coherence/events/declared.js.map +1 -1
  400. package/dist/core/coherence/events/events.js +119 -8
  401. package/dist/core/coherence/events/events.js.map +1 -1
  402. package/dist/core/coherence/events/grades.js +31 -8
  403. package/dist/core/coherence/events/grades.js.map +1 -1
  404. package/dist/core/coherence/events/lookups.js +14 -82
  405. package/dist/core/coherence/events/lookups.js.map +1 -1
  406. package/dist/core/coherence/lookups.js +18 -78
  407. package/dist/core/coherence/lookups.js.map +1 -1
  408. package/dist/core/coherence/retired/archived.js +285 -0
  409. package/dist/core/coherence/retired/archived.js.map +1 -0
  410. package/dist/core/coherence/retired/event-pair.js +62 -0
  411. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  412. package/dist/core/coherence/retired/merged.js +79 -0
  413. package/dist/core/coherence/retired/merged.js.map +1 -0
  414. package/dist/core/coherence/retired/retirement.js +49 -0
  415. package/dist/core/coherence/retired/retirement.js.map +1 -0
  416. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  417. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  418. package/dist/core/coherence/retired/revival/revived.js +297 -0
  419. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  420. package/dist/core/consumers/after.js +113 -0
  421. package/dist/core/consumers/after.js.map +1 -0
  422. package/dist/core/consumers/consumers.js +296 -0
  423. package/dist/core/consumers/consumers.js.map +1 -0
  424. package/dist/core/consumers/current.js +211 -0
  425. package/dist/core/consumers/current.js.map +1 -0
  426. package/dist/core/consumers/scan.js +46 -0
  427. package/dist/core/consumers/scan.js.map +1 -0
  428. package/dist/core/consumers/state.js +27 -0
  429. package/dist/core/consumers/state.js.map +1 -0
  430. package/dist/core/delta/delta.js +1 -1
  431. package/dist/core/delta/delta.js.map +1 -1
  432. package/dist/core/deployment/delta.js +15 -16
  433. package/dist/core/deployment/delta.js.map +1 -1
  434. package/dist/core/diff/base-git.js +94 -12
  435. package/dist/core/diff/base-git.js.map +1 -1
  436. package/dist/core/diff/base-state.js +28 -16
  437. package/dist/core/diff/base-state.js.map +1 -1
  438. package/dist/core/diff/joins.js +136 -0
  439. package/dist/core/diff/joins.js.map +1 -0
  440. package/dist/core/diff/scope/changed-paths.js +22 -6
  441. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  442. package/dist/core/diff/scope/edges.js +82 -0
  443. package/dist/core/diff/scope/edges.js.map +1 -0
  444. package/dist/core/diff/semantic.js +100 -31
  445. package/dist/core/diff/semantic.js.map +1 -1
  446. package/dist/core/doctor/agents.js +34 -18
  447. package/dist/core/doctor/agents.js.map +1 -1
  448. package/dist/core/doctor/report.js.map +1 -1
  449. package/dist/core/document/apply.js +10 -2
  450. package/dist/core/document/apply.js.map +1 -1
  451. package/dist/core/document/frontmatter.js +55 -0
  452. package/dist/core/document/frontmatter.js.map +1 -1
  453. package/dist/core/document/parse.js +29 -6
  454. package/dist/core/document/parse.js.map +1 -1
  455. package/dist/core/document/scenarios.js +17 -0
  456. package/dist/core/document/scenarios.js.map +1 -1
  457. package/dist/core/document/spec.js +4 -11
  458. package/dist/core/document/spec.js.map +1 -1
  459. package/dist/core/envelope/config.js +10 -0
  460. package/dist/core/envelope/config.js.map +1 -1
  461. package/dist/core/envelope/json.js +34 -0
  462. package/dist/core/envelope/json.js.map +1 -1
  463. package/dist/core/explain/families.js +17 -12
  464. package/dist/core/explain/families.js.map +1 -1
  465. package/dist/core/explain/refusals.js +3 -3
  466. package/dist/core/explain/refusals.js.map +1 -1
  467. package/dist/core/explain/terms.js +18 -9
  468. package/dist/core/explain/terms.js.map +1 -1
  469. package/dist/core/explore/explore.js +2 -7
  470. package/dist/core/explore/explore.js.map +1 -1
  471. package/dist/core/fleet-context.js +34 -95
  472. package/dist/core/fleet-context.js.map +1 -1
  473. package/dist/core/gate/checks.js +10 -3
  474. package/dist/core/gate/checks.js.map +1 -1
  475. package/dist/core/gate/partners.js +64 -19
  476. package/dist/core/gate/partners.js.map +1 -1
  477. package/dist/core/gate/report.js.map +1 -1
  478. package/dist/core/gate/verification.js +75 -12
  479. package/dist/core/gate/verification.js.map +1 -1
  480. package/dist/core/gherkin/stale.js +13 -37
  481. package/dist/core/gherkin/stale.js.map +1 -1
  482. package/dist/core/glossary/tree.js +6 -19
  483. package/dist/core/glossary/tree.js.map +1 -1
  484. package/dist/core/kernel/concurrency.js +19 -0
  485. package/dist/core/kernel/concurrency.js.map +1 -1
  486. package/dist/core/kernel/records.js +11 -0
  487. package/dist/core/kernel/records.js.map +1 -1
  488. package/dist/core/links/backlinks.js +1 -3
  489. package/dist/core/links/backlinks.js.map +1 -1
  490. package/dist/core/links/corpus.js +5 -36
  491. package/dist/core/links/corpus.js.map +1 -1
  492. package/dist/core/mcp/argv.js +101 -0
  493. package/dist/core/mcp/argv.js.map +1 -1
  494. package/dist/core/mcp/author-tools.js +2 -1
  495. package/dist/core/mcp/author-tools.js.map +1 -1
  496. package/dist/core/mcp/protocol.js +2 -2
  497. package/dist/core/mcp/protocol.js.map +1 -1
  498. package/dist/core/mcp/tools.js +43 -33
  499. package/dist/core/mcp/tools.js.map +1 -1
  500. package/dist/core/openapi/baseline/gate.js +53 -12
  501. package/dist/core/openapi/baseline/gate.js.map +1 -1
  502. package/dist/core/openapi/baseline/plan.js +22 -8
  503. package/dist/core/openapi/baseline/plan.js.map +1 -1
  504. package/dist/core/openapi/baseline/record.js +0 -1
  505. package/dist/core/openapi/baseline/record.js.map +1 -1
  506. package/dist/core/openapi/contract-text.js +33 -0
  507. package/dist/core/openapi/contract-text.js.map +1 -0
  508. package/dist/core/openapi/contract-tree.js +56 -0
  509. package/dist/core/openapi/contract-tree.js.map +1 -0
  510. package/dist/core/openapi/depth.js +38 -24
  511. package/dist/core/openapi/depth.js.map +1 -1
  512. package/dist/core/openapi/digest.js +52 -2
  513. package/dist/core/openapi/digest.js.map +1 -1
  514. package/dist/core/openapi/doc.js +12 -4
  515. package/dist/core/openapi/doc.js.map +1 -1
  516. package/dist/core/openapi/merge/components.js +25 -60
  517. package/dist/core/openapi/merge/components.js.map +1 -1
  518. package/dist/core/openapi/merge/error.js +3 -13
  519. package/dist/core/openapi/merge/error.js.map +1 -1
  520. package/dist/core/openapi/merge/markers.js +26 -30
  521. package/dist/core/openapi/merge/markers.js.map +1 -1
  522. package/dist/core/openapi/merge/merge.js +26 -52
  523. package/dist/core/openapi/merge/merge.js.map +1 -1
  524. package/dist/core/openapi/merge/pin.js +15 -35
  525. package/dist/core/openapi/merge/pin.js.map +1 -1
  526. package/dist/core/openspec/decide/verdict.js.map +1 -1
  527. package/dist/core/openspec/inventory.js +4 -5
  528. package/dist/core/openspec/inventory.js.map +1 -1
  529. package/dist/core/openspec/model/mapping.js +30 -28
  530. package/dist/core/openspec/model/mapping.js.map +1 -1
  531. package/dist/core/openspec/model/model.js +12 -7
  532. package/dist/core/openspec/model/model.js.map +1 -1
  533. package/dist/core/openspec/scan/changes.js +10 -4
  534. package/dist/core/openspec/scan/changes.js.map +1 -1
  535. package/dist/core/openspec/scan/config.js +7 -16
  536. package/dist/core/openspec/scan/config.js.map +1 -1
  537. package/dist/core/openspec/scan/shape.js +63 -19
  538. package/dist/core/openspec/scan/shape.js.map +1 -1
  539. package/dist/core/openspec/scan/walk.js +21 -38
  540. package/dist/core/openspec/scan/walk.js.map +1 -1
  541. package/dist/core/pack/living.js +2 -16
  542. package/dist/core/pack/living.js.map +1 -1
  543. package/dist/core/provenance/findings.js +48 -12
  544. package/dist/core/provenance/findings.js.map +1 -1
  545. package/dist/core/provenance/git.js +33 -57
  546. package/dist/core/provenance/git.js.map +1 -1
  547. package/dist/core/provenance/gitq/moves.js +71 -114
  548. package/dist/core/provenance/gitq/moves.js.map +1 -1
  549. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  550. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  551. package/dist/core/provenance/sources.js +23 -6
  552. package/dist/core/provenance/sources.js.map +1 -1
  553. package/dist/core/provenance/stamp.js +5 -17
  554. package/dist/core/provenance/stamp.js.map +1 -1
  555. package/dist/core/repo/entries.js.map +1 -1
  556. package/dist/core/repo/paths.js +34 -29
  557. package/dist/core/repo/paths.js.map +1 -1
  558. package/dist/core/repo/repo.js +24 -14
  559. package/dist/core/repo/repo.js.map +1 -1
  560. package/dist/core/repo/tree/find.js +12 -3
  561. package/dist/core/repo/tree/find.js.map +1 -1
  562. package/dist/core/repo/tree/fs.js +27 -1
  563. package/dist/core/repo/tree/fs.js.map +1 -1
  564. package/dist/core/repo/tree/render/body.js +45 -6
  565. package/dist/core/repo/tree/render/body.js.map +1 -1
  566. package/dist/core/repo/tree/render/service/header.js +125 -0
  567. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  568. package/dist/core/repo/tree/render/service/peers.js +28 -0
  569. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  570. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  571. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  572. package/dist/core/repo/tree/render/service/views.js +78 -0
  573. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  574. package/dist/core/repo/tree/render/stale.js +19 -2
  575. package/dist/core/repo/tree/render/stale.js.map +1 -1
  576. package/dist/core/repo/tree/render/views.js +102 -33
  577. package/dist/core/repo/tree/render/views.js.map +1 -1
  578. package/dist/core/repo/tree/walk.js +8 -30
  579. package/dist/core/repo/tree/walk.js.map +1 -1
  580. package/dist/core/results.js +47 -12
  581. package/dist/core/results.js.map +1 -1
  582. package/dist/core/staging/commit.js +10 -12
  583. package/dist/core/staging/commit.js.map +1 -1
  584. package/dist/core/staging/lock.js +74 -12
  585. package/dist/core/staging/lock.js.map +1 -1
  586. package/dist/core/staging/recovery/intent.js +2 -5
  587. package/dist/core/staging/recovery/intent.js.map +1 -1
  588. package/dist/core/staging/recovery/recover.js +60 -1
  589. package/dist/core/staging/recovery/recover.js.map +1 -1
  590. package/dist/core/staging/snapshot/manifest.js +152 -0
  591. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  592. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  593. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  594. package/dist/core/status/actions/execution.js +11 -0
  595. package/dist/core/status/actions/execution.js.map +1 -1
  596. package/dist/core/status/feature/artifacts.js +2 -2
  597. package/dist/core/status/feature/artifacts.js.map +1 -1
  598. package/dist/core/status/feature/feature.js +5 -11
  599. package/dist/core/status/feature/feature.js.map +1 -1
  600. package/dist/core/status/fleet/fleet.js +6 -2
  601. package/dist/core/status/fleet/fleet.js.map +1 -1
  602. package/dist/core/status/fleet/next.js +26 -3
  603. package/dist/core/status/fleet/next.js.map +1 -1
  604. package/dist/core/status/report.js.map +1 -1
  605. package/dist/core/status/verification.js +30 -15
  606. package/dist/core/status/verification.js.map +1 -1
  607. package/dist/core/tasks/packet.js +174 -0
  608. package/dist/core/tasks/packet.js.map +1 -0
  609. package/dist/core/tasks/record.js +43 -0
  610. package/dist/core/tasks/record.js.map +1 -0
  611. package/dist/core/tasks/sources.js +181 -0
  612. package/dist/core/tasks/sources.js.map +1 -0
  613. package/dist/core/tasks/stamps.js +76 -0
  614. package/dist/core/tasks/stamps.js.map +1 -0
  615. package/dist/core/tasks/unit.js +0 -0
  616. package/dist/core/tasks/unit.js.map +1 -0
  617. package/dist/core/usecases/capability.js +11 -14
  618. package/dist/core/usecases/capability.js.map +1 -1
  619. package/dist/core/usecases/delta/flows.js +5 -7
  620. package/dist/core/usecases/delta/flows.js.map +1 -1
  621. package/dist/core/usecases/fleet.js +178 -31
  622. package/dist/core/usecases/fleet.js.map +1 -1
  623. package/dist/core/usecases/operations.js +5 -4
  624. package/dist/core/usecases/operations.js.map +1 -1
  625. package/dist/core/usecases/service/flows.js +9 -6
  626. package/dist/core/usecases/service/flows.js.map +1 -1
  627. package/dist/core/usecases/suites/participants.js +83 -0
  628. package/dist/core/usecases/suites/participants.js.map +1 -0
  629. package/dist/core/usecases/suites/suites.js +45 -0
  630. package/dist/core/usecases/suites/suites.js.map +1 -0
  631. package/dist/core/verify/answers.js +12 -5
  632. package/dist/core/verify/answers.js.map +1 -1
  633. package/dist/core/verify/build.js +102 -82
  634. package/dist/core/verify/build.js.map +1 -1
  635. package/dist/core/verify/checklist.js +45 -10
  636. package/dist/core/verify/checklist.js.map +1 -1
  637. package/dist/core/verify/location.js +67 -0
  638. package/dist/core/verify/location.js.map +1 -0
  639. package/dist/core/verify/pins/lint.js +3 -2
  640. package/dist/core/verify/pins/lint.js.map +1 -1
  641. package/dist/core/verify/pins/pin.js +1 -1
  642. package/dist/core/verify/pins/pin.js.map +1 -1
  643. package/dist/core/verify/read/file.js +224 -0
  644. package/dist/core/verify/read/file.js.map +1 -0
  645. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  646. package/dist/core/verify/read/shape.js.map +1 -0
  647. package/dist/core/verify/read/union.js +105 -0
  648. package/dist/core/verify/read/union.js.map +1 -0
  649. package/dist/core/verify/record.js +35 -12
  650. package/dist/core/verify/record.js.map +1 -1
  651. package/dist/core/verify/store/commit.js +40 -30
  652. package/dist/core/verify/store/commit.js.map +1 -1
  653. package/dist/core/verify/store/render.js +8 -4
  654. package/dist/core/verify/store/render.js.map +1 -1
  655. package/dist/core/vocabulary/issue.js +1 -0
  656. package/dist/core/vocabulary/issue.js.map +1 -1
  657. package/dist/core/vocabulary/maturity.js +53 -3
  658. package/dist/core/vocabulary/maturity.js.map +1 -1
  659. package/examples/README.md +28 -9
  660. package/examples/docs/architecture/landscape.likec4 +21 -7
  661. package/examples/docs/architecture/services.likec4 +70 -0
  662. package/examples/docs/architecture/subsystems.likec4 +3 -0
  663. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  664. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  665. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  666. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  667. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  668. package/examples/docs/services/order-service/model.likec4 +10 -8
  669. package/examples/docs/services/payment-service/model.likec4 +6 -4
  670. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  671. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  672. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  673. package/package.json +1 -1
  674. package/dist/commands/validate/checks/vocabulary.js +0 -16
  675. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  676. package/dist/core/agent/agents-md/artifacts.js +0 -221
  677. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  678. package/dist/core/agent/agents-md/cycle.js +0 -180
  679. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  680. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  681. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  682. package/dist/core/agent/agents-md/spine.js +0 -40
  683. package/dist/core/agent/agents-md/spine.js.map +0 -1
  684. package/dist/core/coherence/events/merged.js +0 -40
  685. package/dist/core/coherence/events/merged.js.map +0 -1
  686. package/dist/core/diff/current-state.js +0 -96
  687. package/dist/core/diff/current-state.js.map +0 -1
  688. package/dist/core/diff/victims.js +0 -256
  689. package/dist/core/diff/victims.js.map +0 -1
  690. package/dist/core/staging/snapshot.js.map +0 -1
  691. package/dist/core/verify/file.js.map +0 -1
@@ -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();
@@ -1 +1 @@
1
- {"version":3,"file":"fleet.js","sourceRoot":"","sources":["../../../../src/core/c4/prefetch/fleet.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every service model ONE command invocation has read, memoised — the fleet\n * map, each model's shape, each per-service project, and each assembled\n * `ServiceModel`.\n *\n * A package of its own rather than four more maps on `core/fleet-context.ts`,\n * and the seam is the one that module's banner already states for its leaves:\n * the rules being memoised here are `../service-model/`'s (which shape a file\n * has, which documents its project holds, how a failed batch degrades), and a\n * memo whose rules live an import away is a memo somebody fills under the wrong\n * key. The read index keeps what it owns — the document parses, the\n * enumerations — and hands them in. It also keeps that file under its line\n * limit, which is the honest second reason: this axis is four memos and a batch\n * plan, and it arrived at a module that was already 336 lines of other axes.\n *\n * The direction is one-way and load-bearing: `fleet-context.ts` imports this,\n * never the reverse. Everything this class needs from the read index arrives as\n * a function on the request (`ModelRead`), so nothing here knows that a\n * FleetContext exists — which is what keeps `core/` → `core/c4/` acyclic.\n *\n * REQUEST-SCOPED exactly as `FleetContext` is: one instance per invocation,\n * dropped when the command finishes. No module-level state, so a later run\n * cannot observe an earlier one's answer, and a `chdir` between two tests\n * cannot leak one docs repo into the next.\n */\nimport { resolve } from \"node:path\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport type { ServicePaths } from \"../../repo/paths.js\";\nimport type { LoadedDoc } from \"../likec4.js\";\nimport { loadArchitecture } from \"../project/architecture.js\";\nimport { asLoadedDoc, loadProject } from \"../project/load.js\";\nimport { modelProjectDocuments } from \"../service-model/documents.js\";\nimport {\n batchServiceProjects,\n serviceModelAt,\n type ServiceModel,\n} from \"../service-model/load.js\";\nimport { readModelShape, readModelShapes, type ModelShape } from \"../service-model/shape.js\";\n\n/**\n * The three things this memo cannot know for itself, supplied once by whoever\n * owns it. Functions rather than values because each is itself memoised on the\n * caller's side: asking again is what makes an answer shared rather than\n * copied.\n */\nexport interface ModelDeps {\n /** Fires once per per-service project actually parsed, batched or not — the caller owns its counters. */\n onProjectLoad: () => void;\n /** The service directories that exist — the caller has enumerated them; this package must not enumerate them again. */\n known: (docsDir: DocsDir) => Promise<ReadonlySet<string>>;\n /** Parse one `.likec4` document ALONE, through the caller's own document memo, so a prefetched parse is a hit. */\n standalone: (path: string) => Promise<LoadedDoc>;\n}\n\n/** One in-flight promise per key, created on first ask — the memo idiom, once. */\nfunction memo<T>(store: Map<string, Promise<T>>, k: string, make: () => Promise<T>): Promise<T> {\n let pending = store.get(k);\n if (pending === undefined) {\n pending = make();\n store.set(k, pending);\n }\n return pending;\n}\n\nexport class ServiceModels {\n private readonly architectures = new Map<string, Promise<LoadedDoc>>();\n private readonly shapeOf = new Map<string, Promise<ModelShape>>();\n private readonly projects = new Map<string, Promise<LoadedDoc>>();\n private readonly models = new Map<string, Promise<ServiceModel>>();\n\n constructor(private readonly deps: ModelDeps) {}\n\n /**\n * The fleet map as the PROJECT it is (`../project/architecture.ts`). Read\n * twice over for every service now rather than once for the run — it is the\n * gate on whether an extending model can be read at all, and the other half\n * of the diff that decides what such a model actually declares — so the memo\n * is what keeps a fleet run to one parse of it.\n *\n * Filled ONLY when somebody asks. `model` below hands `loadServiceModel` a\n * thunk rather than an awaited value, so a fleet whose models all stand alone\n * never parses this project at all — a standalone model is read as one file,\n * and paying for the map (and inheriting its failures) for `loam show` or the\n * adopt brief was cost and risk for an answer nobody used.\n */\n architecture(docsDir: DocsDir): Promise<LoadedDoc> {\n return memo(this.architectures, resolve(docsDir), () => loadArchitecture(docsDir));\n }\n\n /**\n * Which shape each model has, keyed by resolved path — ONE read of each\n * file's bytes per invocation, however many questions are asked of it.\n *\n * The bulk form is the primary one because every caller asks in bulk: the\n * fleet target needs the shapes of the WHOLE enumeration before it can grade\n * a single renderer-file finding, and the prefetch needs them before it can\n * decide which workspace each model belongs in.\n */\n async shapes(paths: readonly string[]): Promise<Map<string, ModelShape>> {\n const keys = [...new Set(paths.map((path) => resolve(path)))];\n const missing = keys.filter((k) => !this.shapeOf.has(k));\n for (const [k, shape] of await readModelShapes(missing)) {\n // A concurrent read's promise stays authoritative — it is the one another\n // caller is already holding — so a second scan of the same bytes can\n // never replace it with a duplicate.\n if (!this.shapeOf.has(k)) this.shapeOf.set(k, Promise.resolve(shape));\n }\n const answered = await Promise.all(keys.map(async (k) => [k, await this.shape(k)] as const));\n return new Map(answered);\n }\n\n /** One model's shape, out of the same memo the bulk form fills. */\n shape(modelPath: string): Promise<ModelShape> {\n return memo(this.shapeOf, resolve(modelPath), () => readModelShape(modelPath));\n }\n\n /** A service's model, read the way its own shape demands. Memoised per model PATH, which is what identifies the file. */\n model(docsDir: DocsDir, paths: ServicePaths): Promise<ServiceModel> {\n return memo(this.models, resolve(paths.model), async () =>\n serviceModelAt({\n docsDir,\n paths,\n shape: await this.shape(paths.model),\n known: await this.deps.known(docsDir),\n // A thunk, so a standalone model never spins the map's workspace: the\n // memo makes the extending case one parse for the run either way.\n architecture: () => this.architecture(docsDir),\n standalone: () => this.deps.standalone(paths.model),\n project: () => this.project(docsDir, paths.model),\n }),\n );\n }\n\n /**\n * Prepare every EXTENDING model's project in one workspace, and answer with\n * the STANDALONE models' paths for the caller to prefetch through its own\n * document batch — the two shapes need different workspaces, and only the\n * caller has the memo the second one seeds.\n *\n * Purely an accelerator: a batch that could not be created seeds nothing and\n * every reader falls back to its own load, so findings can never change\n * because of a tmpdir — only the speed can.\n */\n async prefetch(docsDir: DocsDir, paths: readonly ServicePaths[]): Promise<string[]> {\n const shapes = await this.shapes(paths.map((p) => p.model));\n const models = (shape: ModelShape): string[] =>\n [...shapes].filter(([, found]) => found === shape).map(([model]) => model);\n const extending = models(\"extending\").filter((model) => !this.projects.has(model));\n for (const [k, doc] of await batchServiceProjects(docsDir, extending)) {\n // A concurrent load's promise stays authoritative: it is the one every\n // reader is already holding, and replacing it would count the parse twice.\n if (this.projects.has(k)) continue;\n this.deps.onProjectLoad();\n this.projects.set(k, Promise.resolve(doc));\n }\n return models(\"standalone\");\n }\n\n /**\n * One service's project — the architecture documents and its model, parsed\n * together. Keyed on the model, which is what `prefetch` seeds above, so a\n * batched fleet run and a single-service run answer out of the same memo.\n */\n private project(docsDir: DocsDir, modelPath: string): Promise<LoadedDoc> {\n return memo(this.projects, resolve(modelPath), async () => {\n this.deps.onProjectLoad();\n return asLoadedDoc(await loadProject(docsDir, await modelProjectDocuments(docsDir, modelPath)));\n });\n }\n}\n"]}
1
+ {"version":3,"file":"fleet.js","sourceRoot":"","sources":["../../../../src/core/c4/prefetch/fleet.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every service model ONE command invocation has read, memoised — the fleet\n * map, each model's shape, each per-service project, and each assembled\n * `ServiceModel`.\n *\n * A package of its own rather than four more maps on `core/fleet-context.ts`,\n * and the seam is the one that module's banner already states for its leaves:\n * the rules being memoised here are `../service-model/`'s (which shape a file\n * has, which documents its project holds, how a failed batch degrades), and a\n * memo whose rules live an import away is a memo somebody fills under the wrong\n * key. The read index keeps what it owns — the document parses, the\n * enumerations — and hands them in. It also keeps that file under its line\n * limit, which is the honest second reason: this axis is four memos and a batch\n * plan, and it arrived at a module that was already 336 lines of other axes.\n *\n * The direction is one-way and load-bearing: `fleet-context.ts` imports this,\n * never the reverse. Everything this class needs from the read index arrives as\n * a function on the request (`ModelRead`), so nothing here knows that a\n * FleetContext exists — which is what keeps `core/` → `core/c4/` acyclic.\n *\n * REQUEST-SCOPED exactly as `FleetContext` is: one instance per invocation,\n * dropped when the command finishes. No module-level state, so a later run\n * cannot observe an earlier one's answer, and a `chdir` between two tests\n * cannot leak one docs repo into the next.\n */\nimport { resolve } from \"node:path\";\nimport { memo } from \"../../kernel/concurrency.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport type { ServicePaths } from \"../../repo/paths.js\";\nimport type { LoadedDoc } from \"../likec4.js\";\nimport { loadArchitecture } from \"../project/architecture.js\";\nimport { asLoadedDoc, loadProject } from \"../project/load.js\";\nimport { modelProjectDocuments } from \"../service-model/documents.js\";\nimport {\n batchServiceProjects,\n serviceModelAt,\n type ServiceModel,\n} from \"../service-model/load.js\";\nimport { readModelShape, readModelShapes, type ModelShape } from \"../service-model/shape.js\";\n\n/**\n * The three things this memo cannot know for itself, supplied once by whoever\n * owns it. Functions rather than values because each is itself memoised on the\n * caller's side: asking again is what makes an answer shared rather than\n * copied.\n */\nexport interface ModelDeps {\n /** Fires once per per-service project actually parsed, batched or not — the caller owns its counters. */\n onProjectLoad: () => void;\n /** The service directories that exist — the caller has enumerated them; this package must not enumerate them again. */\n known: (docsDir: DocsDir) => Promise<ReadonlySet<string>>;\n /** Parse one `.likec4` document ALONE, through the caller's own document memo, so a prefetched parse is a hit. */\n standalone: (path: string) => Promise<LoadedDoc>;\n}\n\nexport class ServiceModels {\n private readonly architectures = new Map<string, Promise<LoadedDoc>>();\n private readonly shapeOf = new Map<string, Promise<ModelShape>>();\n private readonly projects = new Map<string, Promise<LoadedDoc>>();\n private readonly models = new Map<string, Promise<ServiceModel>>();\n\n constructor(private readonly deps: ModelDeps) {}\n\n /**\n * The fleet map as the PROJECT it is (`../project/architecture.ts`). Read\n * twice over for every service now rather than once for the run — it is the\n * gate on whether an extending model can be read at all, and the other half\n * of the diff that decides what such a model actually declares — so the memo\n * is what keeps a fleet run to one parse of it.\n *\n * Filled ONLY when somebody asks. `model` below hands `loadServiceModel` a\n * thunk rather than an awaited value, so a fleet whose models all stand alone\n * never parses this project at all — a standalone model is read as one file,\n * and paying for the map (and inheriting its failures) for `loam show` or the\n * adopt brief was cost and risk for an answer nobody used.\n */\n architecture(docsDir: DocsDir): Promise<LoadedDoc> {\n return memo(this.architectures, resolve(docsDir), () => loadArchitecture(docsDir));\n }\n\n /**\n * Which shape each model has, keyed by resolved path — ONE read of each\n * file's bytes per invocation, however many questions are asked of it.\n *\n * The bulk form is the primary one because every caller asks in bulk: the\n * fleet target needs the shapes of the WHOLE enumeration before it can grade\n * a single renderer-file finding, and the prefetch needs them before it can\n * decide which workspace each model belongs in.\n */\n async shapes(paths: readonly string[]): Promise<Map<string, ModelShape>> {\n const keys = [...new Set(paths.map((path) => resolve(path)))];\n const missing = keys.filter((k) => !this.shapeOf.has(k));\n for (const [k, shape] of await readModelShapes(missing)) {\n // A concurrent read's promise stays authoritative — it is the one another\n // caller is already holding — so a second scan of the same bytes can\n // never replace it with a duplicate.\n if (!this.shapeOf.has(k)) this.shapeOf.set(k, Promise.resolve(shape));\n }\n const answered = await Promise.all(keys.map(async (k) => [k, await this.shape(k)] as const));\n return new Map(answered);\n }\n\n /** One model's shape, out of the same memo the bulk form fills. */\n shape(modelPath: string): Promise<ModelShape> {\n return memo(this.shapeOf, resolve(modelPath), () => readModelShape(modelPath));\n }\n\n /** A service's model, read the way its own shape demands. Memoised per model PATH, which is what identifies the file. */\n model(docsDir: DocsDir, paths: ServicePaths): Promise<ServiceModel> {\n return memo(this.models, resolve(paths.model), async () =>\n serviceModelAt({\n docsDir,\n paths,\n shape: await this.shape(paths.model),\n known: await this.deps.known(docsDir),\n // A thunk, so a standalone model never spins the map's workspace: the\n // memo makes the extending case one parse for the run either way.\n architecture: () => this.architecture(docsDir),\n standalone: () => this.deps.standalone(paths.model),\n project: () => this.project(docsDir, paths.model),\n }),\n );\n }\n\n /**\n * Prepare every EXTENDING model's project in one workspace, and answer with\n * the STANDALONE models' paths for the caller to prefetch through its own\n * document batch — the two shapes need different workspaces, and only the\n * caller has the memo the second one seeds.\n *\n * Purely an accelerator: a batch that could not be created seeds nothing and\n * every reader falls back to its own load, so findings can never change\n * because of a tmpdir — only the speed can.\n */\n async prefetch(docsDir: DocsDir, paths: readonly ServicePaths[]): Promise<string[]> {\n const shapes = await this.shapes(paths.map((p) => p.model));\n const models = (shape: ModelShape): string[] =>\n [...shapes].filter(([, found]) => found === shape).map(([model]) => model);\n const extending = models(\"extending\").filter((model) => !this.projects.has(model));\n for (const [k, doc] of await batchServiceProjects(docsDir, extending)) {\n // A concurrent load's promise stays authoritative: it is the one every\n // reader is already holding, and replacing it would count the parse twice.\n if (this.projects.has(k)) continue;\n this.deps.onProjectLoad();\n this.projects.set(k, Promise.resolve(doc));\n }\n return models(\"standalone\");\n }\n\n /**\n * One service's project — the architecture documents and its model, parsed\n * together. Keyed on the model, which is what `prefetch` seeds above, so a\n * batched fleet run and a single-service run answer out of the same memo.\n */\n private project(docsDir: DocsDir, modelPath: string): Promise<LoadedDoc> {\n return memo(this.projects, resolve(modelPath), async () => {\n this.deps.onProjectLoad();\n return asLoadedDoc(await loadProject(docsDir, await modelProjectDocuments(docsDir, modelPath)));\n });\n }\n}\n"]}
@@ -9,9 +9,9 @@
9
9
  * `core/` may not import from `commands/` (AGENTS.md), so the choice was a second
10
10
  * copy of the two-line composition or one copy one level down. A second copy is
11
11
  * the copy that drifts, and the thing that would drift is which documents are in
12
- * the project: the generated `architecture/subsystems.likec4` exclusion is a
13
- * correctness rule (see `./documents.ts`), not a preference, and a reader that
14
- * forgot it would blank a fleet's whole map over a stale generated file.
12
+ * the project: the GENERATED documents' exclusion is a correctness rule (see
13
+ * `./documents.ts`), not a preference, and a reader that forgot it would blank a
14
+ * fleet's whole map over a stale generated file.
15
15
  *
16
16
  * So the wrapper moved here and `validate`'s own module kept the landscape
17
17
  * CONTAINMENT — `readLandscape` / `unreadableLandscape` — which is genuinely
@@ -22,11 +22,33 @@ import { join, relative, resolve } from "node:path";
22
22
  import { excludingPath, readRootExclude } from "../root-project/exclude.js";
23
23
  import { architectureDocuments } from "./documents.js";
24
24
  import { asLoadedDoc, loadProject } from "./load.js";
25
- import { landscapePath, subsystemViewsPath } from "../../repo/paths.js";
25
+ import { GENERATED_ARCHITECTURE_FILES, landscapePath } from "../../repo/paths.js";
26
26
  /** `<docsDir>/architecture` — the LikeC4 project root, spelled once. */
27
27
  export function architectureDir(docsDir) {
28
28
  return join(docsDir, "architecture");
29
29
  }
30
+ /**
31
+ * Every document inside `architecture/` that LOAM GENERATES, absolute — the ONE
32
+ * answer four project reads have to agree on, and the reason it is a SET rather
33
+ * than the single path it used to be.
34
+ *
35
+ * There are two generated files now (`subsystems.likec4` and `services.likec4`,
36
+ * both written by `loam subsystem sync`), and every one of these readers must
37
+ * drop BOTH: this module's own project list, the merge-preview staging beside it
38
+ * (`./staged.ts`), and the renderer's-eye load `c4.fleet-project-invalid` builds
39
+ * (`commands/validate/fleet/views/root-documents.ts`). A views-only document
40
+ * does not parse standalone — its `include` lines name elements the landscape
41
+ * declares — so a reader that keeps one loads a document whose only fault may be
42
+ * that `sync` has not run yet, and ONE parse error in a project blanks the model
43
+ * for every document in it. A file listed by one reader and forgotten by another
44
+ * fails silently in exactly that direction, which is why the NAMES live in one
45
+ * table (`repo/paths.ts`'s `GENERATED_ARCHITECTURE_FILES`) and the join to a
46
+ * docsDir lives here, once, rather than as one `…ViewsPath(docsDir)` call per
47
+ * reader that somebody has to remember to extend.
48
+ */
49
+ export function generatedArchitectureDocuments(docsDir) {
50
+ return Object.keys(GENERATED_ARCHITECTURE_FILES).map((name) => join(architectureDir(docsDir), name));
51
+ }
30
52
  /**
31
53
  * Exactly the documents `loadArchitecture` puts in the fleet map's project, as
32
54
  * one named answer.
@@ -35,10 +57,10 @@ export function architectureDir(docsDir) {
35
57
  * model that extends the map is parsed beside it (`c4/service-model/`), and the
36
58
  * set it must be staged with is this one plus the model. Two spellings of "the
37
59
  * architecture project's documents" would be two chances to forget the
38
- * generated `subsystems.likec4` exclusion — a correctness rule, not an
39
- * optimisation (`./documents.ts` says what it prevents) — and the reader that
40
- * forgot it would blank a service's whole model over a stale generated file it
41
- * has nothing to do with.
60
+ * generated documents' exclusion — a correctness rule, not an optimisation
61
+ * (`./documents.ts` says what it prevents) — and the reader that forgot it would
62
+ * blank a service's whole model over a stale generated file it has nothing to do
63
+ * with.
42
64
  *
43
65
  * THE ROOT `exclude` IS HONOURED HERE, and that is the second correctness rule
44
66
  * this one list carries. A `.likec4` under `architecture/` that the root project
@@ -68,7 +90,7 @@ export function architectureDir(docsDir) {
68
90
  * renderer drops.
69
91
  */
70
92
  export async function architectureProjectDocuments(docsDir) {
71
- const paths = await architectureDocuments(architectureDir(docsDir), [subsystemViewsPath(docsDir)]);
93
+ const paths = await architectureDocuments(architectureDir(docsDir), generatedArchitectureDocuments(docsDir));
72
94
  const exclude = await readRootExclude(docsDir);
73
95
  if (exclude === null || exclude.length === 0)
74
96
  return paths;
@@ -1 +1 @@
1
- {"version":3,"file":"architecture.js","sourceRoot":"","sources":["../../../../src/core/c4/project/architecture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAExE,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,OAAgB;IACjE,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACnG,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AACnH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgB;IACrD,OAAO,WAAW,CAAC,MAAM,WAAW,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC/G,CAAC","sourcesContent":["/**\n * `architecture/` as ONE LikeC4 project — the docsDir-aware wrapper over the two\n * modules beside it.\n *\n * It lived in `commands/validate/fleet/load.ts` until the use-case axis grew a\n * second reader. That was the right home while `validate --all` was the only\n * caller: the command layer is where a `DocsDir` becomes paths. It stopped being\n * the right home the moment `core/usecases/fleet.ts` needed the same load —\n * `core/` may not import from `commands/` (AGENTS.md), so the choice was a second\n * copy of the two-line composition or one copy one level down. A second copy is\n * the copy that drifts, and the thing that would drift is which documents are in\n * the project: the generated `architecture/subsystems.likec4` exclusion is a\n * correctness rule (see `./documents.ts`), not a preference, and a reader that\n * forgot it would blank a fleet's whole map over a stale generated file.\n *\n * So the wrapper moved here and `validate`'s own module kept the landscape\n * CONTAINMENT — `readLandscape` / `unreadableLandscape` — which is genuinely\n * command-layer business: it is about which validate target a failed read is\n * filed against.\n */\nimport { join, relative, resolve } from \"node:path\";\nimport type { LoadedDoc } from \"../likec4.js\";\nimport { excludingPath, readRootExclude } from \"../root-project/exclude.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { landscapePath, subsystemViewsPath } from \"../../repo/paths.js\";\n\n/** `<docsDir>/architecture` — the LikeC4 project root, spelled once. */\nexport function architectureDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\");\n}\n\n/**\n * Exactly the documents `loadArchitecture` puts in the fleet map's project, as\n * one named answer.\n *\n * It exists because a SECOND project now has to contain the same set: a service\n * model that extends the map is parsed beside it (`c4/service-model/`), and the\n * set it must be staged with is this one plus the model. Two spellings of \"the\n * architecture project's documents\" would be two chances to forget the\n * generated `subsystems.likec4` exclusion — a correctness rule, not an\n * optimisation (`./documents.ts` says what it prevents) — and the reader that\n * forgot it would blank a service's whole model over a stale generated file it\n * has nothing to do with.\n *\n * THE ROOT `exclude` IS HONOURED HERE, and that is the second correctness rule\n * this one list carries. A `.likec4` under `architecture/` that the root project\n * excludes is a document the RENDERER never loads, so a global style group\n * declared in one is a name the fleet project cannot resolve — and until this\n * filter existed the generated subsystem views referenced it anyway: `likec4\n * validate` Invalid on every run, `loam validate --all` at 0 errors, `doctor`\n * healthy and `sync` \"current\" (verification 2026-09-04, W5). A root config loam\n * cannot read an `exclude` list out of filters nothing: asserting an exclusion on\n * evidence loam does not have is worse than loading one document too many.\n *\n * THE MAP ITSELF IS THE FLOOR, and it is not a special case for tidiness. The\n * filter had none, so an entry covering `architecture/landscape.likec4` — an\n * `architecture/*.likec4` a team wrote for a palette, say — left this list\n * EMPTY, and an empty project parses with zero errors. Every reader then took\n * that for \"the map declares nothing\": `validate --all` reported\n * `landscape.service-unmodelled` naming the very file that models the service\n * and `c4.invalid` on every extending model, `subsystem sync` rewrote a good\n * generated file down to a title, and nothing in either run mentioned the\n * `exclude` entry that caused it (verification 2026-09-04, review C). Keeping\n * the landscape is the tolerant direction this same banner already chooses for\n * an unreadable config: loam grades one document the renderer may skip, rather\n * than asserting a fleet's whole architecture says nothing. The entry is NAMED\n * to its author one target over — `landscape.excluded`, in\n * `commands/validate/fleet/views/projects.ts`, which reads this same list\n * through this same matcher so the two can never disagree about which file the\n * renderer drops.\n */\nexport async function architectureProjectDocuments(docsDir: DocsDir): Promise<string[]> {\n const paths = await architectureDocuments(architectureDir(docsDir), [subsystemViewsPath(docsDir)]);\n const exclude = await readRootExclude(docsDir);\n if (exclude === null || exclude.length === 0) return paths;\n const map = resolve(landscapePath(docsDir));\n return paths.filter((path) => resolve(path) === map || excludingPath(exclude, relative(docsDir, path)) === null);\n}\n\n/**\n * The fleet map, read as the PROJECT it actually is.\n *\n * `architecture/landscape.likec4` plus every `architecture/usecases/*.likec4`,\n * merged the way the renderer merges them — because a use case declares views\n * over the landscape's elements and does not parse standalone (measured: five\n * errors). `architectureDocuments` owns which files are in and why the\n * generated one is not.\n *\n * A fleet with no use cases loads exactly the landscape and behaves as it\n * always did, which is what keeps this a widening rather than a change.\n */\nexport async function loadArchitecture(docsDir: DocsDir): Promise<LoadedDoc> {\n return asLoadedDoc(await loadProject(architectureDir(docsDir), await architectureProjectDocuments(docsDir)));\n}\n"]}
1
+ {"version":3,"file":"architecture.js","sourceRoot":"","sources":["../../../../src/core/c4/project/architecture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,4BAA4B,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAElF,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,8BAA8B,CAAC,OAAgB;IAC7D,OAAO,MAAM,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AACvG,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,OAAgB;IACjE,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,8BAA8B,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7G,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AACnH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgB;IACrD,OAAO,WAAW,CAAC,MAAM,WAAW,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC/G,CAAC","sourcesContent":["/**\n * `architecture/` as ONE LikeC4 project — the docsDir-aware wrapper over the two\n * modules beside it.\n *\n * It lived in `commands/validate/fleet/load.ts` until the use-case axis grew a\n * second reader. That was the right home while `validate --all` was the only\n * caller: the command layer is where a `DocsDir` becomes paths. It stopped being\n * the right home the moment `core/usecases/fleet.ts` needed the same load —\n * `core/` may not import from `commands/` (AGENTS.md), so the choice was a second\n * copy of the two-line composition or one copy one level down. A second copy is\n * the copy that drifts, and the thing that would drift is which documents are in\n * the project: the GENERATED documents' exclusion is a correctness rule (see\n * `./documents.ts`), not a preference, and a reader that forgot it would blank a\n * fleet's whole map over a stale generated file.\n *\n * So the wrapper moved here and `validate`'s own module kept the landscape\n * CONTAINMENT — `readLandscape` / `unreadableLandscape` — which is genuinely\n * command-layer business: it is about which validate target a failed read is\n * filed against.\n */\nimport { join, relative, resolve } from \"node:path\";\nimport type { LoadedDoc } from \"../likec4.js\";\nimport { excludingPath, readRootExclude } from \"../root-project/exclude.js\";\nimport { architectureDocuments } from \"./documents.js\";\nimport { asLoadedDoc, loadProject } from \"./load.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\nimport { GENERATED_ARCHITECTURE_FILES, landscapePath } from \"../../repo/paths.js\";\n\n/** `<docsDir>/architecture` — the LikeC4 project root, spelled once. */\nexport function architectureDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\");\n}\n\n/**\n * Every document inside `architecture/` that LOAM GENERATES, absolute — the ONE\n * answer four project reads have to agree on, and the reason it is a SET rather\n * than the single path it used to be.\n *\n * There are two generated files now (`subsystems.likec4` and `services.likec4`,\n * both written by `loam subsystem sync`), and every one of these readers must\n * drop BOTH: this module's own project list, the merge-preview staging beside it\n * (`./staged.ts`), and the renderer's-eye load `c4.fleet-project-invalid` builds\n * (`commands/validate/fleet/views/root-documents.ts`). A views-only document\n * does not parse standalone — its `include` lines name elements the landscape\n * declares — so a reader that keeps one loads a document whose only fault may be\n * that `sync` has not run yet, and ONE parse error in a project blanks the model\n * for every document in it. A file listed by one reader and forgotten by another\n * fails silently in exactly that direction, which is why the NAMES live in one\n * table (`repo/paths.ts`'s `GENERATED_ARCHITECTURE_FILES`) and the join to a\n * docsDir lives here, once, rather than as one `…ViewsPath(docsDir)` call per\n * reader that somebody has to remember to extend.\n */\nexport function generatedArchitectureDocuments(docsDir: DocsDir): string[] {\n return Object.keys(GENERATED_ARCHITECTURE_FILES).map((name) => join(architectureDir(docsDir), name));\n}\n\n/**\n * Exactly the documents `loadArchitecture` puts in the fleet map's project, as\n * one named answer.\n *\n * It exists because a SECOND project now has to contain the same set: a service\n * model that extends the map is parsed beside it (`c4/service-model/`), and the\n * set it must be staged with is this one plus the model. Two spellings of \"the\n * architecture project's documents\" would be two chances to forget the\n * generated documents' exclusion — a correctness rule, not an optimisation\n * (`./documents.ts` says what it prevents) — and the reader that forgot it would\n * blank a service's whole model over a stale generated file it has nothing to do\n * with.\n *\n * THE ROOT `exclude` IS HONOURED HERE, and that is the second correctness rule\n * this one list carries. A `.likec4` under `architecture/` that the root project\n * excludes is a document the RENDERER never loads, so a global style group\n * declared in one is a name the fleet project cannot resolve — and until this\n * filter existed the generated subsystem views referenced it anyway: `likec4\n * validate` Invalid on every run, `loam validate --all` at 0 errors, `doctor`\n * healthy and `sync` \"current\" (verification 2026-09-04, W5). A root config loam\n * cannot read an `exclude` list out of filters nothing: asserting an exclusion on\n * evidence loam does not have is worse than loading one document too many.\n *\n * THE MAP ITSELF IS THE FLOOR, and it is not a special case for tidiness. The\n * filter had none, so an entry covering `architecture/landscape.likec4` — an\n * `architecture/*.likec4` a team wrote for a palette, say — left this list\n * EMPTY, and an empty project parses with zero errors. Every reader then took\n * that for \"the map declares nothing\": `validate --all` reported\n * `landscape.service-unmodelled` naming the very file that models the service\n * and `c4.invalid` on every extending model, `subsystem sync` rewrote a good\n * generated file down to a title, and nothing in either run mentioned the\n * `exclude` entry that caused it (verification 2026-09-04, review C). Keeping\n * the landscape is the tolerant direction this same banner already chooses for\n * an unreadable config: loam grades one document the renderer may skip, rather\n * than asserting a fleet's whole architecture says nothing. The entry is NAMED\n * to its author one target over — `landscape.excluded`, in\n * `commands/validate/fleet/views/projects.ts`, which reads this same list\n * through this same matcher so the two can never disagree about which file the\n * renderer drops.\n */\nexport async function architectureProjectDocuments(docsDir: DocsDir): Promise<string[]> {\n const paths = await architectureDocuments(architectureDir(docsDir), generatedArchitectureDocuments(docsDir));\n const exclude = await readRootExclude(docsDir);\n if (exclude === null || exclude.length === 0) return paths;\n const map = resolve(landscapePath(docsDir));\n return paths.filter((path) => resolve(path) === map || excludingPath(exclude, relative(docsDir, path)) === null);\n}\n\n/**\n * The fleet map, read as the PROJECT it actually is.\n *\n * `architecture/landscape.likec4` plus every `architecture/usecases/*.likec4`,\n * merged the way the renderer merges them — because a use case declares views\n * over the landscape's elements and does not parse standalone (measured: five\n * errors). `architectureDocuments` owns which files are in and why the\n * generated one is not.\n *\n * A fleet with no use cases loads exactly the landscape and behaves as it\n * always did, which is what keeps this a widening rather than a change.\n */\nexport async function loadArchitecture(docsDir: DocsDir): Promise<LoadedDoc> {\n return asLoadedDoc(await loadProject(architectureDir(docsDir), await architectureProjectDocuments(docsDir)));\n}\n"]}
@@ -9,8 +9,8 @@
9
9
  * that STANDS ALONE — one `services/<tree>/**` entry per standalone model,
10
10
  * never the whole subtree (`c4/service-model/renderer.ts` owns that list, and
11
11
  * is the one place loam reads that file). loam's own answer here is all of them
12
- * EXCEPT the generated `architecture/subsystems.likec4`, and that exception is
13
- * not an optimisation:
12
+ * EXCEPT the documents loam GENERATES there (`architecture/subsystems.likec4`
13
+ * and `architecture/services.likec4`), and that exception is not an optimisation:
14
14
  *
15
15
  * - docs/DESIGN.md rule 26 excludes the generated file from every read loam
16
16
  * performs. Its staleness is a byte compare (`subsystem.views-stale`) and
@@ -22,11 +22,12 @@
22
22
  * whose only problem is a stale generated file would lose its whole map
23
23
  * instead of getting the one finding that names the repair.
24
24
  *
25
- * The skip is passed in by the caller rather than spelled here, so the filename
26
- * stays written in `repo/paths.ts` alone.
25
+ * The skip is passed in by the caller rather than spelled here, so the filenames
26
+ * stay written in `repo/paths.ts` alone, and every reader takes the same SET out
27
+ * (`./architecture.ts` holds the one join).
27
28
  */
28
29
  import { readdir } from "node:fs/promises";
29
- import { join, resolve } from "node:path";
30
+ import { join, relative, resolve } from "node:path";
30
31
  /**
31
32
  * Every `.likec4` file under `dir`, recursively, minus `skip`, in a stable
32
33
  * order.
@@ -40,24 +41,49 @@ import { join, resolve } from "node:path";
40
41
  */
41
42
  export async function architectureDocuments(dir, skip = []) {
42
43
  const excluded = new Set(skip.map((path) => resolve(path)));
44
+ return likec4DocumentsUnder(dir, { keeps: (path) => !excluded.has(resolve(path)) });
45
+ }
46
+ /**
47
+ * `architectureDocuments(dir)`, each beside its path inside `dir`,
48
+ * `/`-separated — the id a feature-local document axis keys on (a feature's
49
+ * `usecases/` flows, its `deployment/` topology), spelled once for both.
50
+ */
51
+ export async function relativeDocuments(dir) {
52
+ const base = resolve(dir);
53
+ return (await architectureDocuments(dir)).map((path) => ({ rel: relative(base, path).split(/[\\/]/).join("/"), path }));
54
+ }
55
+ /**
56
+ * The walk under `architectureDocuments` and the renderer root project's
57
+ * enumeration (`commands/validate/fleet/views/root-documents.ts`): every
58
+ * `.likec4` under `dir`, recursively, absolute and sorted for the reason
59
+ * above. A directory that cannot be read contributes nothing, never a throw —
60
+ * for the missing-`architecture/` reason above, and because the root walk
61
+ * runs inside `validate --all` over a whole fleet, where the one behaviour it
62
+ * may never have is failing the run it was added to.
63
+ */
64
+ export async function likec4DocumentsUnder(dir, walk) {
43
65
  const found = [];
44
- const walk = async (at) => {
66
+ const visit = async (at) => {
45
67
  let entries;
46
68
  try {
47
69
  entries = await readdir(at, { withFileTypes: true });
48
70
  }
49
71
  catch {
72
+ // Absent or unreadable: nothing to load from here (see above).
50
73
  return;
51
74
  }
52
75
  for (const entry of entries) {
53
76
  const path = join(at, entry.name);
54
- if (entry.isDirectory())
55
- await walk(path);
56
- else if (entry.name.endsWith(".likec4") && !excluded.has(resolve(path)))
77
+ if (entry.isDirectory()) {
78
+ if (walk.enters?.(entry.name) ?? true)
79
+ await visit(path);
80
+ }
81
+ else if (entry.name.endsWith(".likec4") && (walk.keeps?.(path) ?? true)) {
57
82
  found.push(path);
83
+ }
58
84
  }
59
85
  };
60
- await walk(resolve(dir));
86
+ await visit(resolve(dir));
61
87
  return found.sort();
62
88
  }
63
89
  //# sourceMappingURL=documents.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../../src/core/c4/project/documents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,GAAW,EAAE,IAAI,GAAa,EAAE;IAC1E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,KAAK,EAAE,EAAU,EAAiB,EAAE;QAC/C,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;iBACrC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC","sourcesContent":["/**\n * Which `.likec4` documents in a docs repo's `architecture/` loam loads as one\n * project.\n *\n * The renderer's answer is \"all of them\". The root `likec4.config.json` no\n * longer excludes `services/**`: the root project holds the map AND every\n * service model that EXTENDS it, because such a model is only readable beside\n * the map, and `loam subsystem sync` excludes only the directory of each model\n * that STANDS ALONE — one `services/<tree>/**` entry per standalone model,\n * never the whole subtree (`c4/service-model/renderer.ts` owns that list, and\n * is the one place loam reads that file). loam's own answer here is all of them\n * EXCEPT the generated `architecture/subsystems.likec4`, and that exception is\n * not an optimisation:\n *\n * - docs/DESIGN.md rule 26 excludes the generated file from every read loam\n * performs. Its staleness is a byte compare (`subsystem.views-stale`) and\n * nothing anywhere reads its contents.\n * - Including it would invent a failure mode. Its `include` lines name\n * landscape element ids, so a file that has drifted — a service removed\n * since the last `loam subsystem sync` — would be a parse error, and a parse\n * error in one project document blanks the model for all of them. A fleet\n * whose only problem is a stale generated file would lose its whole map\n * instead of getting the one finding that names the repair.\n *\n * The skip is passed in by the caller rather than spelled here, so the filename\n * stays written in `repo/paths.ts` alone.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\n/**\n * Every `.likec4` file under `dir`, recursively, minus `skip`, in a stable\n * order.\n *\n * Sorted because it decides the staging order and therefore nothing at all —\n * LikeC4 merges a project regardless — but a stable list makes a failure\n * reproducible and a diff of one readable. A directory that does not exist is\n * an empty list, never a throw: a fleet with no `architecture/` is a fleet with\n * nothing to load, which is already `landscape.missing`'s question, not this\n * function's.\n */\nexport async function architectureDocuments(dir: string, skip: string[] = []): Promise<string[]> {\n const excluded = new Set(skip.map((path) => resolve(path)));\n const found: string[] = [];\n const walk = async (at: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(at, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const path = join(at, entry.name);\n if (entry.isDirectory()) await walk(path);\n else if (entry.name.endsWith(\".likec4\") && !excluded.has(resolve(path))) found.push(path);\n }\n };\n await walk(resolve(dir));\n return found.sort();\n}\n"]}
1
+ {"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../../src/core/c4/project/documents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,GAAW,EAAE,IAAI,GAAa,EAAE;IAC1E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5D,OAAO,oBAAoB,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;AACtF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAW;IACjD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,CAAC,MAAM,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAC1H,CAAC;AAQD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,GAAW,EAAE,IAAkB;IACxE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,KAAK,EAAE,EAAU,EAAiB,EAAE;QAChD,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,+DAA+D;YAC/D,OAAO;QACT,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI;oBAAE,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;YAC3D,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC;gBAC1E,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC","sourcesContent":["/**\n * Which `.likec4` documents in a docs repo's `architecture/` loam loads as one\n * project.\n *\n * The renderer's answer is \"all of them\". The root `likec4.config.json` no\n * longer excludes `services/**`: the root project holds the map AND every\n * service model that EXTENDS it, because such a model is only readable beside\n * the map, and `loam subsystem sync` excludes only the directory of each model\n * that STANDS ALONE — one `services/<tree>/**` entry per standalone model,\n * never the whole subtree (`c4/service-model/renderer.ts` owns that list, and\n * is the one place loam reads that file). loam's own answer here is all of them\n * EXCEPT the documents loam GENERATES there (`architecture/subsystems.likec4`\n * and `architecture/services.likec4`), and that exception is not an optimisation:\n *\n * - docs/DESIGN.md rule 26 excludes the generated file from every read loam\n * performs. Its staleness is a byte compare (`subsystem.views-stale`) and\n * nothing anywhere reads its contents.\n * - Including it would invent a failure mode. Its `include` lines name\n * landscape element ids, so a file that has drifted — a service removed\n * since the last `loam subsystem sync` — would be a parse error, and a parse\n * error in one project document blanks the model for all of them. A fleet\n * whose only problem is a stale generated file would lose its whole map\n * instead of getting the one finding that names the repair.\n *\n * The skip is passed in by the caller rather than spelled here, so the filenames\n * stay written in `repo/paths.ts` alone, and every reader takes the same SET out\n * (`./architecture.ts` holds the one join).\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative, resolve } from \"node:path\";\n\n/**\n * Every `.likec4` file under `dir`, recursively, minus `skip`, in a stable\n * order.\n *\n * Sorted because it decides the staging order and therefore nothing at all —\n * LikeC4 merges a project regardless — but a stable list makes a failure\n * reproducible and a diff of one readable. A directory that does not exist is\n * an empty list, never a throw: a fleet with no `architecture/` is a fleet with\n * nothing to load, which is already `landscape.missing`'s question, not this\n * function's.\n */\nexport async function architectureDocuments(dir: string, skip: string[] = []): Promise<string[]> {\n const excluded = new Set(skip.map((path) => resolve(path)));\n return likec4DocumentsUnder(dir, { keeps: (path) => !excluded.has(resolve(path)) });\n}\n\n/**\n * `architectureDocuments(dir)`, each beside its path inside `dir`,\n * `/`-separated — the id a feature-local document axis keys on (a feature's\n * `usecases/` flows, its `deployment/` topology), spelled once for both.\n */\nexport async function relativeDocuments(dir: string): Promise<{ rel: string; path: string }[]> {\n const base = resolve(dir);\n return (await architectureDocuments(dir)).map((path) => ({ rel: relative(base, path).split(/[\\\\/]/).join(\"/\"), path }));\n}\n\n/** What one walk leaves out: directories it never enters (by name), documents it never collects (by path). */\nexport interface DocumentWalk {\n enters?: (name: string) => boolean;\n keeps?: (path: string) => boolean;\n}\n\n/**\n * The walk under `architectureDocuments` and the renderer root project's\n * enumeration (`commands/validate/fleet/views/root-documents.ts`): every\n * `.likec4` under `dir`, recursively, absolute and sorted for the reason\n * above. A directory that cannot be read contributes nothing, never a throw —\n * for the missing-`architecture/` reason above, and because the root walk\n * runs inside `validate --all` over a whole fleet, where the one behaviour it\n * may never have is failing the run it was added to.\n */\nexport async function likec4DocumentsUnder(dir: string, walk: DocumentWalk): Promise<string[]> {\n const found: string[] = [];\n const visit = async (at: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(at, { withFileTypes: true });\n } catch {\n // Absent or unreadable: nothing to load from here (see above).\n return;\n }\n for (const entry of entries) {\n const path = join(at, entry.name);\n if (entry.isDirectory()) {\n if (walk.enters?.(entry.name) ?? true) await visit(path);\n } else if (entry.name.endsWith(\".likec4\") && (walk.keeps?.(path) ?? true)) {\n found.push(path);\n }\n }\n };\n await visit(resolve(dir));\n return found.sort();\n}\n"]}