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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (648) hide show
  1. package/CHANGELOG.md +1315 -1
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +40 -22
  5. package/QUICKSTART.md +181 -9
  6. package/README.md +256 -57
  7. package/ROADMAP.md +83 -7
  8. package/SCHEMA.md +569 -98
  9. package/WORKFLOW.md +272 -62
  10. package/dist/cli.js +26 -8
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt/adopt.js +5 -10
  13. package/dist/commands/adopt/adopt.js.map +1 -1
  14. package/dist/commands/archive/archive.js +15 -18
  15. package/dist/commands/archive/archive.js.map +1 -1
  16. package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
  17. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  18. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  19. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/openapi.js +25 -42
  21. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
  23. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  24. package/dist/commands/archive/plan/gate.js +3 -7
  25. package/dist/commands/archive/plan/gate.js.map +1 -1
  26. package/dist/commands/archive/plan/landscape.js +4 -4
  27. package/dist/commands/archive/plan/landscape.js.map +1 -1
  28. package/dist/commands/archive/plan/refusal.js +8 -18
  29. package/dist/commands/archive/plan/refusal.js.map +1 -1
  30. package/dist/commands/archive/plan/specs.js +134 -25
  31. package/dist/commands/archive/plan/specs.js.map +1 -1
  32. package/dist/commands/archive/plan/state.js +1 -0
  33. package/dist/commands/archive/plan/state.js.map +1 -1
  34. package/dist/commands/archive/run.js +33 -11
  35. package/dist/commands/archive/run.js.map +1 -1
  36. package/dist/commands/archive/shipped.js +49 -0
  37. package/dist/commands/archive/shipped.js.map +1 -0
  38. package/dist/commands/context/context.js +4 -7
  39. package/dist/commands/context/context.js.map +1 -1
  40. package/dist/commands/context/print.js +2 -2
  41. package/dist/commands/context/print.js.map +1 -1
  42. package/dist/commands/delta/delta.js +44 -12
  43. package/dist/commands/delta/delta.js.map +1 -1
  44. package/dist/commands/delta/print.js +1 -1
  45. package/dist/commands/delta/print.js.map +1 -1
  46. package/dist/commands/dependencies.js +3 -7
  47. package/dist/commands/dependencies.js.map +1 -1
  48. package/dist/commands/diff/diff.js +20 -27
  49. package/dist/commands/diff/diff.js.map +1 -1
  50. package/dist/commands/explore.js +4 -8
  51. package/dist/commands/explore.js.map +1 -1
  52. package/dist/commands/gate/gate.js +45 -8
  53. package/dist/commands/gate/gate.js.map +1 -1
  54. package/dist/commands/gate/print.js +15 -3
  55. package/dist/commands/gate/print.js.map +1 -1
  56. package/dist/commands/gherkin/commit.js +13 -54
  57. package/dist/commands/gherkin/commit.js.map +1 -1
  58. package/dist/commands/gherkin/gherkin.js +36 -20
  59. package/dist/commands/gherkin/gherkin.js.map +1 -1
  60. package/dist/commands/gherkin/reconcile.js +10 -13
  61. package/dist/commands/gherkin/reconcile.js.map +1 -1
  62. package/dist/commands/init/first-hour.js +44 -0
  63. package/dist/commands/init/first-hour.js.map +1 -1
  64. package/dist/commands/init/init.js +18 -22
  65. package/dist/commands/init/init.js.map +1 -1
  66. package/dist/commands/list/campaign/campaign.js +4 -6
  67. package/dist/commands/list/campaign/campaign.js.map +1 -1
  68. package/dist/commands/list/json.js +0 -4
  69. package/dist/commands/list/json.js.map +1 -1
  70. package/dist/commands/list/list.js +38 -23
  71. package/dist/commands/list/list.js.map +1 -1
  72. package/dist/commands/list/print.js +38 -6
  73. package/dist/commands/list/print.js.map +1 -1
  74. package/dist/commands/list/suites/suites.js +140 -0
  75. package/dist/commands/list/suites/suites.js.map +1 -0
  76. package/dist/commands/list/views.js +28 -33
  77. package/dist/commands/list/views.js.map +1 -1
  78. package/dist/commands/mcp/dispatch.js +91 -36
  79. package/dist/commands/mcp/dispatch.js.map +1 -1
  80. package/dist/commands/mcp/mcp.js +56 -15
  81. package/dist/commands/mcp/mcp.js.map +1 -1
  82. package/dist/commands/mcp/serve.js.map +1 -1
  83. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  84. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  85. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  86. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  87. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  88. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  89. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  90. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  91. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  92. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  93. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  94. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  95. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  96. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  97. package/dist/commands/new/capability-template.js +118 -0
  98. package/dist/commands/new/capability-template.js.map +1 -0
  99. package/dist/commands/new/commit.js +8 -22
  100. package/dist/commands/new/commit.js.map +1 -1
  101. package/dist/commands/new/modifies.js +206 -0
  102. package/dist/commands/new/modifies.js.map +1 -0
  103. package/dist/commands/new/new.js +59 -14
  104. package/dist/commands/new/new.js.map +1 -1
  105. package/dist/commands/new/templates.js +48 -124
  106. package/dist/commands/new/templates.js.map +1 -1
  107. package/dist/commands/open.js +3 -7
  108. package/dist/commands/open.js.map +1 -1
  109. package/dist/commands/policy/gate.js +27 -0
  110. package/dist/commands/policy/gate.js.map +1 -1
  111. package/dist/commands/rebase/plan.js +2 -2
  112. package/dist/commands/rebase/plan.js.map +1 -1
  113. package/dist/commands/rebase/rebase.js +60 -35
  114. package/dist/commands/rebase/rebase.js.map +1 -1
  115. package/dist/commands/seed/commit.js +21 -25
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/seed.js +6 -9
  118. package/dist/commands/seed/seed.js.map +1 -1
  119. package/dist/commands/show/show.js +3 -7
  120. package/dist/commands/show/show.js.map +1 -1
  121. package/dist/commands/status/print.js +13 -4
  122. package/dist/commands/status/print.js.map +1 -1
  123. package/dist/commands/status/status.js +3 -7
  124. package/dist/commands/status/status.js.map +1 -1
  125. package/dist/commands/steps/steps.js +3 -7
  126. package/dist/commands/steps/steps.js.map +1 -1
  127. package/dist/commands/subsystem/edit.js +3 -5
  128. package/dist/commands/subsystem/edit.js.map +1 -1
  129. package/dist/commands/subsystem/move.js +5 -8
  130. package/dist/commands/subsystem/move.js.map +1 -1
  131. package/dist/commands/subsystem/query.js +2 -4
  132. package/dist/commands/subsystem/query.js.map +1 -1
  133. package/dist/commands/subsystem/subsystem.js +4 -7
  134. package/dist/commands/subsystem/subsystem.js.map +1 -1
  135. package/dist/commands/subsystem/sync.js +14 -41
  136. package/dist/commands/subsystem/sync.js.map +1 -1
  137. package/dist/commands/subsystem/txn/txn.js +17 -38
  138. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  139. package/dist/commands/subsystem/txn/views.js +32 -5
  140. package/dist/commands/subsystem/txn/views.js.map +1 -1
  141. package/dist/commands/tasks/render.js +116 -0
  142. package/dist/commands/tasks/render.js.map +1 -0
  143. package/dist/commands/tasks/tasks.js +113 -0
  144. package/dist/commands/tasks/tasks.js.map +1 -0
  145. package/dist/commands/unarchive/manifest.js +6 -9
  146. package/dist/commands/unarchive/manifest.js.map +1 -1
  147. package/dist/commands/unarchive/restore.js +17 -28
  148. package/dist/commands/unarchive/restore.js.map +1 -1
  149. package/dist/commands/unarchive/unarchive.js +22 -30
  150. package/dist/commands/unarchive/unarchive.js.map +1 -1
  151. package/dist/commands/validate/arch-coverage.js +6 -15
  152. package/dist/commands/validate/arch-coverage.js.map +1 -1
  153. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  154. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  155. package/dist/commands/validate/checks/requirements.js +18 -8
  156. package/dist/commands/validate/checks/requirements.js.map +1 -1
  157. package/dist/commands/validate/checks/sources.js +6 -2
  158. package/dist/commands/validate/checks/sources.js.map +1 -1
  159. package/dist/commands/validate/feature.js +23 -19
  160. package/dist/commands/validate/feature.js.map +1 -1
  161. package/dist/commands/validate/fleet/landscape.js +23 -23
  162. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  163. package/dist/commands/validate/fleet/map/attest.js +6 -10
  164. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  165. package/dist/commands/validate/fleet/map/bindings.js +1 -3
  166. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  167. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  168. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  169. package/dist/commands/validate/fleet/obligations.js +1 -5
  170. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  171. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  172. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  173. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  174. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  175. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  176. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  177. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  178. package/dist/commands/validate/fleet/views/root-documents.js +8 -28
  179. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  180. package/dist/commands/validate/links/corpus.js +1 -4
  181. package/dist/commands/validate/links/corpus.js.map +1 -1
  182. package/dist/commands/validate/links/glossary.js.map +1 -1
  183. package/dist/commands/validate/report.js +5 -4
  184. package/dist/commands/validate/report.js.map +1 -1
  185. package/dist/commands/validate/scope.js +113 -0
  186. package/dist/commands/validate/scope.js.map +1 -0
  187. package/dist/commands/validate/service/api.js +16 -104
  188. package/dist/commands/validate/service/api.js.map +1 -1
  189. package/dist/commands/validate/service/contracts/generated.js +134 -0
  190. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  191. package/dist/commands/validate/service/events/events.js +1 -1
  192. package/dist/commands/validate/service/events/events.js.map +1 -1
  193. package/dist/commands/validate/service/events/message-producers.js +1 -1
  194. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  195. package/dist/commands/validate/service/evidence-pins.js +56 -10
  196. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  197. package/dist/commands/validate/service/service.js +16 -26
  198. package/dist/commands/validate/service/service.js.map +1 -1
  199. package/dist/commands/validate/service/specs.js +31 -38
  200. package/dist/commands/validate/service/specs.js.map +1 -1
  201. package/dist/commands/validate/service/spine.js +8 -11
  202. package/dist/commands/validate/service/spine.js.map +1 -1
  203. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  204. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  205. package/dist/commands/validate/validate.js +44 -26
  206. package/dist/commands/validate/validate.js.map +1 -1
  207. package/dist/commands/verify/cross/diff.js +2 -6
  208. package/dist/commands/verify/cross/diff.js.map +1 -1
  209. package/dist/commands/verify/evidence/contract.js +1 -1
  210. package/dist/commands/verify/evidence/contract.js.map +1 -1
  211. package/dist/commands/verify/evidence/read.js +12 -3
  212. package/dist/commands/verify/evidence/read.js.map +1 -1
  213. package/dist/commands/verify/frozen.js +45 -23
  214. package/dist/commands/verify/frozen.js.map +1 -1
  215. package/dist/commands/verify/record.js +69 -17
  216. package/dist/commands/verify/record.js.map +1 -1
  217. package/dist/commands/verify/report.js +27 -44
  218. package/dist/commands/verify/report.js.map +1 -1
  219. package/dist/commands/verify/results.js +112 -30
  220. package/dist/commands/verify/results.js.map +1 -1
  221. package/dist/commands/verify/verify.js +54 -51
  222. package/dist/commands/verify/verify.js.map +1 -1
  223. package/dist/commands/vouch/pack/pack.js +2 -2
  224. package/dist/commands/vouch/pack/pack.js.map +1 -1
  225. package/dist/commands/vouch/pack/print.js +1 -6
  226. package/dist/commands/vouch/pack/print.js.map +1 -1
  227. package/dist/commands/vouch/pack/sections.js +1 -2
  228. package/dist/commands/vouch/pack/sections.js.map +1 -1
  229. package/dist/commands/vouch/run.js +26 -81
  230. package/dist/commands/vouch/run.js.map +1 -1
  231. package/dist/commands/vouch/sample/plan.js +20 -34
  232. package/dist/commands/vouch/sample/plan.js.map +1 -1
  233. package/dist/commands/vouch/vet/verify.js +39 -7
  234. package/dist/commands/vouch/vet/verify.js.map +1 -1
  235. package/dist/commands/vouch/vouch.js +10 -17
  236. package/dist/commands/vouch/vouch.js.map +1 -1
  237. package/dist/core/agent/agents-md/command-map.js +56 -101
  238. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  239. package/dist/core/agent/agents-md/map/explain.js +4 -3
  240. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  241. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  242. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  243. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  244. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  245. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  246. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  247. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  248. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  249. package/dist/core/agent/agents-md/map/status.js +9 -3
  250. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  251. package/dist/core/agent/agents-md/map/subsystem.js +4 -3
  252. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  253. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  254. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  255. package/dist/core/agent/agents-md/refusals.js +32 -18
  256. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  257. package/dist/core/agent/agents-md.js +9 -3
  258. package/dist/core/agent/agents-md.js.map +1 -1
  259. package/dist/core/agent/agents-stamp.js +10 -0
  260. package/dist/core/agent/agents-stamp.js.map +1 -1
  261. package/dist/core/agent/scaffold.js +10 -15
  262. package/dist/core/agent/scaffold.js.map +1 -1
  263. package/dist/core/agent/tools/dialects.js +1 -0
  264. package/dist/core/agent/tools/dialects.js.map +1 -1
  265. package/dist/core/agent/tools/registry.js +41 -76
  266. package/dist/core/agent/tools/registry.js.map +1 -1
  267. package/dist/core/agent/workflows/adopt.js +5 -3
  268. package/dist/core/agent/workflows/adopt.js.map +1 -1
  269. package/dist/core/agent/workflows/check.js +20 -16
  270. package/dist/core/agent/workflows/check.js.map +1 -1
  271. package/dist/core/agent/workflows/closing.js +54 -21
  272. package/dist/core/agent/workflows/closing.js.map +1 -1
  273. package/dist/core/agent/workflows/feature.js +79 -11
  274. package/dist/core/agent/workflows/feature.js.map +1 -1
  275. package/dist/core/agent/workflows/implement.js +91 -13
  276. package/dist/core/agent/workflows/implement.js.map +1 -1
  277. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  278. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  279. package/dist/core/agent/workflows/reference/codes.js +8 -6
  280. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  281. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  282. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  283. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  284. package/dist/core/agent/workflows/reference/spine.js +50 -18
  285. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  286. package/dist/core/asyncapi/baseline/plan.js +14 -81
  287. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  288. package/dist/core/asyncapi/digest.js +30 -25
  289. package/dist/core/asyncapi/digest.js.map +1 -1
  290. package/dist/core/asyncapi/merge/components.js +5 -4
  291. package/dist/core/asyncapi/merge/components.js.map +1 -1
  292. package/dist/core/asyncapi/merge/error.js +7 -2
  293. package/dist/core/asyncapi/merge/error.js.map +1 -1
  294. package/dist/core/asyncapi/merge/markers.js +30 -50
  295. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  296. package/dist/core/asyncapi/merge/merge.js +31 -53
  297. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  298. package/dist/core/asyncapi/merge/pin.js +10 -36
  299. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  300. package/dist/core/asyncapi/model.js.map +1 -1
  301. package/dist/core/asyncapi/read.js +36 -12
  302. package/dist/core/asyncapi/read.js.map +1 -1
  303. package/dist/core/brief/brief.js +6 -18
  304. package/dist/core/brief/brief.js.map +1 -1
  305. package/dist/core/brief/landscape.js +8 -12
  306. package/dist/core/brief/landscape.js.map +1 -1
  307. package/dist/core/brief/map/extend.js +8 -5
  308. package/dist/core/brief/map/extend.js.map +1 -1
  309. package/dist/core/brief/map/owed.js +8 -26
  310. package/dist/core/brief/map/owed.js.map +1 -1
  311. package/dist/core/brief/targets.js +15 -5
  312. package/dist/core/brief/targets.js.map +1 -1
  313. package/dist/core/c4/likec4.js +147 -16
  314. package/dist/core/c4/likec4.js.map +1 -1
  315. package/dist/core/c4/locate/unlocated.js +150 -0
  316. package/dist/core/c4/locate/unlocated.js.map +1 -0
  317. package/dist/core/c4/parsed/values.js +54 -1
  318. package/dist/core/c4/parsed/values.js.map +1 -1
  319. package/dist/core/c4/prefetch/fleet.js +1 -9
  320. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  321. package/dist/core/c4/project/documents.js +31 -6
  322. package/dist/core/c4/project/documents.js.map +1 -1
  323. package/dist/core/c4/project/load.js +6 -17
  324. package/dist/core/c4/project/load.js.map +1 -1
  325. package/dist/core/c4/resolve/external.js +51 -0
  326. package/dist/core/c4/resolve/external.js.map +1 -0
  327. package/dist/core/c4/resolve/service.js +25 -1
  328. package/dist/core/c4/resolve/service.js.map +1 -1
  329. package/dist/core/c4/seed/template.js +15 -1
  330. package/dist/core/c4/seed/template.js.map +1 -1
  331. package/dist/core/c4/service-model/shape.js +2 -22
  332. package/dist/core/c4/service-model/shape.js.map +1 -1
  333. package/dist/core/c4/source-mask.js +288 -4
  334. package/dist/core/c4/source-mask.js.map +1 -1
  335. package/dist/core/c4/source-scan.js +203 -147
  336. package/dist/core/c4/source-scan.js.map +1 -1
  337. package/dist/core/c4/splice/authored-source.js +19 -0
  338. package/dist/core/c4/splice/authored-source.js.map +1 -1
  339. package/dist/core/c4/splice/delta-blocks.js +5 -18
  340. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  341. package/dist/core/c4/splice/landscape-merge.js +13 -8
  342. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  343. package/dist/core/c4/splice/model/merge.js +10 -6
  344. package/dist/core/c4/splice/model/merge.js.map +1 -1
  345. package/dist/core/c4/splice/placement.js +108 -18
  346. package/dist/core/c4/splice/placement.js.map +1 -1
  347. package/dist/core/c4/workspace.js +13 -22
  348. package/dist/core/c4/workspace.js.map +1 -1
  349. package/dist/core/capabilities/capabilities.js +6 -6
  350. package/dist/core/capabilities/capabilities.js.map +1 -1
  351. package/dist/core/capabilities/delta/doc.js +7 -10
  352. package/dist/core/capabilities/delta/doc.js.map +1 -1
  353. package/dist/core/capabilities/findings.js +15 -20
  354. package/dist/core/capabilities/findings.js.map +1 -1
  355. package/dist/core/capabilities/realizes/findings.js +12 -19
  356. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  357. package/dist/core/capabilities/realizes/join.js +0 -20
  358. package/dist/core/capabilities/realizes/join.js.map +1 -1
  359. package/dist/core/capabilities/rollup.js +6 -0
  360. package/dist/core/capabilities/rollup.js.map +1 -1
  361. package/dist/core/capabilities/tree.js +6 -28
  362. package/dist/core/capabilities/tree.js.map +1 -1
  363. package/dist/core/capabilities/usecase-join.js +3 -13
  364. package/dist/core/capabilities/usecase-join.js.map +1 -1
  365. package/dist/core/coherence/authoring/scaffold.js +11 -3
  366. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  367. package/dist/core/coherence/authoring/sentinels.js +2 -1
  368. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  369. package/dist/core/coherence/coherence.js +30 -16
  370. package/dist/core/coherence/coherence.js.map +1 -1
  371. package/dist/core/coherence/consumed/operations.js +70 -0
  372. package/dist/core/coherence/consumed/operations.js.map +1 -0
  373. package/dist/core/coherence/consumed/refusal.js +38 -0
  374. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  375. package/dist/core/coherence/declared.js +60 -34
  376. package/dist/core/coherence/declared.js.map +1 -1
  377. package/dist/core/coherence/events/declared.js +44 -59
  378. package/dist/core/coherence/events/declared.js.map +1 -1
  379. package/dist/core/coherence/events/events.js +119 -8
  380. package/dist/core/coherence/events/events.js.map +1 -1
  381. package/dist/core/coherence/events/grades.js +31 -8
  382. package/dist/core/coherence/events/grades.js.map +1 -1
  383. package/dist/core/coherence/events/lookups.js +14 -82
  384. package/dist/core/coherence/events/lookups.js.map +1 -1
  385. package/dist/core/coherence/lookups.js +18 -78
  386. package/dist/core/coherence/lookups.js.map +1 -1
  387. package/dist/core/coherence/retired/archived.js +285 -0
  388. package/dist/core/coherence/retired/archived.js.map +1 -0
  389. package/dist/core/coherence/retired/event-pair.js +62 -0
  390. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  391. package/dist/core/coherence/retired/merged.js +79 -0
  392. package/dist/core/coherence/retired/merged.js.map +1 -0
  393. package/dist/core/coherence/retired/retirement.js +49 -0
  394. package/dist/core/coherence/retired/retirement.js.map +1 -0
  395. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  396. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  397. package/dist/core/coherence/retired/revival/revived.js +297 -0
  398. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  399. package/dist/core/consumers/after.js +113 -0
  400. package/dist/core/consumers/after.js.map +1 -0
  401. package/dist/core/consumers/consumers.js +296 -0
  402. package/dist/core/consumers/consumers.js.map +1 -0
  403. package/dist/core/consumers/current.js +211 -0
  404. package/dist/core/consumers/current.js.map +1 -0
  405. package/dist/core/consumers/scan.js +46 -0
  406. package/dist/core/consumers/scan.js.map +1 -0
  407. package/dist/core/consumers/state.js +27 -0
  408. package/dist/core/consumers/state.js.map +1 -0
  409. package/dist/core/delta/delta.js +1 -1
  410. package/dist/core/delta/delta.js.map +1 -1
  411. package/dist/core/deployment/delta.js +3 -6
  412. package/dist/core/deployment/delta.js.map +1 -1
  413. package/dist/core/diff/base-git.js +94 -12
  414. package/dist/core/diff/base-git.js.map +1 -1
  415. package/dist/core/diff/base-state.js +28 -16
  416. package/dist/core/diff/base-state.js.map +1 -1
  417. package/dist/core/diff/joins.js +136 -0
  418. package/dist/core/diff/joins.js.map +1 -0
  419. package/dist/core/diff/scope/changed-paths.js +22 -6
  420. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  421. package/dist/core/diff/scope/edges.js +82 -0
  422. package/dist/core/diff/scope/edges.js.map +1 -0
  423. package/dist/core/diff/semantic.js +100 -31
  424. package/dist/core/diff/semantic.js.map +1 -1
  425. package/dist/core/doctor/agents.js +34 -18
  426. package/dist/core/doctor/agents.js.map +1 -1
  427. package/dist/core/doctor/report.js.map +1 -1
  428. package/dist/core/document/apply.js +10 -2
  429. package/dist/core/document/apply.js.map +1 -1
  430. package/dist/core/document/frontmatter.js +55 -0
  431. package/dist/core/document/frontmatter.js.map +1 -1
  432. package/dist/core/document/parse.js +29 -6
  433. package/dist/core/document/parse.js.map +1 -1
  434. package/dist/core/document/scenarios.js +17 -0
  435. package/dist/core/document/scenarios.js.map +1 -1
  436. package/dist/core/document/spec.js +4 -11
  437. package/dist/core/document/spec.js.map +1 -1
  438. package/dist/core/envelope/config.js +10 -0
  439. package/dist/core/envelope/config.js.map +1 -1
  440. package/dist/core/envelope/json.js +34 -0
  441. package/dist/core/envelope/json.js.map +1 -1
  442. package/dist/core/explain/families.js +17 -12
  443. package/dist/core/explain/families.js.map +1 -1
  444. package/dist/core/explain/refusals.js +3 -3
  445. package/dist/core/explain/refusals.js.map +1 -1
  446. package/dist/core/explain/terms.js +18 -9
  447. package/dist/core/explain/terms.js.map +1 -1
  448. package/dist/core/explore/explore.js +2 -7
  449. package/dist/core/explore/explore.js.map +1 -1
  450. package/dist/core/fleet-context.js +34 -95
  451. package/dist/core/fleet-context.js.map +1 -1
  452. package/dist/core/gate/checks.js +5 -2
  453. package/dist/core/gate/checks.js.map +1 -1
  454. package/dist/core/gate/partners.js +4 -2
  455. package/dist/core/gate/partners.js.map +1 -1
  456. package/dist/core/gate/report.js.map +1 -1
  457. package/dist/core/gate/verification.js +75 -12
  458. package/dist/core/gate/verification.js.map +1 -1
  459. package/dist/core/gherkin/stale.js +13 -37
  460. package/dist/core/gherkin/stale.js.map +1 -1
  461. package/dist/core/glossary/tree.js +6 -19
  462. package/dist/core/glossary/tree.js.map +1 -1
  463. package/dist/core/kernel/concurrency.js +19 -0
  464. package/dist/core/kernel/concurrency.js.map +1 -1
  465. package/dist/core/kernel/records.js +11 -0
  466. package/dist/core/kernel/records.js.map +1 -1
  467. package/dist/core/links/backlinks.js +1 -3
  468. package/dist/core/links/backlinks.js.map +1 -1
  469. package/dist/core/links/corpus.js +5 -36
  470. package/dist/core/links/corpus.js.map +1 -1
  471. package/dist/core/mcp/argv.js +101 -0
  472. package/dist/core/mcp/argv.js.map +1 -1
  473. package/dist/core/mcp/author-tools.js +2 -1
  474. package/dist/core/mcp/author-tools.js.map +1 -1
  475. package/dist/core/mcp/protocol.js +2 -2
  476. package/dist/core/mcp/protocol.js.map +1 -1
  477. package/dist/core/mcp/tools.js +43 -33
  478. package/dist/core/mcp/tools.js.map +1 -1
  479. package/dist/core/openapi/baseline/gate.js +53 -12
  480. package/dist/core/openapi/baseline/gate.js.map +1 -1
  481. package/dist/core/openapi/baseline/plan.js +22 -8
  482. package/dist/core/openapi/baseline/plan.js.map +1 -1
  483. package/dist/core/openapi/baseline/record.js +0 -1
  484. package/dist/core/openapi/baseline/record.js.map +1 -1
  485. package/dist/core/openapi/contract-text.js +33 -0
  486. package/dist/core/openapi/contract-text.js.map +1 -0
  487. package/dist/core/openapi/contract-tree.js +56 -0
  488. package/dist/core/openapi/contract-tree.js.map +1 -0
  489. package/dist/core/openapi/depth.js +38 -24
  490. package/dist/core/openapi/depth.js.map +1 -1
  491. package/dist/core/openapi/digest.js +52 -2
  492. package/dist/core/openapi/digest.js.map +1 -1
  493. package/dist/core/openapi/doc.js +12 -4
  494. package/dist/core/openapi/doc.js.map +1 -1
  495. package/dist/core/openapi/merge/components.js +25 -60
  496. package/dist/core/openapi/merge/components.js.map +1 -1
  497. package/dist/core/openapi/merge/error.js +3 -13
  498. package/dist/core/openapi/merge/error.js.map +1 -1
  499. package/dist/core/openapi/merge/markers.js +26 -30
  500. package/dist/core/openapi/merge/markers.js.map +1 -1
  501. package/dist/core/openapi/merge/merge.js +26 -52
  502. package/dist/core/openapi/merge/merge.js.map +1 -1
  503. package/dist/core/openapi/merge/pin.js +15 -35
  504. package/dist/core/openapi/merge/pin.js.map +1 -1
  505. package/dist/core/openspec/decide/verdict.js.map +1 -1
  506. package/dist/core/openspec/inventory.js +4 -5
  507. package/dist/core/openspec/inventory.js.map +1 -1
  508. package/dist/core/openspec/model/mapping.js +30 -28
  509. package/dist/core/openspec/model/mapping.js.map +1 -1
  510. package/dist/core/openspec/model/model.js +12 -7
  511. package/dist/core/openspec/model/model.js.map +1 -1
  512. package/dist/core/openspec/scan/changes.js +10 -4
  513. package/dist/core/openspec/scan/changes.js.map +1 -1
  514. package/dist/core/openspec/scan/config.js +7 -16
  515. package/dist/core/openspec/scan/config.js.map +1 -1
  516. package/dist/core/openspec/scan/shape.js +63 -19
  517. package/dist/core/openspec/scan/shape.js.map +1 -1
  518. package/dist/core/openspec/scan/walk.js +21 -38
  519. package/dist/core/openspec/scan/walk.js.map +1 -1
  520. package/dist/core/pack/living.js +2 -16
  521. package/dist/core/pack/living.js.map +1 -1
  522. package/dist/core/provenance/findings.js +48 -12
  523. package/dist/core/provenance/findings.js.map +1 -1
  524. package/dist/core/provenance/git.js +33 -57
  525. package/dist/core/provenance/git.js.map +1 -1
  526. package/dist/core/provenance/gitq/moves.js +71 -114
  527. package/dist/core/provenance/gitq/moves.js.map +1 -1
  528. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  529. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  530. package/dist/core/provenance/sources.js +23 -6
  531. package/dist/core/provenance/sources.js.map +1 -1
  532. package/dist/core/provenance/stamp.js +5 -17
  533. package/dist/core/provenance/stamp.js.map +1 -1
  534. package/dist/core/repo/entries.js.map +1 -1
  535. package/dist/core/repo/repo.js +24 -14
  536. package/dist/core/repo/repo.js.map +1 -1
  537. package/dist/core/repo/tree/find.js +12 -3
  538. package/dist/core/repo/tree/find.js.map +1 -1
  539. package/dist/core/repo/tree/fs.js +27 -1
  540. package/dist/core/repo/tree/fs.js.map +1 -1
  541. package/dist/core/repo/tree/walk.js +8 -30
  542. package/dist/core/repo/tree/walk.js.map +1 -1
  543. package/dist/core/results.js +47 -12
  544. package/dist/core/results.js.map +1 -1
  545. package/dist/core/staging/commit.js +10 -12
  546. package/dist/core/staging/commit.js.map +1 -1
  547. package/dist/core/staging/lock.js +74 -12
  548. package/dist/core/staging/lock.js.map +1 -1
  549. package/dist/core/staging/recovery/intent.js +2 -5
  550. package/dist/core/staging/recovery/intent.js.map +1 -1
  551. package/dist/core/staging/recovery/recover.js +60 -1
  552. package/dist/core/staging/recovery/recover.js.map +1 -1
  553. package/dist/core/staging/snapshot/manifest.js +152 -0
  554. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  555. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  556. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  557. package/dist/core/status/actions/execution.js +11 -0
  558. package/dist/core/status/actions/execution.js.map +1 -1
  559. package/dist/core/status/feature/artifacts.js +2 -2
  560. package/dist/core/status/feature/artifacts.js.map +1 -1
  561. package/dist/core/status/feature/feature.js +5 -11
  562. package/dist/core/status/feature/feature.js.map +1 -1
  563. package/dist/core/status/fleet/fleet.js +6 -2
  564. package/dist/core/status/fleet/fleet.js.map +1 -1
  565. package/dist/core/status/fleet/next.js +26 -3
  566. package/dist/core/status/fleet/next.js.map +1 -1
  567. package/dist/core/status/report.js.map +1 -1
  568. package/dist/core/status/verification.js +30 -15
  569. package/dist/core/status/verification.js.map +1 -1
  570. package/dist/core/tasks/packet.js +174 -0
  571. package/dist/core/tasks/packet.js.map +1 -0
  572. package/dist/core/tasks/record.js +43 -0
  573. package/dist/core/tasks/record.js.map +1 -0
  574. package/dist/core/tasks/sources.js +181 -0
  575. package/dist/core/tasks/sources.js.map +1 -0
  576. package/dist/core/tasks/stamps.js +76 -0
  577. package/dist/core/tasks/stamps.js.map +1 -0
  578. package/dist/core/tasks/unit.js +0 -0
  579. package/dist/core/tasks/unit.js.map +1 -0
  580. package/dist/core/usecases/capability.js +11 -14
  581. package/dist/core/usecases/capability.js.map +1 -1
  582. package/dist/core/usecases/delta/flows.js +5 -7
  583. package/dist/core/usecases/delta/flows.js.map +1 -1
  584. package/dist/core/usecases/fleet.js +178 -31
  585. package/dist/core/usecases/fleet.js.map +1 -1
  586. package/dist/core/usecases/operations.js +5 -4
  587. package/dist/core/usecases/operations.js.map +1 -1
  588. package/dist/core/usecases/service/flows.js +9 -6
  589. package/dist/core/usecases/service/flows.js.map +1 -1
  590. package/dist/core/usecases/suites/participants.js +83 -0
  591. package/dist/core/usecases/suites/participants.js.map +1 -0
  592. package/dist/core/usecases/suites/suites.js +45 -0
  593. package/dist/core/usecases/suites/suites.js.map +1 -0
  594. package/dist/core/verify/answers.js +12 -5
  595. package/dist/core/verify/answers.js.map +1 -1
  596. package/dist/core/verify/build.js +102 -82
  597. package/dist/core/verify/build.js.map +1 -1
  598. package/dist/core/verify/checklist.js +3 -0
  599. package/dist/core/verify/checklist.js.map +1 -1
  600. package/dist/core/verify/location.js +67 -0
  601. package/dist/core/verify/location.js.map +1 -0
  602. package/dist/core/verify/pins/lint.js +3 -2
  603. package/dist/core/verify/pins/lint.js.map +1 -1
  604. package/dist/core/verify/pins/pin.js +1 -1
  605. package/dist/core/verify/pins/pin.js.map +1 -1
  606. package/dist/core/verify/read/file.js +224 -0
  607. package/dist/core/verify/read/file.js.map +1 -0
  608. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  609. package/dist/core/verify/read/shape.js.map +1 -0
  610. package/dist/core/verify/read/union.js +105 -0
  611. package/dist/core/verify/read/union.js.map +1 -0
  612. package/dist/core/verify/record.js +35 -12
  613. package/dist/core/verify/record.js.map +1 -1
  614. package/dist/core/verify/store/commit.js +40 -30
  615. package/dist/core/verify/store/commit.js.map +1 -1
  616. package/dist/core/verify/store/render.js +8 -4
  617. package/dist/core/verify/store/render.js.map +1 -1
  618. package/dist/core/vocabulary/issue.js +1 -0
  619. package/dist/core/vocabulary/issue.js.map +1 -1
  620. package/dist/core/vocabulary/maturity.js +12 -2
  621. package/dist/core/vocabulary/maturity.js.map +1 -1
  622. package/examples/README.md +21 -6
  623. package/examples/docs/architecture/landscape.likec4 +8 -0
  624. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  625. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  626. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  627. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  628. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  629. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  630. package/package.json +1 -1
  631. package/dist/commands/validate/checks/vocabulary.js +0 -16
  632. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  633. package/dist/core/agent/agents-md/artifacts.js +0 -221
  634. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  635. package/dist/core/agent/agents-md/cycle.js +0 -180
  636. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  637. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  638. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  639. package/dist/core/agent/agents-md/spine.js +0 -40
  640. package/dist/core/agent/agents-md/spine.js.map +0 -1
  641. package/dist/core/coherence/events/merged.js +0 -40
  642. package/dist/core/coherence/events/merged.js.map +0 -1
  643. package/dist/core/diff/current-state.js +0 -96
  644. package/dist/core/diff/current-state.js.map +0 -1
  645. package/dist/core/diff/victims.js +0 -256
  646. package/dist/core/diff/victims.js.map +0 -1
  647. package/dist/core/staging/snapshot.js.map +0 -1
  648. package/dist/core/verify/file.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"doc.js","sourceRoot":"","sources":["../../../src/core/openapi/doc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A service's OpenAPI contract as loam reads it: which keys hold operations,\n * what an operation is once loam's own markers are accounted for, and what the\n * fleet will provide after a feature is applied.\n *\n * Split from `./digest.ts` because the two answer different questions about the\n * same file. This one is about STRUCTURE — a path item's operation-bearing\n * keys, an `operationId`, a removal marker — and every reader of a contract\n * comes through here. The digest is about IDENTITY, and only the pin and the\n * merge need it.\n */\nimport { isUtf8 } from \"node:buffer\";\nimport { danglingRefs, failureCodesOf, responseUndescribed } from \"./depth.js\";\nimport { operationBaselineOf, operationDigest } from \"./digest.js\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { parse } from \"yaml\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { featureSpecPaths } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * The path-item keys that hold operations. A path item also carries `summary`,\n * `parameters`, `servers` and vendor `x-*` extensions — an object-valued one with\n * an `operationId` inside (x-legacy and friends) is not an operation, and a\n * phantom id from it would make a broken contract look \"available\" to the\n * op-exists checks. Shared with archive's merge, which asks the same question\n * of the same keys.\n */\nexport const HTTP_METHODS = new Set([\"get\", \"put\", \"post\", \"delete\", \"options\", \"head\", \"patch\", \"trace\"]);\n\n/** One operation as the reader sees it, including loam's feature-only removal marker. */\nexport interface Operation {\n id: string;\n /**\n * The standard per-operation OpenAPI `deprecated` flag — lifecycle\n * visibility, the documented first step of retiring an op.\n */\n deprecated: boolean;\n /**\n * `x-loam-remove: true` asks a FEATURE delta to remove this exact operation\n * on archive. The marker is never a callable operation and is illegal in a\n * living contract.\n */\n remove: boolean;\n /** Exact slot the marker/upsert addresses; removal is deliberately path+method-safe. */\n path: string;\n method: string;\n /**\n * `operationDigest` of this operation as it stands in ITS OWN document —\n * computed here so every caller compares content the one way the merge does.\n */\n digest: string;\n /**\n * The living version this operation was written against, from\n * `x-loam-based-on`. Feature deltas only: a living contract never carries one\n * (the merge strips it), and an adopted contract never had one.\n */\n basedOn?: string;\n /**\n * Present when no response declares content with a schema while at least one\n * should carry a body (`depth.ts`). Set only when true, so the many callers\n * that compare operations structurally never see the field on a described\n * contract.\n */\n undescribed?: true;\n /**\n * Declared non-2xx response codes, in document order — the refusals this\n * operation promises. `default` and any `4XX`/`5XX` wildcard are excluded:\n * neither names a case a scenario could be written for.\n *\n * The contract is the only place in the corpus that enumerates what an\n * operation REFUSES, and until this was read nothing joined those refusals to\n * anything. `api.ungoverned` grades whole operations against requirements,\n * so an operation documented by its happy path alone — one requirement, one\n * scenario, twelve declared failure codes — passed every check loam had.\n */\n failureCodes: string[];\n}\n\n/** A `x-loam-remove: true` marker with no usable operationId — a slot named but no operation named. */\nexport interface AnonymousRemoval {\n path: string;\n method: string;\n}\n\n/** The parse of one OpenAPI document: its operations, and whether the file could be read at all. */\nexport interface OpenapiDoc {\n ops: Operation[];\n /**\n * operationIds this document defines in more than one (path, method) slot.\n * A LIVING contract with two slots claiming one id is ambiguous to every\n * check that joins on the id — a requirement's `Operations:` line, an edge's\n * `metadata { op }`, a removal marker — and the join silently picks one.\n */\n duplicateIds: string[];\n /**\n * Removal markers the operation reader cannot name. They are invisible to\n * every id-keyed check, so a delta carrying one used to archive cleanly and\n * write `x-loam-remove: true` straight into a living contract.\n */\n anonymousRemovals: AnonymousRemoval[];\n /**\n * Paths carrying `x-loam-remove: true` at PATH level, beside the methods\n * rather than inside one.\n *\n * Not an operation, so nothing keyed by (path, method) could see it — and this\n * reader is method-keyed like the merge was. A marker written there addressed\n * no operation, so archive copied it into the living contract verbatim, where\n * it was invisible to `openapi.remove-marker-living` for the same reason and\n * kept the empty-path cleanup from ever firing. Reported as its own list\n * because the answer differs by document: in a FEATURE delta it is an\n * authoring mistake, in a LIVING contract it is a marker that must never have\n * been published.\n */\n pathLevelRemovals: string[];\n /**\n * True when the file EXISTS but cannot be read as an OpenAPI document —\n * broken YAML, or a document that is not a mapping. A missing file is not\n * unreadable (absence is `service.no-openapi`'s question), and an empty one\n * parses to null and honestly defines nothing.\n */\n unreadable: boolean;\n /** The parser's own message, when there is one to quote back. */\n error?: string;\n /** Internal `#/` refs that resolve to nothing in this document (`depth.ts`). */\n danglingRefs: string[];\n}\n\n/**\n * Extract operations from an OpenAPI document by walking the parsed YAML\n * structure (paths.<path>.<method>, method filtered to the HTTP set).\n * Structure-aware on purpose: a regex scan both drops legal ids (kebab-case,\n * dotted) and picks up phantom ids from description text. Each operation\n * carries its id and its `deprecated: true` flag (exactly `true` counts —\n * strings and truthy noise do not). `ops` is the defined set, keyed by the\n * (path, method) SLOT — not by operationId.\n *\n * Keying by id used to drop the second slot claiming a name, which made the\n * single most common contract edit invisible: relocating an endpoint is a\n * removal marker at the old slot plus an upsert at the new one, the SAME\n * operationId twice. Whichever came second in document order vanished, so\n * either the marker addressed a slot loam could not see (`remove-target-missing`\n * on a perfectly good delta) or the new definition did. Two slots claiming one\n * id in a LIVING contract is a real ambiguity rather than a merge failure, so\n * it rides out as `duplicateIds` for the caller to grade.\n *\n * An unreadable document yields [] ops PLUS the `unreadable` flag. Parsers\n * never diagnose — the policy of every reader here — so the failure travels as\n * a flag for validate to grade (`openapi.invalid`). Swallowing it into a bare\n * [] made a broken contract indistinguishable from an EMPTY one, and the spine\n * check then reported every inbound edge broken (`spine.op-undefined`) — a\n * false diagnosis pointing at the landscape when the truth was this file.\n */\nexport async function readOpenapi(openapiPath: string, context?: FleetContext): Promise<OpenapiDoc> {\n if (context !== undefined) return context.readOpenapi(openapiPath);\n if (!existsSync(openapiPath)) return empty();\n const bytes = await readFile(openapiPath);\n // A contract whose bytes are not UTF-8 is as unreadable as broken YAML, and\n // for a worse reason: `toString(\"utf8\")` would substitute U+FFFD and hand the\n // parser a document nobody wrote. It travels as `unreadable` so validate\n // grades it `openapi.invalid` — the file is the error, and every check that\n // reads the contract stays suspended.\n if (!isUtf8(bytes)) return { ...empty(), unreadable: true, error: \"file is not valid UTF-8\" };\n return openapiFromText(bytes.toString(\"utf8\"));\n}\n\n/**\n * The same read over text already in hand — the parse half of `readOpenapi`,\n * split out so `loam diff` can read a contract fetched from a base git ref\n * (core/diff/base-state.ts) without writing it to disk. One spelling of the\n * walk for both entries, so a base-ref contract can never parse differently\n * from the living file the same bytes would produce; the byte concerns\n * (existence, UTF-8) stay with the reader that holds the bytes.\n */\nexport function openapiFromText(text: string): OpenapiDoc {\n let doc: unknown;\n try {\n doc = parse(text);\n } catch (e) {\n return { ...empty(), unreadable: true, error: e instanceof Error ? e.message : String(e) };\n }\n // A scalar or sequence document is as unreadable as broken YAML: there is no\n // mapping to look `paths` up in, so nothing can be concluded from it. null\n // (an empty file) stays readable — it defines nothing, and says so honestly.\n if (doc !== null && (typeof doc !== \"object\" || Array.isArray(doc))) {\n return { ...empty(), unreadable: true, error: \"document is not a YAML mapping\" };\n }\n // Refs are probed over the WHOLE document: a schema in `components` that\n // `$ref`s a neighbour nobody wrote is exactly as broken as one under a path.\n const refs = danglingRefs(doc);\n const paths = (doc as { paths?: unknown } | null)?.paths;\n if (!paths || typeof paths !== \"object\") return { ...empty(), danglingRefs: refs };\n const ops: Operation[] = [];\n const anonymousRemovals: AnonymousRemoval[] = [];\n const pathLevelRemovals: string[] = [];\n const seen = new Map<string, number>();\n for (const [path, item] of Object.entries(paths as Record<string, unknown>)) {\n if (!item || typeof item !== \"object\") continue;\n if ((item as Record<string, unknown>)[\"x-loam-remove\"] === true) pathLevelRemovals.push(path);\n for (const [method, op] of Object.entries(item as Record<string, unknown>)) {\n if (!HTTP_METHODS.has(method)) continue;\n if (!op || typeof op !== \"object\") continue;\n const id = (op as Record<string, unknown>)[\"operationId\"];\n const remove = (op as Record<string, unknown>)[\"x-loam-remove\"] === true;\n if (typeof id !== \"string\" || id.length === 0) {\n if (remove) anonymousRemovals.push({ path, method });\n continue;\n }\n seen.set(id, (seen.get(id) ?? 0) + 1);\n const basedOn = operationBaselineOf(op);\n ops.push({\n id,\n deprecated: (op as Record<string, unknown>)[\"deprecated\"] === true,\n remove,\n path,\n method,\n digest: operationDigest(op),\n ...(basedOn === undefined ? {} : { basedOn }),\n ...(responseUndescribed(op) ? { undescribed: true as const } : {}),\n failureCodes: failureCodesOf(op),\n });\n }\n }\n const duplicateIds = [...seen].filter(([, n]) => n > 1).map(([id]) => id);\n return { ops, duplicateIds, anonymousRemovals, pathLevelRemovals, unreadable: false, danglingRefs: refs };\n}\n\nfunction empty(): OpenapiDoc {\n return {\n ops: [],\n duplicateIds: [],\n anonymousRemovals: [],\n pathLevelRemovals: [],\n unreadable: false,\n danglingRefs: [],\n };\n}\n\n/** The operations alone — for every caller whose own finding already covers the unreadable case. */\nexport async function operations(openapiPath: string, context?: FleetContext): Promise<Operation[]> {\n return (await readOpenapi(openapiPath, context)).ops;\n}\n\n/** The operationIds alone — `operations` for every caller that asks only \"does it exist\". */\nexport async function operationIds(openapiPath: string, context?: FleetContext): Promise<string[]> {\n return (await operations(openapiPath, context)).map((o) => o.id);\n}\n\n/**\n * The operationIds a service will provide after a feature is applied: living\n * operations, plus feature upserts, minus explicit feature removals.\n *\n * Removals are applied BEFORE upserts, which is what makes the answer\n * independent of the order the feature's document happens to spell them in.\n * Interleaving them one operation at a time meant a relocation — the same id\n * removed from `/old` and defined on `/new` — answered \"defined\" or \"gone\"\n * depending on which path sorted first in the YAML, and the requirement\n * governing it was reported undefined (`spec-api.op-undefined`) on exactly one\n * of the two spellings of the same change.\n *\n * A context is threaded into the READS and never asked for the answer. It used\n * to own a second copy of this function, and that copy interleaved removals\n * with upserts — the exact bug the paragraph above describes as fixed. So the\n * commands that pass a context (`validate`, `status`) and the one that does not\n * (`archive`) disagreed about whether a relocated operation still existed, and\n * `spec-api.op-undefined` — an error that gates archive — fired or not\n * depending on which path key the author happened to type first. Memoising is\n * the context's job; the ordering rule is this function's, and there is one of\n * it.\n */\nexport async function serviceOperationIds(\n docsDir: DocsDir,\n service: PathableService,\n featureDir?: FeatureDir,\n context?: FleetContext,\n): Promise<string[]> {\n const ids = new Set(\n (await operations((await locateServicePaths(docsDir, service, context)).openapi, context)).filter((op) => !op.remove).map((op) => op.id),\n );\n // Absence is `undefined`, the shape the optional parameter declares. The\n // truthiness test also read \"\" as \"no feature\", which is a caller mistake\n // worth surfacing rather than answering as though the feature carried no\n // contract delta.\n if (featureDir !== undefined) {\n const featOps = await operations(featureSpecPaths(featureDir, service).openapi, context);\n for (const op of featOps) if (op.remove) ids.delete(op.id);\n for (const op of featOps) if (!op.remove) ids.add(op.id);\n }\n return [...ids];\n}\n\n"]}
1
+ {"version":3,"file":"doc.js","sourceRoot":"","sources":["../../../src/core/openapi/doc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A service's OpenAPI contract as loam reads it: which keys hold operations,\n * what an operation is once loam's own markers are accounted for, and what the\n * fleet will provide after a feature is applied.\n *\n * Split from `./digest.ts` because the two answer different questions about the\n * same file. This one is about STRUCTURE — a path item's operation-bearing\n * keys, an `operationId`, a removal marker — and every reader of a contract\n * comes through here. The digest is about IDENTITY, and only the pin and the\n * merge need it.\n */\nimport { isUtf8 } from \"node:buffer\";\nimport { danglingRefs, failureCodesOf, responseUndescribed } from \"./depth.js\";\nimport { closureDigester, operationBaselineOf, operationDigest } from \"./digest.js\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { parse } from \"yaml\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { featureSpecPaths } from \"../repo/paths.js\";\nimport { locateServicePaths } from \"../repo/service-target.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\n\n/**\n * The path-item keys that hold operations. A path item also carries `summary`,\n * `parameters`, `servers` and vendor `x-*` extensions — an object-valued one with\n * an `operationId` inside (x-legacy and friends) is not an operation, and a\n * phantom id from it would make a broken contract look \"available\" to the\n * op-exists checks. Shared with archive's merge, which asks the same question\n * of the same keys.\n */\nexport const HTTP_METHODS = new Set([\"get\", \"put\", \"post\", \"delete\", \"options\", \"head\", \"patch\", \"trace\"]);\n\n/** One operation as the reader sees it, including loam's feature-only removal marker. */\nexport interface Operation {\n id: string;\n /**\n * The standard per-operation OpenAPI `deprecated` flag — lifecycle\n * visibility, the documented first step of retiring an op.\n */\n deprecated: boolean;\n /**\n * `x-loam-remove: true` asks a FEATURE delta to remove this exact operation\n * on archive. The marker is never a callable operation and is illegal in a\n * living contract.\n */\n remove: boolean;\n /** Exact slot the marker/upsert addresses; removal is deliberately path+method-safe. */\n path: string;\n method: string;\n /**\n * `operationDigest` of this operation as it stands in ITS OWN document —\n * computed here so every caller compares content the one way the merge does.\n */\n digest: string;\n /**\n * `closureDigester`'s answer for this operation: its own node PLUS every\n * internal `$ref` it reaches, transitively. What `loam diff` compares to say\n * an operation changed — `digest` above stops at the node, so a component-only\n * edit (a renamed field of a shared schema) never moves it. Never a pin: pins\n * stay on `digest`, because a component carries its own baseline. The\n * path item's own keys (shared `parameters`, `servers`) are outside it, as\n * they are outside `digest`: a change there moves no operation's identity.\n */\n closureDigest: string;\n /**\n * The living version this operation was written against, from\n * `x-loam-based-on`. Feature deltas only: a living contract never carries one\n * (the merge strips it), and an adopted contract never had one.\n */\n basedOn?: string;\n /**\n * Present when no response declares content with a schema while at least one\n * should carry a body (`depth.ts`). Set only when true, so the many callers\n * that compare operations structurally never see the field on a described\n * contract.\n */\n undescribed?: true;\n /**\n * Declared non-2xx response codes, in document order — the refusals this\n * operation promises. `default` and any `4XX`/`5XX` wildcard are excluded:\n * neither names a case a scenario could be written for.\n *\n * The contract is the only place in the corpus that enumerates what an\n * operation REFUSES, and until this was read nothing joined those refusals to\n * anything. `api.ungoverned` grades whole operations against requirements,\n * so an operation documented by its happy path alone — one requirement, one\n * scenario, twelve declared failure codes — passed every check loam had.\n */\n failureCodes: string[];\n}\n\n/** A `x-loam-remove: true` marker with no usable operationId — a slot named but no operation named. */\nexport interface AnonymousRemoval {\n path: string;\n method: string;\n}\n\n/**\n * An operation written with no usable operationId — legal OpenAPI, and a slot\n * the merge replaces like any other, but one no id-keyed join can name. Kept\n * apart from `ops` so every join on the id stays blind to it, as it must: a\n * requirement, an edge or a removal marker can only ever name an id.\n */\nexport interface AnonymousOperation {\n path: string;\n method: string;\n /** `operationDigest` of it — what a pin over its slot is graded against, as for a named one. */\n digest: string;\n /** `x-loam-based-on`, as on a named operation — a feature delta's pin. */\n basedOn?: string;\n}\n\n/** The parse of one OpenAPI document: its operations, and whether the file could be read at all. */\nexport interface OpenapiDoc {\n ops: Operation[];\n /**\n * operationIds this document defines in more than one (path, method) slot.\n * A LIVING contract with two slots claiming one id is ambiguous to every\n * check that joins on the id — a requirement's `Operations:` line, an edge's\n * `metadata { op }`, a removal marker — and the join silently picks one.\n */\n duplicateIds: string[];\n /**\n * Removal markers the operation reader cannot name. They are invisible to\n * every id-keyed check, so a delta carrying one used to archive cleanly and\n * write `x-loam-remove: true` straight into a living contract.\n */\n anonymousRemovals: AnonymousRemoval[];\n /**\n * Operations with no usable operationId that are not removal markers.\n *\n * Dropped outright until they were kept here, and the slot-keyed questions\n * paid for it. The baseline gate could not see a living one, so a delta\n * `loam rebase` pinned over it (rebase reads the slot) was refused by\n * `validate` as pinned over \"no operation at that slot\" — the two never\n * agreed, and an unpinned quote over it drew no `openapi.baseline-missing`.\n * And the revival check, asked what an arriving operation would replace,\n * called an address it held one \"the living contract no longer has\", so\n * the remedy it gave deleted the operation with nothing naming it. Slots\n * only, never ids: nothing that joins on an operationId reads this list.\n */\n anonymousOps: AnonymousOperation[];\n /**\n * Paths carrying `x-loam-remove: true` at PATH level, beside the methods\n * rather than inside one.\n *\n * Not an operation, so nothing keyed by (path, method) could see it — and this\n * reader is method-keyed like the merge was. A marker written there addressed\n * no operation, so archive copied it into the living contract verbatim, where\n * it was invisible to `openapi.remove-marker-living` for the same reason and\n * kept the empty-path cleanup from ever firing. Reported as its own list\n * because the answer differs by document: in a FEATURE delta it is an\n * authoring mistake, in a LIVING contract it is a marker that must never have\n * been published.\n */\n pathLevelRemovals: string[];\n /**\n * True when the file EXISTS but cannot be read as an OpenAPI document —\n * broken YAML, or a document that is not a mapping. A missing file is not\n * unreadable (absence is `service.no-openapi`'s question), and an empty one\n * parses to null and honestly defines nothing.\n */\n unreadable: boolean;\n /** The parser's own message, when there is one to quote back. */\n error?: string;\n /** Internal `#/` refs that resolve to nothing in this document (`depth.ts`). */\n danglingRefs: string[];\n}\n\n/**\n * Extract operations from an OpenAPI document by walking the parsed YAML\n * structure (paths.<path>.<method>, method filtered to the HTTP set).\n * Structure-aware on purpose: a regex scan both drops legal ids (kebab-case,\n * dotted) and picks up phantom ids from description text. Each operation\n * carries its id and its `deprecated: true` flag (exactly `true` counts —\n * strings and truthy noise do not). `ops` is the defined set, keyed by the\n * (path, method) SLOT — not by operationId. An operation written with no\n * operationId is not in it — nothing that joins on the id could name it —\n * but it is in `anonymousOps`, by its slot, for the questions asked BY slot.\n *\n * Keying by id used to drop the second slot claiming a name, which made the\n * single most common contract edit invisible: relocating an endpoint is a\n * removal marker at the old slot plus an upsert at the new one, the SAME\n * operationId twice. Whichever came second in document order vanished, so\n * either the marker addressed a slot loam could not see (`remove-target-missing`\n * on a perfectly good delta) or the new definition did. Two slots claiming one\n * id in a LIVING contract is a real ambiguity rather than a merge failure, so\n * it rides out as `duplicateIds` for the caller to grade.\n *\n * An unreadable document yields [] ops PLUS the `unreadable` flag. Parsers\n * never diagnose — the policy of every reader here — so the failure travels as\n * a flag for validate to grade (`openapi.invalid`). Swallowing it into a bare\n * [] made a broken contract indistinguishable from an EMPTY one, and the spine\n * check then reported every inbound edge broken (`spine.op-undefined`) — a\n * false diagnosis pointing at the landscape when the truth was this file.\n */\nexport async function readOpenapi(openapiPath: string, context?: FleetContext): Promise<OpenapiDoc> {\n if (context !== undefined) return context.readOpenapi(openapiPath);\n if (!existsSync(openapiPath)) return empty();\n const bytes = await readFile(openapiPath);\n // A contract whose bytes are not UTF-8 is as unreadable as broken YAML, and\n // for a worse reason: `toString(\"utf8\")` would substitute U+FFFD and hand the\n // parser a document nobody wrote. It travels as `unreadable` so validate\n // grades it `openapi.invalid` — the file is the error, and every check that\n // reads the contract stays suspended.\n if (!isUtf8(bytes)) return { ...empty(), unreadable: true, error: \"file is not valid UTF-8\" };\n return openapiFromText(bytes.toString(\"utf8\"));\n}\n\n/**\n * The same read over text already in hand — the parse half of `readOpenapi`,\n * split out so `loam diff` can read a contract fetched from a base git ref\n * (core/diff/base-state.ts) without writing it to disk. One spelling of the\n * walk for both entries, so a base-ref contract can never parse differently\n * from the living file the same bytes would produce; the byte concerns\n * (existence, UTF-8) stay with the reader that holds the bytes.\n */\nexport function openapiFromText(text: string): OpenapiDoc {\n let doc: unknown;\n try {\n doc = parse(text);\n } catch (e) {\n return { ...empty(), unreadable: true, error: e instanceof Error ? e.message : String(e) };\n }\n // A scalar or sequence document is as unreadable as broken YAML: there is no\n // mapping to look `paths` up in, so nothing can be concluded from it. null\n // (an empty file) stays readable — it defines nothing, and says so honestly.\n if (doc !== null && (typeof doc !== \"object\" || Array.isArray(doc))) {\n return { ...empty(), unreadable: true, error: \"document is not a YAML mapping\" };\n }\n // Refs are probed over the WHOLE document: a schema in `components` that\n // `$ref`s a neighbour nobody wrote is exactly as broken as one under a path.\n const refs = danglingRefs(doc);\n const paths = (doc as { paths?: unknown } | null)?.paths;\n if (!paths || typeof paths !== \"object\") return { ...empty(), danglingRefs: refs };\n const ops: Operation[] = [];\n const anonymousRemovals: AnonymousRemoval[] = [];\n const anonymousOps: AnonymousOperation[] = [];\n const pathLevelRemovals: string[] = [];\n const seen = new Map<string, number>();\n const closureDigest = closureDigester(doc);\n for (const [path, item] of Object.entries(paths as Record<string, unknown>)) {\n if (!item || typeof item !== \"object\") continue;\n if ((item as Record<string, unknown>)[\"x-loam-remove\"] === true) pathLevelRemovals.push(path);\n for (const [method, op] of Object.entries(item as Record<string, unknown>)) {\n if (!HTTP_METHODS.has(method)) continue;\n if (!op || typeof op !== \"object\") continue;\n const id = (op as Record<string, unknown>)[\"operationId\"];\n const remove = (op as Record<string, unknown>)[\"x-loam-remove\"] === true;\n const basedOn = operationBaselineOf(op);\n if (typeof id !== \"string\" || id.length === 0) {\n if (remove) anonymousRemovals.push({ path, method });\n else anonymousOps.push({ path, method, digest: operationDigest(op), ...(basedOn === undefined ? {} : { basedOn }) });\n continue;\n }\n seen.set(id, (seen.get(id) ?? 0) + 1);\n ops.push({\n id,\n deprecated: (op as Record<string, unknown>)[\"deprecated\"] === true,\n remove,\n path,\n method,\n digest: operationDigest(op),\n closureDigest: closureDigest(op),\n ...(basedOn === undefined ? {} : { basedOn }),\n ...(responseUndescribed(op) ? { undescribed: true as const } : {}),\n failureCodes: failureCodesOf(op),\n });\n }\n }\n const duplicateIds = [...seen].filter(([, n]) => n > 1).map(([id]) => id);\n return { ops, duplicateIds, anonymousRemovals, anonymousOps, pathLevelRemovals, unreadable: false, danglingRefs: refs };\n}\n\nfunction empty(): OpenapiDoc {\n return {\n ops: [],\n duplicateIds: [],\n anonymousRemovals: [],\n anonymousOps: [],\n pathLevelRemovals: [],\n unreadable: false,\n danglingRefs: [],\n };\n}\n\n/** The operations alone — for every caller whose own finding already covers the unreadable case. */\nexport async function operations(openapiPath: string, context?: FleetContext): Promise<Operation[]> {\n return (await readOpenapi(openapiPath, context)).ops;\n}\n\n/** The operationIds alone — `operations` for every caller that asks only \"does it exist\". */\nexport async function operationIds(openapiPath: string, context?: FleetContext): Promise<string[]> {\n return (await operations(openapiPath, context)).map((o) => o.id);\n}\n\n/**\n * The operationIds a service will provide after a feature is applied: living\n * operations, plus feature upserts, minus explicit feature removals.\n *\n * Removals are applied BEFORE upserts, which is what makes the answer\n * independent of the order the feature's document happens to spell them in.\n * Interleaving them one operation at a time meant a relocation — the same id\n * removed from `/old` and defined on `/new` — answered \"defined\" or \"gone\"\n * depending on which path sorted first in the YAML, and the requirement\n * governing it was reported undefined (`spec-api.op-undefined`) on exactly one\n * of the two spellings of the same change.\n *\n * A context is threaded into the READS and never asked for the answer. It used\n * to own a second copy of this function, and that copy interleaved removals\n * with upserts — the exact bug the paragraph above describes as fixed. So the\n * commands that pass a context (`validate`, `status`) and the one that does not\n * (`archive`) disagreed about whether a relocated operation still existed, and\n * `spec-api.op-undefined` — an error that gates archive — fired or not\n * depending on which path key the author happened to type first. Memoising is\n * the context's job; the ordering rule is this function's, and there is one of\n * it.\n */\nexport async function serviceOperationIds(\n docsDir: DocsDir,\n service: PathableService,\n featureDir?: FeatureDir,\n context?: FleetContext,\n): Promise<string[]> {\n const ids = new Set(\n (await operations((await locateServicePaths(docsDir, service, context)).openapi, context)).filter((op) => !op.remove).map((op) => op.id),\n );\n // Absence is `undefined`, the shape the optional parameter declares. The\n // truthiness test also read \"\" as \"no feature\", which is a caller mistake\n // worth surfacing rather than answering as though the feature carried no\n // contract delta.\n if (featureDir !== undefined) {\n const featOps = await operations(featureSpecPaths(featureDir, service).openapi, context);\n for (const op of featOps) if (op.remove) ids.delete(op.id);\n for (const op of featOps) if (!op.remove) ids.add(op.id);\n }\n return [...ids];\n}\n\n"]}
@@ -1,7 +1,8 @@
1
1
  /**
2
- * `$ref` as a pair of plain questions — which ones does this tree contain, and
3
- * what does one point at — and the component closure the merge answers them
4
- * for.
2
+ * The component closure the merge computes over `$ref`s. The two plain
3
+ * questions it asks — which refs does this tree contain, and what does one
4
+ * point at — moved to `../depth.ts` once the reader needed them too (that
5
+ * module's header says why the root, not here).
5
6
  *
6
7
  * The closure is verdict-driven, not reachability-driven: every component the
7
8
  * feature DECLARES is classified against its `x-loam-baselines` entry exactly
@@ -23,46 +24,27 @@ import { isDeepStrictEqual } from "node:util";
23
24
  import { isMap, isScalar } from "yaml";
24
25
  import { classifyBaselineDigests, valueDigest, withoutFeatureKeysDeep } from "../digest.js";
25
26
  import { entryFor, restatedSurfaces, surfaceIn } from "../baseline/record.js";
26
- export function collectRefs(node) {
27
- const out = [];
28
- const walk = (current) => {
29
- if (Array.isArray(current)) {
30
- for (const value of current)
31
- walk(value);
32
- return;
33
- }
34
- if (current !== null && typeof current === "object") {
35
- for (const [key, value] of Object.entries(current)) {
36
- if (key === "$ref" && typeof value === "string")
37
- out.push(value);
38
- else
39
- walk(value);
40
- }
41
- }
42
- };
43
- walk(node);
44
- return out;
27
+ import { collectRefs, resolvePointer } from "../depth.js";
28
+ /** One mapping node's pair for a key, matched AS A STRING — `setKeyed`'s find, reusable at any depth. */
29
+ export function pairAt(node, key) {
30
+ return isMap(node) ? node.items.find((it) => isScalar(it.key) && String(it.key.value) === key) : undefined;
45
31
  }
46
- /** Resolve a local JSON pointer (`#/a/b~1c`) against a plain JS tree. */
47
- export function resolvePointer(root, ref) {
48
- let current = root;
49
- for (const raw of ref.slice(2).split("/")) {
50
- const segment = raw.replace(/~1/g, "/").replace(/~0/g, "~");
51
- if (Array.isArray(current)) {
52
- const index = Number(segment);
53
- if (!Number.isInteger(index) || index < 0 || index >= current.length) {
54
- return { found: false, value: undefined };
55
- }
56
- current = current[index];
57
- }
58
- else if (current !== null && typeof current === "object" && segment in current) {
59
- current = current[segment];
60
- }
61
- else {
62
- return { found: false, value: undefined };
63
- }
64
- }
65
- return { found: true, value: current };
32
+ /**
33
+ * Write one value under the mapping at `parent`, matching the existing key AS A
34
+ * STRING: a living `404:` response is the YAML number 404, and a plain `setIn`
35
+ * with the string "404" misses it and APPENDS a second pair — the contract then
36
+ * declares the key twice, forever, with the pre-merge copy first in reading
37
+ * order. Path-agnostic on purpose, so it is the one upsert both contract axes
38
+ * write through: a component here and in the strip's twin write on the feature
39
+ * side, a component surface or a slot on the event axis
40
+ * (../../asyncapi/merge/{components,markers}.ts).
41
+ */
42
+ export function setKeyed(doc, parent, key, value) {
43
+ const existing = pairAt(doc.getIn(parent), key);
44
+ if (existing !== undefined)
45
+ existing.value = doc.createNode(value);
46
+ else
47
+ doc.setIn([...parent, key], value);
66
48
  }
67
49
  /**
68
50
  * Merge the feature's components into the living document by verdict:
@@ -84,23 +66,6 @@ export function resolvePointer(root, ref) {
84
66
  * written content and copied components must resolve in the copy set or the
85
67
  * living document.
86
68
  */
87
- /**
88
- * Write one component's value, matching the existing key AS A STRING: a
89
- * living `404:` response is the YAML number 404, and a plain `setIn` with the
90
- * string "404" misses it and APPENDS a second pair — the contract then
91
- * declares the component twice, forever, with the pre-merge copy first in
92
- * reading order. Exported for the strip's twin write on the feature side.
93
- */
94
- export function setComponentValue(doc, kind, name, value) {
95
- const kindNode = doc.getIn(["components", kind]);
96
- const existing = isMap(kindNode)
97
- ? kindNode.items.find((it) => isScalar(it.key) && String(it.key.value) === name)
98
- : undefined;
99
- if (existing !== undefined)
100
- existing.value = doc.createNode(value);
101
- else
102
- doc.setIn(["components", kind, name], value);
103
- }
104
69
  export function mergeComponentClosure(input) {
105
70
  const { living, featPlain, livingPlain, record, written, componentsAreTheDelta } = input;
106
71
  const componentsModified = [];
@@ -227,7 +192,7 @@ export function mergeComponentClosure(input) {
227
192
  // walks, and `--approve` once published `x-loam-remove: true` into a
228
193
  // living component. Promoted copies go through this same write, so a
229
194
  // components-only delta cannot be the one shape that publishes a marker.
230
- setComponentValue(living, kind, name, withoutFeatureKeysDeep(value));
195
+ setKeyed(living, ["components", kind], name, withoutFeatureKeysDeep(value));
231
196
  }
232
197
  return { componentsModified, componentsAdded, componentsQuoted, componentsStale, unresolved };
233
198
  }
@@ -1 +1 @@
1
- {"version":3,"file":"components.js","sourceRoot":"","sources":["../../../../src/core/openapi/merge/components.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `$ref` as a pair of plain questions — which ones does this tree contain, and\n * what does one point at — and the component closure the merge answers them\n * for.\n *\n * The closure is verdict-driven, not reachability-driven: every component the\n * feature DECLARES is classified against its `x-loam-baselines` entry exactly\n * as an operation is classified against its pin, and reachability decides only\n * one thing — whether a genuinely NEW component rides along — whenever the\n * delta merges paths at all. The old rule, \"copy everything reachable from any\n * feature path item\", let a QUOTED operation drag its authoring-time component\n * copy over someone else's landed change; the walk now starts from what this\n * merge actually WROTE, so a quote pulls nothing.\n *\n * Where the delta merges no path, that walk starts from nothing, and \"is a new\n * component reachable?\" has no meaningful answer to give: a components-only\n * delta declared every one of them on purpose and there is no operation for\n * them to hide behind. Parking them forever is what made such a delta merge\n * nothing at all, at exit 0 — `componentsAreTheDelta` is that case, and only\n * that case.\n */\nimport { isDeepStrictEqual } from \"node:util\";\nimport { isMap, isScalar, type Document } from \"yaml\";\nimport { classifyBaselineDigests, valueDigest, withoutFeatureKeysDeep } from \"../digest.js\";\nimport { entryFor, restatedSurfaces, surfaceIn, type BaselineRecord } from \"../baseline/record.js\";\n\nexport function collectRefs(node: unknown): string[] {\n const out: string[] = [];\n const walk = (current: unknown): void => {\n if (Array.isArray(current)) {\n for (const value of current) walk(value);\n return;\n }\n if (current !== null && typeof current === \"object\") {\n for (const [key, value] of Object.entries(current)) {\n if (key === \"$ref\" && typeof value === \"string\") out.push(value);\n else walk(value);\n }\n }\n };\n walk(node);\n return out;\n}\n\n/** Resolve a local JSON pointer (`#/a/b~1c`) against a plain JS tree. */\nexport function resolvePointer(root: unknown, ref: string): { found: boolean; value: unknown } {\n let current: unknown = root;\n for (const raw of ref.slice(2).split(\"/\")) {\n const segment = raw.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n if (Array.isArray(current)) {\n const index = Number(segment);\n if (!Number.isInteger(index) || index < 0 || index >= current.length) {\n return { found: false, value: undefined };\n }\n current = current[index];\n } else if (current !== null && typeof current === \"object\" && segment in (current as Record<string, unknown>)) {\n current = (current as Record<string, unknown>)[segment];\n } else {\n return { found: false, value: undefined };\n }\n }\n return { found: true, value: current };\n}\n\n/** Everything the closure needs; one object because the merge is its only caller. */\nexport interface ComponentClosureInput {\n /** The living document the copies are written into. */\n living: Document;\n featPlain: unknown;\n livingPlain: unknown;\n /** The feature's `x-loam-baselines` record, read once by the merge. */\n record: BaselineRecord;\n /** The plain values the path merge actually wrote, labelled for the ref sweep. */\n written: Array<{ from: string; value: unknown }>;\n /**\n * True only when the feature's `paths` holds no entry at all, so `written` is\n * empty and nothing this merge wrote can reach a new component. The merge\n * computes it from the path ENTRIES, not from what the path loop happened to\n * write: one path entry, even an entirely quoted one, means the reachability\n * question is real and this stays false. Widening it past that would copy the\n * authoring-time component a quoted operation carries — the exact revert the\n * verdict-driven closure exists to stop.\n */\n componentsAreTheDelta: boolean;\n}\n\n/** What the closure computed: components overwritten, added, quoted, stale, and refs nothing resolves. */\nexport interface ComponentClosureOutcome {\n componentsModified: string[];\n /** `<kind>/<name>` of components written where the living contract had none. */\n componentsAdded: string[];\n componentsQuoted: string[];\n componentsStale: string[];\n unresolved: Array<{ ref: string; from: string }>;\n}\n\n/**\n * Merge the feature's components into the living document by verdict:\n *\n * - quote → skipped; the living copy stays, whatever landed on it since.\n * - edit, stale (under --approve), or unpinned-and-differing → copied; the\n * unpinned copy is the old upsert, kept for never-rebased deltas.\n * - unpinned and living-equal → skipped, as it always was: nothing to write.\n * - unfounded (pinned, living gone — the gate refused; --approve) → copied,\n * because a written operation may reference it and the merge must never\n * leave a dangling ref behind.\n * - NEW (unpinned, living-absent) → copied only when reachable from written\n * content or from another copied component's own refs. The visited-set\n * fixpoint is what keeps A↔B cycles terminating and unrelated namespaces\n * uncopied. When the components ARE the delta there is no written content\n * to be reachable from, so every one of them is copied outright.\n *\n * The unresolved sweep is unchanged in meaning: every local ref reachable from\n * written content and copied components must resolve in the copy set or the\n * living document.\n */\n/**\n * Write one component's value, matching the existing key AS A STRING: a\n * living `404:` response is the YAML number 404, and a plain `setIn` with the\n * string \"404\" misses it and APPENDS a second pair — the contract then\n * declares the component twice, forever, with the pre-merge copy first in\n * reading order. Exported for the strip's twin write on the feature side.\n */\nexport function setComponentValue(doc: Document, kind: string, name: string, value: unknown): void {\n const kindNode = doc.getIn([\"components\", kind]);\n const existing = isMap(kindNode)\n ? kindNode.items.find((it) => isScalar(it.key) && String(it.key.value) === name)\n : undefined;\n if (existing !== undefined) existing.value = doc.createNode(value);\n else doc.setIn([\"components\", kind, name], value);\n}\n\nexport function mergeComponentClosure(input: ComponentClosureInput): ComponentClosureOutcome {\n const { living, featPlain, livingPlain, record, written, componentsAreTheDelta } = input;\n const componentsModified: string[] = [];\n const componentsAdded: string[] = [];\n const componentsQuoted: string[] = [];\n const componentsStale: string[] = [];\n const unresolved: ComponentClosureOutcome[\"unresolved\"] = [];\n const copies: Array<{ kind: string; name: string; value: unknown }> = [];\n const copied = new Set<string>();\n /** Genuinely new components, parked until something written reaches them. */\n const parked = new Map<string, { kind: string; name: string; value: unknown }>();\n /**\n * Which declared surfaces the living contract does not have — recorded here,\n * during classification, because this is the one place that asks. What was\n * ADDED is decided at the write below instead: a parked component the\n * fixpoint never reaches is absent from living and stays that way, and\n * calling it added would name a component the merge did not write.\n */\n const absentFromLiving = new Set<string>();\n\n // ONE enumeration (baseline/record.ts) with the rebase plan and the gate —\n // and ONE living lookup — so \"which components does this delta declare, and\n // do they exist\" cannot drift between the command that pins, the gate that\n // grades and this closure that writes.\n for (const surface of restatedSurfaces(featPlain)) {\n if (surface.kind !== \"component\") continue;\n const cut = surface.id.indexOf(\"/\");\n const entry = { kind: surface.id.slice(0, cut), name: surface.id.slice(cut + 1), value: surface.value };\n const inLiving = surfaceIn(livingPlain, surface);\n if (!inLiving.found) absentFromLiving.add(surface.id);\n const verdict = classifyBaselineDigests(\n entryFor(record, surface),\n valueDigest(surface.value),\n inLiving.found ? valueDigest(inLiving.value) : undefined,\n );\n if (verdict === \"quote\") {\n componentsQuoted.push(surface.id);\n continue;\n }\n if (verdict === \"unpinned\") {\n if (!inLiving.found) {\n parked.set(surface.id, entry);\n continue;\n }\n if (isDeepStrictEqual(inLiving.value, surface.value)) continue;\n componentsModified.push(surface.id);\n } else {\n if (verdict === \"stale\") componentsStale.push(surface.id);\n if (inLiving.found && !isDeepStrictEqual(inLiving.value, surface.value)) componentsModified.push(surface.id);\n }\n copies.push(entry);\n copied.add(surface.id);\n }\n\n // A components-only delta wrote no path, so the fixpoint below would start\n // from an empty queue and every parked component would stay parked forever —\n // the merge published a document byte-identical to the living one and archive\n // said `ok`. The promotion happens HERE, before the queue is built, and that\n // order is the whole point: a component promoted afterwards would carry its\n // own `$ref`s past the sweep, and `openapi.ref-unresolved` would stop firing\n // for exactly the deltas that most need it — a new schema pointing at a\n // second one the author forgot to bring. Iterated as a SNAPSHOT: this loop\n // empties `parked` as it goes and `visitRef` deletes from the same Map below,\n // and neither may read it live while writing it.\n if (componentsAreTheDelta) {\n const promotions = [...parked.entries()];\n for (const [id, entry] of promotions) {\n parked.delete(id);\n copies.push(entry);\n copied.add(id);\n }\n }\n\n // The reachability fixpoint: refs out of written content and out of every\n // copy — classification copies and any promotions now, parked promotions as\n // they join.\n const queue: Array<{ node: unknown; from: string }> = [\n ...written.map((w) => ({ node: w.value, from: w.from })),\n ...copies.map((c) => ({ node: c.value, from: `components/${c.kind}/${c.name}` })),\n ];\n const visited = new Set<string>();\n const visitRef = (ref: string, from: string): void => {\n if (!ref.startsWith(\"#/\")) return;\n const match = /^#\\/components\\/([^/]+)\\/([^/]+)(?:\\/|$)/.exec(ref);\n if (!match) {\n if (!resolvePointer(featPlain, ref).found && !resolvePointer(livingPlain, ref).found) {\n unresolved.push({ ref, from });\n }\n return;\n }\n const key = `${match[1]!}/${match[2]!}`;\n if (visited.has(key)) return;\n visited.add(key);\n // Already copied by verdict: its own refs are in the queue from the start.\n if (copied.has(key)) return;\n const parkedEntry = parked.get(key);\n if (parkedEntry !== undefined) {\n parked.delete(key);\n copies.push(parkedEntry);\n copied.add(key);\n queue.push({ node: parkedEntry.value, from: `components/${key}` });\n return;\n }\n // Not copied and not new: a quoted or unchanged declaration, or something\n // the feature never spells. Either way the LIVING document must have it.\n if (!resolvePointer(livingPlain, `#/components/${key}`).found) {\n unresolved.push({ ref, from });\n }\n };\n while (queue.length > 0) {\n const { node, from } = queue.pop()!;\n for (const ref of collectRefs(node)) visitRef(ref, from);\n }\n\n for (const { kind, name, value } of copies) {\n // Decided at the write, over the copies that actually happened: a promoted\n // component is an addition, a parked one that nothing reached is not.\n if (absentFromLiving.has(`${kind}/${name}`)) componentsAdded.push(`${kind}/${name}`);\n // The value is deep-stripped of loam's bookkeeping keys — a component\n // holding an operation shape nests them where the path-level strip never\n // walks, and `--approve` once published `x-loam-remove: true` into a\n // living component. Promoted copies go through this same write, so a\n // components-only delta cannot be the one shape that publishes a marker.\n setComponentValue(living, kind, name, withoutFeatureKeysDeep(value));\n }\n\n return { componentsModified, componentsAdded, componentsQuoted, componentsStale, unresolved };\n}\n"]}
1
+ {"version":3,"file":"components.js","sourceRoot":"","sources":["../../../../src/core/openapi/merge/components.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The component closure the merge computes over `$ref`s. The two plain\n * questions it asks — which refs does this tree contain, and what does one\n * point at — moved to `../depth.ts` once the reader needed them too (that\n * module's header says why the root, not here).\n *\n * The closure is verdict-driven, not reachability-driven: every component the\n * feature DECLARES is classified against its `x-loam-baselines` entry exactly\n * as an operation is classified against its pin, and reachability decides only\n * one thing — whether a genuinely NEW component rides along — whenever the\n * delta merges paths at all. The old rule, \"copy everything reachable from any\n * feature path item\", let a QUOTED operation drag its authoring-time component\n * copy over someone else's landed change; the walk now starts from what this\n * merge actually WROTE, so a quote pulls nothing.\n *\n * Where the delta merges no path, that walk starts from nothing, and \"is a new\n * component reachable?\" has no meaningful answer to give: a components-only\n * delta declared every one of them on purpose and there is no operation for\n * them to hide behind. Parking them forever is what made such a delta merge\n * nothing at all, at exit 0 — `componentsAreTheDelta` is that case, and only\n * that case.\n */\nimport { isDeepStrictEqual } from \"node:util\";\nimport { isMap, isScalar, type Document, type Pair } from \"yaml\";\nimport { classifyBaselineDigests, valueDigest, withoutFeatureKeysDeep } from \"../digest.js\";\nimport { entryFor, restatedSurfaces, surfaceIn, type BaselineRecord } from \"../baseline/record.js\";\nimport { collectRefs, resolvePointer } from \"../depth.js\";\n\n/** Everything the closure needs; one object because the merge is its only caller. */\nexport interface ComponentClosureInput {\n /** The living document the copies are written into. */\n living: Document;\n featPlain: unknown;\n livingPlain: unknown;\n /** The feature's `x-loam-baselines` record, read once by the merge. */\n record: BaselineRecord;\n /** The plain values the path merge actually wrote, labelled for the ref sweep. */\n written: Array<{ from: string; value: unknown }>;\n /**\n * True only when the feature's `paths` holds no entry at all, so `written` is\n * empty and nothing this merge wrote can reach a new component. The merge\n * computes it from the path ENTRIES, not from what the path loop happened to\n * write: one path entry, even an entirely quoted one, means the reachability\n * question is real and this stays false. Widening it past that would copy the\n * authoring-time component a quoted operation carries — the exact revert the\n * verdict-driven closure exists to stop.\n */\n componentsAreTheDelta: boolean;\n}\n\n/** What the closure computed: components overwritten, added, quoted, stale, and refs nothing resolves. */\nexport interface ComponentClosureOutcome {\n componentsModified: string[];\n /** `<kind>/<name>` of components written where the living contract had none. */\n componentsAdded: string[];\n componentsQuoted: string[];\n componentsStale: string[];\n unresolved: Array<{ ref: string; from: string }>;\n}\n\n/** One mapping node's pair for a key, matched AS A STRING — `setKeyed`'s find, reusable at any depth. */\nexport function pairAt(node: unknown, key: string): Pair | undefined {\n return isMap(node) ? node.items.find((it) => isScalar(it.key) && String(it.key.value) === key) : undefined;\n}\n\n/**\n * Write one value under the mapping at `parent`, matching the existing key AS A\n * STRING: a living `404:` response is the YAML number 404, and a plain `setIn`\n * with the string \"404\" misses it and APPENDS a second pair — the contract then\n * declares the key twice, forever, with the pre-merge copy first in reading\n * order. Path-agnostic on purpose, so it is the one upsert both contract axes\n * write through: a component here and in the strip's twin write on the feature\n * side, a component surface or a slot on the event axis\n * (../../asyncapi/merge/{components,markers}.ts).\n */\nexport function setKeyed(doc: Document, parent: string[], key: string, value: unknown): void {\n const existing = pairAt(doc.getIn(parent), key);\n if (existing !== undefined) existing.value = doc.createNode(value);\n else doc.setIn([...parent, key], value);\n}\n\n/**\n * Merge the feature's components into the living document by verdict:\n *\n * - quote → skipped; the living copy stays, whatever landed on it since.\n * - edit, stale (under --approve), or unpinned-and-differing → copied; the\n * unpinned copy is the old upsert, kept for never-rebased deltas.\n * - unpinned and living-equal → skipped, as it always was: nothing to write.\n * - unfounded (pinned, living gone — the gate refused; --approve) → copied,\n * because a written operation may reference it and the merge must never\n * leave a dangling ref behind.\n * - NEW (unpinned, living-absent) → copied only when reachable from written\n * content or from another copied component's own refs. The visited-set\n * fixpoint is what keeps A↔B cycles terminating and unrelated namespaces\n * uncopied. When the components ARE the delta there is no written content\n * to be reachable from, so every one of them is copied outright.\n *\n * The unresolved sweep is unchanged in meaning: every local ref reachable from\n * written content and copied components must resolve in the copy set or the\n * living document.\n */\nexport function mergeComponentClosure(input: ComponentClosureInput): ComponentClosureOutcome {\n const { living, featPlain, livingPlain, record, written, componentsAreTheDelta } = input;\n const componentsModified: string[] = [];\n const componentsAdded: string[] = [];\n const componentsQuoted: string[] = [];\n const componentsStale: string[] = [];\n const unresolved: ComponentClosureOutcome[\"unresolved\"] = [];\n const copies: Array<{ kind: string; name: string; value: unknown }> = [];\n const copied = new Set<string>();\n /** Genuinely new components, parked until something written reaches them. */\n const parked = new Map<string, { kind: string; name: string; value: unknown }>();\n /**\n * Which declared surfaces the living contract does not have — recorded here,\n * during classification, because this is the one place that asks. What was\n * ADDED is decided at the write below instead: a parked component the\n * fixpoint never reaches is absent from living and stays that way, and\n * calling it added would name a component the merge did not write.\n */\n const absentFromLiving = new Set<string>();\n\n // ONE enumeration (baseline/record.ts) with the rebase plan and the gate —\n // and ONE living lookup — so \"which components does this delta declare, and\n // do they exist\" cannot drift between the command that pins, the gate that\n // grades and this closure that writes.\n for (const surface of restatedSurfaces(featPlain)) {\n if (surface.kind !== \"component\") continue;\n const cut = surface.id.indexOf(\"/\");\n const entry = { kind: surface.id.slice(0, cut), name: surface.id.slice(cut + 1), value: surface.value };\n const inLiving = surfaceIn(livingPlain, surface);\n if (!inLiving.found) absentFromLiving.add(surface.id);\n const verdict = classifyBaselineDigests(\n entryFor(record, surface),\n valueDigest(surface.value),\n inLiving.found ? valueDigest(inLiving.value) : undefined,\n );\n if (verdict === \"quote\") {\n componentsQuoted.push(surface.id);\n continue;\n }\n if (verdict === \"unpinned\") {\n if (!inLiving.found) {\n parked.set(surface.id, entry);\n continue;\n }\n if (isDeepStrictEqual(inLiving.value, surface.value)) continue;\n componentsModified.push(surface.id);\n } else {\n if (verdict === \"stale\") componentsStale.push(surface.id);\n if (inLiving.found && !isDeepStrictEqual(inLiving.value, surface.value)) componentsModified.push(surface.id);\n }\n copies.push(entry);\n copied.add(surface.id);\n }\n\n // A components-only delta wrote no path, so the fixpoint below would start\n // from an empty queue and every parked component would stay parked forever —\n // the merge published a document byte-identical to the living one and archive\n // said `ok`. The promotion happens HERE, before the queue is built, and that\n // order is the whole point: a component promoted afterwards would carry its\n // own `$ref`s past the sweep, and `openapi.ref-unresolved` would stop firing\n // for exactly the deltas that most need it — a new schema pointing at a\n // second one the author forgot to bring. Iterated as a SNAPSHOT: this loop\n // empties `parked` as it goes and `visitRef` deletes from the same Map below,\n // and neither may read it live while writing it.\n if (componentsAreTheDelta) {\n const promotions = [...parked.entries()];\n for (const [id, entry] of promotions) {\n parked.delete(id);\n copies.push(entry);\n copied.add(id);\n }\n }\n\n // The reachability fixpoint: refs out of written content and out of every\n // copy — classification copies and any promotions now, parked promotions as\n // they join.\n const queue: Array<{ node: unknown; from: string }> = [\n ...written.map((w) => ({ node: w.value, from: w.from })),\n ...copies.map((c) => ({ node: c.value, from: `components/${c.kind}/${c.name}` })),\n ];\n const visited = new Set<string>();\n const visitRef = (ref: string, from: string): void => {\n if (!ref.startsWith(\"#/\")) return;\n const match = /^#\\/components\\/([^/]+)\\/([^/]+)(?:\\/|$)/.exec(ref);\n if (!match) {\n if (!resolvePointer(featPlain, ref).found && !resolvePointer(livingPlain, ref).found) {\n unresolved.push({ ref, from });\n }\n return;\n }\n const key = `${match[1]!}/${match[2]!}`;\n if (visited.has(key)) return;\n visited.add(key);\n // Already copied by verdict: its own refs are in the queue from the start.\n if (copied.has(key)) return;\n const parkedEntry = parked.get(key);\n if (parkedEntry !== undefined) {\n parked.delete(key);\n copies.push(parkedEntry);\n copied.add(key);\n queue.push({ node: parkedEntry.value, from: `components/${key}` });\n return;\n }\n // Not copied and not new: a quoted or unchanged declaration, or something\n // the feature never spells. Either way the LIVING document must have it.\n if (!resolvePointer(livingPlain, `#/components/${key}`).found) {\n unresolved.push({ ref, from });\n }\n };\n while (queue.length > 0) {\n const { node, from } = queue.pop()!;\n for (const ref of collectRefs(node)) visitRef(ref, from);\n }\n\n for (const { kind, name, value } of copies) {\n // Decided at the write, over the copies that actually happened: a promoted\n // component is an addition, a parked one that nothing reached is not.\n if (absentFromLiving.has(`${kind}/${name}`)) componentsAdded.push(`${kind}/${name}`);\n // The value is deep-stripped of loam's bookkeeping keys — a component\n // holding an operation shape nests them where the path-level strip never\n // walks, and `--approve` once published `x-loam-remove: true` into a\n // living component. Promoted copies go through this same write, so a\n // components-only delta cannot be the one shape that publishes a marker.\n setKeyed(living, [\"components\", kind], name, withoutFeatureKeysDeep(value));\n }\n\n return { componentsModified, componentsAdded, componentsQuoted, componentsStale, unresolved };\n}\n"]}
@@ -1,14 +1,3 @@
1
- /**
2
- * The one refusal every stage of the contract delta path raises: a document the
3
- * planner cannot read, named by which side it came from.
4
- *
5
- * It is the package's leaf for the same reason the pin, the markers and the
6
- * merge each throw it: whichever of them fails, the command above has to map
7
- * one domain error onto its own envelope. A module that owned the error would
8
- * be imported by its two siblings for that reason alone.
9
- */
10
- // Every `isRecord` below asks one question of a resolved plain tree: is this a
11
- // node that can hold OpenAPI keys — an object, not an array?
12
1
  /** A document the merge planner cannot read. Commands map this domain error to their own envelope. */
13
2
  export class OpenapiMergeError extends Error {
14
3
  source;
@@ -20,7 +9,8 @@ export class OpenapiMergeError extends Error {
20
9
  this.service = service;
21
10
  }
22
11
  }
23
- export function errorMessage(error) {
24
- return error instanceof Error ? error.message : String(error);
12
+ /** The `fail` the tree helpers take, for one side of this axis. */
13
+ export function openapiRefusal(source, service) {
14
+ return (detail) => new OpenapiMergeError(source, service, detail);
25
15
  }
26
16
  //# sourceMappingURL=error.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"error.js","sourceRoot":"","sources":["../../../../src/core/openapi/merge/error.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,+EAA+E;AAC/E,6DAA6D;AAI7D,sGAAsG;AACtG,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAI/B,MAAM;IACN,OAAO;IAJA,IAAI,GAAG,mBAAmB,CAAC;IAE7C,YACW,MAA0B,EAC1B,OAAe,EACxB,MAAc;QAEd,KAAK,CAAC,GAAG,MAAM,gBAAgB,OAAO,uBAAuB,MAAM,EAAE,CAAC,CAAC;sBAJ9D,MAAM;uBACN,OAAO;IAIlB,CAAC;CACF;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC","sourcesContent":["/**\n * The one refusal every stage of the contract delta path raises: a document the\n * planner cannot read, named by which side it came from.\n *\n * It is the package's leaf for the same reason the pin, the markers and the\n * merge each throw it: whichever of them fails, the command above has to map\n * one domain error onto its own envelope. A module that owned the error would\n * be imported by its two siblings for that reason alone.\n */\n// Every `isRecord` below asks one question of a resolved plain tree: is this a\n// node that can hold OpenAPI keys — an object, not an array?\n\nexport type OpenapiMergeSource = \"feature\" | \"living\";\n\n/** A document the merge planner cannot read. Commands map this domain error to their own envelope. */\nexport class OpenapiMergeError extends Error {\n override readonly name = \"OpenapiMergeError\";\n\n constructor(\n readonly source: OpenapiMergeSource,\n readonly service: string,\n detail: string,\n ) {\n super(`${source} openapi for ${service} is not valid YAML: ${detail}`);\n }\n}\n\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
1
+ {"version":3,"file":"error.js","sourceRoot":"","sources":["../../../../src/core/openapi/merge/error.ts"],"names":[],"mappings":"AAeA,sGAAsG;AACtG,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAI/B,MAAM;IACN,OAAO;IAJA,IAAI,GAAG,mBAAmB,CAAC;IAE7C,YACW,MAA0B,EAC1B,OAAe,EACxB,MAAc;QAEd,KAAK,CAAC,GAAG,MAAM,gBAAgB,OAAO,uBAAuB,MAAM,EAAE,CAAC,CAAC;sBAJ9D,MAAM;uBACN,OAAO;IAIlB,CAAC;CACF;AAED,mEAAmE;AACnE,MAAM,UAAU,cAAc,CAAC,MAA0B,EAAE,OAAe;IACxE,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AACpE,CAAC","sourcesContent":["/**\n * The one refusal every stage of the contract delta path raises: a document the\n * planner cannot read, named by which side it came from.\n *\n * It is the package's leaf for the same reason the pin, the markers and the\n * merge each throw it: whichever of them fails, the command above has to map\n * one domain error onto its own envelope. A module that owned the error would\n * be imported by its two siblings for that reason alone.\n *\n * Every stage reads and writes its contract through ../contract-tree.ts, and\n * hands those helpers `openapiRefusal` as the error to raise — which is what\n * makes each of them fail with this one identically.\n */\nexport type OpenapiMergeSource = \"feature\" | \"living\";\n\n/** A document the merge planner cannot read. Commands map this domain error to their own envelope. */\nexport class OpenapiMergeError extends Error {\n override readonly name = \"OpenapiMergeError\";\n\n constructor(\n readonly source: OpenapiMergeSource,\n readonly service: string,\n detail: string,\n ) {\n super(`${source} openapi for ${service} is not valid YAML: ${detail}`);\n }\n}\n\n/** The `fail` the tree helpers take, for one side of this axis. */\nexport function openapiRefusal(source: OpenapiMergeSource, service: string): (detail: string) => OpenapiMergeError {\n return (detail) => new OpenapiMergeError(source, service, detail);\n}\n"]}
@@ -10,14 +10,15 @@
10
10
  * "which keys are feature-only" is how one of the two starts leaking a marker.
11
11
  */
12
12
  import { isDeepStrictEqual } from "node:util";
13
- import { isMap, isScalar, parseDocument } from "yaml";
13
+ import { isMap, isScalar } from "yaml";
14
14
  // Every `isRecord` below asks one question of a resolved plain tree: is this a
15
15
  // node that can hold OpenAPI keys — an object, not an array?
16
16
  import { isRecord } from "../../kernel/records.js";
17
17
  import { HTTP_METHODS } from "../doc.js";
18
18
  import { FEATURE_ONLY_KEYS, isRemoval, operationBaselineOf, withoutFeatureKeysDeep, withoutOperationBaseline, OPENAPI_BASELINE_KEY, OPENAPI_BASELINES_KEY, } from "../digest.js";
19
- import { setComponentValue } from "./components.js";
20
- import { errorMessage, OpenapiMergeError } from "./error.js";
19
+ import { setKeyed } from "./components.js";
20
+ import { openapiRefusal } from "./error.js";
21
+ import { readTree, writeTree } from "../contract-tree.js";
21
22
  /**
22
23
  * Remove feature-only operation-removal markers — and the root
23
24
  * `x-loam-baselines` record — before a feature contract is used to create a
@@ -30,17 +31,9 @@ import { errorMessage, OpenapiMergeError } from "./error.js";
30
31
  * early returns below.
31
32
  */
32
33
  export function stripOpenapiRemovalMarkers(featureText, service) {
33
- const feature = parseDocument(featureText);
34
- if (feature.errors.length > 0) {
35
- throw new OpenapiMergeError("feature", service, feature.errors[0].message);
36
- }
37
- let plain;
38
- try {
39
- plain = feature.toJS() ?? {};
40
- }
41
- catch (error) {
42
- throw new OpenapiMergeError("feature", service, errorMessage(error));
43
- }
34
+ // One refusal for every branch below, so each fails the same way.
35
+ const fail = openapiRefusal("feature", service);
36
+ const { doc: feature, plain } = readTree(featureText, fail);
44
37
  const hasRecord = isRecord(plain) && OPENAPI_BASELINES_KEY in plain;
45
38
  // Shape from the RESOLVED tree, for the same reason the merge reads it there:
46
39
  // an aliased `paths` or path item is not a map node, and treating it as
@@ -50,7 +43,7 @@ export function stripOpenapiRemovalMarkers(featureText, service) {
50
43
  if (!hasRecord)
51
44
  return featureText;
52
45
  feature.deleteIn([OPENAPI_BASELINES_KEY]);
53
- return serialize(feature, service);
46
+ return writeTree(feature, fail);
54
47
  }
55
48
  const cleaned = {};
56
49
  let stripped = false;
@@ -67,7 +60,7 @@ export function stripOpenapiRemovalMarkers(featureText, service) {
67
60
  // holding an operation shape (a `pathItems` component, a callback) nests
68
61
  // the feature-only keys where the paths walk never looks, and the create
69
62
  // branch publishes this function's output verbatim. The write is
70
- // string-key-matched (setComponentValue) so a numeric component name is
63
+ // string-key-matched (setKeyed) so a numeric component name is
71
64
  // replaced, never duplicated.
72
65
  const comps = plainChild(plain, "components");
73
66
  if (isRecord(comps)) {
@@ -77,7 +70,7 @@ export function stripOpenapiRemovalMarkers(featureText, service) {
77
70
  for (const [name, value] of Object.entries(names)) {
78
71
  const kept = withoutFeatureKeysDeep(value);
79
72
  if (kept !== value) {
80
- setComponentValue(feature, kind, name, kept);
73
+ setKeyed(feature, ["components", kind], name, kept);
81
74
  stripped = true;
82
75
  }
83
76
  }
@@ -87,7 +80,7 @@ export function stripOpenapiRemovalMarkers(featureText, service) {
87
80
  if (!hasRecord)
88
81
  return featureText;
89
82
  feature.deleteIn([OPENAPI_BASELINES_KEY]);
90
- return serialize(feature, service);
83
+ return writeTree(feature, fail);
91
84
  }
92
85
  // Edit the AST in place when every node the strip touches IS a node — the
93
86
  // author's comments, key order and formatting are theirs to keep. An alias
@@ -143,12 +136,9 @@ export function stripOpenapiRemovalMarkers(featureText, service) {
143
136
  feature.deleteIn(["paths", path, m, OPENAPI_BASELINE_KEY]);
144
137
  }
145
138
  }
146
- const remaining = feature.getIn(["paths", path]);
147
139
  // The merge's twin: no OPERATIONS left means the path goes, surviving
148
140
  // path-level keys included — `{}` was only the easy shape of dead.
149
- if (isMap(remaining) && !remaining.items.some((it) => isScalar(it.key) && HTTP_METHODS.has(String(it.key.value)))) {
150
- feature.deleteIn(["paths", path]);
151
- }
141
+ dropPathIfNoOperations(feature, path);
152
142
  }
153
143
  }
154
144
  // The root record goes on BOTH serialization branches: it is feature
@@ -156,15 +146,21 @@ export function stripOpenapiRemovalMarkers(featureText, service) {
156
146
  // only `paths`, so the root key survives either way without this.
157
147
  if (hasRecord)
158
148
  feature.deleteIn([OPENAPI_BASELINES_KEY]);
159
- return serialize(feature, service);
149
+ return writeTree(feature, fail);
160
150
  }
161
- /** One spelling of "hand the document back", so every strip branch fails the same way. */
162
- function serialize(feature, service) {
163
- try {
164
- return feature.toString();
165
- }
166
- catch (error) {
167
- throw new OpenapiMergeError("feature", service, errorMessage(error));
151
+ /**
152
+ * Removing the last method leaves a path the contract still advertises and
153
+ * nothing answers — and `{}` is only the easy shape of that state: a surviving
154
+ * path-LEVEL key (`parameters`, `summary`) kept the path alive with zero
155
+ * operations just as misleadingly. No methods left means the path goes,
156
+ * whatever else survived beside them. One cleanup for both sides of the delta
157
+ * path, so they cannot disagree about it: the merge runs it on the living
158
+ * document, the strip on the feature's.
159
+ */
160
+ export function dropPathIfNoOperations(doc, path) {
161
+ const remaining = doc.getIn(["paths", path]);
162
+ if (isMap(remaining) && !remaining.items.some((it) => isScalar(it.key) && HTTP_METHODS.has(String(it.key.value)))) {
163
+ doc.deleteIn(["paths", path]);
168
164
  }
169
165
  }
170
166
  export function opLabel(before, after, method, path) {
@@ -1 +1 @@
1
- {"version":3,"file":"markers.js","sourceRoot":"","sources":["../../../../src/core/openapi/merge/markers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The keys loam adds to a feature's contract that must never reach the living\n * one: `x-loam-remove`, `x-loam-based-on`, and the root `x-loam-baselines`\n * record.\n *\n * They are gathered here rather than spread through the merge because they are\n * the vocabulary both ends of the delta path share — `stripOpenapiRemovalMarkers`\n * takes them out for the projection a service repository sees, and the merge\n * takes them out again on the way into the living document. Two spellings of\n * \"which keys are feature-only\" is how one of the two starts leaking a marker.\n */\nimport { isDeepStrictEqual } from \"node:util\";\nimport { isMap, isScalar, parseDocument } from \"yaml\";\n// Every `isRecord` below asks one question of a resolved plain tree: is this a\n// node that can hold OpenAPI keys — an object, not an array?\nimport { isRecord } from \"../../kernel/records.js\";\nimport { HTTP_METHODS } from \"../doc.js\";\nimport {\n FEATURE_ONLY_KEYS,\n isRemoval,\n operationBaselineOf,\n withoutFeatureKeysDeep,\n withoutOperationBaseline,\n OPENAPI_BASELINE_KEY,\n OPENAPI_BASELINES_KEY,\n} from \"../digest.js\";\nimport { setComponentValue } from \"./components.js\";\nimport { errorMessage, OpenapiMergeError } from \"./error.js\";\n\n/**\n * Remove feature-only operation-removal markers — and the root\n * `x-loam-baselines` record — before a feature contract is used to create a\n * brand-new living document. Normally coherence refuses the removal shape\n * because there cannot be a removal target; this guard also keeps `--approve`\n * from ever persisting a marker into living docs. The record needs the strip\n * on THIS path even more than the pins do: archive's create branch publishes\n * this function's output verbatim, and a components-only delta has nothing\n * strippable inside `paths` at all — the root key must not ride out on the\n * early returns below.\n */\nexport function stripOpenapiRemovalMarkers(featureText: string, service: string): string {\n const feature = parseDocument(featureText);\n if (feature.errors.length > 0) {\n throw new OpenapiMergeError(\"feature\", service, feature.errors[0]!.message);\n }\n let plain: unknown;\n try {\n plain = feature.toJS() ?? {};\n } catch (error) {\n throw new OpenapiMergeError(\"feature\", service, errorMessage(error));\n }\n const hasRecord = isRecord(plain) && OPENAPI_BASELINES_KEY in plain;\n // Shape from the RESOLVED tree, for the same reason the merge reads it there:\n // an aliased `paths` or path item is not a map node, and treating it as\n // \"nothing to strip\" is how a feature-only marker reached a living contract.\n const paths = plainChild(plain, \"paths\");\n if (!isRecord(paths)) {\n if (!hasRecord) return featureText;\n feature.deleteIn([OPENAPI_BASELINES_KEY]);\n return serialize(feature, service);\n }\n\n const cleaned: Record<string, unknown> = {};\n let stripped = false;\n for (const [path, item] of Object.entries(paths)) {\n const kept = withoutFeatureMarkers(item);\n if (!isDeepStrictEqual(kept, item)) stripped = true;\n // A path the strip emptied goes with its last operation: `\\/x: {}` is a\n // path the contract advertises and nothing answers.\n if (kept !== undefined) cleaned[path] = kept;\n }\n // Component values get the same discipline as path items: a component\n // holding an operation shape (a `pathItems` component, a callback) nests\n // the feature-only keys where the paths walk never looks, and the create\n // branch publishes this function's output verbatim. The write is\n // string-key-matched (setComponentValue) so a numeric component name is\n // replaced, never duplicated.\n const comps = plainChild(plain, \"components\");\n if (isRecord(comps)) {\n for (const [kind, names] of Object.entries(comps)) {\n if (!isRecord(names)) continue;\n for (const [name, value] of Object.entries(names)) {\n const kept = withoutFeatureKeysDeep(value);\n if (kept !== value) {\n setComponentValue(feature, kind, name, kept);\n stripped = true;\n }\n }\n }\n }\n\n if (!stripped) {\n if (!hasRecord) return featureText;\n feature.deleteIn([OPENAPI_BASELINES_KEY]);\n return serialize(feature, service);\n }\n\n // Edit the AST in place when every node the strip touches IS a node — the\n // author's comments, key order and formatting are theirs to keep. An alias\n // cannot be edited in place (there is one shared value behind it, and\n // deleting a key through it would change every use), so those documents get\n // their `paths` rewritten from the resolved tree instead: losing formatting\n // is a cost, shipping the marker is a corruption.\n //\n // The test has to reach the OPERATION nodes too, not just the path items. The\n // in-place branch strips a surviving operation's pin through\n // `[\"paths\", p, m, x-loam-based-on]`, and an aliased operation is exactly one\n // level below where the check used to look — so `deleteIn` walked into a node\n // that is not a collection and threw out of archive as `internal`, on a\n // document whose only fault was writing one operation twice.\n //\n // What disqualifies a method is \"there is something here to strip and I cannot\n // reach it\", not \"this is not a map node\". Only a resolved MAPPING can be a\n // removal marker or carry a pin, so a method spelled as a scalar — `get: ~`,\n // or a string — is nothing the in-place branch would have touched; demanding a\n // map node of it too demoted whole documents to the rewrite branch and cost\n // their author every comment, anchor and hand-chosen key order inside `paths`,\n // in the bytes archive installs as the living contract. What is left is the\n // real hazard: a mapping the AST does not hand back as a map. An alias is the\n // one node kind that resolves to a mapping without being one, and a method\n // whose KEY is an alias is not findable by name at all, so `deleteIn` reports\n // nothing deleted and the marker ships in silence. Both answer `undefined` or\n // a non-map here, and both belong on the safe branch.\n const editable =\n isMap(feature.get(\"paths\")) &&\n Object.entries(paths).every(\n ([p, item]) =>\n isMap(feature.getIn([\"paths\", p])) &&\n (!isRecord(item) ||\n Object.entries(item).every(\n ([m, op]) => !HTTP_METHODS.has(m) || !isRecord(op) || isMap(feature.getIn([\"paths\", p, m])),\n )),\n );\n if (!editable) {\n feature.setIn([\"paths\"], cleaned);\n } else {\n for (const [path, item] of Object.entries(paths)) {\n if (!isRecord(item)) continue;\n for (const [m, value] of Object.entries(item)) {\n // A loam marker at PATH level, deleted here for the same reason the\n // resolved-tree branch drops it: the two branches must agree exactly\n // about what a living contract may carry.\n if (FEATURE_ONLY_KEYS.has(m)) feature.deleteIn([\"paths\", path, m]);\n if (!HTTP_METHODS.has(m)) continue;\n if (isRemoval(value)) feature.deleteIn([\"paths\", path, m]);\n // The surviving operations keep their formatting but lose their pin:\n // the in-place branch must strip everything the resolved-tree branch\n // does, or the two disagree about what a living contract may carry.\n else if (operationBaselineOf(value) !== undefined) {\n feature.deleteIn([\"paths\", path, m, OPENAPI_BASELINE_KEY]);\n }\n }\n const remaining = feature.getIn([\"paths\", path]);\n // The merge's twin: no OPERATIONS left means the path goes, surviving\n // path-level keys included — `{}` was only the easy shape of dead.\n if (isMap(remaining) && !remaining.items.some((it) => isScalar(it.key) && HTTP_METHODS.has(String(it.key.value)))) {\n feature.deleteIn([\"paths\", path]);\n }\n }\n }\n // The root record goes on BOTH serialization branches: it is feature\n // bookkeeping exactly like the pins, and the rewrite branch above replaced\n // only `paths`, so the root key survives either way without this.\n if (hasRecord) feature.deleteIn([OPENAPI_BASELINES_KEY]);\n return serialize(feature, service);\n}\n\n/** One spelling of \"hand the document back\", so every strip branch fails the same way. */\nfunction serialize(feature: ReturnType<typeof parseDocument>, service: string): string {\n try {\n return feature.toString();\n } catch (error) {\n throw new OpenapiMergeError(\"feature\", service, errorMessage(error));\n }\n}\n\nexport function opLabel(before: unknown, after: unknown, method: string, path: string): string {\n const operation = operationIdOf(after) ?? operationIdOf(before);\n return operation !== undefined ? `'${operation}' (${method} ${path})` : `${method} ${path}`;\n}\n\n/** Read an operationId from a YAML or plain operation node. */\nexport function operationIdOf(node: unknown): string | undefined {\n const value = isMap(node)\n ? node.get(\"operationId\")\n : node !== null && typeof node === \"object\"\n ? (node as Record<string, unknown>)[\"operationId\"]\n : undefined;\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\n\n\n/**\n * Copy a path item without anything that belongs to the FEATURE rather than to\n * the contract: removal-marker operations, the `x-loam-based-on` pins on the\n * operations that survive, and a loam marker written at PATH level. All three\n * are instructions to loam, and a living contract that carries any of them is\n * lying to the next reader — and, for the pin, to the next feature's baseline.\n *\n * The path-level case used to fall through: the filter guarded only HTTP\n * methods, so `x-loam-remove: true` beside the methods was copied verbatim into\n * the fleet's living contract, where it also kept the empty-path cleanup from\n * ever seeing an empty path.\n */\nexport function withoutFeatureMarkers(node: unknown): unknown {\n if (node === null || typeof node !== \"object\" || Array.isArray(node)) return node;\n const entries = Object.entries(node as Record<string, unknown>)\n .filter(([key, value]) => !FEATURE_ONLY_KEYS.has(key) && !(HTTP_METHODS.has(key) && isRemoval(value)))\n .map(([key, value]) => [key, HTTP_METHODS.has(key) ? withoutOperationBaseline(value) : value] as const);\n return entries.length === 0 ? undefined : Object.fromEntries(entries);\n}\n\nexport function plainChild(parent: unknown, key: string): unknown {\n return parent !== null && typeof parent === \"object\"\n ? (parent as Record<string, unknown>)[key]\n : undefined;\n}\n"]}
1
+ {"version":3,"file":"markers.js","sourceRoot":"","sources":["../../../../src/core/openapi/merge/markers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The keys loam adds to a feature's contract that must never reach the living\n * one: `x-loam-remove`, `x-loam-based-on`, and the root `x-loam-baselines`\n * record.\n *\n * They are gathered here rather than spread through the merge because they are\n * the vocabulary both ends of the delta path share — `stripOpenapiRemovalMarkers`\n * takes them out for the projection a service repository sees, and the merge\n * takes them out again on the way into the living document. Two spellings of\n * \"which keys are feature-only\" is how one of the two starts leaking a marker.\n */\nimport { isDeepStrictEqual } from \"node:util\";\nimport { isMap, isScalar, type Document } from \"yaml\";\n// Every `isRecord` below asks one question of a resolved plain tree: is this a\n// node that can hold OpenAPI keys — an object, not an array?\nimport { isRecord } from \"../../kernel/records.js\";\nimport { HTTP_METHODS } from \"../doc.js\";\nimport {\n FEATURE_ONLY_KEYS,\n isRemoval,\n operationBaselineOf,\n withoutFeatureKeysDeep,\n withoutOperationBaseline,\n OPENAPI_BASELINE_KEY,\n OPENAPI_BASELINES_KEY,\n} from \"../digest.js\";\nimport { setKeyed } from \"./components.js\";\nimport { openapiRefusal } from \"./error.js\";\nimport { readTree, writeTree } from \"../contract-tree.js\";\n\n/**\n * Remove feature-only operation-removal markers — and the root\n * `x-loam-baselines` record — before a feature contract is used to create a\n * brand-new living document. Normally coherence refuses the removal shape\n * because there cannot be a removal target; this guard also keeps `--approve`\n * from ever persisting a marker into living docs. The record needs the strip\n * on THIS path even more than the pins do: archive's create branch publishes\n * this function's output verbatim, and a components-only delta has nothing\n * strippable inside `paths` at all — the root key must not ride out on the\n * early returns below.\n */\nexport function stripOpenapiRemovalMarkers(featureText: string, service: string): string {\n // One refusal for every branch below, so each fails the same way.\n const fail = openapiRefusal(\"feature\", service);\n const { doc: feature, plain } = readTree(featureText, fail);\n const hasRecord = isRecord(plain) && OPENAPI_BASELINES_KEY in plain;\n // Shape from the RESOLVED tree, for the same reason the merge reads it there:\n // an aliased `paths` or path item is not a map node, and treating it as\n // \"nothing to strip\" is how a feature-only marker reached a living contract.\n const paths = plainChild(plain, \"paths\");\n if (!isRecord(paths)) {\n if (!hasRecord) return featureText;\n feature.deleteIn([OPENAPI_BASELINES_KEY]);\n return writeTree(feature, fail);\n }\n\n const cleaned: Record<string, unknown> = {};\n let stripped = false;\n for (const [path, item] of Object.entries(paths)) {\n const kept = withoutFeatureMarkers(item);\n if (!isDeepStrictEqual(kept, item)) stripped = true;\n // A path the strip emptied goes with its last operation: `\\/x: {}` is a\n // path the contract advertises and nothing answers.\n if (kept !== undefined) cleaned[path] = kept;\n }\n // Component values get the same discipline as path items: a component\n // holding an operation shape (a `pathItems` component, a callback) nests\n // the feature-only keys where the paths walk never looks, and the create\n // branch publishes this function's output verbatim. The write is\n // string-key-matched (setKeyed) so a numeric component name is\n // replaced, never duplicated.\n const comps = plainChild(plain, \"components\");\n if (isRecord(comps)) {\n for (const [kind, names] of Object.entries(comps)) {\n if (!isRecord(names)) continue;\n for (const [name, value] of Object.entries(names)) {\n const kept = withoutFeatureKeysDeep(value);\n if (kept !== value) {\n setKeyed(feature, [\"components\", kind], name, kept);\n stripped = true;\n }\n }\n }\n }\n\n if (!stripped) {\n if (!hasRecord) return featureText;\n feature.deleteIn([OPENAPI_BASELINES_KEY]);\n return writeTree(feature, fail);\n }\n\n // Edit the AST in place when every node the strip touches IS a node — the\n // author's comments, key order and formatting are theirs to keep. An alias\n // cannot be edited in place (there is one shared value behind it, and\n // deleting a key through it would change every use), so those documents get\n // their `paths` rewritten from the resolved tree instead: losing formatting\n // is a cost, shipping the marker is a corruption.\n //\n // The test has to reach the OPERATION nodes too, not just the path items. The\n // in-place branch strips a surviving operation's pin through\n // `[\"paths\", p, m, x-loam-based-on]`, and an aliased operation is exactly one\n // level below where the check used to look — so `deleteIn` walked into a node\n // that is not a collection and threw out of archive as `internal`, on a\n // document whose only fault was writing one operation twice.\n //\n // What disqualifies a method is \"there is something here to strip and I cannot\n // reach it\", not \"this is not a map node\". Only a resolved MAPPING can be a\n // removal marker or carry a pin, so a method spelled as a scalar — `get: ~`,\n // or a string — is nothing the in-place branch would have touched; demanding a\n // map node of it too demoted whole documents to the rewrite branch and cost\n // their author every comment, anchor and hand-chosen key order inside `paths`,\n // in the bytes archive installs as the living contract. What is left is the\n // real hazard: a mapping the AST does not hand back as a map. An alias is the\n // one node kind that resolves to a mapping without being one, and a method\n // whose KEY is an alias is not findable by name at all, so `deleteIn` reports\n // nothing deleted and the marker ships in silence. Both answer `undefined` or\n // a non-map here, and both belong on the safe branch.\n const editable =\n isMap(feature.get(\"paths\")) &&\n Object.entries(paths).every(\n ([p, item]) =>\n isMap(feature.getIn([\"paths\", p])) &&\n (!isRecord(item) ||\n Object.entries(item).every(\n ([m, op]) => !HTTP_METHODS.has(m) || !isRecord(op) || isMap(feature.getIn([\"paths\", p, m])),\n )),\n );\n if (!editable) {\n feature.setIn([\"paths\"], cleaned);\n } else {\n for (const [path, item] of Object.entries(paths)) {\n if (!isRecord(item)) continue;\n for (const [m, value] of Object.entries(item)) {\n // A loam marker at PATH level, deleted here for the same reason the\n // resolved-tree branch drops it: the two branches must agree exactly\n // about what a living contract may carry.\n if (FEATURE_ONLY_KEYS.has(m)) feature.deleteIn([\"paths\", path, m]);\n if (!HTTP_METHODS.has(m)) continue;\n if (isRemoval(value)) feature.deleteIn([\"paths\", path, m]);\n // The surviving operations keep their formatting but lose their pin:\n // the in-place branch must strip everything the resolved-tree branch\n // does, or the two disagree about what a living contract may carry.\n else if (operationBaselineOf(value) !== undefined) {\n feature.deleteIn([\"paths\", path, m, OPENAPI_BASELINE_KEY]);\n }\n }\n // The merge's twin: no OPERATIONS left means the path goes, surviving\n // path-level keys included — `{}` was only the easy shape of dead.\n dropPathIfNoOperations(feature, path);\n }\n }\n // The root record goes on BOTH serialization branches: it is feature\n // bookkeeping exactly like the pins, and the rewrite branch above replaced\n // only `paths`, so the root key survives either way without this.\n if (hasRecord) feature.deleteIn([OPENAPI_BASELINES_KEY]);\n return writeTree(feature, fail);\n}\n\n/**\n * Removing the last method leaves a path the contract still advertises and\n * nothing answers — and `{}` is only the easy shape of that state: a surviving\n * path-LEVEL key (`parameters`, `summary`) kept the path alive with zero\n * operations just as misleadingly. No methods left means the path goes,\n * whatever else survived beside them. One cleanup for both sides of the delta\n * path, so they cannot disagree about it: the merge runs it on the living\n * document, the strip on the feature's.\n */\nexport function dropPathIfNoOperations(doc: Document, path: string): void {\n const remaining = doc.getIn([\"paths\", path]);\n if (isMap(remaining) && !remaining.items.some((it) => isScalar(it.key) && HTTP_METHODS.has(String(it.key.value)))) {\n doc.deleteIn([\"paths\", path]);\n }\n}\n\nexport function opLabel(before: unknown, after: unknown, method: string, path: string): string {\n const operation = operationIdOf(after) ?? operationIdOf(before);\n return operation !== undefined ? `'${operation}' (${method} ${path})` : `${method} ${path}`;\n}\n\n/** Read an operationId from a YAML or plain operation node. */\nexport function operationIdOf(node: unknown): string | undefined {\n const value = isMap(node)\n ? node.get(\"operationId\")\n : node !== null && typeof node === \"object\"\n ? (node as Record<string, unknown>)[\"operationId\"]\n : undefined;\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\n\n\n/**\n * Copy a path item without anything that belongs to the FEATURE rather than to\n * the contract: removal-marker operations, the `x-loam-based-on` pins on the\n * operations that survive, and a loam marker written at PATH level. All three\n * are instructions to loam, and a living contract that carries any of them is\n * lying to the next reader — and, for the pin, to the next feature's baseline.\n *\n * The path-level case used to fall through: the filter guarded only HTTP\n * methods, so `x-loam-remove: true` beside the methods was copied verbatim into\n * the fleet's living contract, where it also kept the empty-path cleanup from\n * ever seeing an empty path.\n */\nexport function withoutFeatureMarkers(node: unknown): unknown {\n if (node === null || typeof node !== \"object\" || Array.isArray(node)) return node;\n const entries = Object.entries(node as Record<string, unknown>)\n .filter(([key, value]) => !FEATURE_ONLY_KEYS.has(key) && !(HTTP_METHODS.has(key) && isRemoval(value)))\n .map(([key, value]) => [key, HTTP_METHODS.has(key) ? withoutOperationBaseline(value) : value] as const);\n return entries.length === 0 ? undefined : Object.fromEntries(entries);\n}\n\nexport function plainChild(parent: unknown, key: string): unknown {\n return parent !== null && typeof parent === \"object\"\n ? (parent as Record<string, unknown>)[key]\n : undefined;\n}\n"]}