@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,7 @@
1
- import { ASYNCAPI_BASELINES_KEY } from "../../asyncapi/digest.js";
2
- import {} from "../../asyncapi/model.js";
1
+ import { ASYNCAPI_BASELINES_KEY, slotKey } from "../../asyncapi/digest.js";
2
+ import { moveBackRemedy } from "../retired/revival/revived.js";
3
3
  import { asyncapiSurfaces } from "../../asyncapi/baseline/surfaces.js";
4
+ import { messageName } from "../../asyncapi/read.js";
4
5
  import { entryFor, readBaselineRecord, surfaceIn } from "../../openapi/baseline/record.js";
5
6
  import { classifyBaselineDigests, OPERATION_DIGEST_LENGTH, OPERATION_DIGEST_RE, valueDigest, } from "../../openapi/digest.js";
6
7
  /**
@@ -11,7 +12,7 @@ import { classifyBaselineDigests, OPERATION_DIGEST_LENGTH, OPERATION_DIGEST_RE,
11
12
  * graded once or the two halves drift into different ideas of what it restates.
12
13
  */
13
14
  export function gradeBaselines(input, issues) {
14
- const { featSlots, livingSlots, svc, featureId } = input;
15
+ const { featSlots, livingSlots, livingDoc, svc, featureId } = input;
15
16
  let unpinned = 0;
16
17
  for (const slot of featSlots) {
17
18
  // A removal marker is not a restatement of anything; its own exactness
@@ -19,7 +20,7 @@ export function gradeBaselines(input, issues) {
19
20
  if (slot.remove)
20
21
  continue;
21
22
  const label = `${slot.section}.${slot.key}`;
22
- const living = livingSlots.get(`${slot.section}\0${slot.key}`);
23
+ const living = livingSlots.get(slotKey(slot));
23
24
  if (slot.basedOn !== undefined && !OPERATION_DIGEST_RE.test(slot.basedOn)) {
24
25
  issues.push({
25
26
  severity: "error",
@@ -31,11 +32,33 @@ export function gradeBaselines(input, issues) {
31
32
  }
32
33
  const verdict = classifyBaselineDigests(slot.basedOn, slot.digest, living?.digest);
33
34
  if (verdict === "unfounded") {
35
+ // openapi/baseline/gate.ts's reading, on the slot: a pin is only ever
36
+ // written over a living slot, so one with nothing behind it usually
37
+ // means a parallel feature retired the slot — and "drop the marker",
38
+ // which this used to say, archived the quote as an addition and put the
39
+ // retired slot back. Or MOVED it, on the one slot a move vacates on
40
+ // this axis: a `components.messages` key whose message name the living
41
+ // contract now declares elsewhere. A channel or operation slot's
42
+ // identity is its key, so it is never "moved" — it is retired. The
43
+ // move-back remedy is the revival finding's own (../retired/revival/revived.ts):
44
+ // where the name now stands inline under a channel, a marker there
45
+ // retires nothing, so the remedy names the channel instead. The two
46
+ // places are said as a condition ("while it is still declared there"),
47
+ // not as what this delta does: one that already restates or retires
48
+ // that channel takes the name out of it, and only the pin is left.
49
+ const name = slot.section === "components.messages" ? messageName(slot.node, slot.key) : undefined;
50
+ const movedTo = livingDoc.messages.filter((m) => m.remove !== true && m.name === name).map((m) => m.slot);
51
+ // The channels still pointing at a key the remedy retires are read off
52
+ // the contract the merge would leave; where it refuses (and the archive
53
+ // with it), off the living one, which is what the merge would start from.
54
+ const remedy = () => moveBackRemedy(movedTo, label, input.merged()?.root ?? input.livingPlain);
34
55
  issues.push({
35
56
  severity: "error",
36
57
  code: "asyncapi.baseline-invalid",
37
58
  subject: svc,
38
- message: `${svc}: ${label} carries x-loam-based-on, but the living contract has no slot there — a new channel, operation or message has no living version to be based on; drop the marker`,
59
+ message: movedTo.length > 0
60
+ ? `${svc}: ${label} carries x-loam-based-on, but the living contract has no slot there — message '${name}' is declared at ${movedTo.join(" and ")} now: a parallel feature moved it there (or retired it here, and another re-declared it there). Re-read it there and delete this slot from the delta, making any change at its new key; archived while it is still declared there, it would declare '${name}' at ${label} as well. To move it back on purpose, drop the pin and ${remedy()} in this delta.`
61
+ : `${svc}: ${label} carries x-loam-based-on, but the living contract has no slot there — the version it was pinned to is gone. If a parallel feature retired it, re-read the living contract and delete this slot from the delta, unless this feature deliberately brings it back (then drop the pin and archive with --approve past \`asyncapi.slot-revived\`). A genuinely new channel, operation or message has no living version to be based on and carries no pin.`,
39
62
  });
40
63
  }
41
64
  else if (verdict === "stale") {
@@ -129,13 +152,13 @@ function gradeSurfaces(input, issues) {
129
152
  return unpinned;
130
153
  }
131
154
  /** Marker exactness: the slot must exist, and a message marker must name the living declaration. */
132
- export function gradeMarkers(input, svc, issues) {
133
- const { featDoc, livingDoc, featSlots, livingSlots } = input;
155
+ export function gradeMarkers(pair, svc, issues) {
156
+ const { featDoc, livingDoc, featSlots, livingSlots } = pair;
134
157
  for (const slot of featSlots) {
135
158
  if (!slot.remove)
136
159
  continue;
137
160
  const label = `${slot.section}.${slot.key}`;
138
- const living = livingSlots.get(`${slot.section}\0${slot.key}`);
161
+ const living = livingSlots.get(slotKey(slot));
139
162
  if (living === undefined) {
140
163
  issues.push({
141
164
  severity: "error",
@@ -1 +1 @@
1
- {"version":3,"file":"grades.js","sourceRoot":"","sources":["../../../../src/core/coherence/events/grades.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,sBAAsB,EAAqB,MAAM,0BAA0B,CAAC;AACrF,OAAO,EAAoB,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAC3F,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,WAAW,GACZ,MAAM,yBAAyB,CAAC;AAcjC;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,KAA4B,EAAE,MAAe;IAC1E,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IACzD,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,uEAAuE;QACvE,yBAAyB;QACzB,IAAI,IAAI,CAAC,MAAM;YAAE,SAAS;QAC1B,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/D,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1E,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,2BAA2B;gBACjC,OAAO,EAAE,GAAG;gBACZ,OAAO,EAAE,GAAG,GAAG,KAAK,KAAK,iCAAiC,IAAI,CAAC,OAAO,gBAAgB,uBAAuB,2DAA2D;aACzK,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,uBAAuB,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACnF,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;YAC5B,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,2BAA2B;gBACjC,OAAO,EAAE,GAAG;gBACZ,OAAO,EAAE,GAAG,GAAG,KAAK,KAAK,iKAAiK;aAC3L,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,yBAAyB;gBAC/B,OAAO,EAAE,GAAG;gBACZ,OAAO,EAAE,GAAG,GAAG,KAAK,KAAK,uCAAuC,IAAI,CAAC,OAAO,uCAAuC,MAAO,CAAC,MAAM,8KAA8K,SAAS,KAAK;aAC9T,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,OAAO,KAAK,UAAU,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC1D,+DAA+D;YAC/D,mEAAmE;YACnE,4DAA4D;YAC5D,QAAQ,IAAI,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;IACD,uEAAuE;IACvE,uEAAuE;IACvE,wEAAwE;IACxE,kDAAkD;IAClD,QAAQ,IAAI,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACzC,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QACjB,MAAM,CAAC,IAAI,CAAC;YACV,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,IAAI;YACX,IAAI,EAAE,2BAA2B;YACjC,OAAO,EAAE,GAAG;YACZ,OAAO,EAAE,GAAG,GAAG,KAAK,QAAQ,8GAA8G,sBAAsB,iMAAiM,SAAS,cAAc,GAAG,+DAA+D;SAC3b,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,aAAa,CAAC,KAA4B,EAAE,MAAe;IAClE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IACzD,MAAM,OAAO,GAAG,CAAC,OAAe,EAAQ,EAAE;QACxC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,2BAA2B,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,GAAG,KAAK,OAAO,EAAE,EAAE,CAAC,CAAC;IACrH,CAAC,CAAC;IACF,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC3D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,OAAO,0GAA0G,SAAS,kBAAkB,CAAC,CAAC;IAC3J,CAAC;IACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC7C,yEAAyE;IACzE,yEAAyE;IACzE,0EAA0E;IAC1E,4EAA4E;IAC5E,6EAA6E;IAC7E,+DAA+D;IAC/D,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpD,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAChD,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,SAAS;QAC/B,OAAO,CAAC,GAAG,sBAAsB,oBAAoB,EAAE,kFAAkF,SAAS,0BAA0B,CAAC,CAAC;IAChL,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QACjD,OAAO,CAAC,GAAG,sBAAsB,oBAAoB,IAAI,kGAAkG,SAAS,0BAA0B,CAAC,CAAC;IAClM,CAAC;IAED,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACzC,MAAM,OAAO,GAAG,uBAAuB,CACrC,KAAK,EACL,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,EACpB,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CACrD,CAAC;QACF,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;YAC3B,uEAAuE;YACvE,kEAAkE;YAClE,IAAI,MAAM,CAAC,KAAK;gBAAE,QAAQ,IAAI,CAAC,CAAC;QAClC,CAAC;aAAM,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,yBAAyB;gBAC/B,OAAO,EAAE,GAAG;gBACZ,OAAO,EAAE,GAAG,GAAG,gBAAgB,CAAC,CAAC,EAAE,kCAAkC,KAAK,uCAAuC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,+KAA+K,SAAS,KAAK;aACxU,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;YACnC,OAAO,CAAC,cAAc,CAAC,CAAC,EAAE,kBAAkB,KAAK,oJAAoJ,SAAS,IAAI,CAAC,CAAC;QACtN,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,YAAY,CAC1B,KAA0H,EAC1H,GAAW,EACX,MAAe;IAEf,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC;IAC7D,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,SAAS;QAC3B,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,gCAAgC;gBACtC,OAAO,EAAE,GAAG;gBACZ,OAAO,EAAE,GAAG,GAAG,uBAAuB,KAAK,0JAA0J;aACtM,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,KAAK,qBAAqB;YAAE,SAAS;QACrD,kEAAkE;QAClE,mEAAmE;QACnE,oEAAoE;QACpE,gDAAgD;QAChD,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC;QACzG,MAAM,UAAU,GAAG,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC;QACtF,IAAI,UAAU,KAAK,UAAU,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,iCAAiC;gBACvC,OAAO,EAAE,GAAG;gBACZ,OAAO,EAAE,GAAG,GAAG,uBAAuB,KAAK,mBAAmB,UAAU,2CAA2C,UAAU,+DAA+D;aAC7L,CAAC,CAAC;QACL,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["/**\n * The per-slot grades of one service's pair of event contracts: baseline\n * pins and removal-marker exactness. Split from ./declared.ts along the\n * subject seam — that module owns the WALK (which files, which services,\n * which sets), these functions own the verdict each slot gets once both\n * sides are in hand — when the walk grew past the line limit; the split\n * moved them verbatim.\n */\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport { ASYNCAPI_BASELINES_KEY, type AsyncapiSlot } from \"../../asyncapi/digest.js\";\nimport { type AsyncapiDoc } from \"../../asyncapi/model.js\";\nimport { asyncapiSurfaces } from \"../../asyncapi/baseline/surfaces.js\";\nimport { entryFor, readBaselineRecord, surfaceIn } from \"../../openapi/baseline/record.js\";\nimport {\n classifyBaselineDigests,\n OPERATION_DIGEST_LENGTH,\n OPERATION_DIGEST_RE,\n valueDigest,\n} from \"../../openapi/digest.js\";\n\n/** Everything the two baseline graders need about one service's pair of contracts. */\nexport interface AsyncapiBaselineInput {\n featSlots: AsyncapiSlot[];\n livingSlots: Map<string, AsyncapiSlot>;\n /** The FEATURE document's resolved parse — where the record and the surfaces are read from. */\n featPlain: unknown;\n /** The LIVING document's resolved parse; `{}` where no living contract exists. */\n livingPlain: unknown;\n svc: string;\n featureId: string;\n}\n\n/**\n * One service's baselines against the living contract — openapi/baseline/gate.ts's\n * verdicts, over BOTH halves of this axis: the slot pins in-value, and the\n * component surfaces the root `x-loam-baselines` record pins. One function\n * because the unpinned count is shared (see below), and because a delta is\n * graded once or the two halves drift into different ideas of what it restates.\n */\nexport function gradeBaselines(input: AsyncapiBaselineInput, issues: Issue[]): void {\n const { featSlots, livingSlots, svc, featureId } = input;\n let unpinned = 0;\n for (const slot of featSlots) {\n // A removal marker is not a restatement of anything; its own exactness\n // checks guard the slot.\n if (slot.remove) continue;\n const label = `${slot.section}.${slot.key}`;\n const living = livingSlots.get(`${slot.section}\\0${slot.key}`);\n if (slot.basedOn !== undefined && !OPERATION_DIGEST_RE.test(slot.basedOn)) {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.baseline-invalid\",\n subject: svc,\n message: `${svc}: ${label} has invalid x-loam-based-on '${slot.basedOn}' — expected ${OPERATION_DIGEST_LENGTH} lowercase hex characters, as \\`loam rebase\\` writes them`,\n });\n continue;\n }\n const verdict = classifyBaselineDigests(slot.basedOn, slot.digest, living?.digest);\n if (verdict === \"unfounded\") {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.baseline-invalid\",\n subject: svc,\n message: `${svc}: ${label} carries x-loam-based-on, but the living contract has no slot there — a new channel, operation or message has no living version to be based on; drop the marker`,\n });\n } else if (verdict === \"stale\") {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.baseline-stale\",\n subject: svc,\n message: `${svc}: ${label} was written against living version ${slot.basedOn}, but the living contract now holds ${living!.digest} — somebody landed a change to it in between, and merging this would replace theirs outright. Re-read the living slot, fold in what you still mean, then run \\`loam rebase ${featureId}\\`.`,\n });\n } else if (verdict === \"unpinned\" && living !== undefined) {\n // Counted, not listed — a delta quotes most of the contract it\n // restates, and one warning per quote teaches people to filter the\n // code out (openapi.baseline-missing's doctrine, verbatim).\n unpinned += 1;\n }\n }\n // The surface half's unpinned count folds into the SAME counter, not a\n // second warning: \"counted, not listed\" is the doctrine above, and one\n // warning per axis-half would have a delta over a schema-heavy contract\n // print two findings naming the same one command.\n unpinned += gradeSurfaces(input, issues);\n if (unpinned > 0) {\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"asyncapi.baseline-missing\",\n subject: svc,\n message: `${svc}: ${unpinned} slot(s) and component surface(s) in this feature's asyncapi.yaml carry no baseline pin (x-loam-based-on / ${ASYNCAPI_BASELINES_KEY}), so the merge cannot tell which ones this delta EDITS from the ones it merely restates — it will upsert all of them, reverting anything that landed on the restated ones. Run \\`loam rebase ${featureId} --service ${svc}\\`, or archive with --approve to merge unpinned deliberately.`,\n });\n }\n}\n\n/**\n * The surface half: every `components/<kind>/<name>` outside `messages`,\n * graded from the root `x-loam-baselines` record by the same classifier the\n * slots go through. Returns the unpinned count so the caller can fold it into\n * the one per-service warning rather than emitting a second.\n *\n * The codes are REUSED rather than multiplied — `asyncapi.baseline-invalid`\n * and `asyncapi.baseline-stale` say exactly the same thing about a surface as\n * about a slot, and a caller that branches on them wants one branch.\n */\nfunction gradeSurfaces(input: AsyncapiBaselineInput, issues: Issue[]): number {\n const { featPlain, livingPlain, svc, featureId } = input;\n const invalid = (message: string): void => {\n issues.push({ severity: \"error\", code: \"asyncapi.baseline-invalid\", subject: svc, message: `${svc}: ${message}` });\n };\n const { record, problems } = readBaselineRecord(featPlain);\n for (const problem of problems) {\n invalid(`${problem} — the record is \\`loam rebase\\`'s bookkeeping and cannot be graded as it stands; re-run \\`loam rebase ${featureId}\\` to rebuild it`);\n }\n const surfaces = asyncapiSurfaces(featPlain);\n // An entry pinning a surface the delta does not declare is a claim about\n // nothing — usually one the author deleted after rebasing. Distinct from\n // `unfounded` below, where the surface IS declared and the LIVING side is\n // what vanished. A `pathItems` section is the same fault at the axis level:\n // an AsyncAPI document has no path items, so the whole section pins nothing,\n // and rebase's wholesale rebuild would drop it without a word.\n const declared = new Set(surfaces.map((s) => s.id));\n for (const id of Object.keys(record.components)) {\n if (declared.has(id)) continue;\n invalid(`${ASYNCAPI_BASELINES_KEY} pins component '${id}', but this feature's asyncapi.yaml does not declare it — re-run \\`loam rebase ${featureId}\\` to rebuild the record`);\n }\n for (const path of Object.keys(record.pathItems)) {\n invalid(`${ASYNCAPI_BASELINES_KEY} pins path item '${path}', but an AsyncAPI document has no path items — the section pins nothing; re-run \\`loam rebase ${featureId}\\` to rebuild the record`);\n }\n\n let unpinned = 0;\n for (const s of surfaces) {\n const entry = entryFor(record, s);\n const living = surfaceIn(livingPlain, s);\n const verdict = classifyBaselineDigests(\n entry,\n valueDigest(s.value),\n living.found ? valueDigest(living.value) : undefined,\n );\n if (verdict === \"unpinned\") {\n // Only a surface the living contract ALREADY has can be reverted by an\n // unpinned merge; a genuinely new schema never trips the counter.\n if (living.found) unpinned += 1;\n } else if (verdict === \"stale\") {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.baseline-stale\",\n subject: svc,\n message: `${svc}: component '${s.id}' was pinned to living version ${entry}, but the living contract now holds ${valueDigest(living.value)} — somebody landed a change to it in between, and merging this would replace theirs outright. Re-read the living value, fold in what you still mean, then run \\`loam rebase ${featureId}\\`.`,\n });\n } else if (verdict === \"unfounded\") {\n invalid(`component '${s.id}' is pinned to ${entry}, but the living contract no longer has that surface — the pin is structurally unresolvable; re-read the living contract, then run \\`loam rebase ${featureId}\\``);\n }\n }\n return unpinned;\n}\n\n/** Marker exactness: the slot must exist, and a message marker must name the living declaration. */\nexport function gradeMarkers(\n input: { featDoc: AsyncapiDoc; livingDoc: AsyncapiDoc; featSlots: AsyncapiSlot[]; livingSlots: Map<string, AsyncapiSlot> },\n svc: string,\n issues: Issue[],\n): void {\n const { featDoc, livingDoc, featSlots, livingSlots } = input;\n for (const slot of featSlots) {\n if (!slot.remove) continue;\n const label = `${slot.section}.${slot.key}`;\n const living = livingSlots.get(`${slot.section}\\0${slot.key}`);\n if (living === undefined) {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.remove-target-missing\",\n subject: svc,\n message: `${svc}: removal marker at ${label} addresses no living slot — the contract has nothing there to retire; update the stale marker to the current key, or drop it if the slot is already gone`,\n });\n continue;\n }\n if (slot.section !== \"components.messages\") continue;\n // Removal is exact for messages the way it is for operations: the\n // marker names both the key and the message identity it expects to\n // delete, so a slot whose NAME moved under the delta is caught here\n // rather than deleting somebody else's message.\n const markerName = featDoc.messages.find((m) => m.remove === true && m.slot === label)?.name ?? slot.key;\n const livingName = livingDoc.messages.find((m) => m.slot === label)?.name ?? slot.key;\n if (markerName !== livingName) {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.remove-target-mismatch\",\n subject: svc,\n message: `${svc}: removal marker at ${label} names message '${markerName}', but the living declaration there is '${livingName}' — loam never deletes a different message occupying the slot`,\n });\n }\n }\n}\n"]}
1
+ {"version":3,"file":"grades.js","sourceRoot":"","sources":["../../../../src/core/coherence/events/grades.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The per-slot grades of one service's pair of event contracts: baseline\n * pins and removal-marker exactness. Split from ./declared.ts along the\n * subject seam — that module owns the WALK (which files, which services,\n * which sets), these functions own the verdict each slot gets once both\n * sides are in hand — when the walk grew past the line limit; the split\n * moved them verbatim.\n */\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport { ASYNCAPI_BASELINES_KEY, slotKey } from \"../../asyncapi/digest.js\";\nimport type { EventPair, EventPairRead } from \"../retired/event-pair.js\";\nimport { moveBackRemedy } from \"../retired/revival/revived.js\";\nimport { asyncapiSurfaces } from \"../../asyncapi/baseline/surfaces.js\";\nimport { messageName } from \"../../asyncapi/read.js\";\nimport { entryFor, readBaselineRecord, surfaceIn } from \"../../openapi/baseline/record.js\";\nimport {\n classifyBaselineDigests,\n OPERATION_DIGEST_LENGTH,\n OPERATION_DIGEST_RE,\n valueDigest,\n} from \"../../openapi/digest.js\";\n\n/** Everything the two baseline graders need about one service's pair of contracts. */\nexport interface AsyncapiBaselineInput extends EventPair, Pick<EventPairRead, \"merged\"> {\n /** The FEATURE document's resolved parse — where the record and the surfaces are read from. */\n featPlain: unknown;\n /** The LIVING document's resolved parse; `{}` where no living contract exists. */\n livingPlain: unknown;\n svc: string;\n featureId: string;\n}\n\n/**\n * One service's baselines against the living contract — openapi/baseline/gate.ts's\n * verdicts, over BOTH halves of this axis: the slot pins in-value, and the\n * component surfaces the root `x-loam-baselines` record pins. One function\n * because the unpinned count is shared (see below), and because a delta is\n * graded once or the two halves drift into different ideas of what it restates.\n */\nexport function gradeBaselines(input: AsyncapiBaselineInput, issues: Issue[]): void {\n const { featSlots, livingSlots, livingDoc, svc, featureId } = input;\n let unpinned = 0;\n for (const slot of featSlots) {\n // A removal marker is not a restatement of anything; its own exactness\n // checks guard the slot.\n if (slot.remove) continue;\n const label = `${slot.section}.${slot.key}`;\n const living = livingSlots.get(slotKey(slot));\n if (slot.basedOn !== undefined && !OPERATION_DIGEST_RE.test(slot.basedOn)) {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.baseline-invalid\",\n subject: svc,\n message: `${svc}: ${label} has invalid x-loam-based-on '${slot.basedOn}' — expected ${OPERATION_DIGEST_LENGTH} lowercase hex characters, as \\`loam rebase\\` writes them`,\n });\n continue;\n }\n const verdict = classifyBaselineDigests(slot.basedOn, slot.digest, living?.digest);\n if (verdict === \"unfounded\") {\n // openapi/baseline/gate.ts's reading, on the slot: a pin is only ever\n // written over a living slot, so one with nothing behind it usually\n // means a parallel feature retired the slot — and \"drop the marker\",\n // which this used to say, archived the quote as an addition and put the\n // retired slot back. Or MOVED it, on the one slot a move vacates on\n // this axis: a `components.messages` key whose message name the living\n // contract now declares elsewhere. A channel or operation slot's\n // identity is its key, so it is never \"moved\" — it is retired. The\n // move-back remedy is the revival finding's own (../retired/revival/revived.ts):\n // where the name now stands inline under a channel, a marker there\n // retires nothing, so the remedy names the channel instead. The two\n // places are said as a condition (\"while it is still declared there\"),\n // not as what this delta does: one that already restates or retires\n // that channel takes the name out of it, and only the pin is left.\n const name = slot.section === \"components.messages\" ? messageName(slot.node, slot.key) : undefined;\n const movedTo = livingDoc.messages.filter((m) => m.remove !== true && m.name === name).map((m) => m.slot);\n // The channels still pointing at a key the remedy retires are read off\n // the contract the merge would leave; where it refuses (and the archive\n // with it), off the living one, which is what the merge would start from.\n const remedy = (): string => moveBackRemedy(movedTo, label, input.merged()?.root ?? input.livingPlain);\n issues.push({\n severity: \"error\",\n code: \"asyncapi.baseline-invalid\",\n subject: svc,\n message: movedTo.length > 0\n ? `${svc}: ${label} carries x-loam-based-on, but the living contract has no slot there — message '${name}' is declared at ${movedTo.join(\" and \")} now: a parallel feature moved it there (or retired it here, and another re-declared it there). Re-read it there and delete this slot from the delta, making any change at its new key; archived while it is still declared there, it would declare '${name}' at ${label} as well. To move it back on purpose, drop the pin and ${remedy()} in this delta.`\n : `${svc}: ${label} carries x-loam-based-on, but the living contract has no slot there — the version it was pinned to is gone. If a parallel feature retired it, re-read the living contract and delete this slot from the delta, unless this feature deliberately brings it back (then drop the pin and archive with --approve past \\`asyncapi.slot-revived\\`). A genuinely new channel, operation or message has no living version to be based on and carries no pin.`,\n });\n } else if (verdict === \"stale\") {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.baseline-stale\",\n subject: svc,\n message: `${svc}: ${label} was written against living version ${slot.basedOn}, but the living contract now holds ${living!.digest} — somebody landed a change to it in between, and merging this would replace theirs outright. Re-read the living slot, fold in what you still mean, then run \\`loam rebase ${featureId}\\`.`,\n });\n } else if (verdict === \"unpinned\" && living !== undefined) {\n // Counted, not listed — a delta quotes most of the contract it\n // restates, and one warning per quote teaches people to filter the\n // code out (openapi.baseline-missing's doctrine, verbatim).\n unpinned += 1;\n }\n }\n // The surface half's unpinned count folds into the SAME counter, not a\n // second warning: \"counted, not listed\" is the doctrine above, and one\n // warning per axis-half would have a delta over a schema-heavy contract\n // print two findings naming the same one command.\n unpinned += gradeSurfaces(input, issues);\n if (unpinned > 0) {\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"asyncapi.baseline-missing\",\n subject: svc,\n message: `${svc}: ${unpinned} slot(s) and component surface(s) in this feature's asyncapi.yaml carry no baseline pin (x-loam-based-on / ${ASYNCAPI_BASELINES_KEY}), so the merge cannot tell which ones this delta EDITS from the ones it merely restates — it will upsert all of them, reverting anything that landed on the restated ones. Run \\`loam rebase ${featureId} --service ${svc}\\`, or archive with --approve to merge unpinned deliberately.`,\n });\n }\n}\n\n/**\n * The surface half: every `components/<kind>/<name>` outside `messages`,\n * graded from the root `x-loam-baselines` record by the same classifier the\n * slots go through. Returns the unpinned count so the caller can fold it into\n * the one per-service warning rather than emitting a second.\n *\n * The codes are REUSED rather than multiplied — `asyncapi.baseline-invalid`\n * and `asyncapi.baseline-stale` say exactly the same thing about a surface as\n * about a slot, and a caller that branches on them wants one branch.\n */\nfunction gradeSurfaces(input: AsyncapiBaselineInput, issues: Issue[]): number {\n const { featPlain, livingPlain, svc, featureId } = input;\n const invalid = (message: string): void => {\n issues.push({ severity: \"error\", code: \"asyncapi.baseline-invalid\", subject: svc, message: `${svc}: ${message}` });\n };\n const { record, problems } = readBaselineRecord(featPlain);\n for (const problem of problems) {\n invalid(`${problem} — the record is \\`loam rebase\\`'s bookkeeping and cannot be graded as it stands; re-run \\`loam rebase ${featureId}\\` to rebuild it`);\n }\n const surfaces = asyncapiSurfaces(featPlain);\n // An entry pinning a surface the delta does not declare is a claim about\n // nothing — usually one the author deleted after rebasing. Distinct from\n // `unfounded` below, where the surface IS declared and the LIVING side is\n // what vanished. A `pathItems` section is the same fault at the axis level:\n // an AsyncAPI document has no path items, so the whole section pins nothing,\n // and rebase's wholesale rebuild would drop it without a word.\n const declared = new Set(surfaces.map((s) => s.id));\n for (const id of Object.keys(record.components)) {\n if (declared.has(id)) continue;\n invalid(`${ASYNCAPI_BASELINES_KEY} pins component '${id}', but this feature's asyncapi.yaml does not declare it — re-run \\`loam rebase ${featureId}\\` to rebuild the record`);\n }\n for (const path of Object.keys(record.pathItems)) {\n invalid(`${ASYNCAPI_BASELINES_KEY} pins path item '${path}', but an AsyncAPI document has no path items — the section pins nothing; re-run \\`loam rebase ${featureId}\\` to rebuild the record`);\n }\n\n let unpinned = 0;\n for (const s of surfaces) {\n const entry = entryFor(record, s);\n const living = surfaceIn(livingPlain, s);\n const verdict = classifyBaselineDigests(\n entry,\n valueDigest(s.value),\n living.found ? valueDigest(living.value) : undefined,\n );\n if (verdict === \"unpinned\") {\n // Only a surface the living contract ALREADY has can be reverted by an\n // unpinned merge; a genuinely new schema never trips the counter.\n if (living.found) unpinned += 1;\n } else if (verdict === \"stale\") {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.baseline-stale\",\n subject: svc,\n message: `${svc}: component '${s.id}' was pinned to living version ${entry}, but the living contract now holds ${valueDigest(living.value)} — somebody landed a change to it in between, and merging this would replace theirs outright. Re-read the living value, fold in what you still mean, then run \\`loam rebase ${featureId}\\`.`,\n });\n } else if (verdict === \"unfounded\") {\n invalid(`component '${s.id}' is pinned to ${entry}, but the living contract no longer has that surface — the pin is structurally unresolvable; re-read the living contract, then run \\`loam rebase ${featureId}\\``);\n }\n }\n return unpinned;\n}\n\n/** Marker exactness: the slot must exist, and a message marker must name the living declaration. */\nexport function gradeMarkers(pair: EventPair, svc: string, issues: Issue[]): void {\n const { featDoc, livingDoc, featSlots, livingSlots } = pair;\n for (const slot of featSlots) {\n if (!slot.remove) continue;\n const label = `${slot.section}.${slot.key}`;\n const living = livingSlots.get(slotKey(slot));\n if (living === undefined) {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.remove-target-missing\",\n subject: svc,\n message: `${svc}: removal marker at ${label} addresses no living slot — the contract has nothing there to retire; update the stale marker to the current key, or drop it if the slot is already gone`,\n });\n continue;\n }\n if (slot.section !== \"components.messages\") continue;\n // Removal is exact for messages the way it is for operations: the\n // marker names both the key and the message identity it expects to\n // delete, so a slot whose NAME moved under the delta is caught here\n // rather than deleting somebody else's message.\n const markerName = featDoc.messages.find((m) => m.remove === true && m.slot === label)?.name ?? slot.key;\n const livingName = livingDoc.messages.find((m) => m.slot === label)?.name ?? slot.key;\n if (markerName !== livingName) {\n issues.push({\n severity: \"error\",\n code: \"asyncapi.remove-target-mismatch\",\n subject: svc,\n message: `${svc}: removal marker at ${label} names message '${markerName}', but the living declaration there is '${livingName}' — loam never deletes a different message occupying the slot`,\n });\n }\n }\n}\n"]}
@@ -1,96 +1,28 @@
1
1
  /**
2
- * The event axis's fleet questions, each read lazily — ../lookups.ts's
3
- * pattern on the message join.
2
+ * The event axis's fleet question, read lazily — ../lookups.ts's pattern on
3
+ * the message join.
4
4
  *
5
- * Both reach outside the delta: the consumer scan is a landscape spin-up
6
- * plus a walk of every other service's living requirements, and the
7
- * in-flight scan reads every active feature's asyncapi delta. Closures over
8
- * a cache rather than arguments computed up front, so each is paid only
9
- * when its question is first asked — but the two prices differ, and the
10
- * difference is deliberate: the consumer scan is paid only by a feature
11
- * that actually retires production, while the in-flight scan is paid by ANY
12
- * feature that declares a message at all, because
5
+ * It reaches outside the delta: the in-flight scan reads every active
6
+ * feature's asyncapi delta. A closure over a cache rather than an argument
7
+ * computed up front, so it is paid only when its question is first asked —
8
+ * and it is paid by ANY feature that declares a message at all, because
13
9
  * `asyncapi.message-conflict`'s question ("does an in-flight feature define
14
10
  * this elsewhere?") is asked of every healthy declaration — unlike the
15
11
  * OpenAPI mirror, whose `definedElsewhere` is reached only inside a breach
16
12
  * branch. That standing cost matters most on `archive`'s gate, which runs
17
13
  * without a FleetContext, so each read here is a real parse.
14
+ *
15
+ * The consumer scan that used to sit beside it — who still consumes a message
16
+ * a service retires — is the consumer index's now (`core/consumers/`), paid
17
+ * only by a feature that actually retires production: `loam diff` asks the
18
+ * same question, and the two copies had drifted apart in which documents they
19
+ * read.
18
20
  */
19
- import { existsSync } from "node:fs";
20
- import { readFile } from "node:fs/promises";
21
- import { loadFile } from "../../c4/likec4.js";
22
- import { serviceResolver } from "../../c4/resolve/service.js";
23
- import {} from "../../kernel/ids/service.js";
24
- import { featureSpecPaths, landscapePath, SPEC_AXES } from "../../repo/paths.js";
25
- import { enumeratedServiceIds, locateServicePaths } from "../../repo/service-target.js";
21
+ import { featureSpecPaths } from "../../repo/paths.js";
26
22
  import { listFeatures } from "../../repo/repo.js";
27
23
  import { readAsyncapi } from "../../asyncapi/read.js";
28
- import { parseRequirements } from "../../document/parse.js";
29
- import {} from "../../document/spec.js";
30
24
  export function eventLookups(scope, context) {
31
25
  const { docsDir, featureId } = scope;
32
- // The enumerated fleet, read once and shared by both halves of the
33
- // consumer scan — ../lookups.ts's discipline, verbatim.
34
- let serviceIds;
35
- const enumeratedServices = async () => (serviceIds ??= await enumeratedServiceIds(docsDir, context));
36
- // Living requirements per service, BOTH axes: `Publishes:`/`Consumes:`
37
- // live in arch.spec.md at least as often as in spec.md (the outbox
38
- // requirement), so a scan of spec.md alone would answer "nobody consumes
39
- // it" over the file most authors use.
40
- const livingReqs = new Map();
41
- const livingRequirements = async (service) => {
42
- let reqs = livingReqs.get(service);
43
- if (reqs === undefined) {
44
- reqs = [];
45
- for (const axis of SPEC_AXES) {
46
- const p = (await locateServicePaths(docsDir, service, context))[axis.key];
47
- if (!existsSync(p))
48
- continue;
49
- reqs.push(...(context === undefined
50
- ? parseRequirements(await readFile(p, "utf8"))
51
- : await context.readRequirements(p)));
52
- }
53
- livingReqs.set(service, reqs);
54
- }
55
- return reqs;
56
- };
57
- // The landscape half: a `consumes` edge is a consumer somebody drew. The
58
- // message join is fleet-global (names are not service-scoped), so every
59
- // edge naming the message counts, whoever its producer is — resolved
60
- // through the enumerated fleet so a landscape that models CONTAINERS
61
- // stays visible, ../lookups.ts's edgeConsumers lesson.
62
- // (`core/diff/victims.ts` mirrors this scan over an already-read fleet —
63
- // change the rules or the victim strings here and that module owes the
64
- // same change; a third copy forces the shared extraction, rule 13.)
65
- let livingLandscape;
66
- const messageConsumers = async (service, message) => {
67
- if (livingLandscape === undefined) {
68
- const path = landscapePath(docsDir);
69
- livingLandscape = existsSync(path)
70
- ? context === undefined
71
- ? await loadFile(path)
72
- : await context.loadLikeC4(path)
73
- : null;
74
- }
75
- const out = [];
76
- // An unreadable landscape proves nothing either way; `landscape.invalid`
77
- // is validate's finding to make.
78
- if (livingLandscape !== null && livingLandscape.errors.length === 0) {
79
- const resolve = serviceResolver(livingLandscape.elements, new Set(await enumeratedServices()));
80
- for (const r of livingLandscape.relationships) {
81
- if (r.consumes !== message || resolve(r.target) === service)
82
- continue;
83
- out.push(`edge ${resolve(r.source)} → ${resolve(r.target)}${r.title === undefined ? "" : ` ("${r.title}")`}`);
84
- }
85
- }
86
- for (const other of (await enumeratedServices()).filter((id) => id !== service)) {
87
- for (const r of await livingRequirements(other)) {
88
- if (r.consumes.includes(message))
89
- out.push(`${other}'s living requirement '${r.name}'`);
90
- }
91
- }
92
- return out;
93
- };
94
26
  // What OTHER features in flight declare, per (service, message) — the
95
27
  // asyncapi mirror of pending.ts's activeOpAdditions, living HERE because
96
28
  // it shares this module's laziness and its consumers. Removal markers do
@@ -117,6 +49,6 @@ export function eventLookups(scope, context) {
117
49
  }
118
50
  return inFlight.get(`${service} ${message}`);
119
51
  };
120
- return { messageConsumers, messageDefinedElsewhere };
52
+ return { messageDefinedElsewhere };
121
53
  }
122
54
  //# sourceMappingURL=lookups.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lookups.js","sourceRoot":"","sources":["../../../../src/core/coherence/events/lookups.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The event axis's fleet questions, each read lazily — ../lookups.ts's\n * pattern on the message join.\n *\n * Both reach outside the delta: the consumer scan is a landscape spin-up\n * plus a walk of every other service's living requirements, and the\n * in-flight scan reads every active feature's asyncapi delta. Closures over\n * a cache rather than arguments computed up front, so each is paid only\n * when its question is first asked — but the two prices differ, and the\n * difference is deliberate: the consumer scan is paid only by a feature\n * that actually retires production, while the in-flight scan is paid by ANY\n * feature that declares a message at all, because\n * `asyncapi.message-conflict`'s question (\"does an in-flight feature define\n * this elsewhere?\") is asked of every healthy declaration — unlike the\n * OpenAPI mirror, whose `definedElsewhere` is reached only inside a breach\n * branch. That standing cost matters most on `archive`'s gate, which runs\n * without a FleetContext, so each read here is a real parse.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { loadFile, type LoadedDoc } from \"../../c4/likec4.js\";\nimport { serviceResolver } from \"../../c4/resolve/service.js\";\nimport { type PathableService } from \"../../kernel/ids/service.js\";\nimport { featureSpecPaths, landscapePath, SPEC_AXES } from \"../../repo/paths.js\";\nimport { enumeratedServiceIds, locateServicePaths } from \"../../repo/service-target.js\";\nimport { listFeatures } from \"../../repo/repo.js\";\nimport { readAsyncapi } from \"../../asyncapi/read.js\";\nimport { parseRequirements } from \"../../document/parse.js\";\nimport { type Requirement } from \"../../document/spec.js\";\nimport type { FleetContext } from \"../../fleet-context.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\n\n/** What the event checks may ask about the world outside this feature's own files. */\nexport interface EventLookups {\n /**\n * Living consumers of one message a `service` is retiring: landscape\n * `consumes` edges, and other services' living requirements' `Consumes:`\n * lines (spec.md and arch.spec.md both — the outbox file is the canonical\n * home of an event line). The retiring service itself is excluded from\n * both: its own consumption leaves in the same feature, and counting it\n * would gate every self-consuming producer on its own retirement.\n */\n messageConsumers(service: string, message: string): Promise<string[]>;\n /** The in-flight feature (if any) whose asyncapi delta adds or edits (service, message). */\n messageDefinedElsewhere(service: string, message: string): Promise<string | undefined>;\n}\n\n/** The identity of the feature being graded — what the lazy scans exclude. */\nexport interface EventScope {\n docsDir: DocsDir;\n featureId: string;\n}\n\nexport function eventLookups(scope: EventScope, context?: FleetContext): EventLookups {\n const { docsDir, featureId } = scope;\n\n // The enumerated fleet, read once and shared by both halves of the\n // consumer scan — ../lookups.ts's discipline, verbatim.\n let serviceIds: PathableService[] | undefined;\n const enumeratedServices = async (): Promise<PathableService[]> =>\n (serviceIds ??= await enumeratedServiceIds(docsDir, context));\n\n // Living requirements per service, BOTH axes: `Publishes:`/`Consumes:`\n // live in arch.spec.md at least as often as in spec.md (the outbox\n // requirement), so a scan of spec.md alone would answer \"nobody consumes\n // it\" over the file most authors use.\n const livingReqs = new Map<string, Requirement[]>();\n const livingRequirements = async (service: PathableService): Promise<Requirement[]> => {\n let reqs = livingReqs.get(service);\n if (reqs === undefined) {\n reqs = [];\n for (const axis of SPEC_AXES) {\n const p = (await locateServicePaths(docsDir, service, context))[axis.key];\n if (!existsSync(p)) continue;\n reqs.push(\n ...(context === undefined\n ? parseRequirements(await readFile(p, \"utf8\"))\n : await context.readRequirements(p)),\n );\n }\n livingReqs.set(service, reqs);\n }\n return reqs;\n };\n\n // The landscape half: a `consumes` edge is a consumer somebody drew. The\n // message join is fleet-global (names are not service-scoped), so every\n // edge naming the message counts, whoever its producer is — resolved\n // through the enumerated fleet so a landscape that models CONTAINERS\n // stays visible, ../lookups.ts's edgeConsumers lesson.\n // (`core/diff/victims.ts` mirrors this scan over an already-read fleet —\n // change the rules or the victim strings here and that module owes the\n // same change; a third copy forces the shared extraction, rule 13.)\n let livingLandscape: LoadedDoc | null | undefined;\n const messageConsumers = async (service: string, message: string): Promise<string[]> => {\n if (livingLandscape === undefined) {\n const path = landscapePath(docsDir);\n livingLandscape = existsSync(path)\n ? context === undefined\n ? await loadFile(path)\n : await context.loadLikeC4(path)\n : null;\n }\n const out: string[] = [];\n // An unreadable landscape proves nothing either way; `landscape.invalid`\n // is validate's finding to make.\n if (livingLandscape !== null && livingLandscape.errors.length === 0) {\n const resolve = serviceResolver(livingLandscape.elements, new Set(await enumeratedServices()));\n for (const r of livingLandscape.relationships) {\n if (r.consumes !== message || resolve(r.target) === service) continue;\n out.push(`edge ${resolve(r.source)} → ${resolve(r.target)}${r.title === undefined ? \"\" : ` (\"${r.title}\")`}`);\n }\n }\n for (const other of (await enumeratedServices()).filter((id) => id !== service)) {\n for (const r of await livingRequirements(other)) {\n if (r.consumes.includes(message)) out.push(`${other}'s living requirement '${r.name}'`);\n }\n }\n return out;\n };\n\n // What OTHER features in flight declare, per (service, message) — the\n // asyncapi mirror of pending.ts's activeOpAdditions, living HERE because\n // it shares this module's laziness and its consumers. Removal markers do\n // not count: a feature retiring a message is not adding it. First feature\n // wins a clash — one name to archive first is enough to make progress.\n let inFlight: Map<string, string> | null = null;\n const messageDefinedElsewhere = async (service: string, message: string): Promise<string | undefined> => {\n if (inFlight === null) {\n inFlight = new Map<string, string>();\n for (const feature of await listFeatures(docsDir, {}, context)) {\n if (feature.id === featureId) continue;\n for (const svc of feature.services) {\n const doc = await readAsyncapi(featureSpecPaths(feature.dir, svc).asyncapi, context);\n for (const m of doc.messages) {\n if (m.remove === true) continue;\n const k = `${svc} ${m.name}`;\n if (!inFlight.has(k)) inFlight.set(k, feature.id);\n }\n }\n }\n }\n return inFlight.get(`${service} ${message}`);\n };\n\n return { messageConsumers, messageDefinedElsewhere };\n}\n"]}
1
+ {"version":3,"file":"lookups.js","sourceRoot":"","sources":["../../../../src/core/coherence/events/lookups.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAgBtD,MAAM,UAAU,YAAY,CAAC,KAAiB,EAAE,OAAsB;IACpE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAErC,sEAAsE;IACtE,yEAAyE;IACzE,yEAAyE;IACzE,0EAA0E;IAC1E,uEAAuE;IACvE,IAAI,QAAQ,GAA+B,IAAI,CAAC;IAChD,MAAM,uBAAuB,GAAG,KAAK,EAAE,OAAe,EAAE,OAAe,EAA+B,EAAE;QACtG,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;YACrC,KAAK,MAAM,OAAO,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC;gBAC/D,IAAI,OAAO,CAAC,EAAE,KAAK,SAAS;oBAAE,SAAS;gBACvC,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;oBACnC,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,gBAAgB,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;oBACrF,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;wBAC7B,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI;4BAAE,SAAS;wBAChC,MAAM,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;wBAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;4BAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;oBACpD,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC,CAAC;IAC/C,CAAC,CAAC;IAEF,OAAO,EAAE,uBAAuB,EAAE,CAAC;AACrC,CAAC","sourcesContent":["/**\n * The event axis's fleet question, read lazily — ../lookups.ts's pattern on\n * the message join.\n *\n * It reaches outside the delta: the in-flight scan reads every active\n * feature's asyncapi delta. A closure over a cache rather than an argument\n * computed up front, so it is paid only when its question is first asked —\n * and it is paid by ANY feature that declares a message at all, because\n * `asyncapi.message-conflict`'s question (\"does an in-flight feature define\n * this elsewhere?\") is asked of every healthy declaration — unlike the\n * OpenAPI mirror, whose `definedElsewhere` is reached only inside a breach\n * branch. That standing cost matters most on `archive`'s gate, which runs\n * without a FleetContext, so each read here is a real parse.\n *\n * The consumer scan that used to sit beside it — who still consumes a message\n * a service retires — is the consumer index's now (`core/consumers/`), paid\n * only by a feature that actually retires production: `loam diff` asks the\n * same question, and the two copies had drifted apart in which documents they\n * read.\n */\nimport { featureSpecPaths } from \"../../repo/paths.js\";\nimport { listFeatures } from \"../../repo/repo.js\";\nimport { readAsyncapi } from \"../../asyncapi/read.js\";\nimport type { FleetContext } from \"../../fleet-context.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\n\n/** What the event checks may ask about the world outside this feature's own files. */\nexport interface EventLookups {\n /** The in-flight feature (if any) whose asyncapi delta adds or edits (service, message). */\n messageDefinedElsewhere(service: string, message: string): Promise<string | undefined>;\n}\n\n/** The identity of the feature being graded — what the lazy scans exclude. */\nexport interface EventScope {\n docsDir: DocsDir;\n featureId: string;\n}\n\nexport function eventLookups(scope: EventScope, context?: FleetContext): EventLookups {\n const { docsDir, featureId } = scope;\n\n // What OTHER features in flight declare, per (service, message) — the\n // asyncapi mirror of pending.ts's activeOpAdditions, living HERE because\n // it shares this module's laziness and its consumers. Removal markers do\n // not count: a feature retiring a message is not adding it. First feature\n // wins a clash — one name to archive first is enough to make progress.\n let inFlight: Map<string, string> | null = null;\n const messageDefinedElsewhere = async (service: string, message: string): Promise<string | undefined> => {\n if (inFlight === null) {\n inFlight = new Map<string, string>();\n for (const feature of await listFeatures(docsDir, {}, context)) {\n if (feature.id === featureId) continue;\n for (const svc of feature.services) {\n const doc = await readAsyncapi(featureSpecPaths(feature.dir, svc).asyncapi, context);\n for (const m of doc.messages) {\n if (m.remove === true) continue;\n const k = `${svc} ${m.name}`;\n if (!inFlight.has(k)) inFlight.set(k, feature.id);\n }\n }\n }\n }\n return inFlight.get(`${service} ${message}`);\n };\n\n return { messageDefinedElsewhere };\n}\n"]}
@@ -2,25 +2,28 @@
2
2
  * The questions the coherence checks ask lazily, each behind a per-service or
3
3
  * per-repo cache.
4
4
  *
5
- * All but one reach outside the delta — into a living spec, the fleet map, or
6
- * another feature in flight — and those are expensive: the landscape is a full
7
- * LikeC4 workspace spin-up, and the cross-feature scan is a walk of every
8
- * active delta. The exception is `undeprecatedByFeature`, which reads the
9
- * FEATURE's own openapi delta: it lives here anyway because it is the second
10
- * half of the one question `deprecatedInLiving` opens — "is this op dying, and
11
- * is this feature the resurrection?" — and splitting the pair across modules
12
- * would leave each half's cache discipline unexplained by the other. A feature
13
- * whose checks never need a lookup must not pay for it, which is why these are
14
- * closures over a cache rather than arguments computed up front.
5
+ * All but one reach outside the delta — into a living spec, a living
6
+ * contract, or another feature in flight — and the cross-feature scan is a
7
+ * walk of every active delta. The exception is `undeprecatedByFeature`, which
8
+ * reads the FEATURE's own openapi delta: it lives here anyway because it is the
9
+ * second half of the one question `deprecatedInLiving` opens — "is this op
10
+ * dying, and is this feature the resurrection?" — and splitting the pair across
11
+ * modules would leave each half's cache discipline unexplained by the other. A
12
+ * feature whose checks never need a lookup must not pay for it, which is why
13
+ * these are closures over a cache rather than arguments computed up front.
14
+ *
15
+ * The one question that used to live here and does not — who still CONSUMES an
16
+ * operation the feature takes out (`edgeConsumers`, `requirementConsumers`,
17
+ * `governorsAfterMerge`) — is the consumer index's now (`core/consumers/`),
18
+ * because `loam diff` and the archive planner ask it too and the three copies
19
+ * had drifted apart in which documents they read.
15
20
  */
16
21
  import { existsSync } from "node:fs";
17
22
  import { readFile } from "node:fs/promises";
18
- import { loadFile } from "../c4/likec4.js";
19
- import { serviceResolver } from "../c4/resolve/service.js";
20
23
  import {} from "../kernel/ids/service.js";
21
- import { featureSpecPaths, landscapePath } from "../repo/paths.js";
24
+ import { featureSpecPaths } from "../repo/paths.js";
22
25
  import { operations } from "../openapi/doc.js";
23
- import { enumeratedServiceIds, locateServicePaths } from "../repo/service-target.js";
26
+ import { locateServicePaths } from "../repo/service-target.js";
24
27
  import { activeOpAdditions } from "./pending.js";
25
28
  import { parseRequirements } from "../document/parse.js";
26
29
  import {} from "../document/spec.js";
@@ -44,66 +47,6 @@ export function coherenceLookups(scope, context) {
44
47
  const governedByLivingSpec = async (service, op) => {
45
48
  return (await livingRequirements(service)).some((r) => r.operations.includes(op));
46
49
  };
47
- // The enumerated fleet, read once and shared by both consumer scans below.
48
- let serviceIds;
49
- const enumeratedServices = async () => (serviceIds ??= await enumeratedServiceIds(docsDir, context));
50
- // --- retiring an operation the fleet still calls ---
51
- //
52
- // A removal marker is checked against the living CONTRACT (does the slot
53
- // exist) and the feature's own requirements (is the retirement governed).
54
- // Neither question asks the one that matters to the other ninety-nine repos:
55
- // is anybody still calling it? The living landscape is the fleet's own answer
56
- // — an edge with `metadata { op }` is a consumer somebody drew — and another
57
- // service's living requirements naming the operation are a second. Both are
58
- // gating: the merge deletes the operation from the contract while the edge
59
- // and the requirement stay, so the very next `validate --all` reports a
60
- // broken contract on a repository whose author was never in this feature.
61
- //
62
- // Lazy on purpose: the landscape is a full LikeC4 workspace spin-up and the
63
- // requirement scan reads every service's living spec, and a feature that
64
- // removes nothing must pay for neither.
65
- //
66
- // `core/diff/victims.ts` carries a second copy of this join (an index over
67
- // an already-read fleet — these closures are scoped to a DeltaScope and
68
- // cannot serve it). The victim strings and scan rules are kept in step by
69
- // hand; changing either module owes the other the same change, and a THIRD
70
- // copy forces the shared extraction (docs/DESIGN.md rule 13).
71
- let livingLandscape;
72
- const edgeConsumers = async (service, op) => {
73
- if (livingLandscape === undefined) {
74
- const path = landscapePath(docsDir);
75
- livingLandscape = existsSync(path)
76
- ? context === undefined
77
- ? await loadFile(path)
78
- : await context.loadLikeC4(path)
79
- : null;
80
- }
81
- // An unreadable landscape proves nothing either way; `landscape.invalid`
82
- // is validate's finding to make, and inventing a removal refusal out of a
83
- // parse error would point the author at the wrong file.
84
- if (livingLandscape === null || livingLandscape.errors.length > 0)
85
- return [];
86
- // The enumerated fleet rides along so a landscape that models CONTAINERS
87
- // stays visible: without it, an edge into `payment.api` resolves to a
88
- // service called "api" that has never existed, the `=== service` join
89
- // below finds nothing, and the one check standing between a removal and
90
- // its last consumer answers "nobody calls it".
91
- const resolve = serviceResolver(livingLandscape.elements, new Set(await enumeratedServices()));
92
- return livingLandscape.relationships
93
- .filter((r) => r.op === op && resolve(r.target) === service)
94
- .map((r) => `edge ${resolve(r.source)} → ${resolve(r.target)}${r.title === undefined ? "" : ` ("${r.title}")`}`);
95
- };
96
- const requirementConsumers = async (service, op) => {
97
- const others = (await enumeratedServices()).filter((id) => id !== service);
98
- const out = [];
99
- for (const other of others) {
100
- for (const r of await livingRequirements(other)) {
101
- if (r.operations.includes(op))
102
- out.push(`${other}'s living requirement '${r.name}'`);
103
- }
104
- }
105
- return out;
106
- };
107
50
  // What OTHER features in flight define, per (service, op). Cross-service work
108
51
  // normally lands as feature A calling an op that in-flight feature B introduces;
109
52
  // that is an ordering dependency, not a broken contract, and the requirements
@@ -147,9 +90,6 @@ export function coherenceLookups(scope, context) {
147
90
  }
148
91
  return set.has(op);
149
92
  };
150
- return {
151
- governedByLivingSpec, edgeConsumers, requirementConsumers, definedElsewhere,
152
- deprecatedInLiving, undeprecatedByFeature,
153
- };
93
+ return { governedByLivingSpec, definedElsewhere, deprecatedInLiving, undeprecatedByFeature };
154
94
  }
155
95
  //# sourceMappingURL=lookups.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lookups.js","sourceRoot":"","sources":["../../../src/core/coherence/lookups.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The questions the coherence checks ask lazily, each behind a per-service or\n * per-repo cache.\n *\n * All but one reach outside the delta — into a living spec, the fleet map, or\n * another feature in flight — and those are expensive: the landscape is a full\n * LikeC4 workspace spin-up, and the cross-feature scan is a walk of every\n * active delta. The exception is `undeprecatedByFeature`, which reads the\n * FEATURE's own openapi delta: it lives here anyway because it is the second\n * half of the one question `deprecatedInLiving` opens — \"is this op dying, and\n * is this feature the resurrection?\" — and splitting the pair across modules\n * would leave each half's cache discipline unexplained by the other. A feature\n * whose checks never need a lookup must not pay for it, which is why these are\n * closures over a cache rather than arguments computed up front.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { loadFile, type LoadedDoc } from \"../c4/likec4.js\";\nimport { serviceResolver } from \"../c4/resolve/service.js\";\nimport { type PathableService } from \"../kernel/ids/service.js\";\nimport { featureSpecPaths, landscapePath } from \"../repo/paths.js\";\nimport { operations } from \"../openapi/doc.js\";\nimport { enumeratedServiceIds, locateServicePaths } from \"../repo/service-target.js\";\nimport { activeOpAdditions } from \"./pending.js\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport { type Requirement } from \"../document/spec.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { type DeltaScope } from \"./declared.js\";\n\n/** What the checks may ask lazily — fleet questions, plus the deprecation pair. */\nexport interface Lookups {\n governedByLivingSpec(service: PathableService, op: string): Promise<boolean>;\n edgeConsumers(service: string, op: string): Promise<string[]>;\n requirementConsumers(service: string, op: string): Promise<string[]>;\n definedElsewhere(service: string, op: string): Promise<string | undefined>;\n deprecatedInLiving(service: PathableService, op: string): Promise<boolean>;\n undeprecatedByFeature(service: PathableService, op: string): Promise<boolean>;\n}\n\nexport function coherenceLookups(scope: DeltaScope, context?: FleetContext): Lookups {\n const { docsDir, featureDir, featureId } = scope;\n const livingReqs = new Map<string, Requirement[]>();\n const livingRequirements = async (service: PathableService): Promise<Requirement[]> => {\n let reqs = livingReqs.get(service);\n if (reqs === undefined) {\n const p = (await locateServicePaths(docsDir, service, context)).spec;\n reqs = existsSync(p)\n ? context === undefined\n ? parseRequirements(await readFile(p, \"utf8\"))\n : await context.readRequirements(p)\n : [];\n livingReqs.set(service, reqs);\n }\n return reqs;\n };\n const governedByLivingSpec = async (service: PathableService, op: string): Promise<boolean> => {\n return (await livingRequirements(service)).some((r) => r.operations.includes(op));\n };\n\n // The enumerated fleet, read once and shared by both consumer scans below.\n let serviceIds: PathableService[] | undefined;\n const enumeratedServices = async (): Promise<PathableService[]> =>\n (serviceIds ??= await enumeratedServiceIds(docsDir, context));\n\n // --- retiring an operation the fleet still calls ---\n //\n // A removal marker is checked against the living CONTRACT (does the slot\n // exist) and the feature's own requirements (is the retirement governed).\n // Neither question asks the one that matters to the other ninety-nine repos:\n // is anybody still calling it? The living landscape is the fleet's own answer\n // — an edge with `metadata { op }` is a consumer somebody drew — and another\n // service's living requirements naming the operation are a second. Both are\n // gating: the merge deletes the operation from the contract while the edge\n // and the requirement stay, so the very next `validate --all` reports a\n // broken contract on a repository whose author was never in this feature.\n //\n // Lazy on purpose: the landscape is a full LikeC4 workspace spin-up and the\n // requirement scan reads every service's living spec, and a feature that\n // removes nothing must pay for neither.\n //\n // `core/diff/victims.ts` carries a second copy of this join (an index over\n // an already-read fleet — these closures are scoped to a DeltaScope and\n // cannot serve it). The victim strings and scan rules are kept in step by\n // hand; changing either module owes the other the same change, and a THIRD\n // copy forces the shared extraction (docs/DESIGN.md rule 13).\n let livingLandscape: LoadedDoc | null | undefined;\n const edgeConsumers = async (service: string, op: string): Promise<string[]> => {\n if (livingLandscape === undefined) {\n const path = landscapePath(docsDir);\n livingLandscape = existsSync(path)\n ? context === undefined\n ? await loadFile(path)\n : await context.loadLikeC4(path)\n : null;\n }\n // An unreadable landscape proves nothing either way; `landscape.invalid`\n // is validate's finding to make, and inventing a removal refusal out of a\n // parse error would point the author at the wrong file.\n if (livingLandscape === null || livingLandscape.errors.length > 0) return [];\n // The enumerated fleet rides along so a landscape that models CONTAINERS\n // stays visible: without it, an edge into `payment.api` resolves to a\n // service called \"api\" that has never existed, the `=== service` join\n // below finds nothing, and the one check standing between a removal and\n // its last consumer answers \"nobody calls it\".\n const resolve = serviceResolver(livingLandscape.elements, new Set(await enumeratedServices()));\n return livingLandscape.relationships\n .filter((r) => r.op === op && resolve(r.target) === service)\n .map((r) => `edge ${resolve(r.source)} → ${resolve(r.target)}${r.title === undefined ? \"\" : ` (\"${r.title}\")`}`);\n };\n const requirementConsumers = async (service: string, op: string): Promise<string[]> => {\n const others = (await enumeratedServices()).filter((id) => id !== service);\n const out: string[] = [];\n for (const other of others) {\n for (const r of await livingRequirements(other)) {\n if (r.operations.includes(op)) out.push(`${other}'s living requirement '${r.name}'`);\n }\n }\n return out;\n };\n\n // What OTHER features in flight define, per (service, op). Cross-service work\n // normally lands as feature A calling an op that in-flight feature B introduces;\n // that is an ordering dependency, not a broken contract, and the requirements\n // axis already grades the same shape as a warn (delta.modified-pending). Lazy\n // like delta.ts's activeAdditions — the common case (op resolves) never pays\n // for the fleet scan.\n let inFlightOps: Map<string, string> | null = null;\n const definedElsewhere = async (service: string, op: string): Promise<string | undefined> => {\n inFlightOps ??= await activeOpAdditions(docsDir, featureId, context);\n return inFlightOps.get(`${service} ${op}`);\n };\n\n // What the LIVING provider contracts mark `deprecated: true`, read lazily\n // per service. Living only, on purpose: the feature's own openapi delta\n // restates the full API, and the question here is whether the fleet as\n // shipped is already retiring the op this feature starts leaning on.\n const livingDeprecated = new Map<string, Set<string>>();\n const deprecatedInLiving = async (service: PathableService, op: string): Promise<boolean> => {\n let set = livingDeprecated.get(service);\n if (!set) {\n const list = await operations((await locateServicePaths(docsDir, service, context)).openapi, context);\n set = new Set(list.filter((o) => o.deprecated).map((o) => o.id));\n livingDeprecated.set(service, set);\n }\n return set.has(op);\n };\n\n // ...unless this feature IS the un-deprecation: an openapi delta that\n // restates the op WITHOUT `deprecated: true` retires the flag on archive\n // (the path-item overwrite is wholesale), so \"prefer the replacement\n // operation\" would point the author away from the exact change they are\n // shipping. A delta that restates the op still deprecated — or has no\n // delta for the service at all — keeps the warning. The one lookup here\n // that reads the delta's own files rather than the world outside it (the\n // header says why it lives beside its living twin regardless).\n const featureUndeprecated = new Map<string, Set<string>>();\n const undeprecatedByFeature = async (service: PathableService, op: string): Promise<boolean> => {\n let set = featureUndeprecated.get(service);\n if (!set) {\n const list = await operations(featureSpecPaths(featureDir, service).openapi, context);\n set = new Set(list.filter((o) => !o.deprecated).map((o) => o.id));\n featureUndeprecated.set(service, set);\n }\n return set.has(op);\n };\n return {\n governedByLivingSpec, edgeConsumers, requirementConsumers, definedElsewhere,\n deprecatedInLiving, undeprecatedByFeature,\n };\n}\n"]}
1
+ {"version":3,"file":"lookups.js","sourceRoot":"","sources":["../../../src/core/coherence/lookups.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The questions the coherence checks ask lazily, each behind a per-service or\n * per-repo cache.\n *\n * All but one reach outside the delta — into a living spec, a living\n * contract, or another feature in flight — and the cross-feature scan is a\n * walk of every active delta. The exception is `undeprecatedByFeature`, which\n * reads the FEATURE's own openapi delta: it lives here anyway because it is the\n * second half of the one question `deprecatedInLiving` opens — \"is this op\n * dying, and is this feature the resurrection?\" — and splitting the pair across\n * modules would leave each half's cache discipline unexplained by the other. A\n * feature whose checks never need a lookup must not pay for it, which is why\n * these are closures over a cache rather than arguments computed up front.\n *\n * The one question that used to live here and does not — who still CONSUMES an\n * operation the feature takes out (`edgeConsumers`, `requirementConsumers`,\n * `governorsAfterMerge`) — is the consumer index's now (`core/consumers/`),\n * because `loam diff` and the archive planner ask it too and the three copies\n * had drifted apart in which documents they read.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type PathableService } from \"../kernel/ids/service.js\";\nimport { featureSpecPaths } from \"../repo/paths.js\";\nimport { operations } from \"../openapi/doc.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport { activeOpAdditions } from \"./pending.js\";\nimport { parseRequirements } from \"../document/parse.js\";\nimport { type Requirement } from \"../document/spec.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { type DeltaScope } from \"./declared.js\";\n\n/** What the checks may ask lazily — fleet questions, plus the deprecation pair. */\nexport interface Lookups {\n governedByLivingSpec(service: PathableService, op: string): Promise<boolean>;\n definedElsewhere(service: string, op: string): Promise<string | undefined>;\n deprecatedInLiving(service: PathableService, op: string): Promise<boolean>;\n undeprecatedByFeature(service: PathableService, op: string): Promise<boolean>;\n}\n\nexport function coherenceLookups(scope: DeltaScope, context?: FleetContext): Lookups {\n const { docsDir, featureDir, featureId } = scope;\n const livingReqs = new Map<string, Requirement[]>();\n const livingRequirements = async (service: PathableService): Promise<Requirement[]> => {\n let reqs = livingReqs.get(service);\n if (reqs === undefined) {\n const p = (await locateServicePaths(docsDir, service, context)).spec;\n reqs = existsSync(p)\n ? context === undefined\n ? parseRequirements(await readFile(p, \"utf8\"))\n : await context.readRequirements(p)\n : [];\n livingReqs.set(service, reqs);\n }\n return reqs;\n };\n const governedByLivingSpec = async (service: PathableService, op: string): Promise<boolean> => {\n return (await livingRequirements(service)).some((r) => r.operations.includes(op));\n };\n\n // What OTHER features in flight define, per (service, op). Cross-service work\n // normally lands as feature A calling an op that in-flight feature B introduces;\n // that is an ordering dependency, not a broken contract, and the requirements\n // axis already grades the same shape as a warn (delta.modified-pending). Lazy\n // like delta.ts's activeAdditions — the common case (op resolves) never pays\n // for the fleet scan.\n let inFlightOps: Map<string, string> | null = null;\n const definedElsewhere = async (service: string, op: string): Promise<string | undefined> => {\n inFlightOps ??= await activeOpAdditions(docsDir, featureId, context);\n return inFlightOps.get(`${service} ${op}`);\n };\n\n // What the LIVING provider contracts mark `deprecated: true`, read lazily\n // per service. Living only, on purpose: the feature's own openapi delta\n // restates the full API, and the question here is whether the fleet as\n // shipped is already retiring the op this feature starts leaning on.\n const livingDeprecated = new Map<string, Set<string>>();\n const deprecatedInLiving = async (service: PathableService, op: string): Promise<boolean> => {\n let set = livingDeprecated.get(service);\n if (!set) {\n const list = await operations((await locateServicePaths(docsDir, service, context)).openapi, context);\n set = new Set(list.filter((o) => o.deprecated).map((o) => o.id));\n livingDeprecated.set(service, set);\n }\n return set.has(op);\n };\n\n // ...unless this feature IS the un-deprecation: an openapi delta that\n // restates the op WITHOUT `deprecated: true` retires the flag on archive\n // (the path-item overwrite is wholesale), so \"prefer the replacement\n // operation\" would point the author away from the exact change they are\n // shipping. A delta that restates the op still deprecated — or has no\n // delta for the service at all — keeps the warning. The one lookup here\n // that reads the delta's own files rather than the world outside it (the\n // header says why it lives beside its living twin regardless).\n const featureUndeprecated = new Map<string, Set<string>>();\n const undeprecatedByFeature = async (service: PathableService, op: string): Promise<boolean> => {\n let set = featureUndeprecated.get(service);\n if (!set) {\n const list = await operations(featureSpecPaths(featureDir, service).openapi, context);\n set = new Set(list.filter((o) => !o.deprecated).map((o) => o.id));\n featureUndeprecated.set(service, set);\n }\n return set.has(op);\n };\n return { governedByLivingSpec, definedElsewhere, deprecatedInLiving, undeprecatedByFeature };\n}\n"]}