@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
@@ -1,6 +1,27 @@
1
1
  import {} from "../likec4.js";
2
2
  import { elementService, serviceOf } from "../resolve/service.js";
3
+ import { maskSource, pastComments, tokenEnd } from "../source-mask.js";
3
4
  import { relSortKey } from "./identity/edges.js";
5
+ /**
6
+ * The block an addition with no anchor lands in: the document's FIRST
7
+ * `model { }` block.
8
+ *
9
+ * First, because it is the block every unanchored addition already landed in
10
+ * before the scan could see a second one — the scan stopped at the first
11
+ * match — so a single-block map places byte-identically, and a map that grew
12
+ * a second block keeps receiving new services where its older ones went. It
13
+ * is also invariant under every addition this merge can make (an archive never
14
+ * creates or reorders a block), which is what order-independence needs of it:
15
+ * a block chosen by size or content would move as the archives landed, and the
16
+ * same two features would compose to different bytes by archive order. The
17
+ * service-model splice (`./model/merge.ts`) uses the same block for the same
18
+ * reasons.
19
+ */
20
+ export const HOME_BLOCK = 0;
21
+ /** The statements of one block, in document order — what a trailing walk may step over. */
22
+ function inBlock(stmts, block) {
23
+ return stmts.filter((s) => s.block === block);
24
+ }
4
25
  /** The model's top-level statements in document order, joined to the parsed elements for services. */
5
26
  export function topStatements(scan, els) {
6
27
  // An `extend <fqn>` frame is never a top-level declaration of THIS document —
@@ -17,11 +38,20 @@ export function topStatements(scan, els) {
17
38
  services: [bound.get(e.id) ?? e.id],
18
39
  start: e.start,
19
40
  end: e.end,
41
+ block: e.block,
20
42
  }));
43
+ // Every top-level FRAME a statement can sit inside: the elements, and the
44
+ // `extend` frames, which hold edges as well as children.
45
+ const frames = scan.elements.filter((e) => e.extend === true || !e.id.includes("."));
21
46
  for (const r of scan.rels) {
22
47
  // A rel declared inside an element's body is not a top-level statement —
23
- // anchoring after it would splice into the parent's block.
24
- if (topEls.some((e) => r.start > e.start && r.start < e.end))
48
+ // anchoring after it would splice into the parent's block. Inside an
49
+ // `extend` frame it is worse than misplaced: the frame is a SCOPE, so an
50
+ // edge spliced in after it re-resolves there, and the delta's `zulu ->
51
+ // beta` landed inside `extend alpha { beta = … }` as an edge into
52
+ // `alpha.beta` — a merge that parses, written at exit 0 (probed
53
+ // 2026-10-20). Only the extend frames' edges are new to this filter.
54
+ if (frames.some((e) => r.start > e.start && r.start < e.end))
25
55
  continue;
26
56
  stmts.push({
27
57
  kind: "rel",
@@ -35,6 +65,7 @@ export function topStatements(scan, els) {
35
65
  services: [serviceOf(els, r.source), serviceOf(els, r.target)],
36
66
  start: r.start,
37
67
  end: r.end,
68
+ block: r.block,
38
69
  });
39
70
  }
40
71
  return stmts.sort((a, b) => a.start - b.start);
@@ -54,14 +85,16 @@ export function topStatements(scan, els) {
54
85
  * exact regime the scheme guarantees.
55
86
  */
56
87
  export function elementSpot(region, id, service) {
57
- const { text, stmts, close } = region;
88
+ const { text, blocks } = region;
58
89
  let anchor;
59
- for (const s of stmts) {
90
+ for (const s of region.stmts) {
60
91
  if (s.kind === "element" && s.services.includes(service))
61
92
  anchor = s;
62
93
  }
63
94
  if (anchor !== undefined)
64
- return spotAfter(text, close, anchor.end);
95
+ return spotAfter(text, blocks[anchor.block].close, anchor.end);
96
+ const stmts = inBlock(region.stmts, HOME_BLOCK);
97
+ const close = blocks[HOME_BLOCK].close;
65
98
  let i = stmts.length - 1;
66
99
  let before;
67
100
  while (i >= 0 && stmts[i].kind === "rel") {
@@ -99,6 +132,12 @@ function relClusterHeads(stmts) {
99
132
  let services = null;
100
133
  for (let i = 0; i < stmts.length; i += 1) {
101
134
  const s = stmts[i];
135
+ // A block boundary ends a cluster exactly as an element does: the first
136
+ // edge of the next block is not contiguous with the last of this one.
137
+ if (i > 0 && stmts[i - 1].block !== s.block) {
138
+ head = -1;
139
+ services = null;
140
+ }
102
141
  if (s.kind !== "rel") {
103
142
  head = -1;
104
143
  services = null;
@@ -117,7 +156,27 @@ function relClusterHeads(stmts) {
117
156
  return heads;
118
157
  }
119
158
  export function relSpot(region, sourceService, key) {
120
- const { text, stmts, close } = region;
159
+ const anchored = anchoredRelSpot(region, sourceService, key);
160
+ if (anchored !== null)
161
+ return anchored;
162
+ const { text, blocks } = region;
163
+ const stmts = inBlock(region.stmts, HOME_BLOCK);
164
+ const close = blocks[HOME_BLOCK].close;
165
+ const heads = relClusterHeads(stmts);
166
+ let i = stmts.length - 1;
167
+ let before;
168
+ while (i >= 0 && stmts[i].kind === "rel") {
169
+ const j = heads[i];
170
+ if (!(stmts[j].key > key))
171
+ break;
172
+ before = stmts[j];
173
+ i = j - 1;
174
+ }
175
+ return before !== undefined ? beforeSpot(text, before) : closeSpot(text, close);
176
+ }
177
+ /** After the last relationship touching the source's service, in that relationship's block — null when none does. */
178
+ function anchoredRelSpot(region, sourceService, key) {
179
+ const { text, stmts, blocks } = region;
121
180
  const heads = relClusterHeads(stmts);
122
181
  let anchorAt = -1;
123
182
  for (let i = 0; i < stmts.length; i += 1) {
@@ -138,18 +197,9 @@ export function relSpot(region, sourceService, key) {
138
197
  break;
139
198
  last = s;
140
199
  }
141
- return spotAfter(text, close, last.end);
142
- }
143
- let i = stmts.length - 1;
144
- let before;
145
- while (i >= 0 && stmts[i].kind === "rel") {
146
- const j = heads[i];
147
- if (!(stmts[j].key > key))
148
- break;
149
- before = stmts[j];
150
- i = j - 1;
200
+ return spotAfter(text, blocks[last.block].close, last.end);
151
201
  }
152
- return before !== undefined ? beforeSpot(text, before) : closeSpot(text, close);
202
+ return null;
153
203
  }
154
204
  /**
155
205
  * Insertion before an existing statement: at the start of its line when only
@@ -169,10 +219,36 @@ function beforeSpot(text, before) {
169
219
  * brace's own spot. Exported for the service-model splice (`./model/merge.ts`),
170
220
  * which asks the same two questions of a `model { }` block it does not group by
171
221
  * service: after this statement, or last thing in the block.
222
+ *
223
+ * Unless code follows `end` on its line: then the spot is `end` itself, bare.
224
+ * The next line belongs to whatever is written there, and when that is a
225
+ * declaration whose body opens on the same line — `alpha = … { … } beta = … {`
226
+ * — the line after is INSIDE beta: an addition anchored to alpha's service
227
+ * landed as `beta.kiwi`, in a merge that parsed and was written (exit 0,
228
+ * probed on main at 171555d). Right after the anchor is also what keeps two
229
+ * archives order-free when both anchor on that line: one anchored to alpha and
230
+ * one to beta used to share the next line's start, and whichever ran second
231
+ * landed above the other. A comment trailing the anchor is not code — the
232
+ * next line stays the spot, byte for byte as before — but a block comment
233
+ * it does not close is: its next line is inside the comment.
234
+ *
235
+ * A line comment ends where LikeC4's NL does, at `\r` as well as `\n` — the
236
+ * source mask agrees since the bare-CR fix — so `// note\r beta = … {` is a
237
+ * comment and then code on the anchor's `\n`-line, and the spot after that
238
+ * line is inside beta: the same `beta.kiwi` merge, written at exit 0, which
239
+ * 171555d refused because it could not locate beta at all. A CRLF ending
240
+ * leaves nothing after the comment and keeps the next line, byte for byte.
172
241
  */
173
242
  export function spotAfter(text, close, end) {
174
243
  const nl = text.indexOf("\n", end);
175
244
  const at = nl === -1 ? text.length : nl + 1;
245
+ let rest = pastComments(text.slice(end, Math.min(at, close)));
246
+ while (rest.startsWith("//")) {
247
+ const eol = rest.search(/[\r\n]/);
248
+ rest = eol === -1 ? "" : pastComments(rest.slice(eol));
249
+ }
250
+ if (rest !== "")
251
+ return { at: end, bare: true };
176
252
  return at > close ? closeSpot(text, close) : { at, bare: false };
177
253
  }
178
254
  /** The last resort: directly before the model's closing brace. */
@@ -192,7 +268,7 @@ function lineStartAt(text, at) {
192
268
  */
193
269
  export function nestedInsert(text, parent, block) {
194
270
  if (parent.bodyOpen === -1) {
195
- return { at: parent.end, insert: ` {\n${block}\n${parent.indent}}` };
271
+ return { at: bodyAt(text, parent), insert: ` {\n${block}\n${parent.indent}}` };
196
272
  }
197
273
  const lineStart = text.lastIndexOf("\n", parent.bodyClose - 1) + 1;
198
274
  if (/^[ \t]*$/.test(text.slice(lineStart, parent.bodyClose))) {
@@ -201,4 +277,18 @@ export function nestedInsert(text, parent, block) {
201
277
  // `{ ... }` on one line — break the brace onto its own line to make room.
202
278
  return { at: parent.bodyClose, insert: `\n${block}\n${parent.indent}` };
203
279
  }
280
+ /**
281
+ * Where a bodyless parent's new body opens: where its span ends, unless a line
282
+ * comment trails its last token there. The span carries that comment — it is
283
+ * the declaration's own authored text (`statementEnd`) — so the `{` landed
284
+ * INSIDE `// the api`, everything after it on the line was comment, and the
285
+ * parse net refused a valid map ("Expecting end of file"). It opens before the
286
+ * comment now, which then trails the new closing brace. A closed block comment
287
+ * is no such trap, and keeps the bytes it always had.
288
+ */
289
+ function bodyAt(text, parent) {
290
+ const span = text.slice(parent.start, parent.end);
291
+ const last = parent.start + tokenEnd(maskSource(span), 0, span.length);
292
+ return pastComments(text.slice(last, parent.end)) === "" ? parent.end : last;
293
+ }
204
294
  //# sourceMappingURL=placement.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"placement.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/placement.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Elem } from \"../likec4.js\";\nimport { elementService, serviceOf } from \"../resolve/service.js\";\nimport type { ScannedElement, ScannedModel } from \"../source-scan.js\";\nimport { relSortKey } from \"./identity/edges.js\";\n\n/* ------------------------------------------------------------------ */\n/* Landscape placement — where a top-level addition lands */\n/* ------------------------------------------------------------------ */\n\n/**\n * The placement scheme, in full. It never moves an authored byte — placement\n * only chooses where NEW spliced blocks go — and it is deterministic in a\n * stronger sense: archiving feature A then feature B (touching different\n * services) yields the SAME final bytes as B then A, which is what lets two\n * concurrent archive PRs merge in git instead of colliding on one shared\n * append point.\n *\n * - An ELEMENT whose service already has top-level elements in the model\n * (the elementService join — binding first, title as the fallback) lands\n * immediately after the last of them: it joins its service's neighborhood.\n * Two features touching the SAME service therefore land adjacently, and\n * their concurrent PRs still conflict — expected, and documented\n * (SCHEMA.md sends that conflict to the PR flow).\n * - An element opening a NEW service lands in the trailing region before the\n * model's closing brace: skip the trailing relationship run, then walk left\n * per same-service RUN, past every run whose leading id is\n * lexicographically greater — an insertion sort of neighborhood heads\n * against whatever is already there, which is what makes the region's\n * final order independent of archive order (see the note on elementSpot:\n * per-statement comparison was order-dependent).\n * - A RELATIONSHIP lands after the last top-level relationship touching its\n * SOURCE's service (either endpoint counts), then past the anchor's own\n * CLUSTER members with a smaller sort key — two edges anchored to the same\n * statement must order by key, not by which archived first. With no such\n * neighborhood it falls to the trailing region, cluster-head-key-sorted\n * the same insertion-sort way, after the trailing elements.\n *\n * LikeC4 reference resolution is not order-dependent, so a relationship\n * placed before an element it names still parses; the in-memory parse net in\n * landscape-merge.ts's planLandscapeMerge stays as the backstop regardless.\n */\nexport interface Spot {\n /** Offset into the living text: a line start, or a point inside a shared line. */\n at: number;\n /** True when `at` shares its line with other text (the closing brace, or a displaced statement) — the block must bring its own newlines. */\n bare: boolean;\n}\n\n/** A top-level statement of the living model, with everything placement asks of it. */\nexport interface TopStmt {\n kind: \"element\" | \"rel\";\n /** Element: its id. */\n id: string;\n /** Relationship: its placement sort key (relSortKey). */\n key: string;\n /** The service(s) this statement resolves to — one for an element, both endpoints for a rel. */\n services: string[];\n start: number;\n end: number;\n}\n\n/**\n * The living model as placement reads it: the text being spliced into, its\n * top-level statement layout, and the model block's closing brace. One value\n * because every spot is computed against all three at the same instant — a\n * layout paired with text a splice has since moved would place against a\n * document that no longer exists (see `modelRegion` in landscape-merge.ts's\n * planLandscapeMerge).\n */\nexport interface ModelRegion {\n text: string;\n stmts: TopStmt[];\n close: number;\n}\n\n/** The model's top-level statements in document order, joined to the parsed elements for services. */\nexport function topStatements(scan: ScannedModel, els: Elem[]): TopStmt[] {\n // An `extend <fqn>` frame is never a top-level declaration of THIS document —\n // it reopens one another document owns — so it anchors nothing and displaces\n // nothing. Undotted fqns are the reason the tag is checked rather than the\n // dot: `extend paymentService { }` would otherwise read as a declaration of\n // `paymentService` here.\n const topEls = scan.elements.filter((e) => e.extend !== true && !e.id.includes(\".\"));\n const bound = new Map(els.map((e) => [e.id, elementService(e)]));\n const stmts: TopStmt[] = topEls.map((e) => ({\n kind: \"element\" as const,\n id: e.id,\n key: \"\",\n services: [bound.get(e.id) ?? e.id],\n start: e.start,\n end: e.end,\n }));\n for (const r of scan.rels) {\n // A rel declared inside an element's body is not a top-level statement —\n // anchoring after it would splice into the parent's block.\n if (topEls.some((e) => r.start > e.start && r.start < e.end)) continue;\n stmts.push({\n kind: \"rel\",\n id: \"\",\n key: relSortKey(els, r),\n // Deliberately no `known` fleet set here: this resolution only decides\n // WHERE in the file a statement anchors, both sides of every comparison\n // resolve through the same call, and the merged document is re-parsed\n // before anything is written — a wrong grouping is cosmetic, never a\n // wrong merge.\n services: [serviceOf(els, r.source), serviceOf(els, r.target)],\n start: r.start,\n end: r.end,\n });\n }\n return stmts.sort((a, b) => a.start - b.start);\n}\n\n\n/**\n * The trailing walks move in PLACEMENT UNITS, never single statements. A unit\n * is a service neighborhood: a contiguous run of top-level elements resolving\n * to one service, or a contiguous cluster of relationships grown around the\n * statements touching one — keyed by its LEADING statement's id/key. Anchored\n * inserts extend a neighborhood without changing its head, so the insertion\n * sort the trailing region maintains is over neighborhood heads, which\n * archive order cannot reshuffle. Comparing per statement instead let an\n * anchored insert legally sit a LOWER id/key after a higher one (its anchor\n * decides where it lives, not its own sort key), and a later trailing walk\n * then stopped at different statements depending on which archive had run\n * first — order-DEPENDENT bytes for features touching disjoint services, the\n * exact regime the scheme guarantees.\n */\nexport function elementSpot(region: ModelRegion, id: string, service: string): Spot {\n const { text, stmts, close } = region;\n let anchor: TopStmt | undefined;\n for (const s of stmts) {\n if (s.kind === \"element\" && s.services.includes(service)) anchor = s;\n }\n if (anchor !== undefined) return spotAfter(text, close, anchor.end);\n let i = stmts.length - 1;\n let before: TopStmt | undefined;\n while (i >= 0 && stmts[i]!.kind === \"rel\") {\n before = stmts[i];\n i -= 1;\n }\n while (i >= 0 && stmts[i]!.kind === \"element\") {\n // The same-service run ending here is one unit: skip it whole or stop\n // before it whole — an anchored member may carry any id, the run's\n // LEADING id is its sort key.\n let j = i;\n while (j > 0 && stmts[j - 1]!.kind === \"element\" && stmts[j - 1]!.services[0] === stmts[i]!.services[0]) j -= 1;\n if (!(stmts[j]!.id > id)) break;\n before = stmts[j];\n i = j - 1;\n }\n return before !== undefined ? beforeSpot(text, before) : closeSpot(text, close);\n}\n\n/**\n * Each top-level statement's cluster, as the index of the cluster's leading\n * statement. A relationship whose SOURCE's service some earlier relationship\n * in the same contiguous stretch already touches is an anchored join — it\n * belongs to that neighborhood wherever its own key would sort. One whose\n * source's service nothing before it touches opens a new cluster; sharing\n * only a TARGET does not join (every trailing edge into a busy hub would\n * otherwise dissolve into one giant cluster, and the trailing sort with it).\n * Recomputed from bytes alone, so the next archive sees the same units this\n * one placed.\n */\nfunction relClusterHeads(stmts: TopStmt[]): number[] {\n const heads = stmts.map((_, i) => i);\n let head = -1;\n let services: Set<string> | null = null;\n for (let i = 0; i < stmts.length; i += 1) {\n const s = stmts[i]!;\n if (s.kind !== \"rel\") {\n head = -1;\n services = null;\n continue;\n }\n if (services !== null && services.has(s.services[0]!)) {\n for (const svc of s.services) services.add(svc);\n } else {\n head = i;\n services = new Set(s.services);\n }\n heads[i] = head;\n }\n return heads;\n}\n\nexport function relSpot(region: ModelRegion, sourceService: string, key: string): Spot {\n const { text, stmts, close } = region;\n const heads = relClusterHeads(stmts);\n let anchorAt = -1;\n for (let i = 0; i < stmts.length; i += 1) {\n const s = stmts[i]!;\n if (s.kind === \"rel\" && s.services.includes(sourceService)) anchorAt = i;\n }\n if (anchorAt !== -1) {\n // Past the anchor, order among the cluster's OWN members is by key — two\n // edges anchored to the same statement must order by key, not by which\n // archived first — but the walk never leaves the cluster: the next\n // cluster is another neighborhood's, and stepping over it would depend\n // on which archive put it there.\n let last = stmts[anchorAt]!;\n for (let j = anchorAt + 1; j < stmts.length; j += 1) {\n const s = stmts[j]!;\n if (s.kind !== \"rel\" || heads[j] !== heads[anchorAt] || s.key >= key) break;\n last = s;\n }\n return spotAfter(text, close, last.end);\n }\n let i = stmts.length - 1;\n let before: TopStmt | undefined;\n while (i >= 0 && stmts[i]!.kind === \"rel\") {\n const j = heads[i]!;\n if (!(stmts[j]!.key > key)) break;\n before = stmts[j];\n i = j - 1;\n }\n return before !== undefined ? beforeSpot(text, before) : closeSpot(text, close);\n}\n\n/**\n * Insertion before an existing statement: at the start of its line when only\n * whitespace precedes it there, at the statement itself — bare, displacing it\n * onto a fresh line — when other text shares the line (a statement written on\n * the `model {` line, say). The unguarded line start spliced the block BEFORE\n * the model keyword, and the parse net refused a perfectly legal landscape.\n */\nfunction beforeSpot(text: string, before: TopStmt): Spot {\n const ls = lineStartAt(text, before.start);\n if (/^[ \\t]*$/.test(text.slice(ls, before.start))) return { at: ls, bare: false };\n return { at: before.start, bare: true };\n}\n\n/**\n * The line following the statement that ends at `end` — clamped to the closing\n * brace's own spot. Exported for the service-model splice (`./model/merge.ts`),\n * which asks the same two questions of a `model { }` block it does not group by\n * service: after this statement, or last thing in the block.\n */\nexport function spotAfter(text: string, close: number, end: number): Spot {\n const nl = text.indexOf(\"\\n\", end);\n const at = nl === -1 ? text.length : nl + 1;\n return at > close ? closeSpot(text, close) : { at, bare: false };\n}\n\n/** The last resort: directly before the model's closing brace. */\nexport function closeSpot(text: string, close: number): Spot {\n const ls = lineStartAt(text, close);\n if (/^[ \\t]*$/.test(text.slice(ls, close))) return { at: ls, bare: false };\n return { at: close, bare: true };\n}\n\nfunction lineStartAt(text: string, at: number): number {\n return text.lastIndexOf(\"\\n\", at - 1) + 1;\n}\n\n/**\n * Where a child block enters an existing living parent: before the closing\n * brace of the parent's body, on its own line at the parent's inner indent —\n * and a parent that never had a body gains one around the child.\n */\nexport function nestedInsert(text: string, parent: ScannedElement, block: string): { at: number; insert: string } {\n if (parent.bodyOpen === -1) {\n return { at: parent.end, insert: ` {\\n${block}\\n${parent.indent}}` };\n }\n const lineStart = text.lastIndexOf(\"\\n\", parent.bodyClose - 1) + 1;\n if (/^[ \\t]*$/.test(text.slice(lineStart, parent.bodyClose))) {\n return { at: lineStart, insert: `${block}\\n` };\n }\n // `{ ... }` on one line — break the brace onto its own line to make room.\n return { at: parent.bodyClose, insert: `\\n${block}\\n${parent.indent}` };\n}\n"]}
1
+ {"version":3,"file":"placement.js","sourceRoot":"","sources":["../../../../src/core/c4/splice/placement.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Elem } from \"../likec4.js\";\nimport { elementService, serviceOf } from \"../resolve/service.js\";\nimport { maskSource, pastComments, tokenEnd } from \"../source-mask.js\";\nimport type { ScannedBlock, ScannedElement, ScannedModel } from \"../source-scan.js\";\nimport { relSortKey } from \"./identity/edges.js\";\n\n/* ------------------------------------------------------------------ */\n/* Landscape placement — where a top-level addition lands */\n/* ------------------------------------------------------------------ */\n\n/**\n * The placement scheme, in full. It never moves an authored byte — placement\n * only chooses where NEW spliced blocks go — and it is deterministic in a\n * stronger sense: archiving feature A then feature B (touching different\n * services) yields the SAME final bytes as B then A, which is what lets two\n * concurrent archive PRs merge in git instead of colliding on one shared\n * append point.\n *\n * - An ELEMENT whose service already has top-level elements in the model\n * (the elementService join — binding first, title as the fallback) lands\n * immediately after the last of them: it joins its service's neighborhood.\n * Two features touching the SAME service therefore land adjacently, and\n * their concurrent PRs still conflict — expected, and documented\n * (SCHEMA.md sends that conflict to the PR flow).\n * - An element opening a NEW service lands in the trailing region before the\n * model's closing brace: skip the trailing relationship run, then walk left\n * per same-service RUN, past every run whose leading id is\n * lexicographically greater — an insertion sort of neighborhood heads\n * against whatever is already there, which is what makes the region's\n * final order independent of archive order (see the note on elementSpot:\n * per-statement comparison was order-dependent).\n * - A RELATIONSHIP lands after the last top-level relationship touching its\n * SOURCE's service (either endpoint counts), then past the anchor's own\n * CLUSTER members with a smaller sort key — two edges anchored to the same\n * statement must order by key, not by which archived first. With no such\n * neighborhood it falls to the trailing region, cluster-head-key-sorted\n * the same insertion-sort way, after the trailing elements.\n *\n * A landscape may hold SEVERAL top-level `model { }` blocks, and LikeC4 reads\n * them as one model. Placement keeps every rule above and adds one: an\n * ANCHORED addition lands in its anchor's block (after the neighbor, clamped\n * to that block's closing brace), and an UNANCHORED one — the trailing-region\n * walks — lands in `HOME_BLOCK`, never crossing a block boundary. A same-service\n * run and a relationship cluster are contiguous statements of ONE block: the\n * views or the specification between two blocks are not a neighborhood.\n *\n * LikeC4 reference resolution is not order-dependent, so a relationship\n * placed before an element it names still parses; the in-memory parse net in\n * landscape-merge.ts's planLandscapeMerge stays as the backstop regardless.\n */\nexport interface Spot {\n /** Offset into the living text: a line start, or a point inside a shared line. */\n at: number;\n /** True when `at` shares its line with other text (the closing brace, or a displaced statement) — the block must bring its own newlines. */\n bare: boolean;\n}\n\n/**\n * The block an addition with no anchor lands in: the document's FIRST\n * `model { }` block.\n *\n * First, because it is the block every unanchored addition already landed in\n * before the scan could see a second one — the scan stopped at the first\n * match — so a single-block map places byte-identically, and a map that grew\n * a second block keeps receiving new services where its older ones went. It\n * is also invariant under every addition this merge can make (an archive never\n * creates or reorders a block), which is what order-independence needs of it:\n * a block chosen by size or content would move as the archives landed, and the\n * same two features would compose to different bytes by archive order. The\n * service-model splice (`./model/merge.ts`) uses the same block for the same\n * reasons.\n */\nexport const HOME_BLOCK = 0;\n\n/** A top-level statement of the living model, with everything placement asks of it. */\nexport interface TopStmt {\n kind: \"element\" | \"rel\";\n /** Element: its id. */\n id: string;\n /** Relationship: its placement sort key (relSortKey). */\n key: string;\n /** The service(s) this statement resolves to — one for an element, both endpoints for a rel. */\n services: string[];\n start: number;\n end: number;\n /** The `model { }` block it is written in, as `ScannedModel.blocks` indexes it. */\n block: number;\n}\n\n/**\n * The living model as placement reads it: the text being spliced into, its\n * top-level statement layout, and the model blocks' braces. One value\n * because every spot is computed against all three at the same instant — a\n * layout paired with text a splice has since moved would place against a\n * document that no longer exists (see `modelRegion` in landscape-merge.ts's\n * planLandscapeMerge).\n */\nexport interface ModelRegion {\n text: string;\n stmts: TopStmt[];\n /** Every top-level `model { }` block, by the index `TopStmt.block` names. */\n blocks: readonly ScannedBlock[];\n}\n\n/** The statements of one block, in document order — what a trailing walk may step over. */\nfunction inBlock(stmts: TopStmt[], block: number): TopStmt[] {\n return stmts.filter((s) => s.block === block);\n}\n\n/** The model's top-level statements in document order, joined to the parsed elements for services. */\nexport function topStatements(scan: ScannedModel, els: Elem[]): TopStmt[] {\n // An `extend <fqn>` frame is never a top-level declaration of THIS document —\n // it reopens one another document owns — so it anchors nothing and displaces\n // nothing. Undotted fqns are the reason the tag is checked rather than the\n // dot: `extend paymentService { }` would otherwise read as a declaration of\n // `paymentService` here.\n const topEls = scan.elements.filter((e) => e.extend !== true && !e.id.includes(\".\"));\n const bound = new Map(els.map((e) => [e.id, elementService(e)]));\n const stmts: TopStmt[] = topEls.map((e) => ({\n kind: \"element\" as const,\n id: e.id,\n key: \"\",\n services: [bound.get(e.id) ?? e.id],\n start: e.start,\n end: e.end,\n block: e.block,\n }));\n // Every top-level FRAME a statement can sit inside: the elements, and the\n // `extend` frames, which hold edges as well as children.\n const frames = scan.elements.filter((e) => e.extend === true || !e.id.includes(\".\"));\n for (const r of scan.rels) {\n // A rel declared inside an element's body is not a top-level statement —\n // anchoring after it would splice into the parent's block. Inside an\n // `extend` frame it is worse than misplaced: the frame is a SCOPE, so an\n // edge spliced in after it re-resolves there, and the delta's `zulu ->\n // beta` landed inside `extend alpha { beta = … }` as an edge into\n // `alpha.beta` — a merge that parses, written at exit 0 (probed\n // 2026-10-20). Only the extend frames' edges are new to this filter.\n if (frames.some((e) => r.start > e.start && r.start < e.end)) continue;\n stmts.push({\n kind: \"rel\",\n id: \"\",\n key: relSortKey(els, r),\n // Deliberately no `known` fleet set here: this resolution only decides\n // WHERE in the file a statement anchors, both sides of every comparison\n // resolve through the same call, and the merged document is re-parsed\n // before anything is written — a wrong grouping is cosmetic, never a\n // wrong merge.\n services: [serviceOf(els, r.source), serviceOf(els, r.target)],\n start: r.start,\n end: r.end,\n block: r.block,\n });\n }\n return stmts.sort((a, b) => a.start - b.start);\n}\n\n\n/**\n * The trailing walks move in PLACEMENT UNITS, never single statements. A unit\n * is a service neighborhood: a contiguous run of top-level elements resolving\n * to one service, or a contiguous cluster of relationships grown around the\n * statements touching one — keyed by its LEADING statement's id/key. Anchored\n * inserts extend a neighborhood without changing its head, so the insertion\n * sort the trailing region maintains is over neighborhood heads, which\n * archive order cannot reshuffle. Comparing per statement instead let an\n * anchored insert legally sit a LOWER id/key after a higher one (its anchor\n * decides where it lives, not its own sort key), and a later trailing walk\n * then stopped at different statements depending on which archive had run\n * first — order-DEPENDENT bytes for features touching disjoint services, the\n * exact regime the scheme guarantees.\n */\nexport function elementSpot(region: ModelRegion, id: string, service: string): Spot {\n const { text, blocks } = region;\n let anchor: TopStmt | undefined;\n for (const s of region.stmts) {\n if (s.kind === \"element\" && s.services.includes(service)) anchor = s;\n }\n if (anchor !== undefined) return spotAfter(text, blocks[anchor.block]!.close, anchor.end);\n const stmts = inBlock(region.stmts, HOME_BLOCK);\n const close = blocks[HOME_BLOCK]!.close;\n let i = stmts.length - 1;\n let before: TopStmt | undefined;\n while (i >= 0 && stmts[i]!.kind === \"rel\") {\n before = stmts[i];\n i -= 1;\n }\n while (i >= 0 && stmts[i]!.kind === \"element\") {\n // The same-service run ending here is one unit: skip it whole or stop\n // before it whole — an anchored member may carry any id, the run's\n // LEADING id is its sort key.\n let j = i;\n while (j > 0 && stmts[j - 1]!.kind === \"element\" && stmts[j - 1]!.services[0] === stmts[i]!.services[0]) j -= 1;\n if (!(stmts[j]!.id > id)) break;\n before = stmts[j];\n i = j - 1;\n }\n return before !== undefined ? beforeSpot(text, before) : closeSpot(text, close);\n}\n\n/**\n * Each top-level statement's cluster, as the index of the cluster's leading\n * statement. A relationship whose SOURCE's service some earlier relationship\n * in the same contiguous stretch already touches is an anchored join — it\n * belongs to that neighborhood wherever its own key would sort. One whose\n * source's service nothing before it touches opens a new cluster; sharing\n * only a TARGET does not join (every trailing edge into a busy hub would\n * otherwise dissolve into one giant cluster, and the trailing sort with it).\n * Recomputed from bytes alone, so the next archive sees the same units this\n * one placed.\n */\nfunction relClusterHeads(stmts: TopStmt[]): number[] {\n const heads = stmts.map((_, i) => i);\n let head = -1;\n let services: Set<string> | null = null;\n for (let i = 0; i < stmts.length; i += 1) {\n const s = stmts[i]!;\n // A block boundary ends a cluster exactly as an element does: the first\n // edge of the next block is not contiguous with the last of this one.\n if (i > 0 && stmts[i - 1]!.block !== s.block) {\n head = -1;\n services = null;\n }\n if (s.kind !== \"rel\") {\n head = -1;\n services = null;\n continue;\n }\n if (services !== null && services.has(s.services[0]!)) {\n for (const svc of s.services) services.add(svc);\n } else {\n head = i;\n services = new Set(s.services);\n }\n heads[i] = head;\n }\n return heads;\n}\n\nexport function relSpot(region: ModelRegion, sourceService: string, key: string): Spot {\n const anchored = anchoredRelSpot(region, sourceService, key);\n if (anchored !== null) return anchored;\n const { text, blocks } = region;\n const stmts = inBlock(region.stmts, HOME_BLOCK);\n const close = blocks[HOME_BLOCK]!.close;\n const heads = relClusterHeads(stmts);\n let i = stmts.length - 1;\n let before: TopStmt | undefined;\n while (i >= 0 && stmts[i]!.kind === \"rel\") {\n const j = heads[i]!;\n if (!(stmts[j]!.key > key)) break;\n before = stmts[j];\n i = j - 1;\n }\n return before !== undefined ? beforeSpot(text, before) : closeSpot(text, close);\n}\n\n/** After the last relationship touching the source's service, in that relationship's block — null when none does. */\nfunction anchoredRelSpot(region: ModelRegion, sourceService: string, key: string): Spot | null {\n const { text, stmts, blocks } = region;\n const heads = relClusterHeads(stmts);\n let anchorAt = -1;\n for (let i = 0; i < stmts.length; i += 1) {\n const s = stmts[i]!;\n if (s.kind === \"rel\" && s.services.includes(sourceService)) anchorAt = i;\n }\n if (anchorAt !== -1) {\n // Past the anchor, order among the cluster's OWN members is by key — two\n // edges anchored to the same statement must order by key, not by which\n // archived first — but the walk never leaves the cluster: the next\n // cluster is another neighborhood's, and stepping over it would depend\n // on which archive put it there.\n let last = stmts[anchorAt]!;\n for (let j = anchorAt + 1; j < stmts.length; j += 1) {\n const s = stmts[j]!;\n if (s.kind !== \"rel\" || heads[j] !== heads[anchorAt] || s.key >= key) break;\n last = s;\n }\n return spotAfter(text, blocks[last.block]!.close, last.end);\n }\n return null;\n}\n\n/**\n * Insertion before an existing statement: at the start of its line when only\n * whitespace precedes it there, at the statement itself — bare, displacing it\n * onto a fresh line — when other text shares the line (a statement written on\n * the `model {` line, say). The unguarded line start spliced the block BEFORE\n * the model keyword, and the parse net refused a perfectly legal landscape.\n */\nfunction beforeSpot(text: string, before: TopStmt): Spot {\n const ls = lineStartAt(text, before.start);\n if (/^[ \\t]*$/.test(text.slice(ls, before.start))) return { at: ls, bare: false };\n return { at: before.start, bare: true };\n}\n\n/**\n * The line following the statement that ends at `end` — clamped to the closing\n * brace's own spot. Exported for the service-model splice (`./model/merge.ts`),\n * which asks the same two questions of a `model { }` block it does not group by\n * service: after this statement, or last thing in the block.\n *\n * Unless code follows `end` on its line: then the spot is `end` itself, bare.\n * The next line belongs to whatever is written there, and when that is a\n * declaration whose body opens on the same line — `alpha = … { … } beta = … {`\n * — the line after is INSIDE beta: an addition anchored to alpha's service\n * landed as `beta.kiwi`, in a merge that parsed and was written (exit 0,\n * probed on main at 171555d). Right after the anchor is also what keeps two\n * archives order-free when both anchor on that line: one anchored to alpha and\n * one to beta used to share the next line's start, and whichever ran second\n * landed above the other. A comment trailing the anchor is not code — the\n * next line stays the spot, byte for byte as before — but a block comment\n * it does not close is: its next line is inside the comment.\n *\n * A line comment ends where LikeC4's NL does, at `\\r` as well as `\\n` — the\n * source mask agrees since the bare-CR fix — so `// note\\r beta = … {` is a\n * comment and then code on the anchor's `\\n`-line, and the spot after that\n * line is inside beta: the same `beta.kiwi` merge, written at exit 0, which\n * 171555d refused because it could not locate beta at all. A CRLF ending\n * leaves nothing after the comment and keeps the next line, byte for byte.\n */\nexport function spotAfter(text: string, close: number, end: number): Spot {\n const nl = text.indexOf(\"\\n\", end);\n const at = nl === -1 ? text.length : nl + 1;\n let rest = pastComments(text.slice(end, Math.min(at, close)));\n while (rest.startsWith(\"//\")) {\n const eol = rest.search(/[\\r\\n]/);\n rest = eol === -1 ? \"\" : pastComments(rest.slice(eol));\n }\n if (rest !== \"\") return { at: end, bare: true };\n return at > close ? closeSpot(text, close) : { at, bare: false };\n}\n\n/** The last resort: directly before the model's closing brace. */\nexport function closeSpot(text: string, close: number): Spot {\n const ls = lineStartAt(text, close);\n if (/^[ \\t]*$/.test(text.slice(ls, close))) return { at: ls, bare: false };\n return { at: close, bare: true };\n}\n\nfunction lineStartAt(text: string, at: number): number {\n return text.lastIndexOf(\"\\n\", at - 1) + 1;\n}\n\n/**\n * Where a child block enters an existing living parent: before the closing\n * brace of the parent's body, on its own line at the parent's inner indent —\n * and a parent that never had a body gains one around the child.\n */\nexport function nestedInsert(text: string, parent: ScannedElement, block: string): { at: number; insert: string } {\n if (parent.bodyOpen === -1) {\n return { at: bodyAt(text, parent), insert: ` {\\n${block}\\n${parent.indent}}` };\n }\n const lineStart = text.lastIndexOf(\"\\n\", parent.bodyClose - 1) + 1;\n if (/^[ \\t]*$/.test(text.slice(lineStart, parent.bodyClose))) {\n return { at: lineStart, insert: `${block}\\n` };\n }\n // `{ ... }` on one line — break the brace onto its own line to make room.\n return { at: parent.bodyClose, insert: `\\n${block}\\n${parent.indent}` };\n}\n\n/**\n * Where a bodyless parent's new body opens: where its span ends, unless a line\n * comment trails its last token there. The span carries that comment — it is\n * the declaration's own authored text (`statementEnd`) — so the `{` landed\n * INSIDE `// the api`, everything after it on the line was comment, and the\n * parse net refused a valid map (\"Expecting end of file\"). It opens before the\n * comment now, which then trails the new closing brace. A closed block comment\n * is no such trap, and keeps the bytes it always had.\n */\nfunction bodyAt(text: string, parent: ScannedElement): number {\n const span = text.slice(parent.start, parent.end);\n const last = parent.start + tokenEnd(maskSource(span), 0, span.length);\n return pastComments(text.slice(last, parent.end)) === \"\" ? parent.end : last;\n}\n"]}
@@ -7,8 +7,8 @@
7
7
  * 120-service benchmark fixture, docs/BENCHMARKS.md). This module copies every
8
8
  * document into a mkdtemp workspace as one single-file PROJECT each (a
9
9
  * `likec4.config.json` per folder), calls `LikeC4.fromWorkspace` once, splits
10
- * `getErrors()` per document, and flattens each clean project through the same
11
- * `flattenModel` the per-file path uses. Parity — byte-identical error
10
+ * `getErrors()` per document, and reads each clean project through the same
11
+ * `cleanDoc` the per-file path uses. Parity — byte-identical error
12
12
  * messages/lines and identical elements/relationships — is pinned by
13
13
  * test/likec4-batch-parity.test.ts, which is also the tripwire for the exact
14
14
  * likec4 version pin: multi-project workspaces are public API, but this use of
@@ -41,11 +41,7 @@ import { copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
41
41
  import { tmpdir } from "node:os";
42
42
  import { basename, join, resolve } from "node:path";
43
43
  import { LikeC4 } from "likec4";
44
- import { flattenModel } from "./likec4.js";
45
- import { readDynamicViews } from "./parsed/dynamic-views.js";
46
- import { readGlobalStyleIds, readViewIds } from "./parsed/view-ids.js";
47
- import { readSpecification } from "./parsed/specification.js";
48
- import { readDeployment } from "./parsed/deployment.js";
44
+ import { cleanDoc, distinctErrors } from "./likec4.js";
49
45
  /**
50
46
  * Stage every readable document as its own single-file project. A copy failure
51
47
  * removes the half-made folder and drops the path — an empty project must not
@@ -85,11 +81,14 @@ async function stage(root, targets) {
85
81
  * (`/var/...` becomes `/private/var/...`) in some environments and the literal
86
82
  * mkdtemp path in others, and a prefix compare silently attributes every error
87
83
  * to nobody in exactly one of those two worlds. The segment survives both.
84
+ *
85
+ * Read through `distinctErrors` exactly as `loadSource` reads them, so a
86
+ * document's list is the same one in both modes.
88
87
  */
89
88
  function groupErrors(likec4, staged) {
90
89
  const pathOf = new Map(staged.map((doc) => [doc.project, doc.path]));
91
90
  const grouped = new Map();
92
- for (const err of likec4.getErrors()) {
91
+ for (const err of distinctErrors(likec4.getErrors())) {
93
92
  const project = err.sourceFsPath.split(/[\\/]/).find((segment) => pathOf.has(segment));
94
93
  if (project === undefined) {
95
94
  // An error this walk cannot attribute would otherwise be DROPPED — and a
@@ -143,20 +142,12 @@ export async function loadBatch(paths) {
143
142
  out.set(path, { errors, elements: [], relationships: [] });
144
143
  }
145
144
  else {
146
- const model = (await likec4.parsedModel(project));
147
- // The specification travels with the batch loader too, and must: this
148
- // is the loader `validate --all` reads the landscape through, so a
149
- // kind-tag check wired only into `loadSource` would be a check the
150
- // fleet gate — the one run it exists for — never actually ran.
151
- out.set(path, {
152
- errors,
153
- specification: readSpecification(model.specification),
154
- views: readDynamicViews(model),
155
- viewIds: readViewIds(model),
156
- globalStyles: readGlobalStyleIds(model),
157
- deployment: readDeployment(model),
158
- ...flattenModel(model),
159
- });
145
+ // Through `cleanDoc`, the one assembly `loadSource` uses too. The
146
+ // specification travels with the batch loader and must: this is the
147
+ // loader `validate --all` reads the landscape through, so a kind-tag
148
+ // check wired only into `loadSource` would be a check the fleet gate
149
+ // — the one run it exists for — never actually ran.
150
+ out.set(path, cleanDoc((await likec4.parsedModel(project))));
160
151
  }
161
152
  }
162
153
  }
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/core/c4/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Batch loader: many self-contained `.likec4` documents through ONE\n * LikeC4/Langium workspace.\n *\n * `loadFile` pays a fresh workspace per call — ~100ms even warm — which is what\n * made `validate --all` O(documents) workspace spins (13.7s median over the\n * 120-service benchmark fixture, docs/BENCHMARKS.md). This module copies every\n * document into a mkdtemp workspace as one single-file PROJECT each (a\n * `likec4.config.json` per folder), calls `LikeC4.fromWorkspace` once, splits\n * `getErrors()` per document, and flattens each clean project through the same\n * `flattenModel` the per-file path uses. Parity — byte-identical error\n * messages/lines and identical elements/relationships — is pinned by\n * test/likec4-batch-parity.test.ts, which is also the tripwire for the exact\n * likec4 version pin: multi-project workspaces are public API, but this use of\n * them is not documented, so an upgrade must fail that suite before it lands.\n *\n * Isolation is load-bearing, not cosmetic: `fromSource` gives each document a\n * workspace of its own, so an author-written `import ... from 'name'` can never\n * resolve. In a shared workspace it COULD — a sibling project is one lucky name\n * away — so every folder carries one crypto-random per-invocation hex token in\n * its project name. A document cannot import what it cannot name, and the\n * parity suite pins that a guessed sibling name errors identically in both\n * modes.\n *\n * Callers own the failure story: a document that cannot be staged (unreadable,\n * missing) is silently DROPPED from the result so the caller's ordinary\n * per-path load reproduces today's error, and a batch-infrastructure failure\n * (tmpdir denied — the sandboxed-runner class ROADMAP documents) rejects the\n * whole call for the caller to catch and degrade. FleetContext.prefetchLikeC4\n * does exactly that: findings can never change because a workspace could not be\n * created, only the speed can.\n *\n * This file took core/c4/ to exactly five files, and the seam that note\n * predicted is now open: `./project/load.ts` loads a DIRECTORY as one project,\n * which is the opposite isolation contract and belongs beside this one rather\n * than inside it.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { copyFile, mkdir, mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join, resolve } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { flattenModel, type LikeC4Error, type LoadedDoc, type ReadableModel } from \"./likec4.js\";\nimport { readDynamicViews } from \"./parsed/dynamic-views.js\";\nimport { readGlobalStyleIds, readViewIds } from \"./parsed/view-ids.js\";\nimport { readSpecification } from \"./parsed/specification.js\";\nimport { readDeployment } from \"./parsed/deployment.js\";\n\n/** A staged document: the real path it came from, and its workspace project. */\ninterface StagedDoc {\n path: string;\n project: string;\n}\n\n/**\n * Stage every readable document as its own single-file project. A copy failure\n * removes the half-made folder and drops the path — an empty project must not\n * reach the workspace scan, and the caller's per-path load owns the error. The\n * copy is `copyFile` (raw bytes), not read/decode/write: BOM, CRLF and even\n * non-UTF-8 bytes reach LikeC4's own reader exactly as they would from the\n * original file on disk.\n */\nasync function stage(root: string, targets: string[]): Promise<StagedDoc[]> {\n const token = randomBytes(8).toString(\"hex\");\n // Concurrent, capped, in input order (kernel/concurrency's contract): the\n // per-document folders share nothing, and on a network-mounted docs repo\n // the sequential form paid three round-trips per document. Nulls mark the\n // drops so indices — and with them the p<i> project names — stay aligned.\n const results = await inOrder([...targets.entries()], async ([i, path]) => {\n const project = `p${i}_${token}`;\n const folder = join(root, project);\n try {\n await mkdir(folder);\n await writeFile(join(folder, \"likec4.config.json\"), JSON.stringify({ name: project }), \"utf8\");\n await copyFile(path, join(folder, basename(path)));\n return { path, project };\n } catch {\n await rm(folder, { recursive: true, force: true });\n return null;\n }\n });\n return results.filter((r): r is StagedDoc => r !== null);\n}\n\n/**\n * `getErrors()` split per document, each error's `sourceFsPath` rewritten from\n * the workspace copy to the document's real absolute path.\n *\n * Grouping keys on the unique `p<i>_<token>` path SEGMENT, never on a\n * tmp-root prefix compare: darwin hands back realpathed workspace paths\n * (`/var/...` becomes `/private/var/...`) in some environments and the literal\n * mkdtemp path in others, and a prefix compare silently attributes every error\n * to nobody in exactly one of those two worlds. The segment survives both.\n */\nfunction groupErrors(likec4: LikeC4, staged: StagedDoc[]): Map<string, LikeC4Error[]> {\n const pathOf = new Map(staged.map((doc) => [doc.project, doc.path]));\n const grouped = new Map<string, LikeC4Error[]>();\n for (const err of likec4.getErrors()) {\n const project = err.sourceFsPath.split(/[\\\\/]/).find((segment) => pathOf.has(segment));\n if (project === undefined) {\n // An error this walk cannot attribute would otherwise be DROPPED — and a\n // dropped error grades its document clean, which is failing open in the\n // fleet gate. Unreachable at the pinned likec4 version (every corpus\n // error carries its project folder in sourceFsPath), so the one place a\n // dependency upgrade could break the attribution is the one place that\n // must reject the whole batch: the caller degrades to per-path loads,\n // where the error reappears.\n throw new Error(`unattributable LikeC4 error in batch workspace: ${err.message}`);\n }\n const list = grouped.get(project) ?? [];\n list.push({ ...err, sourceFsPath: pathOf.get(project) });\n grouped.set(project, list);\n }\n return grouped;\n}\n\n/**\n * Load and validate many self-contained `.likec4` documents in one workspace.\n * Returns `resolve(path)` → LoadedDoc; paths are deduped through the same\n * resolution, and a path that could not be staged is absent from the result.\n *\n * A document with errors keeps `loadSource`'s exact rule — empty elements and\n * relationships, WITHOUT calling `parsedModel`: the parsed stage succeeds even\n * over unresolved references, so the \"errors mean no model\" boundary is loam's\n * to apply, not LikeC4's. A clean document is flattened from\n * `parsedModel(project)` — parsed, never computed: the computed stage RESOLVES\n * every view against the model — which loam never wants, because it reads a\n * view's DECLARATIONS and never a view's resolved contents (docs/DESIGN.md\n * rule 26) — and which is superlinear in edge count (see loadSource).\n */\nexport async function loadBatch(paths: string[]): Promise<Map<string, LoadedDoc>> {\n const targets = [...new Set(paths.map((path) => resolve(path)))];\n const out = new Map<string, LoadedDoc>();\n if (targets.length === 0) return out;\n const root = await mkdtemp(join(tmpdir(), \"loam-c4-\"));\n try {\n const staged = await stage(root, targets);\n if (staged.length === 0) return out;\n // One workspace for the whole batch — the entire point. The logger is\n // disabled exactly as in loadSource: diagnostics are returned per document,\n // not written out-of-band to stderr (core never prints).\n const likec4 = await LikeC4.fromWorkspace(root, { logger: false });\n try {\n const grouped = groupErrors(likec4, staged);\n for (const { path, project } of staged) {\n const errors = grouped.get(project) ?? [];\n if (errors.length > 0) {\n out.set(path, { errors, elements: [], relationships: [] });\n } else {\n const model = (await likec4.parsedModel(project)) as ReadableModel;\n // The specification travels with the batch loader too, and must: this\n // is the loader `validate --all` reads the landscape through, so a\n // kind-tag check wired only into `loadSource` would be a check the\n // fleet gate — the one run it exists for — never actually ran.\n out.set(path, {\n errors,\n specification: readSpecification(model.specification),\n views: readDynamicViews(model),\n viewIds: readViewIds(model),\n globalStyles: readGlobalStyleIds(model),\n deployment: readDeployment(model),\n ...flattenModel(model),\n });\n }\n }\n } finally {\n await likec4.dispose();\n }\n return out;\n } finally {\n // The workspace is this invocation's alone; nothing else may observe it.\n // A kill between mkdtemp and here can strand a loam-c4-* dir in OS tmp —\n // never in the docs repo — which docs/DESIGN.md records as accepted.\n await rm(root, { recursive: true, force: true });\n }\n}\n"]}
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/core/c4/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Batch loader: many self-contained `.likec4` documents through ONE\n * LikeC4/Langium workspace.\n *\n * `loadFile` pays a fresh workspace per call — ~100ms even warm — which is what\n * made `validate --all` O(documents) workspace spins (13.7s median over the\n * 120-service benchmark fixture, docs/BENCHMARKS.md). This module copies every\n * document into a mkdtemp workspace as one single-file PROJECT each (a\n * `likec4.config.json` per folder), calls `LikeC4.fromWorkspace` once, splits\n * `getErrors()` per document, and reads each clean project through the same\n * `cleanDoc` the per-file path uses. Parity — byte-identical error\n * messages/lines and identical elements/relationships — is pinned by\n * test/likec4-batch-parity.test.ts, which is also the tripwire for the exact\n * likec4 version pin: multi-project workspaces are public API, but this use of\n * them is not documented, so an upgrade must fail that suite before it lands.\n *\n * Isolation is load-bearing, not cosmetic: `fromSource` gives each document a\n * workspace of its own, so an author-written `import ... from 'name'` can never\n * resolve. In a shared workspace it COULD — a sibling project is one lucky name\n * away — so every folder carries one crypto-random per-invocation hex token in\n * its project name. A document cannot import what it cannot name, and the\n * parity suite pins that a guessed sibling name errors identically in both\n * modes.\n *\n * Callers own the failure story: a document that cannot be staged (unreadable,\n * missing) is silently DROPPED from the result so the caller's ordinary\n * per-path load reproduces today's error, and a batch-infrastructure failure\n * (tmpdir denied — the sandboxed-runner class ROADMAP documents) rejects the\n * whole call for the caller to catch and degrade. FleetContext.prefetchLikeC4\n * does exactly that: findings can never change because a workspace could not be\n * created, only the speed can.\n *\n * This file took core/c4/ to exactly five files, and the seam that note\n * predicted is now open: `./project/load.ts` loads a DIRECTORY as one project,\n * which is the opposite isolation contract and belongs beside this one rather\n * than inside it.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { inOrder } from \"../kernel/concurrency.js\";\nimport { copyFile, mkdir, mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join, resolve } from \"node:path\";\nimport { LikeC4 } from \"likec4\";\nimport { cleanDoc, distinctErrors, type LikeC4Error, type LoadedDoc, type ReadableModel } from \"./likec4.js\";\n\n/** A staged document: the real path it came from, and its workspace project. */\ninterface StagedDoc {\n path: string;\n project: string;\n}\n\n/**\n * Stage every readable document as its own single-file project. A copy failure\n * removes the half-made folder and drops the path — an empty project must not\n * reach the workspace scan, and the caller's per-path load owns the error. The\n * copy is `copyFile` (raw bytes), not read/decode/write: BOM, CRLF and even\n * non-UTF-8 bytes reach LikeC4's own reader exactly as they would from the\n * original file on disk.\n */\nasync function stage(root: string, targets: string[]): Promise<StagedDoc[]> {\n const token = randomBytes(8).toString(\"hex\");\n // Concurrent, capped, in input order (kernel/concurrency's contract): the\n // per-document folders share nothing, and on a network-mounted docs repo\n // the sequential form paid three round-trips per document. Nulls mark the\n // drops so indices — and with them the p<i> project names — stay aligned.\n const results = await inOrder([...targets.entries()], async ([i, path]) => {\n const project = `p${i}_${token}`;\n const folder = join(root, project);\n try {\n await mkdir(folder);\n await writeFile(join(folder, \"likec4.config.json\"), JSON.stringify({ name: project }), \"utf8\");\n await copyFile(path, join(folder, basename(path)));\n return { path, project };\n } catch {\n await rm(folder, { recursive: true, force: true });\n return null;\n }\n });\n return results.filter((r): r is StagedDoc => r !== null);\n}\n\n/**\n * `getErrors()` split per document, each error's `sourceFsPath` rewritten from\n * the workspace copy to the document's real absolute path.\n *\n * Grouping keys on the unique `p<i>_<token>` path SEGMENT, never on a\n * tmp-root prefix compare: darwin hands back realpathed workspace paths\n * (`/var/...` becomes `/private/var/...`) in some environments and the literal\n * mkdtemp path in others, and a prefix compare silently attributes every error\n * to nobody in exactly one of those two worlds. The segment survives both.\n *\n * Read through `distinctErrors` exactly as `loadSource` reads them, so a\n * document's list is the same one in both modes.\n */\nfunction groupErrors(likec4: LikeC4, staged: StagedDoc[]): Map<string, LikeC4Error[]> {\n const pathOf = new Map(staged.map((doc) => [doc.project, doc.path]));\n const grouped = new Map<string, LikeC4Error[]>();\n for (const err of distinctErrors(likec4.getErrors())) {\n const project = err.sourceFsPath.split(/[\\\\/]/).find((segment) => pathOf.has(segment));\n if (project === undefined) {\n // An error this walk cannot attribute would otherwise be DROPPED — and a\n // dropped error grades its document clean, which is failing open in the\n // fleet gate. Unreachable at the pinned likec4 version (every corpus\n // error carries its project folder in sourceFsPath), so the one place a\n // dependency upgrade could break the attribution is the one place that\n // must reject the whole batch: the caller degrades to per-path loads,\n // where the error reappears.\n throw new Error(`unattributable LikeC4 error in batch workspace: ${err.message}`);\n }\n const list = grouped.get(project) ?? [];\n list.push({ ...err, sourceFsPath: pathOf.get(project) });\n grouped.set(project, list);\n }\n return grouped;\n}\n\n/**\n * Load and validate many self-contained `.likec4` documents in one workspace.\n * Returns `resolve(path)` → LoadedDoc; paths are deduped through the same\n * resolution, and a path that could not be staged is absent from the result.\n *\n * A document with errors keeps `loadSource`'s exact rule — empty elements and\n * relationships, WITHOUT calling `parsedModel`: the parsed stage succeeds even\n * over unresolved references, so the \"errors mean no model\" boundary is loam's\n * to apply, not LikeC4's. A clean document is flattened from\n * `parsedModel(project)` — parsed, never computed: the computed stage RESOLVES\n * every view against the model — which loam never wants, because it reads a\n * view's DECLARATIONS and never a view's resolved contents (docs/DESIGN.md\n * rule 26) — and which is superlinear in edge count (see loadSource).\n */\nexport async function loadBatch(paths: string[]): Promise<Map<string, LoadedDoc>> {\n const targets = [...new Set(paths.map((path) => resolve(path)))];\n const out = new Map<string, LoadedDoc>();\n if (targets.length === 0) return out;\n const root = await mkdtemp(join(tmpdir(), \"loam-c4-\"));\n try {\n const staged = await stage(root, targets);\n if (staged.length === 0) return out;\n // One workspace for the whole batch — the entire point. The logger is\n // disabled exactly as in loadSource: diagnostics are returned per document,\n // not written out-of-band to stderr (core never prints).\n const likec4 = await LikeC4.fromWorkspace(root, { logger: false });\n try {\n const grouped = groupErrors(likec4, staged);\n for (const { path, project } of staged) {\n const errors = grouped.get(project) ?? [];\n if (errors.length > 0) {\n out.set(path, { errors, elements: [], relationships: [] });\n } else {\n // Through `cleanDoc`, the one assembly `loadSource` uses too. The\n // specification travels with the batch loader and must: this is the\n // loader `validate --all` reads the landscape through, so a kind-tag\n // check wired only into `loadSource` would be a check the fleet gate\n // — the one run it exists for — never actually ran.\n out.set(path, cleanDoc((await likec4.parsedModel(project)) as ReadableModel));\n }\n }\n } finally {\n await likec4.dispose();\n }\n return out;\n } finally {\n // The workspace is this invocation's alone; nothing else may observe it.\n // A kill between mkdtemp and here can strand a loam-c4-* dir in OS tmp —\n // never in the docs repo — which docs/DESIGN.md records as accepted.\n await rm(root, { recursive: true, force: true });\n }\n}\n"]}
@@ -41,7 +41,7 @@ const EMPTY = { present: false, byId: new Map(), tree: NO_TREE };
41
41
  * and a declaration whose body is not a mapping is treated as `{}` rather than
42
42
  * refusing the whole file over one leaf.
43
43
  */
44
- export async function readCapabilities(path) {
44
+ async function readCapabilities(path) {
45
45
  if (!existsSync(path))
46
46
  return EMPTY;
47
47
  let doc;
@@ -77,11 +77,11 @@ export async function readCapabilities(path) {
77
77
  * THE vocabulary — both sides read and merged, and the only shape a grader
78
78
  * should ever be handed.
79
79
  *
80
- * `readCapabilities` above stays exported for the one reader that genuinely
81
- * means the file alone (its own tests), but nothing that grades a fleet may use
82
- * it: a `Capability:` line naming an id the tree declares and the YAML does not
83
- * would resolve through the union and fail through the file, so a second
84
- * spelling of "the vocabulary" is a second answer to `capability.unknown`.
80
+ * `readCapabilities` above is the file alone, and it is private to this
81
+ * module because nothing that grades a fleet may use it: a `Capability:` line
82
+ * naming an id the tree declares and the YAML does not would resolve through
83
+ * the union and fail through the file, so a second spelling of "the
84
+ * vocabulary" is a second answer to `capability.unknown`.
85
85
  *
86
86
  * WHERE AN ID SITS ON BOTH SIDES THE YAML'S METADATA SURVIVES, and not by
87
87
  * precedence — the document has nothing to overwrite it with. `description` and
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../../src/core/capabilities/capabilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet's capability vocabulary: which business capabilities exist, by\n * declared name, so a requirement's `Capability:` line resolves or refuses\n * instead of silently labelling.\n *\n * It is a fleet document (`architecture/capabilities.yaml`, beside the\n * landscape and the permissions vocabulary) because a capability crosses\n * services by definition — \"registration\" is realized by requirements in four\n * repositories, and a per-service copy would be N spellings of one word.\n *\n * What this is NOT, deliberately: a domain model, and not an authored prose\n * layer either. SCHEMA.md rejected a free-text `capability:` label because\n * nothing could check it; a DECLARED name is different in exactly the way that\n * mattered there — the id exists in this file or it does not, so an unknown\n * name is an error with close candidates, a declaration nothing realizes is a\n * warning, and the vocabulary cannot drift while looking authoritative. The\n * long-lived capability documents themselves (one page per capability, revised\n * by features) stay outside loam — that is the roadmap's evidence-gated Later\n * item, and the rollup built over these names is the evidence that decides it.\n *\n * The OPT-IN also differs from the authorization axis, on purpose: there the\n * `Requires:` line is the opt-in and a missing permissions.yaml is an error at\n * the line; here the FILE is the opt-in — a fleet with no capabilities.yaml\n * produces no capability findings at all (`core/capabilities/findings.ts`\n * holds the rule and the reason).\n */\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { parse as parseYaml } from \"yaml\";\nimport { isRecord } from \"../kernel/records.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { capabilitiesPath } from \"../repo/paths.js\";\nimport { capabilityDocsDir } from \"../repo/authored/paths.js\";\nimport { readCapabilityTree, type CapabilityTree } from \"./tree.js\";\n\n/** One declared capability. The id is the full key, slashes preserved. */\nexport interface Capability {\n /**\n * The token a requirement's `Capability:` line writes. Nesting is spelled\n * INSIDE the id (`payments/refunds`) and never collapsed — the key is\n * exactly the string a requirement names, greppable as written.\n */\n id: string;\n description?: string;\n /**\n * A team or a person — deliberately `owner:`, not permissions' `owned_by:`,\n * because that field carries \"defining service\" semantics a capability does\n * not have. Never resolved against anything; a blank is a worse answer than\n * an unverified one.\n */\n owner?: string;\n /**\n * Which side declared this id. `yaml` is a name in\n * `architecture/capabilities.yaml` with no document; `tree` is a\n * `capabilities/<id>/spec.md` the YAML does not name; `both` is the same id\n * on both sides, which is normal rather than a duplicate — the YAML declared\n * the word first and somebody has since written the document for it.\n *\n * Recorded rather than derived at each reader, because the two sides carry\n * different fixes: a `yaml`-only capability is fixed by writing the document,\n * a `tree`-only one needs no fix at all, and a message that could not tell\n * them apart would advise editing a file that may not exist.\n */\n source: \"yaml\" | \"tree\" | \"both\";\n /** `capabilities/<id>/spec.md`, absolute, when the tree declares this id. */\n spec?: string;\n}\n\nexport interface CapabilityVocabulary {\n /**\n * False when NEITHER `architecture/capabilities.yaml` nor `capabilities/`\n * exists. Either one is the axis's opt-in, so a fleet holding neither\n * produces no capability finding at all, however many `Capability:` lines its\n * requirements already carry.\n */\n present: boolean;\n /**\n * Why `architecture/capabilities.yaml` could not be read as a vocabulary,\n * when it could not. The TREE has no such state: a directory either lists or\n * it does not, and an unreadable one is a filesystem failure the walk lets\n * throw rather than a document loam misread.\n */\n invalid?: string;\n /** Declared capabilities, keyed by the full id — the union of both sides. */\n byId: Map<string, Capability>;\n /** What the `capabilities/` walk found, for the graders that need the paths. */\n tree: CapabilityTree;\n}\n\nconst NO_TREE: CapabilityTree = { present: false, docs: [], undocumented: [] };\nconst EMPTY: CapabilityVocabulary = { present: false, byId: new Map(), tree: NO_TREE };\n\n/**\n * Read the vocabulary — exactly `permissions.ts`'s defensive ladder, because\n * the two files fail the same ways: a missing file is silence, a YAML error or\n * a non-mapping shape is `invalid` (one finding for the whole run, the family\n * suppressed behind it), an empty document is a present-and-empty vocabulary,\n * and a declaration whose body is not a mapping is treated as `{}` rather than\n * refusing the whole file over one leaf.\n */\nexport async function readCapabilities(path: string): Promise<CapabilityVocabulary> {\n if (!existsSync(path)) return EMPTY;\n let doc: unknown;\n try {\n doc = parseYaml(await readFile(path, \"utf8\"));\n } catch (e) {\n return { ...EMPTY, present: true, invalid: e instanceof Error ? e.message : String(e) };\n }\n if (doc === null || doc === undefined) return { ...EMPTY, present: true };\n if (!isRecord(doc)) return { ...EMPTY, present: true, invalid: \"the document is not a mapping\" };\n\n const block = doc[\"capabilities\"];\n if (block === undefined) return { ...EMPTY, present: true };\n if (!isRecord(block)) {\n return { ...EMPTY, present: true, invalid: \"`capabilities` is not a mapping of capability id to its declaration\" };\n }\n\n const byId = new Map<string, Capability>();\n for (const [id, body] of Object.entries(block)) {\n const decl = isRecord(body) ? body : {};\n byId.set(id, {\n id,\n source: \"yaml\",\n ...(typeof decl[\"description\"] === \"string\" ? { description: decl[\"description\"] } : {}),\n ...(typeof decl[\"owner\"] === \"string\" ? { owner: decl[\"owner\"] } : {}),\n });\n }\n return { ...EMPTY, present: true, byId };\n}\n\n/**\n * THE vocabulary — both sides read and merged, and the only shape a grader\n * should ever be handed.\n *\n * `readCapabilities` above stays exported for the one reader that genuinely\n * means the file alone (its own tests), but nothing that grades a fleet may use\n * it: a `Capability:` line naming an id the tree declares and the YAML does not\n * would resolve through the union and fail through the file, so a second\n * spelling of \"the vocabulary\" is a second answer to `capability.unknown`.\n *\n * WHERE AN ID SITS ON BOTH SIDES THE YAML'S METADATA SURVIVES, and not by\n * precedence — the document has nothing to overwrite it with. `description` and\n * `owner` are fields, and this axis's own rule is that an entry without prose\n * stays a line in YAML; the document's contribution is the narrative and the\n * requirements, which are prose, and loam reads frontmatter, never prose. So a\n * capability declared only in the tree has no owner in `loam list\n * capabilities`, and that is the honest reading rather than a gap: nobody wrote\n * one down. What the merge adds on that side is `spec` and `source`.\n *\n * An UNREADABLE YAML suspends the whole family (`gradableCapabilityIds` in\n * `./findings.ts` is the one statement of that ladder), and the tree is still\n * walked and still carried: `capability.doc-missing` is a fact about\n * directories that no YAML failure makes untrue, and suppressing it behind an\n * unrelated broken file would hide a half-created capability until somebody\n * fixed a document it has nothing to do with.\n */\nexport async function readCapabilityVocabulary(docsDir: DocsDir): Promise<CapabilityVocabulary> {\n const [yaml, tree] = await Promise.all([\n readCapabilities(capabilitiesPath(docsDir)),\n readCapabilityTree(capabilityDocsDir(docsDir)),\n ]);\n const byId = new Map(yaml.byId);\n for (const doc of tree.docs) {\n const declared = byId.get(doc.id);\n byId.set(doc.id, {\n ...declared,\n id: doc.id,\n source: declared === undefined ? \"tree\" : \"both\",\n spec: doc.spec,\n });\n }\n return {\n ...yaml,\n present: yaml.present || tree.present,\n byId,\n tree,\n };\n}\n"]}
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../../src/core/capabilities/capabilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet's capability vocabulary: which business capabilities exist, by\n * declared name, so a requirement's `Capability:` line resolves or refuses\n * instead of silently labelling.\n *\n * It is a fleet document (`architecture/capabilities.yaml`, beside the\n * landscape and the permissions vocabulary) because a capability crosses\n * services by definition — \"registration\" is realized by requirements in four\n * repositories, and a per-service copy would be N spellings of one word.\n *\n * What this is NOT, deliberately: a domain model, and not an authored prose\n * layer either. SCHEMA.md rejected a free-text `capability:` label because\n * nothing could check it; a DECLARED name is different in exactly the way that\n * mattered there — the id exists in this file or it does not, so an unknown\n * name is an error with close candidates, a declaration nothing realizes is a\n * warning, and the vocabulary cannot drift while looking authoritative. The\n * long-lived capability documents themselves (one page per capability, revised\n * by features) stay outside loam — that is the roadmap's evidence-gated Later\n * item, and the rollup built over these names is the evidence that decides it.\n *\n * The OPT-IN also differs from the authorization axis, on purpose: there the\n * `Requires:` line is the opt-in and a missing permissions.yaml is an error at\n * the line; here the FILE is the opt-in — a fleet with no capabilities.yaml\n * produces no capability findings at all (`core/capabilities/findings.ts`\n * holds the rule and the reason).\n */\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { parse as parseYaml } from \"yaml\";\nimport { isRecord } from \"../kernel/records.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { capabilitiesPath } from \"../repo/paths.js\";\nimport { capabilityDocsDir } from \"../repo/authored/paths.js\";\nimport { readCapabilityTree, type CapabilityTree } from \"./tree.js\";\n\n/** One declared capability. The id is the full key, slashes preserved. */\nexport interface Capability {\n /**\n * The token a requirement's `Capability:` line writes. Nesting is spelled\n * INSIDE the id (`payments/refunds`) and never collapsed — the key is\n * exactly the string a requirement names, greppable as written.\n */\n id: string;\n description?: string;\n /**\n * A team or a person — deliberately `owner:`, not permissions' `owned_by:`,\n * because that field carries \"defining service\" semantics a capability does\n * not have. Never resolved against anything; a blank is a worse answer than\n * an unverified one.\n */\n owner?: string;\n /**\n * Which side declared this id. `yaml` is a name in\n * `architecture/capabilities.yaml` with no document; `tree` is a\n * `capabilities/<id>/spec.md` the YAML does not name; `both` is the same id\n * on both sides, which is normal rather than a duplicate — the YAML declared\n * the word first and somebody has since written the document for it.\n *\n * Recorded rather than derived at each reader, because the two sides carry\n * different fixes: a `yaml`-only capability is fixed by writing the document,\n * a `tree`-only one needs no fix at all, and a message that could not tell\n * them apart would advise editing a file that may not exist.\n */\n source: \"yaml\" | \"tree\" | \"both\";\n /** `capabilities/<id>/spec.md`, absolute, when the tree declares this id. */\n spec?: string;\n}\n\nexport interface CapabilityVocabulary {\n /**\n * False when NEITHER `architecture/capabilities.yaml` nor `capabilities/`\n * exists. Either one is the axis's opt-in, so a fleet holding neither\n * produces no capability finding at all, however many `Capability:` lines its\n * requirements already carry.\n */\n present: boolean;\n /**\n * Why `architecture/capabilities.yaml` could not be read as a vocabulary,\n * when it could not. The TREE has no such state: a directory either lists or\n * it does not, and an unreadable one is a filesystem failure the walk lets\n * throw rather than a document loam misread.\n */\n invalid?: string;\n /** Declared capabilities, keyed by the full id — the union of both sides. */\n byId: Map<string, Capability>;\n /** What the `capabilities/` walk found, for the graders that need the paths. */\n tree: CapabilityTree;\n}\n\nconst NO_TREE: CapabilityTree = { present: false, docs: [], undocumented: [] };\nconst EMPTY: CapabilityVocabulary = { present: false, byId: new Map(), tree: NO_TREE };\n\n/**\n * Read the vocabulary — exactly `permissions.ts`'s defensive ladder, because\n * the two files fail the same ways: a missing file is silence, a YAML error or\n * a non-mapping shape is `invalid` (one finding for the whole run, the family\n * suppressed behind it), an empty document is a present-and-empty vocabulary,\n * and a declaration whose body is not a mapping is treated as `{}` rather than\n * refusing the whole file over one leaf.\n */\nasync function readCapabilities(path: string): Promise<CapabilityVocabulary> {\n if (!existsSync(path)) return EMPTY;\n let doc: unknown;\n try {\n doc = parseYaml(await readFile(path, \"utf8\"));\n } catch (e) {\n return { ...EMPTY, present: true, invalid: e instanceof Error ? e.message : String(e) };\n }\n if (doc === null || doc === undefined) return { ...EMPTY, present: true };\n if (!isRecord(doc)) return { ...EMPTY, present: true, invalid: \"the document is not a mapping\" };\n\n const block = doc[\"capabilities\"];\n if (block === undefined) return { ...EMPTY, present: true };\n if (!isRecord(block)) {\n return { ...EMPTY, present: true, invalid: \"`capabilities` is not a mapping of capability id to its declaration\" };\n }\n\n const byId = new Map<string, Capability>();\n for (const [id, body] of Object.entries(block)) {\n const decl = isRecord(body) ? body : {};\n byId.set(id, {\n id,\n source: \"yaml\",\n ...(typeof decl[\"description\"] === \"string\" ? { description: decl[\"description\"] } : {}),\n ...(typeof decl[\"owner\"] === \"string\" ? { owner: decl[\"owner\"] } : {}),\n });\n }\n return { ...EMPTY, present: true, byId };\n}\n\n/**\n * THE vocabulary — both sides read and merged, and the only shape a grader\n * should ever be handed.\n *\n * `readCapabilities` above is the file alone, and it is private to this\n * module because nothing that grades a fleet may use it: a `Capability:` line\n * naming an id the tree declares and the YAML does not would resolve through\n * the union and fail through the file, so a second spelling of \"the\n * vocabulary\" is a second answer to `capability.unknown`.\n *\n * WHERE AN ID SITS ON BOTH SIDES THE YAML'S METADATA SURVIVES, and not by\n * precedence — the document has nothing to overwrite it with. `description` and\n * `owner` are fields, and this axis's own rule is that an entry without prose\n * stays a line in YAML; the document's contribution is the narrative and the\n * requirements, which are prose, and loam reads frontmatter, never prose. So a\n * capability declared only in the tree has no owner in `loam list\n * capabilities`, and that is the honest reading rather than a gap: nobody wrote\n * one down. What the merge adds on that side is `spec` and `source`.\n *\n * An UNREADABLE YAML suspends the whole family (`gradableCapabilityIds` in\n * `./findings.ts` is the one statement of that ladder), and the tree is still\n * walked and still carried: `capability.doc-missing` is a fact about\n * directories that no YAML failure makes untrue, and suppressing it behind an\n * unrelated broken file would hide a half-created capability until somebody\n * fixed a document it has nothing to do with.\n */\nexport async function readCapabilityVocabulary(docsDir: DocsDir): Promise<CapabilityVocabulary> {\n const [yaml, tree] = await Promise.all([\n readCapabilities(capabilitiesPath(docsDir)),\n readCapabilityTree(capabilityDocsDir(docsDir)),\n ]);\n const byId = new Map(yaml.byId);\n for (const doc of tree.docs) {\n const declared = byId.get(doc.id);\n byId.set(doc.id, {\n ...declared,\n id: doc.id,\n source: declared === undefined ? \"tree\" : \"both\",\n spec: doc.spec,\n });\n }\n return {\n ...yaml,\n present: yaml.present || tree.present,\n byId,\n tree,\n };\n}\n"]}
@@ -2,11 +2,13 @@
2
2
  * The capability document's own three grades, applied to a FEATURE'S DELTA.
3
3
  *
4
4
  * `../findings.ts` grades the living `capabilities/<id>/spec.md` and files
5
- * Findings for `loam validate`. This is the same three rules shaped as archive
6
- * Issues — the twin `capabilityUnknownFindings`/`capabilityUnknownIssues`
7
- * already are one directory over, and for the identical reason: severity says
5
+ * Findings for `loam validate`. This is the same three rules filed as archive
6
+ * Issues — the split `capabilityUnknownIssues` one directory over makes
7
+ * between its two commands, and for the identical reason: severity says
8
8
  * whether the DOCUMENT is valid, gating says whether the MERGE is safe, and the
9
- * two questions are asked by two different commands about the same text.
9
+ * two questions are asked by two different commands about the same text. A grade
10
+ * is already an Issue in shape (its return type pins severity and code), so
11
+ * the rules' own objects are returned, not copied field by field.
10
12
  *
11
13
  * WITHOUT THIS THE DELTA PATH IS A HOLE THROUGH THE RULE. A capability
12
14
  * requirement carrying `Operations:` is a service requirement filed at the
@@ -40,11 +42,6 @@ import { capabilityDocFindings } from "../findings.js";
40
42
  */
41
43
  export function capabilityDocIssues(reqs, target) {
42
44
  const merging = reqs.filter((r) => r.kind === "ADDED" || r.kind === "MODIFIED");
43
- return capabilityDocFindings(merging, target.subject, target.where).map((f) => ({
44
- severity: f.severity,
45
- code: f.code,
46
- subject: f.subject,
47
- message: f.message,
48
- }));
45
+ return capabilityDocFindings(merging, target.subject, target.where);
49
46
  }
50
47
  //# sourceMappingURL=doc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doc.js","sourceRoot":"","sources":["../../../../src/core/capabilities/delta/doc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,qBAAqB,EAAyB,MAAM,gBAAgB,CAAC;AAI9E;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAmB,EAAE,MAAwB;IAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;IAChF,OAAO,qBAAqB,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9E,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,OAAO,EAAE,CAAC,CAAC,OAAO;KACnB,CAAC,CAAC,CAAC;AACN,CAAC","sourcesContent":["/**\n * The capability document's own three grades, applied to a FEATURE'S DELTA.\n *\n * `../findings.ts` grades the living `capabilities/<id>/spec.md` and files\n * Findings for `loam validate`. This is the same three rules shaped as archive\n * Issues — the twin `capabilityUnknownFindings`/`capabilityUnknownIssues`\n * already are one directory over, and for the identical reason: severity says\n * whether the DOCUMENT is valid, gating says whether the MERGE is safe, and the\n * two questions are asked by two different commands about the same text.\n *\n * WITHOUT THIS THE DELTA PATH IS A HOLE THROUGH THE RULE. A capability\n * requirement carrying `Operations:` is a service requirement filed at the\n * business altitude — the one thing that makes a second requirements corpus\n * worth having is that it does not become a second place to write the first\n * one. Graded only on the living document, that requirement merges cleanly and\n * earns its error afterwards, against a file whoever reads the finding did not\n * write and cannot un-merge. So the delta is graded BEFORE the merge, where the\n * author is still holding the document.\n *\n * ADDED AND MODIFIED ONLY. A REMOVED requirement is being retired: demanding a\n * `Requirement-ID:` on it, or refusing the `Operations:` line it is deleting,\n * would ask for an edit to text that is about to stop existing. A BASE\n * requirement is a delta quoting living context, already named by\n * `delta.requirement-not-merged`, and it merges nothing — two findings for one\n * mistake is the failure to avoid.\n *\n * NO CODES OF ITS OWN, and no message of its own. Every sentence comes from\n * `capabilityDocFindings`, whose `where` parameter is what points them at the\n * feature's copy of the document.\n */\nimport { capabilityDocFindings, type CapabilityTarget } from \"../findings.js\";\nimport type { Requirement } from \"../../document/spec.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\n\n/**\n * The three grades, as archive-gating Issues, over the requirements a\n * capability delta would actually merge.\n *\n * `target.where` is the delta's repo-relative path so a reader is sent to the\n * file they wrote, and `target.subject` is the capability id so a `--json`\n * consumer groups by promise rather than by path — the same split every other\n * capability grade uses.\n */\nexport function capabilityDocIssues(reqs: Requirement[], target: CapabilityTarget): Issue[] {\n const merging = reqs.filter((r) => r.kind === \"ADDED\" || r.kind === \"MODIFIED\");\n return capabilityDocFindings(merging, target.subject, target.where).map((f) => ({\n severity: f.severity,\n code: f.code,\n subject: f.subject,\n message: f.message,\n }));\n}\n"]}
1
+ {"version":3,"file":"doc.js","sourceRoot":"","sources":["../../../../src/core/capabilities/delta/doc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAE,qBAAqB,EAAyB,MAAM,gBAAgB,CAAC;AAI9E;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAmB,EAAE,MAAwB;IAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;IAChF,OAAO,qBAAqB,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC","sourcesContent":["/**\n * The capability document's own three grades, applied to a FEATURE'S DELTA.\n *\n * `../findings.ts` grades the living `capabilities/<id>/spec.md` and files\n * Findings for `loam validate`. This is the same three rules filed as archive\n * Issues — the split `capabilityUnknownIssues` one directory over makes\n * between its two commands, and for the identical reason: severity says\n * whether the DOCUMENT is valid, gating says whether the MERGE is safe, and the\n * two questions are asked by two different commands about the same text. A grade\n * is already an Issue in shape (its return type pins severity and code), so\n * the rules' own objects are returned, not copied field by field.\n *\n * WITHOUT THIS THE DELTA PATH IS A HOLE THROUGH THE RULE. A capability\n * requirement carrying `Operations:` is a service requirement filed at the\n * business altitude — the one thing that makes a second requirements corpus\n * worth having is that it does not become a second place to write the first\n * one. Graded only on the living document, that requirement merges cleanly and\n * earns its error afterwards, against a file whoever reads the finding did not\n * write and cannot un-merge. So the delta is graded BEFORE the merge, where the\n * author is still holding the document.\n *\n * ADDED AND MODIFIED ONLY. A REMOVED requirement is being retired: demanding a\n * `Requirement-ID:` on it, or refusing the `Operations:` line it is deleting,\n * would ask for an edit to text that is about to stop existing. A BASE\n * requirement is a delta quoting living context, already named by\n * `delta.requirement-not-merged`, and it merges nothing — two findings for one\n * mistake is the failure to avoid.\n *\n * NO CODES OF ITS OWN, and no message of its own. Every sentence comes from\n * `capabilityDocFindings`, whose `where` parameter is what points them at the\n * feature's copy of the document.\n */\nimport { capabilityDocFindings, type CapabilityTarget } from \"../findings.js\";\nimport type { Requirement } from \"../../document/spec.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\n\n/**\n * The three grades, as archive-gating Issues, over the requirements a\n * capability delta would actually merge.\n *\n * `target.where` is the delta's repo-relative path so a reader is sent to the\n * file they wrote, and `target.subject` is the capability id so a `--json`\n * consumer groups by promise rather than by path — the same split every other\n * capability grade uses.\n */\nexport function capabilityDocIssues(reqs: Requirement[], target: CapabilityTarget): Issue[] {\n const merging = reqs.filter((r) => r.kind === \"ADDED\" || r.kind === \"MODIFIED\");\n return capabilityDocFindings(merging, target.subject, target.where);\n}\n"]}