@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,21 +1,30 @@
1
1
  /**
2
2
  * The diff computation: join the base fleet (`./base-state.ts`) against the
3
- * working tree (`./current-state.ts`) and say, per service, what changed in
3
+ * working tree (`core/consumers/current.ts`) and say, per service, what changed in
4
4
  * FLEET-MEANINGFUL terms — requirements by identity and digest, operations
5
- * and messages by their join tokens, cross-service joins by tuple — with the
6
- * current consumers of every removal named (`./victims.ts`).
5
+ * and messages by their join tokens and their `$ref`-closure digests,
6
+ * cross-service joins by tuple — with the authored consumers of every
7
+ * removal, deprecation and modification named (`core/consumers/consumers.ts`),
8
+ * and the cross-service joins that appeared or went away (`./joins.ts`).
7
9
  *
8
10
  * Two disciplines rule every branch here. Removal is graded against who still
9
11
  * depends on it: a removal nobody names is a warning, a removal the fleet
10
- * still joins to is an error and sets `breaking`. And "nobody could look" is
12
+ * still joins to is an error and sets `breaking`. A MODIFICATION is never an
13
+ * error, whoever is joined to it: whether a changed schema breaks its readers
14
+ * is a field-level question loam hands to oasdiff on purpose, so the finding
15
+ * names the consumers and the tool, and stops there. And "nobody could look" is
11
16
  * never "nothing changed": an unreadable artifact on EITHER side suspends
12
17
  * that subject's axis — recorded in `unreadable[]`, counted in the summary,
13
18
  * carried into the exit code by the command — instead of letting an
14
19
  * unreadable base read as everything-added or an unreadable working file
15
- * read as everything-removed.
20
+ * read as everything-removed. The one thing that is not a suspension is a
21
+ * contract the base held byte-for-byte: somebody COULD look, at the bytes,
22
+ * and they did not change (`comparable`, `UNTOUCHED_SKIPS`).
16
23
  */
17
24
  import { requirementDigest } from "../document/spec.js";
18
- import { consumerJoinChanges } from "./victims.js";
25
+ import { rawServiceId } from "../kernel/ids/service.js";
26
+ import { consumerStance, scanDetails } from "../consumers/scan.js";
27
+ import { consumerJoinChanges } from "./joins.js";
19
28
  /** Severity is a fact about the code, not a call-site choice — one table, no drift. */
20
29
  const DIFF_SEVERITY = {
21
30
  "diff.service-added": "ok",
@@ -27,9 +36,11 @@ const DIFF_SEVERITY = {
27
36
  "diff.op-removed": "warn",
28
37
  "diff.op-removed-consumed": "error",
29
38
  "diff.op-deprecated": "warn",
39
+ "diff.op-modified": "warn",
30
40
  "diff.message-added": "ok",
31
41
  "diff.message-removed": "warn",
32
42
  "diff.message-removed-consumed": "error",
43
+ "diff.message-modified": "warn",
33
44
  "diff.consumer-added": "ok",
34
45
  "diff.consumer-removed": "ok",
35
46
  };
@@ -37,15 +48,43 @@ function found(code, message, details) {
37
48
  const carry = details !== undefined && details.length > 0;
38
49
  return { severity: DIFF_SEVERITY[code], code, message, ...(carry ? { details } : {}) };
39
50
  }
40
- /** A side the service does not exist on: absence everywhere, honestly. */
51
+ /**
52
+ * A side the service does not exist on: absence everywhere, honestly. The id is
53
+ * a key of one of the two sides' maps, each keyed by a directory's leaf name.
54
+ */
41
55
  const ABSENT = { kind: "absent" };
42
- const emptyState = (id) => ({ id, dir: "", spec: ABSENT, archSpec: ABSENT, openapi: ABSENT, asyncapi: ABSENT });
56
+ const emptyState = (id) => ({ id: rawServiceId(id), dir: "", spec: ABSENT, archSpec: ABSENT, openapi: ABSENT, asyncapi: ABSENT });
57
+ /**
58
+ * The axes on which an unreadable document the branch left byte-identical is
59
+ * SKIPPED rather than suspended: the two contracts. Identical bytes are a
60
+ * proof, not a guess — the same parser over the same bytes cannot read
61
+ * differently on the two sides, so there is nothing of this document's own
62
+ * to diff, and suspending it anyway made every `loam diff` exit 1 over a
63
+ * fleet that holds one AsyncAPI 2.x file (unreadable by policy since
64
+ * core/asyncapi/read.ts refused pre-3.0), whatever the branch changed.
65
+ *
66
+ * Not the spec axes, and that is the victims scan's doing, not an oversight:
67
+ * an unreadable spec.md or arch.spec.md is also the document ANOTHER
68
+ * service's removal could not be checked against (`core/consumers/consumers.ts`'s
69
+ * `scanRequirements`), and its `unreadable[]` entry here is what carries that
70
+ * suspension to exit 1 (`opsDiff`'s removal branch says so). No scan reads a
71
+ * contract it could not parse as evidence of anything — `producers` takes
72
+ * positive evidence only — so skipping one hides no other answer.
73
+ */
74
+ const UNTOUCHED_SKIPS = new Set(["openapi", "asyncapi"]);
43
75
  /**
44
76
  * Guard one axis pair: record unreadable sides on the sink and say whether
45
77
  * the axis may be compared at all. Reading one good side against a suspended
46
- * one would grade absence it never proved.
78
+ * one would grade absence it never proved. A contract unreadable on BOTH
79
+ * sides with one byte hash is the branch's untouched file: not compared and
80
+ * not recorded either (`UNTOUCHED_SKIPS`). A changed one, or one only one
81
+ * side cannot read, still suspends.
47
82
  */
48
83
  function comparable(sink, axis, pair) {
84
+ const { base, current } = pair;
85
+ if (UNTOUCHED_SKIPS.has(axis) && base.kind === "unreadable" && current.kind === "unreadable" &&
86
+ base.bytes !== undefined && base.bytes === current.bytes)
87
+ return false;
49
88
  for (const side of ["base", "current"]) {
50
89
  const state = pair[side];
51
90
  if (state.kind === "unreadable")
@@ -92,6 +131,13 @@ function requirementsDiff(sink, pair) {
92
131
  sink.findings.push(found("diff.requirement-removed", `living requirement removed since base: '${reqLabel(r)}'`));
93
132
  }
94
133
  }
134
+ /**
135
+ * The second half of every modification message. loam joins consumers to the
136
+ * thing that moved; it does not read what a schema SAYS (core/brief/unchecked.ts
137
+ * lists that as unchecked), so a breaking rename and a new optional field are
138
+ * the same finding here — the specialist diff tells them apart.
139
+ */
140
+ const notGraded = (tool) => `loam does not grade whether the change is breaking — run ${tool} on the two states of this contract`;
95
141
  async function opsDiff(sink, pair, victims) {
96
142
  if (!comparable(sink, "openapi", { base: pair.base.openapi, current: pair.current.openapi }))
97
143
  return;
@@ -104,11 +150,17 @@ async function opsDiff(sink, pair, victims) {
104
150
  }
105
151
  else if (!before.deprecated && op.deprecated) {
106
152
  const scan = await victims.opConsumers(sink.id, id);
107
- // Three honest phrasings — never a confident negative over a scan that skipped an unreadable consumer document.
108
- const stance = scan.victims.length > 0 ? `${scan.victims.length} current consumer(s) still name it`
109
- : scan.suspended.length > 0 ? "whether current consumers name it could not be fully answered"
110
- : "no current consumer names it";
111
- sink.findings.push(found("diff.op-deprecated", `operation '${id}' is deprecated since base — ${stance}`, [...scan.victims, ...scan.suspended]));
153
+ sink.findings.push(found("diff.op-deprecated", `operation '${id}' is deprecated since base — ${consumerStance(scan)}`, scanDetails(scan)));
154
+ }
155
+ else if (before.closureDigest !== op.closureDigest || before.path !== op.path || before.method !== op.method) {
156
+ // `else`, because setting the flag moves the digest by itself (it is a
157
+ // key of the node): a deprecation already names the same consumers, and
158
+ // a second finding over them would say nothing new. The digest covers
159
+ // the `$ref` closure — a field renamed in a shared component is exactly
160
+ // the change this code exists for, and the operation's own bytes never
161
+ // move for it. A relocated slot is a change too, though the node is not.
162
+ const scan = await victims.opConsumers(sink.id, id);
163
+ sink.findings.push(found("diff.op-modified", `operation '${id}' (${op.method.toUpperCase()} ${op.path}) changed since base, in its own definition or a schema it references — ${consumerStance(scan)}; ${notGraded("`oasdiff breaking`")}`, scanDetails(scan)));
112
164
  }
113
165
  }
114
166
  for (const [id] of [...base].sort(byKey)) {
@@ -116,16 +168,18 @@ async function opsDiff(sink, pair, victims) {
116
168
  continue;
117
169
  const scan = await victims.opConsumers(sink.id, id);
118
170
  if (scan.victims.length > 0) {
119
- sink.findings.push(found("diff.op-removed-consumed", `operation '${id}' was removed since base and the current fleet still names it — these joins break`, [...scan.victims, ...scan.suspended]));
171
+ sink.findings.push(found("diff.op-removed-consumed", `operation '${id}' was removed since base and the current fleet still names it — these joins break`, scanDetails(scan)));
120
172
  }
121
173
  else if (scan.suspended.length > 0) {
122
- // "Nobody could be scanned" is not "nobody names it" — the consumer's own
123
- // unreadable[] entry already carries the run to exit 1; this message must
124
- // not assert a negative the scan never proved.
125
- sink.findings.push(found("diff.op-removed", `operation '${id}' was removed since base; whether the current fleet still names it could NOT be fully answered — ${scan.suspended.length} consumer document(s) were not scannable`, scan.suspended));
174
+ // "Nobody could be checked" is not "nobody names it", so this message
175
+ // must not assert a negative the scan never proved. An unreadable
176
+ // consumer document carries the run to exit 1 through its own
177
+ // unreadable[] entry; a caller drawn with no op does not — it stays this
178
+ // warning, so a seeded, op-less map does not fail every removal.
179
+ sink.findings.push(found("diff.op-removed", `operation '${id}' was removed since base; whether the current fleet still names it could NOT be fully answered — ${scan.suspended.length} possible consumer(s) could not be checked`, scanDetails(scan)));
126
180
  }
127
181
  else {
128
- sink.findings.push(found("diff.op-removed", `operation '${id}' was removed since base; no current landscape edge or living requirement names it`));
182
+ sink.findings.push(found("diff.op-removed", `operation '${id}' was removed since base; no current edge, requirement or use-case hop names it`));
129
183
  }
130
184
  }
131
185
  }
@@ -134,33 +188,46 @@ async function messagesDiff(sink, pair, victims) {
134
188
  return;
135
189
  const base = firstWins(valueOr(pair.base.asyncapi), (m) => m.name);
136
190
  const current = firstWins(valueOr(pair.current.asyncapi), (m) => m.name);
137
- for (const [name] of [...current].sort(byKey)) {
138
- if (!base.has(name))
191
+ for (const [name, message] of [...current].sort(byKey)) {
192
+ const before = base.get(name);
193
+ if (before === undefined) {
139
194
  sink.findings.push(found("diff.message-added", `message '${name}' added since base`));
195
+ }
196
+ else if (before.closureDigest !== message.closureDigest) {
197
+ const scan = await victims.messageConsumers(sink.id, name);
198
+ sink.findings.push(found("diff.message-modified", `message '${name}' changed since base, in its declaration or a schema its payload references — ${consumerStance(scan)}; ${notGraded("`asyncapi diff`")}`, scanDetails(scan)));
199
+ }
140
200
  }
141
201
  for (const [name] of [...base].sort(byKey)) {
142
202
  if (current.has(name))
143
203
  continue;
144
- // Fleet-global join: a name another current service still declares is not
145
- // orphaned (positive evidence only — an unreadable contract never vouches).
146
- const elsewhere = victims.declarers(name).filter((d) => d !== sink.id);
147
- if (elsewhere.length > 0) {
148
- sink.findings.push(found("diff.message-removed", `message '${name}' was removed since base; still declared by ${elsewhere.join(", ")}`));
204
+ // Fleet-global join: a name another current service still SENDS is not
205
+ // orphaned (positive evidence only — an unreadable contract never vouches,
206
+ // and a service that only receives it is one of its consumers, never its
207
+ // producer: `producers` says why that distinction is the gate's too). Nor
208
+ // is one an #external element still publishes, by validate's own rule
209
+ // (`externalProducer` says why): counting the fleet's senders alone made
210
+ // this removal an error that the next `validate --all` called clean.
211
+ const elsewhere = (await victims.producers(name)).filter((d) => d !== sink.id);
212
+ const outside = await victims.externalProducer(name);
213
+ const senders = outside === null ? elsewhere : [...elsewhere, `${outside} (#external)`];
214
+ if (senders.length > 0) {
215
+ sink.findings.push(found("diff.message-removed", `message '${name}' was removed since base; still sent by ${senders.join(", ")}`));
149
216
  continue;
150
217
  }
151
218
  const scan = await victims.messageConsumers(sink.id, name);
152
219
  if (scan.victims.length > 0) {
153
- sink.findings.push(found("diff.message-removed-consumed", `message '${name}' was removed since base and the current fleet still consumes it — these joins break`, [...scan.victims, ...scan.suspended]));
220
+ sink.findings.push(found("diff.message-removed-consumed", `message '${name}' was removed since base and the current fleet still consumes it — these joins break`, scanDetails(scan)));
154
221
  }
155
222
  else if (scan.suspended.length > 0) {
156
- sink.findings.push(found("diff.message-removed", `message '${name}' was removed since base; whether anything still consumes it could NOT be fully answered — ${scan.suspended.length} consumer document(s) were not scannable`, scan.suspended));
223
+ sink.findings.push(found("diff.message-removed", `message '${name}' was removed since base; whether anything still consumes it could NOT be fully answered — ${scan.suspended.length} consumer document(s) were not scannable`, scanDetails(scan)));
157
224
  }
158
225
  else {
159
226
  sink.findings.push(found("diff.message-removed", `message '${name}' was removed since base; nothing currently consumes it`));
160
227
  }
161
228
  }
162
229
  }
163
- /** The graded join changes (`victims.ts`) as findings on each PROVIDER — the service whose surface the join lands on. */
230
+ /** The graded join changes (`./joins.ts`) as findings on each PROVIDER — the service whose surface the join lands on. */
164
231
  function consumerFindings(input) {
165
232
  const changes = consumerJoinChanges({
166
233
  base: input.base.services,
@@ -233,7 +300,9 @@ export async function computeDiff(input) {
233
300
  summary: {
234
301
  added: count((f) => f.code.endsWith("-added")),
235
302
  removed: count((f) => f.code.endsWith("-removed") || f.code.endsWith("-removed-consumed")),
236
- modified: count((f) => f.code === "diff.requirement-modified"),
303
+ // Requirements, operations and messages alike: a reader scanning the one
304
+ // summary line must not see "0 modified" over a PR that renamed a field.
305
+ modified: count((f) => f.code.endsWith("-modified")),
237
306
  deprecated: count((f) => f.code === "diff.op-deprecated"),
238
307
  unreadable: services.reduce((n, s) => n + s.unreadable.length, 0) +
239
308
  services.filter((s) => s.ambiguous !== undefined).length,
@@ -1 +1 @@
1
- {"version":3,"file":"semantic.js","sourceRoot":"","sources":["../../../src/core/diff/semantic.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The diff computation: join the base fleet (`./base-state.ts`) against the\n * working tree (`./current-state.ts`) and say, per service, what changed in\n * FLEET-MEANINGFUL terms — requirements by identity and digest, operations\n * and messages by their join tokens, cross-service joins by tuple — with the\n * current consumers of every removal named (`./victims.ts`).\n *\n * Two disciplines rule every branch here. Removal is graded against who still\n * depends on it: a removal nobody names is a warning, a removal the fleet\n * still joins to is an error and sets `breaking`. And \"nobody could look\" is\n * never \"nothing changed\": an unreadable artifact on EITHER side suspends\n * that subject's axis — recorded in `unreadable[]`, counted in the summary,\n * carried into the exit code by the command — instead of letting an\n * unreadable base read as everything-added or an unreadable working file\n * read as everything-removed.\n */\nimport { requirementDigest, type Requirement } from \"../document/spec.js\";\nimport type { Severity, Finding } from \"../vocabulary/report.js\";\nimport type { AxisState, BaseFleet, ServiceState } from \"./base-state.js\";\nimport type { CurrentFleet } from \"./current-state.js\";\nimport { consumerJoinChanges, type VictimIndex } from \"./victims.js\";\n\n/**\n * The stable finding codes `loam diff` emits. Codes are the machine contract;\n * prose may be reworded, these may not. Documented (backticked) in the\n * generated AGENTS.md's diff section — codes-drift enforces that.\n */\nexport type DiffCode =\n /** A `services/<id>/` directory exists now that did not at the base ref. */\n | \"diff.service-added\"\n /** A service present at the base ref is gone from the working tree. */\n | \"diff.service-removed\"\n /** A living requirement (by `Requirement-ID:`, else heading) not present at base. */\n | \"diff.requirement-added\"\n /** A base requirement no longer in the living spec. */\n | \"diff.requirement-removed\"\n /** Same requirement identity on both sides, `requirementDigest` moved — rebase pins never move it. */\n | \"diff.requirement-modified\"\n /** An operationId in the living contract absent at base. */\n | \"diff.op-added\"\n /** A base operationId gone from the living contract, and no current consumer names it. */\n | \"diff.op-removed\"\n /** Removed since base AND the current fleet still names it — landscape edge or foreign living requirement; details list the victims. Sets `breaking`. */\n | \"diff.op-removed-consumed\"\n /** `deprecated: true` introduced since base; details name the current consumers. */\n | \"diff.op-deprecated\"\n /** An AsyncAPI message name absent at base. */\n | \"diff.message-added\"\n /** A base message gone, nothing currently consumes it (or another service still declares it — the details say). */\n | \"diff.message-removed\"\n /** Removed since base AND still consumed — a `Consumes:` line or a consumes-edge; details list the victims. Sets `breaking`. */\n | \"diff.message-removed-consumed\"\n /** A cross-service join appeared since base: another service's living requirement now names this service's operation or message. */\n | \"diff.consumer-added\"\n /** A cross-service join present at base is gone. */\n | \"diff.consumer-removed\";\n\n/** Severity is a fact about the code, not a call-site choice — one table, no drift. */\nconst DIFF_SEVERITY: Record<DiffCode, Severity> = {\n \"diff.service-added\": \"ok\",\n \"diff.service-removed\": \"warn\",\n \"diff.requirement-added\": \"ok\",\n \"diff.requirement-removed\": \"warn\",\n \"diff.requirement-modified\": \"ok\",\n \"diff.op-added\": \"ok\",\n \"diff.op-removed\": \"warn\",\n \"diff.op-removed-consumed\": \"error\",\n \"diff.op-deprecated\": \"warn\",\n \"diff.message-added\": \"ok\",\n \"diff.message-removed\": \"warn\",\n \"diff.message-removed-consumed\": \"error\",\n \"diff.consumer-added\": \"ok\",\n \"diff.consumer-removed\": \"ok\",\n};\n\nfunction found(code: DiffCode, message: string, details?: string[]): Finding {\n const carry = details !== undefined && details.length > 0;\n return { severity: DIFF_SEVERITY[code], code, message, ...(carry ? { details } : {}) };\n}\n\n/** One suspended axis: which side could not be read, and why. */\nexport interface UnreadableEntry {\n side: \"base\" | \"current\";\n axis: \"spec\" | \"archSpec\" | \"openapi\" | \"asyncapi\";\n path: string;\n error: string;\n}\n\nexport interface ServiceDiff {\n id: string;\n change: \"added\" | \"removed\" | \"changed\" | \"unchanged\";\n findings: Finding[];\n unreadable: UnreadableEntry[];\n /** Directories claiming this id when more than one does, on either side — every finding suspended. */\n ambiguous?: string[];\n}\n\nexport interface FleetDiff {\n services: ServiceDiff[];\n /** True iff any error-severity finding — a removal the fleet still consumes. */\n breaking: boolean;\n /** `unreadable` counts suspended axes plus ambiguous base identities: places nobody could look. */\n summary: { added: number; removed: number; modified: number; deprecated: number; unreadable: number };\n}\n\n/** A side the service does not exist on: absence everywhere, honestly. */\nconst ABSENT = { kind: \"absent\" } as const;\nconst emptyState = (id: string): ServiceState => ({ id, dir: \"\", spec: ABSENT, archSpec: ABSENT, openapi: ABSENT, asyncapi: ABSENT });\n\n/**\n * Guard one axis pair: record unreadable sides on the sink and say whether\n * the axis may be compared at all. Reading one good side against a suspended\n * one would grade absence it never proved.\n */\nfunction comparable(sink: ServiceDiff, axis: UnreadableEntry[\"axis\"], pair: { base: AxisState<unknown>; current: AxisState<unknown> }): boolean {\n for (const side of [\"base\", \"current\"] as const) {\n const state = pair[side];\n if (state.kind === \"unreadable\") sink.unreadable.push({ side, axis, path: state.path, error: state.error });\n }\n return pair.base.kind !== \"unreadable\" && pair.current.kind !== \"unreadable\";\n}\n\nconst valueOr = <T>(axis: AxisState<T[]>): T[] => (axis.kind === \"read\" ? axis.value : []);\n\n/** Identity: `Requirement-ID:` when authored, the heading otherwise — the same join `loam rebase` pins against. */\nconst reqKey = (r: Requirement): string => (r.id !== undefined ? `id:${r.id}` : `name:${r.name}`);\nconst reqLabel = (r: Requirement): string => (r.id !== undefined ? `${r.name} [${r.id}]` : r.name);\n\n/** First declaration wins a duplicated key — the same arbitrary-but-stated pick every join on these axes makes. */\nfunction firstWins<T>(items: T[], key: (item: T) => string): Map<string, T> {\n const map = new Map<string, T>();\n for (const item of items) if (!map.has(key(item))) map.set(key(item), item);\n return map;\n}\n\n/** Entry comparator by map key — explicit, because the default sort stringifies whole entries. */\nconst byKey = <T>(a: [string, T], b: [string, T]): number => (a[0] < b[0] ? -1 : 1);\n\nfunction requirementsDiff(sink: ServiceDiff, pair: { base: ServiceState; current: ServiceState }): void {\n // v1 diffs spec.md's requirements only; arch.spec.md is read for the\n // Consumes: join but its requirement inventory is deferred (the two axes\n // are separate namespaces, and diffing both doubles the surface). Both\n // pairs are guarded BEFORE the early return, so a service whose two spec\n // files are both unreadable reports both suspensions, not just the first.\n const specComparable = comparable(sink, \"spec\", { base: pair.base.spec, current: pair.current.spec });\n comparable(sink, \"archSpec\", { base: pair.base.archSpec, current: pair.current.archSpec });\n if (!specComparable) return;\n const base = firstWins(valueOr(pair.base.spec), reqKey);\n const current = firstWins(valueOr(pair.current.spec), reqKey);\n for (const [key, r] of [...current].sort(byKey)) {\n const before = base.get(key);\n if (before === undefined) sink.findings.push(found(\"diff.requirement-added\", `living requirement added since base: '${reqLabel(r)}'`));\n else if (requirementDigest(before) !== requirementDigest(r)) {\n sink.findings.push(found(\"diff.requirement-modified\", `living requirement modified since base: '${reqLabel(r)}'`));\n }\n }\n for (const [key, r] of [...base].sort(byKey)) {\n if (!current.has(key)) sink.findings.push(found(\"diff.requirement-removed\", `living requirement removed since base: '${reqLabel(r)}'`));\n }\n}\n\nasync function opsDiff(sink: ServiceDiff, pair: { base: ServiceState; current: ServiceState }, victims: VictimIndex): Promise<void> {\n if (!comparable(sink, \"openapi\", { base: pair.base.openapi, current: pair.current.openapi })) return;\n const base = firstWins(valueOr(pair.base.openapi), (op) => op.id);\n const current = firstWins(valueOr(pair.current.openapi), (op) => op.id);\n for (const [id, op] of [...current].sort(byKey)) {\n const before = base.get(id);\n if (before === undefined) {\n sink.findings.push(found(\"diff.op-added\", `operation '${id}' added since base (${op.method.toUpperCase()} ${op.path})`));\n } else if (!before.deprecated && op.deprecated) {\n const scan = await victims.opConsumers(sink.id, id);\n // Three honest phrasings — never a confident negative over a scan that skipped an unreadable consumer document.\n const stance = scan.victims.length > 0 ? `${scan.victims.length} current consumer(s) still name it`\n : scan.suspended.length > 0 ? \"whether current consumers name it could not be fully answered\"\n : \"no current consumer names it\";\n sink.findings.push(found(\"diff.op-deprecated\", `operation '${id}' is deprecated since base — ${stance}`, [...scan.victims, ...scan.suspended]));\n }\n }\n for (const [id] of [...base].sort(byKey)) {\n if (current.has(id)) continue;\n const scan = await victims.opConsumers(sink.id, id);\n if (scan.victims.length > 0) {\n sink.findings.push(found(\"diff.op-removed-consumed\", `operation '${id}' was removed since base and the current fleet still names it — these joins break`, [...scan.victims, ...scan.suspended]));\n } else if (scan.suspended.length > 0) {\n // \"Nobody could be scanned\" is not \"nobody names it\" — the consumer's own\n // unreadable[] entry already carries the run to exit 1; this message must\n // not assert a negative the scan never proved.\n sink.findings.push(found(\"diff.op-removed\", `operation '${id}' was removed since base; whether the current fleet still names it could NOT be fully answered — ${scan.suspended.length} consumer document(s) were not scannable`, scan.suspended));\n } else {\n sink.findings.push(found(\"diff.op-removed\", `operation '${id}' was removed since base; no current landscape edge or living requirement names it`));\n }\n }\n}\n\nasync function messagesDiff(sink: ServiceDiff, pair: { base: ServiceState; current: ServiceState }, victims: VictimIndex): Promise<void> {\n if (!comparable(sink, \"asyncapi\", { base: pair.base.asyncapi, current: pair.current.asyncapi })) return;\n const base = firstWins(valueOr(pair.base.asyncapi), (m) => m.name);\n const current = firstWins(valueOr(pair.current.asyncapi), (m) => m.name);\n for (const [name] of [...current].sort(byKey)) {\n if (!base.has(name)) sink.findings.push(found(\"diff.message-added\", `message '${name}' added since base`));\n }\n for (const [name] of [...base].sort(byKey)) {\n if (current.has(name)) continue;\n // Fleet-global join: a name another current service still declares is not\n // orphaned (positive evidence only — an unreadable contract never vouches).\n const elsewhere = victims.declarers(name).filter((d) => d !== sink.id);\n if (elsewhere.length > 0) {\n sink.findings.push(found(\"diff.message-removed\", `message '${name}' was removed since base; still declared by ${elsewhere.join(\", \")}`));\n continue;\n }\n const scan = await victims.messageConsumers(sink.id, name);\n if (scan.victims.length > 0) {\n sink.findings.push(found(\"diff.message-removed-consumed\", `message '${name}' was removed since base and the current fleet still consumes it — these joins break`, [...scan.victims, ...scan.suspended]));\n } else if (scan.suspended.length > 0) {\n sink.findings.push(found(\"diff.message-removed\", `message '${name}' was removed since base; whether anything still consumes it could NOT be fully answered — ${scan.suspended.length} consumer document(s) were not scannable`, scan.suspended));\n } else {\n sink.findings.push(found(\"diff.message-removed\", `message '${name}' was removed since base; nothing currently consumes it`));\n }\n }\n}\n\n/** The graded join changes (`victims.ts`) as findings on each PROVIDER — the service whose surface the join lands on. */\nfunction consumerFindings(input: { base: BaseFleet; current: CurrentFleet }): Map<string, Finding[]> {\n const changes = consumerJoinChanges({\n base: input.base.services,\n baseAmbiguous: new Set(input.base.ambiguous.keys()),\n current: input.current.services,\n currentAmbiguous: new Set(input.current.ambiguous.keys()),\n });\n const byProvider = new Map<string, Finding[]>();\n for (const { t, change } of changes) {\n const what = t.kind === \"op\" ? `operation '${t.token}'` : `message '${t.token}'`;\n const message = change === \"added\"\n ? `${t.consumer}'s living requirements now name this service's ${what} — a cross-service join appeared since base`\n : `${t.consumer}'s living requirements no longer name this service's ${what} — a cross-service join went away`;\n // The call sits directly in the push — assigned to a `const x = found(…)`\n // it reads as an arrow-function DECLARATION to the stable-code collector,\n // and both consumer codes silently leave the guarded set.\n byProvider.set(t.provider, [\n ...(byProvider.get(t.provider) ?? []),\n found(change === \"added\" ? \"diff.consumer-added\" : \"diff.consumer-removed\", message),\n ]);\n }\n return byProvider;\n}\n\nasync function diffService(unit: { id: string; base?: ServiceState; current?: ServiceState; victims: VictimIndex }): Promise<ServiceDiff> {\n const sink: ServiceDiff = { id: unit.id, change: \"unchanged\", findings: [], unreadable: [] };\n if (unit.base === undefined) sink.findings.push(found(\"diff.service-added\", `service '${unit.id}' added since base`));\n if (unit.current === undefined && unit.base !== undefined) {\n sink.findings.push(found(\"diff.service-removed\", `service '${unit.id}' (${unit.base.dir}) is gone from the working tree`));\n }\n const pair = { base: unit.base ?? emptyState(unit.id), current: unit.current ?? emptyState(unit.id) };\n requirementsDiff(sink, pair);\n await opsDiff(sink, pair, unit.victims);\n await messagesDiff(sink, pair, unit.victims);\n return sink;\n}\n\nexport async function computeDiff(input: { base: BaseFleet; current: CurrentFleet; victims: VictimIndex }): Promise<FleetDiff> {\n const sides = [input.base.services, input.base.ambiguous, input.current.services, input.current.ambiguous];\n const ids = [...new Set(sides.flatMap((side) => [...side.keys()]))].sort();\n const joins = consumerFindings({ base: input.base, current: input.current });\n const services: ServiceDiff[] = [];\n // Sequential on purpose: the report's order IS the contract (identical\n // states must emit identical bytes), the work per service is in-memory\n // joins, and the one expensive read — the victim index's landscape — is\n // memoised behind its first ask, so a fan-out would buy nothing.\n for (const id of ids) {\n const claimants = [...(input.base.ambiguous.get(id) ?? []), ...(input.current.ambiguous.get(id) ?? [])];\n if (claimants.length > 0) {\n // An id two directories claim — either side — has no single state to\n // diff; the whole subject suspends (containment), and the live walk's\n // `subsystem.name-collision` names the same defect for validate.\n services.push({ id, change: \"changed\", findings: [], unreadable: [], ambiguous: claimants });\n continue;\n }\n const sink = await diffService({ id, base: input.base.services.get(id), current: input.current.services.get(id), victims: input.victims });\n sink.findings.push(...(joins.get(id) ?? []));\n if (input.base.services.has(id) && !input.current.services.has(id)) sink.change = \"removed\";\n else if (!input.base.services.has(id)) sink.change = \"added\";\n else sink.change = sink.findings.length > 0 || sink.unreadable.length > 0 ? \"changed\" : \"unchanged\";\n services.push(sink);\n }\n const all = services.flatMap((s) => s.findings);\n const count = (codes: (f: Finding) => boolean): number => all.filter(codes).length;\n return {\n services,\n breaking: all.some((f) => f.severity === \"error\"),\n summary: {\n added: count((f) => f.code.endsWith(\"-added\")),\n removed: count((f) => f.code.endsWith(\"-removed\") || f.code.endsWith(\"-removed-consumed\")),\n modified: count((f) => f.code === \"diff.requirement-modified\"),\n deprecated: count((f) => f.code === \"diff.op-deprecated\"),\n unreadable:\n services.reduce((n, s) => n + s.unreadable.length, 0) +\n services.filter((s) => s.ambiguous !== undefined).length,\n },\n };\n}\n"]}
1
+ {"version":3,"file":"semantic.js","sourceRoot":"","sources":["../../../src/core/diff/semantic.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The diff computation: join the base fleet (`./base-state.ts`) against the\n * working tree (`core/consumers/current.ts`) and say, per service, what changed in\n * FLEET-MEANINGFUL terms — requirements by identity and digest, operations\n * and messages by their join tokens and their `$ref`-closure digests,\n * cross-service joins by tuple — with the authored consumers of every\n * removal, deprecation and modification named (`core/consumers/consumers.ts`),\n * and the cross-service joins that appeared or went away (`./joins.ts`).\n *\n * Two disciplines rule every branch here. Removal is graded against who still\n * depends on it: a removal nobody names is a warning, a removal the fleet\n * still joins to is an error and sets `breaking`. A MODIFICATION is never an\n * error, whoever is joined to it: whether a changed schema breaks its readers\n * is a field-level question loam hands to oasdiff on purpose, so the finding\n * names the consumers and the tool, and stops there. And \"nobody could look\" is\n * never \"nothing changed\": an unreadable artifact on EITHER side suspends\n * that subject's axis — recorded in `unreadable[]`, counted in the summary,\n * carried into the exit code by the command — instead of letting an\n * unreadable base read as everything-added or an unreadable working file\n * read as everything-removed. The one thing that is not a suspension is a\n * contract the base held byte-for-byte: somebody COULD look, at the bytes,\n * and they did not change (`comparable`, `UNTOUCHED_SKIPS`).\n */\nimport { requirementDigest, type Requirement } from \"../document/spec.js\";\nimport type { Severity, Finding } from \"../vocabulary/report.js\";\nimport type { BaseFleet } from \"./base-state.js\";\nimport { rawServiceId } from \"../kernel/ids/service.js\";\nimport type { AxisState, ServiceState } from \"../consumers/state.js\";\nimport type { CurrentFleet } from \"../consumers/current.js\";\nimport { consumerStance, scanDetails } from \"../consumers/scan.js\";\nimport type { ConsumerIndex } from \"../consumers/consumers.js\";\nimport { consumerJoinChanges } from \"./joins.js\";\n\n/**\n * The stable finding codes `loam diff` emits. Codes are the machine contract;\n * prose may be reworded, these may not. Documented (backticked) in the\n * generated AGENTS.md's diff section — codes-drift enforces that.\n */\nexport type DiffCode =\n /** A `services/<id>/` directory exists now that did not at the base ref. */\n | \"diff.service-added\"\n /** A service present at the base ref is gone from the working tree. */\n | \"diff.service-removed\"\n /** A living requirement (by `Requirement-ID:`, else heading) not present at base. */\n | \"diff.requirement-added\"\n /** A base requirement no longer in the living spec. */\n | \"diff.requirement-removed\"\n /** Same requirement identity on both sides, `requirementDigest` moved — rebase pins never move it. */\n | \"diff.requirement-modified\"\n /** An operationId in the living contract absent at base. */\n | \"diff.op-added\"\n /** A base operationId gone from the living contract, and no current consumer names it. */\n | \"diff.op-removed\"\n /** Removed since base AND the current fleet still names it — landscape edge or foreign living requirement; details list the victims. Sets `breaking`. */\n | \"diff.op-removed-consumed\"\n /** `deprecated: true` introduced since base; details name the current consumers. */\n | \"diff.op-deprecated\"\n /** Same operationId on both sides, and its node or anything its internal `$ref`s reach moved; details name the authored consumers. Whether it BREAKS them is oasdiff's question, not loam's. */\n | \"diff.op-modified\"\n /** An AsyncAPI message name absent at base. */\n | \"diff.message-added\"\n /** A base message gone, nothing currently consumes it (or another service still sends it — the message says). */\n | \"diff.message-removed\"\n /** Removed since base AND still consumed — a `Consumes:` line or a consumes-edge; details list the victims. Sets `breaking`. */\n | \"diff.message-removed-consumed\"\n /** Same message name on both sides, and its declaration or anything its internal `$ref`s reach moved; details name the authored consumers. Whether it BREAKS them is not graded. */\n | \"diff.message-modified\"\n /** A cross-service join appeared since base: another service's living requirement now names this service's operation or message. */\n | \"diff.consumer-added\"\n /** A cross-service join present at base is gone. */\n | \"diff.consumer-removed\";\n\n/** Severity is a fact about the code, not a call-site choice — one table, no drift. */\nconst DIFF_SEVERITY: Record<DiffCode, Severity> = {\n \"diff.service-added\": \"ok\",\n \"diff.service-removed\": \"warn\",\n \"diff.requirement-added\": \"ok\",\n \"diff.requirement-removed\": \"warn\",\n \"diff.requirement-modified\": \"ok\",\n \"diff.op-added\": \"ok\",\n \"diff.op-removed\": \"warn\",\n \"diff.op-removed-consumed\": \"error\",\n \"diff.op-deprecated\": \"warn\",\n \"diff.op-modified\": \"warn\",\n \"diff.message-added\": \"ok\",\n \"diff.message-removed\": \"warn\",\n \"diff.message-removed-consumed\": \"error\",\n \"diff.message-modified\": \"warn\",\n \"diff.consumer-added\": \"ok\",\n \"diff.consumer-removed\": \"ok\",\n};\n\nfunction found(code: DiffCode, message: string, details?: string[]): Finding {\n const carry = details !== undefined && details.length > 0;\n return { severity: DIFF_SEVERITY[code], code, message, ...(carry ? { details } : {}) };\n}\n\n/** One suspended axis: which side could not be read, and why. */\nexport interface UnreadableEntry {\n side: \"base\" | \"current\";\n axis: \"spec\" | \"archSpec\" | \"openapi\" | \"asyncapi\";\n path: string;\n error: string;\n}\n\nexport interface ServiceDiff {\n id: string;\n change: \"added\" | \"removed\" | \"changed\" | \"unchanged\";\n findings: Finding[];\n unreadable: UnreadableEntry[];\n /** Directories claiming this id when more than one does, on either side — every finding suspended. */\n ambiguous?: string[];\n}\n\nexport interface FleetDiff {\n services: ServiceDiff[];\n /** True iff any error-severity finding — a removal the fleet still consumes. */\n breaking: boolean;\n /** `unreadable` counts suspended axes plus ambiguous base identities: places nobody could look. */\n summary: { added: number; removed: number; modified: number; deprecated: number; unreadable: number };\n}\n\n/**\n * A side the service does not exist on: absence everywhere, honestly. The id is\n * a key of one of the two sides' maps, each keyed by a directory's leaf name.\n */\nconst ABSENT = { kind: \"absent\" } as const;\nconst emptyState = (id: string): ServiceState => ({ id: rawServiceId(id), dir: \"\", spec: ABSENT, archSpec: ABSENT, openapi: ABSENT, asyncapi: ABSENT });\n\n/**\n * The axes on which an unreadable document the branch left byte-identical is\n * SKIPPED rather than suspended: the two contracts. Identical bytes are a\n * proof, not a guess — the same parser over the same bytes cannot read\n * differently on the two sides, so there is nothing of this document's own\n * to diff, and suspending it anyway made every `loam diff` exit 1 over a\n * fleet that holds one AsyncAPI 2.x file (unreadable by policy since\n * core/asyncapi/read.ts refused pre-3.0), whatever the branch changed.\n *\n * Not the spec axes, and that is the victims scan's doing, not an oversight:\n * an unreadable spec.md or arch.spec.md is also the document ANOTHER\n * service's removal could not be checked against (`core/consumers/consumers.ts`'s\n * `scanRequirements`), and its `unreadable[]` entry here is what carries that\n * suspension to exit 1 (`opsDiff`'s removal branch says so). No scan reads a\n * contract it could not parse as evidence of anything — `producers` takes\n * positive evidence only — so skipping one hides no other answer.\n */\nconst UNTOUCHED_SKIPS: ReadonlySet<UnreadableEntry[\"axis\"]> = new Set([\"openapi\", \"asyncapi\"]);\n\n/**\n * Guard one axis pair: record unreadable sides on the sink and say whether\n * the axis may be compared at all. Reading one good side against a suspended\n * one would grade absence it never proved. A contract unreadable on BOTH\n * sides with one byte hash is the branch's untouched file: not compared and\n * not recorded either (`UNTOUCHED_SKIPS`). A changed one, or one only one\n * side cannot read, still suspends.\n */\nfunction comparable(sink: ServiceDiff, axis: UnreadableEntry[\"axis\"], pair: { base: AxisState<unknown>; current: AxisState<unknown> }): boolean {\n const { base, current } = pair;\n if (UNTOUCHED_SKIPS.has(axis) && base.kind === \"unreadable\" && current.kind === \"unreadable\" &&\n base.bytes !== undefined && base.bytes === current.bytes) return false;\n for (const side of [\"base\", \"current\"] as const) {\n const state = pair[side];\n if (state.kind === \"unreadable\") sink.unreadable.push({ side, axis, path: state.path, error: state.error });\n }\n return pair.base.kind !== \"unreadable\" && pair.current.kind !== \"unreadable\";\n}\n\nconst valueOr = <T>(axis: AxisState<T[]>): T[] => (axis.kind === \"read\" ? axis.value : []);\n\n/** Identity: `Requirement-ID:` when authored, the heading otherwise — the same join `loam rebase` pins against. */\nconst reqKey = (r: Requirement): string => (r.id !== undefined ? `id:${r.id}` : `name:${r.name}`);\nconst reqLabel = (r: Requirement): string => (r.id !== undefined ? `${r.name} [${r.id}]` : r.name);\n\n/** First declaration wins a duplicated key — the same arbitrary-but-stated pick every join on these axes makes. */\nfunction firstWins<T>(items: T[], key: (item: T) => string): Map<string, T> {\n const map = new Map<string, T>();\n for (const item of items) if (!map.has(key(item))) map.set(key(item), item);\n return map;\n}\n\n/** Entry comparator by map key — explicit, because the default sort stringifies whole entries. */\nconst byKey = <T>(a: [string, T], b: [string, T]): number => (a[0] < b[0] ? -1 : 1);\n\nfunction requirementsDiff(sink: ServiceDiff, pair: { base: ServiceState; current: ServiceState }): void {\n // v1 diffs spec.md's requirements only; arch.spec.md is read for the\n // Consumes: join but its requirement inventory is deferred (the two axes\n // are separate namespaces, and diffing both doubles the surface). Both\n // pairs are guarded BEFORE the early return, so a service whose two spec\n // files are both unreadable reports both suspensions, not just the first.\n const specComparable = comparable(sink, \"spec\", { base: pair.base.spec, current: pair.current.spec });\n comparable(sink, \"archSpec\", { base: pair.base.archSpec, current: pair.current.archSpec });\n if (!specComparable) return;\n const base = firstWins(valueOr(pair.base.spec), reqKey);\n const current = firstWins(valueOr(pair.current.spec), reqKey);\n for (const [key, r] of [...current].sort(byKey)) {\n const before = base.get(key);\n if (before === undefined) sink.findings.push(found(\"diff.requirement-added\", `living requirement added since base: '${reqLabel(r)}'`));\n else if (requirementDigest(before) !== requirementDigest(r)) {\n sink.findings.push(found(\"diff.requirement-modified\", `living requirement modified since base: '${reqLabel(r)}'`));\n }\n }\n for (const [key, r] of [...base].sort(byKey)) {\n if (!current.has(key)) sink.findings.push(found(\"diff.requirement-removed\", `living requirement removed since base: '${reqLabel(r)}'`));\n }\n}\n\n/**\n * The second half of every modification message. loam joins consumers to the\n * thing that moved; it does not read what a schema SAYS (core/brief/unchecked.ts\n * lists that as unchecked), so a breaking rename and a new optional field are\n * the same finding here — the specialist diff tells them apart.\n */\nconst notGraded = (tool: string): string =>\n `loam does not grade whether the change is breaking — run ${tool} on the two states of this contract`;\n\nasync function opsDiff(sink: ServiceDiff, pair: { base: ServiceState; current: ServiceState }, victims: ConsumerIndex): Promise<void> {\n if (!comparable(sink, \"openapi\", { base: pair.base.openapi, current: pair.current.openapi })) return;\n const base = firstWins(valueOr(pair.base.openapi), (op) => op.id);\n const current = firstWins(valueOr(pair.current.openapi), (op) => op.id);\n for (const [id, op] of [...current].sort(byKey)) {\n const before = base.get(id);\n if (before === undefined) {\n sink.findings.push(found(\"diff.op-added\", `operation '${id}' added since base (${op.method.toUpperCase()} ${op.path})`));\n } else if (!before.deprecated && op.deprecated) {\n const scan = await victims.opConsumers(sink.id, id);\n sink.findings.push(found(\"diff.op-deprecated\", `operation '${id}' is deprecated since base — ${consumerStance(scan)}`, scanDetails(scan)));\n } else if (before.closureDigest !== op.closureDigest || before.path !== op.path || before.method !== op.method) {\n // `else`, because setting the flag moves the digest by itself (it is a\n // key of the node): a deprecation already names the same consumers, and\n // a second finding over them would say nothing new. The digest covers\n // the `$ref` closure — a field renamed in a shared component is exactly\n // the change this code exists for, and the operation's own bytes never\n // move for it. A relocated slot is a change too, though the node is not.\n const scan = await victims.opConsumers(sink.id, id);\n sink.findings.push(found(\n \"diff.op-modified\",\n `operation '${id}' (${op.method.toUpperCase()} ${op.path}) changed since base, in its own definition or a schema it references — ${consumerStance(scan)}; ${notGraded(\"`oasdiff breaking`\")}`,\n scanDetails(scan),\n ));\n }\n }\n for (const [id] of [...base].sort(byKey)) {\n if (current.has(id)) continue;\n const scan = await victims.opConsumers(sink.id, id);\n if (scan.victims.length > 0) {\n sink.findings.push(found(\"diff.op-removed-consumed\", `operation '${id}' was removed since base and the current fleet still names it — these joins break`, scanDetails(scan)));\n } else if (scan.suspended.length > 0) {\n // \"Nobody could be checked\" is not \"nobody names it\", so this message\n // must not assert a negative the scan never proved. An unreadable\n // consumer document carries the run to exit 1 through its own\n // unreadable[] entry; a caller drawn with no op does not — it stays this\n // warning, so a seeded, op-less map does not fail every removal.\n sink.findings.push(found(\"diff.op-removed\", `operation '${id}' was removed since base; whether the current fleet still names it could NOT be fully answered — ${scan.suspended.length} possible consumer(s) could not be checked`, scanDetails(scan)));\n } else {\n sink.findings.push(found(\"diff.op-removed\", `operation '${id}' was removed since base; no current edge, requirement or use-case hop names it`));\n }\n }\n}\n\nasync function messagesDiff(sink: ServiceDiff, pair: { base: ServiceState; current: ServiceState }, victims: ConsumerIndex): Promise<void> {\n if (!comparable(sink, \"asyncapi\", { base: pair.base.asyncapi, current: pair.current.asyncapi })) return;\n const base = firstWins(valueOr(pair.base.asyncapi), (m) => m.name);\n const current = firstWins(valueOr(pair.current.asyncapi), (m) => m.name);\n for (const [name, message] of [...current].sort(byKey)) {\n const before = base.get(name);\n if (before === undefined) {\n sink.findings.push(found(\"diff.message-added\", `message '${name}' added since base`));\n } else if (before.closureDigest !== message.closureDigest) {\n const scan = await victims.messageConsumers(sink.id, name);\n sink.findings.push(found(\n \"diff.message-modified\",\n `message '${name}' changed since base, in its declaration or a schema its payload references — ${consumerStance(scan)}; ${notGraded(\"`asyncapi diff`\")}`,\n scanDetails(scan),\n ));\n }\n }\n for (const [name] of [...base].sort(byKey)) {\n if (current.has(name)) continue;\n // Fleet-global join: a name another current service still SENDS is not\n // orphaned (positive evidence only — an unreadable contract never vouches,\n // and a service that only receives it is one of its consumers, never its\n // producer: `producers` says why that distinction is the gate's too). Nor\n // is one an #external element still publishes, by validate's own rule\n // (`externalProducer` says why): counting the fleet's senders alone made\n // this removal an error that the next `validate --all` called clean.\n const elsewhere = (await victims.producers(name)).filter((d) => d !== sink.id);\n const outside = await victims.externalProducer(name);\n const senders = outside === null ? elsewhere : [...elsewhere, `${outside} (#external)`];\n if (senders.length > 0) {\n sink.findings.push(found(\"diff.message-removed\", `message '${name}' was removed since base; still sent by ${senders.join(\", \")}`));\n continue;\n }\n const scan = await victims.messageConsumers(sink.id, name);\n if (scan.victims.length > 0) {\n sink.findings.push(found(\"diff.message-removed-consumed\", `message '${name}' was removed since base and the current fleet still consumes it — these joins break`, scanDetails(scan)));\n } else if (scan.suspended.length > 0) {\n sink.findings.push(found(\"diff.message-removed\", `message '${name}' was removed since base; whether anything still consumes it could NOT be fully answered — ${scan.suspended.length} consumer document(s) were not scannable`, scanDetails(scan)));\n } else {\n sink.findings.push(found(\"diff.message-removed\", `message '${name}' was removed since base; nothing currently consumes it`));\n }\n }\n}\n\n/** The graded join changes (`./joins.ts`) as findings on each PROVIDER — the service whose surface the join lands on. */\nfunction consumerFindings(input: { base: BaseFleet; current: CurrentFleet }): Map<string, Finding[]> {\n const changes = consumerJoinChanges({\n base: input.base.services,\n baseAmbiguous: new Set(input.base.ambiguous.keys()),\n current: input.current.services,\n currentAmbiguous: new Set(input.current.ambiguous.keys()),\n });\n const byProvider = new Map<string, Finding[]>();\n for (const { t, change } of changes) {\n const what = t.kind === \"op\" ? `operation '${t.token}'` : `message '${t.token}'`;\n const message = change === \"added\"\n ? `${t.consumer}'s living requirements now name this service's ${what} — a cross-service join appeared since base`\n : `${t.consumer}'s living requirements no longer name this service's ${what} — a cross-service join went away`;\n // The call sits directly in the push — assigned to a `const x = found(…)`\n // it reads as an arrow-function DECLARATION to the stable-code collector,\n // and both consumer codes silently leave the guarded set.\n byProvider.set(t.provider, [\n ...(byProvider.get(t.provider) ?? []),\n found(change === \"added\" ? \"diff.consumer-added\" : \"diff.consumer-removed\", message),\n ]);\n }\n return byProvider;\n}\n\nasync function diffService(unit: { id: string; base?: ServiceState; current?: ServiceState; victims: ConsumerIndex }): Promise<ServiceDiff> {\n const sink: ServiceDiff = { id: unit.id, change: \"unchanged\", findings: [], unreadable: [] };\n if (unit.base === undefined) sink.findings.push(found(\"diff.service-added\", `service '${unit.id}' added since base`));\n if (unit.current === undefined && unit.base !== undefined) {\n sink.findings.push(found(\"diff.service-removed\", `service '${unit.id}' (${unit.base.dir}) is gone from the working tree`));\n }\n const pair = { base: unit.base ?? emptyState(unit.id), current: unit.current ?? emptyState(unit.id) };\n requirementsDiff(sink, pair);\n await opsDiff(sink, pair, unit.victims);\n await messagesDiff(sink, pair, unit.victims);\n return sink;\n}\n\nexport async function computeDiff(input: { base: BaseFleet; current: CurrentFleet; victims: ConsumerIndex }): Promise<FleetDiff> {\n const sides = [input.base.services, input.base.ambiguous, input.current.services, input.current.ambiguous];\n const ids = [...new Set(sides.flatMap((side) => [...side.keys()]))].sort();\n const joins = consumerFindings({ base: input.base, current: input.current });\n const services: ServiceDiff[] = [];\n // Sequential on purpose: the report's order IS the contract (identical\n // states must emit identical bytes), the work per service is in-memory\n // joins, and the one expensive read — the victim index's landscape — is\n // memoised behind its first ask, so a fan-out would buy nothing.\n for (const id of ids) {\n const claimants = [...(input.base.ambiguous.get(id) ?? []), ...(input.current.ambiguous.get(id) ?? [])];\n if (claimants.length > 0) {\n // An id two directories claim — either side — has no single state to\n // diff; the whole subject suspends (containment), and the live walk's\n // `subsystem.name-collision` names the same defect for validate.\n services.push({ id, change: \"changed\", findings: [], unreadable: [], ambiguous: claimants });\n continue;\n }\n const sink = await diffService({ id, base: input.base.services.get(id), current: input.current.services.get(id), victims: input.victims });\n sink.findings.push(...(joins.get(id) ?? []));\n if (input.base.services.has(id) && !input.current.services.has(id)) sink.change = \"removed\";\n else if (!input.base.services.has(id)) sink.change = \"added\";\n else sink.change = sink.findings.length > 0 || sink.unreadable.length > 0 ? \"changed\" : \"unchanged\";\n services.push(sink);\n }\n const all = services.flatMap((s) => s.findings);\n const count = (codes: (f: Finding) => boolean): number => all.filter(codes).length;\n return {\n services,\n breaking: all.some((f) => f.severity === \"error\"),\n summary: {\n added: count((f) => f.code.endsWith(\"-added\")),\n removed: count((f) => f.code.endsWith(\"-removed\") || f.code.endsWith(\"-removed-consumed\")),\n // Requirements, operations and messages alike: a reader scanning the one\n // summary line must not see \"0 modified\" over a PR that renamed a field.\n modified: count((f) => f.code.endsWith(\"-modified\")),\n deprecated: count((f) => f.code === \"diff.op-deprecated\"),\n unreadable:\n services.reduce((n, s) => n + s.unreadable.length, 0) +\n services.filter((s) => s.ambiguous !== undefined).length,\n },\n };\n}\n"]}
@@ -17,7 +17,7 @@ import { LOAM_VERSION } from "../envelope/version.js";
17
17
  import { agentsPath } from "../repo/paths.js";
18
18
  import { DELIVERIES, plannedCommandFiles, } from "../agent/scaffold.js";
19
19
  import { AGENT_TOOLS } from "../agent/tools/registry.js";
20
- import { agentsStaleFinding, binaryBehindFinding, agentsStampLine, agentsStampVersion, versionTrails, } from "../agent/agents-stamp.js";
20
+ import { agentsStaleFinding, binaryBehindFinding, agentsStampLine, agentsStampVersion, restamped, versionTrails, } from "../agent/agents-stamp.js";
21
21
  import {} from "./report.js";
22
22
  export function recordedTools(config) {
23
23
  const ids = (config?.agentTools ?? []).filter((t) => t in AGENT_TOOLS);
@@ -60,20 +60,34 @@ export function heldDeliveries(repoRoot, tool, profile = "full") {
60
60
  }
61
61
  /**
62
62
  * Of the planned files that ARE here, the ones whose stamp is missing or older
63
- * than this binary.
63
+ * than this binary AND whose body is not the one this binary writes.
64
64
  *
65
- * The STALENESS check reads only the stamp, never the body. Safe refresh is a
66
- * separate decision made by `init`: the current bytes must match the digest
67
- * recorded when loam wrote them. That proof lets an unchanged pointer refresh
68
- * without turning a differing body into an offer to overwrite it. A stamp is a
69
- * different claim — it says which loam wrote these instructions — and it can
70
- * be false while every edit around it is legitimate. Same doctrine as
71
- * AGENTS.md (agents-stamp.ts); a customized file is repaired only when a human
72
- * reviews it and bumps the stamp or deletes it for `loam init` to recreate.
65
+ * Both halves, and the second is what makes the finding mean something. The
66
+ * stamp sits inside the bytes, so every release moves it in every generated
67
+ * file — and while the stamp was the whole test, every pointer in every repo
68
+ * of a fleet went stale on every upgrade, even when the release had not
69
+ * changed a word of the pointer. A warning that is always on is one people
70
+ * learn to read past, and it then hid the one release where a pointer's body
71
+ * really had moved. So a file whose bytes, its stamp line aside, are exactly
72
+ * what this binary would write is current: it instructs an agent with this
73
+ * binary's text, whichever release happened to write it. Line endings are
74
+ * compared the same way, because a Windows checkout with `core.autocrlf`
75
+ * hands back CRLF for a body nobody touched.
76
+ *
77
+ * A file that differs in its body falls back to the stamp, which is the claim
78
+ * it always was — which loam a person last checked these instructions
79
+ * against. That is why a CURRENT stamp over a customized body stays quiet:
80
+ * somebody reviewed it and said so. Safe refresh is a separate decision made by
81
+ * `init`: the current bytes must match the digest recorded when loam wrote
82
+ * them, which lets an unchanged pointer refresh without turning a differing
83
+ * body into an offer to overwrite it. Same doctrine as AGENTS.md
84
+ * (agents-stamp.ts); a customized file is repaired only when a human reviews it
85
+ * and bumps the stamp or deletes it for `loam init` to recreate.
73
86
  *
74
87
  * Absence of a stamp counts, because that is what every file written before
75
88
  * stamping existed looks like — precisely the "initialized by an older loam"
76
- * population this check is for.
89
+ * population this check is for — and such a file cannot match the current
90
+ * body, which opens with the stamp.
77
91
  */
78
92
  export async function staleAgentFiles(repoRoot, planned) {
79
93
  // Read them together rather than one after another. `planned` is bounded by
@@ -92,10 +106,12 @@ export async function staleAgentFiles(repoRoot, planned) {
92
106
  return null;
93
107
  }
94
108
  const stamp = agentsStampVersion(text);
95
- if (stamp === null || versionTrails(stamp, LOAM_VERSION)) {
96
- return relative(repoRoot, file.path).split(/[\\/]/).join("/");
97
- }
98
- return null;
109
+ if (stamp !== null && !versionTrails(stamp, LOAM_VERSION))
110
+ return null;
111
+ const lf = (s) => s.replace(/\r\n/g, "\n");
112
+ if (lf(restamped(text, LOAM_VERSION)) === lf(file.content))
113
+ return null;
114
+ return relative(repoRoot, file.path).split(/[\\/]/).join("/");
99
115
  }));
100
116
  return graded.filter((p) => p !== null);
101
117
  }
@@ -145,9 +161,9 @@ export async function inspectAgentSurface(repoRoot, config, docsDir, findings) {
145
161
  findings.push({
146
162
  severity: "warning",
147
163
  code: "doctor.agent-files-stale",
148
- message: `${staleFiles.length} of the ${planned.length} command and skill files here carry no `
149
- + `\`${agentsStampLine(LOAM_VERSION)}\` stamp, or one older than this loam — the protocol and `
150
- + `code tables they instruct an agent with may describe a loam that no longer exists: `
164
+ message: `${staleFiles.length} of the ${planned.length} command and skill files here differ from the body `
165
+ + `loam v${LOAM_VERSION} writes and carry no \`${agentsStampLine(LOAM_VERSION)}\` stamp, or one older `
166
+ + `than this loam — what they instruct an agent with may describe a loam that no longer exists: `
151
167
  + `${staleFiles.join(", ")}`,
152
168
  fix: "Read each against the body this loam writes, then set its stamp line to "
153
169
  + `\`${agentsStampLine(LOAM_VERSION)}\`; or re-run \`loam init\`. A file whose recorded digest `
@@ -1 +1 @@
1
- {"version":3,"file":"agents.js","sourceRoot":"","sources":["../../../src/core/doctor/agents.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The agent surface: which tool files this repo should hold, which it does, and\n * whether they still describe this binary.\n *\n * The registry filter lives here rather than in the config loader because a\n * config written by a newer binary may name a tool this one has never heard of,\n * and refusing to load it would break every command rather than just this\n * check. A tool loam cannot plan files for is a tool it has nothing to say\n * about — so the preflight stays read-only instead of crashing on a config from\n * the future.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { relative } from \"node:path\";\nimport { type LoamConfig } from \"../envelope/config.js\";\nimport { LOAM_VERSION } from \"../envelope/version.js\";\nimport { agentsPath } from \"../repo/paths.js\";\nimport {\n DELIVERIES,\n plannedCommandFiles,\n type AgentProfile,\n type Delivery,\n} from \"../agent/scaffold.js\";\nimport { AGENT_TOOLS } from \"../agent/tools/registry.js\";\nimport {\n agentsStaleFinding,\n binaryBehindFinding,\n agentsStampLine,\n agentsStampVersion,\n versionTrails,\n} from \"../agent/agents-stamp.js\";\nimport { type AgentSurface, type DoctorFinding } from \"./report.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\nexport function recordedTools(config: LoamConfig | null): string[] | null {\n const ids = (config?.agentTools ?? []).filter((t) => t in AGENT_TOOLS);\n return ids.length === 0 ? null : ids;\n}\n\n/**\n * Which tools this repo has agent files for, asked of the layout itself: a tool\n * counts when any file it would be given is already there.\n *\n * The fallback for a loam.json written before `agentTools` existed, and the\n * only answer a hand-written one can have — so its absence must never be the\n * thing that reports a problem. This asks a narrower question than `init`'s\n * own scan, and on purpose: init scans for a tool's dot-directory to decide\n * what a repo would LIKE, while doctor grades what loam has already put there.\n * A repo that runs Claude Code and took `--no-commands --no-skills` has chosen\n * to have no loam files, and is not behind the binary for it.\n */\nexport function detectedTools(repoRoot: string): string[] {\n return Object.keys(AGENT_TOOLS).filter((id) =>\n plannedCommandFiles(repoRoot, [id]).some((f) => existsSync(f.path)));\n}\n\n/**\n * Which deliveries this repo actually HOLDS for one tool — a delivery counts\n * when at least one of its files is on disk.\n *\n * `loam init --no-skills` (or `--no-commands`) records the tool in `agentTools`\n * and writes one delivery, so planning both and subtracting what exists reported\n * every skill file as missing on a brand-new repo — under a message saying it\n * \"was initialized by an older loam\" and a `fix` that re-runs init without the\n * flag that was the whole point. Which deliveries a repo asked for is written\n * down nowhere, and must not be: the files ARE the record (there is no state\n * file loam can disagree with). So the question is asked of them.\n *\n * A tool with NO files at all is the other story — recorded, then deleted — and\n * the caller falls back to the full plan there, which is what the finding's\n * \"or they were deleted\" already says.\n */\nexport function heldDeliveries(\n repoRoot: string,\n tool: string,\n profile: AgentProfile = \"full\",\n): readonly Delivery[] {\n const held = DELIVERIES.filter((d) =>\n plannedCommandFiles(repoRoot, [tool], [d], profile).some((f) => existsSync(f.path)));\n return held.length > 0 ? held : DELIVERIES;\n}\n\n/**\n * Of the planned files that ARE here, the ones whose stamp is missing or older\n * than this binary.\n *\n * The STALENESS check reads only the stamp, never the body. Safe refresh is a\n * separate decision made by `init`: the current bytes must match the digest\n * recorded when loam wrote them. That proof lets an unchanged pointer refresh\n * without turning a differing body into an offer to overwrite it. A stamp is a\n * different claim — it says which loam wrote these instructions — and it can\n * be false while every edit around it is legitimate. Same doctrine as\n * AGENTS.md (agents-stamp.ts); a customized file is repaired only when a human\n * reviews it and bumps the stamp or deletes it for `loam init` to recreate.\n *\n * Absence of a stamp counts, because that is what every file written before\n * stamping existed looks like — precisely the \"initialized by an older loam\"\n * population this check is for.\n */\nexport async function staleAgentFiles(\n repoRoot: string,\n planned: Array<{ path: string }>,\n): Promise<string[]> {\n // Read them together rather than one after another. `planned` is bounded by\n // the tool registry — every delivery of every tool loam knows, a couple of\n // hundred small files — so there is nothing here worth a concurrency cap, and\n // `Promise.all` preserves input order, which is the only property the caller\n // asks of the list it gets back.\n const graded = await Promise.all(planned.map(async (file): Promise<string | null> => {\n let text: string;\n try {\n text = await readFile(file.path, \"utf8\");\n } catch {\n // Unreadable is not stale: it is either the missing-file finding above or\n // a permissions problem, and neither is answered by editing a stamp.\n return null;\n }\n const stamp = agentsStampVersion(text);\n if (stamp === null || versionTrails(stamp, LOAM_VERSION)) {\n return relative(repoRoot, file.path).split(/[\\\\/]/).join(\"/\");\n }\n return null;\n }));\n return graded.filter((p) => p !== null);\n}\n\n/**\n * What the running binary would write for this repo's tools that is not here,\n * and what is here under an older loam's name.\n *\n * Presence is `existsSync`, the same probe `init` uses to compute its `skipped`\n * list, so doctor cannot disagree with init about which files init would write.\n */\nexport async function inspectAgentSurface(\n repoRoot: string,\n config: LoamConfig | null,\n docsDir: DocsDir | null,\n findings: DoctorFinding[],\n): Promise<AgentSurface> {\n const recorded = recordedTools(config);\n const tools = recorded ?? detectedTools(repoRoot);\n const profile = config?.agentProfile ?? \"full\";\n const planned = tools.flatMap((id) =>\n plannedCommandFiles(repoRoot, [id], heldDeliveries(repoRoot, id, profile), profile));\n const present = planned.filter((f) => existsSync(f.path));\n const missingFiles = planned\n .filter((f) => !existsSync(f.path))\n .map((f) => relative(repoRoot, f.path).split(/[\\\\/]/).join(\"/\"));\n const staleFiles = await staleAgentFiles(repoRoot, present);\n\n if (missingFiles.length > 0) {\n // A warning, never a blocker: an out-of-date command set is a repo whose\n // agents have fewer entry points than they could, not one where anything\n // refuses to run. doctor's blockers are for the second kind.\n //\n // The fix spells `--docs` and `--tools` and deliberately NOT `--service`:\n // `init` spreads the committed config forward, and a `--docs`-less re-run\n // keeps the pointer the repo already commits, so the binding survives\n // untouched. Every interpolation here stands for exactly one argument —\n // test/agent-commands-runnable.test.ts parses this string, and a `${flags}`\n // holding three of them is a command it cannot check.\n findings.push({\n severity: \"warning\",\n code: \"doctor.agent-files-missing\",\n message:\n `${missingFiles.length} of the ${planned.length} command and skill files loam v${LOAM_VERSION} `\n + `lays down for ${tools.join(\", \")} are not in this repo — it was initialized by an older loam, `\n + `or they were deleted: ${missingFiles.join(\", \")}`,\n fix:\n `Re-run \\`loam init --docs ${config?.docsDirAsWritten ?? \"<dir>\"} --tools ${tools.join(\",\")}\\` `\n + \"here — it creates files that are absent and keeps this repo's service binding. Files unchanged \"\n + \"since loam wrote them refresh safely; customized files stay untouched.\",\n });\n }\n\n if (staleFiles.length > 0) {\n // Also a warning, and for a stronger reason than the one above: these files\n // are the team's now. Only a person can say whether their edits still mean\n // what the current binary's tables say, so loam reports and stops.\n findings.push({\n severity: \"warning\",\n code: \"doctor.agent-files-stale\",\n message:\n `${staleFiles.length} of the ${planned.length} command and skill files here carry no `\n + `\\`${agentsStampLine(LOAM_VERSION)}\\` stamp, or one older than this loam — the protocol and `\n + `code tables they instruct an agent with may describe a loam that no longer exists: `\n + `${staleFiles.join(\", \")}`,\n fix:\n \"Read each against the body this loam writes, then set its stamp line to \"\n + `\\`${agentsStampLine(LOAM_VERSION)}\\`; or re-run \\`loam init\\`. A file whose recorded digest `\n + \"still matches is refreshed; a customized file is preserved and remains yours to review.\",\n });\n }\n\n // AGENTS.md lives in the DOCS repo, not here: it travels with the thing it\n // describes. The comparison is `agentsStaleFinding`'s, not a second copy of\n // it — doctor only adds the `fix` column its own findings owe the reader.\n const agentsFile = docsDir === null ? null : agentsPath(docsDir);\n let agentsText: string | null = null;\n if (agentsFile !== null) {\n try {\n agentsText = await readFile(agentsFile, \"utf8\");\n } catch {\n // Unreadable reads as absent, exactly as `validate --all` treats it: no\n // file, no contract to have drifted. An unreadable docs repo is already\n // its own finding.\n }\n }\n const stale = agentsStaleFinding(agentsText, LOAM_VERSION);\n if (stale !== null) {\n findings.push({\n severity: \"warning\",\n code: stale.code,\n message: stale.message,\n fix:\n `Review ${agentsFile} against the current \\`loam --help\\`, then set its stamp line to `\n + `\\`${agentsStampLine(LOAM_VERSION)}\\`. loam never rewrites that file — bumping the stamp IS `\n + \"the record that somebody looked.\",\n });\n }\n // The mirror, and a different fix: nothing is wrong with the documents, the\n // reader is behind them — so this one never points at AGENTS.md.\n const behind = binaryBehindFinding(agentsText, LOAM_VERSION);\n if (behind !== null) {\n findings.push({\n severity: \"warning\",\n code: behind.code,\n message: behind.message,\n fix: \"Upgrade loam (`npm i -g @ybotok/loam`), then re-run. Do not edit AGENTS.md: its stamp is correct and this binary is the older half.\",\n });\n }\n\n return {\n tools,\n profile,\n toolsSource: recorded === null ? \"disk\" : \"config\",\n plannedFiles: planned.length,\n missingFiles,\n staleFiles,\n stamp: {\n path: agentsFile,\n present: agentsText !== null,\n version: agentsText === null ? null : agentsStampVersion(agentsText),\n stale: stale !== null,\n },\n };\n}\n\n/**\n * Grade what a write that did not finish left in the docs repo.\n *\n * Three of the four are blockers, which is unusual for doctor and deliberate:\n * they are not \"you have fewer entry points than you could\", they are \"the\n * living docs may be half-written and the next reader cannot tell\". Before this\n * existed, a SIGKILL between two of archive's renames left `doctor: healthy:\n * true` over merged spec.md + openapi.yaml and an unmerged landscape, and the\n * next `loam archive` reported loam's own half-merge as the author's bug.\n *\n * The temp files are the exception: a `.loam-*.tmp` was never linked into place,\n * so nothing reads it and nothing depends on it. It is litter, not damage.\n */\n"]}
1
+ {"version":3,"file":"agents.js","sourceRoot":"","sources":["../../../src/core/doctor/agents.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The agent surface: which tool files this repo should hold, which it does, and\n * whether they still describe this binary.\n *\n * The registry filter lives here rather than in the config loader because a\n * config written by a newer binary may name a tool this one has never heard of,\n * and refusing to load it would break every command rather than just this\n * check. A tool loam cannot plan files for is a tool it has nothing to say\n * about — so the preflight stays read-only instead of crashing on a config from\n * the future.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { relative } from \"node:path\";\nimport { type LoamConfig } from \"../envelope/config.js\";\nimport { LOAM_VERSION } from \"../envelope/version.js\";\nimport { agentsPath } from \"../repo/paths.js\";\nimport {\n DELIVERIES,\n plannedCommandFiles,\n type AgentProfile,\n type Delivery,\n} from \"../agent/scaffold.js\";\nimport { AGENT_TOOLS } from \"../agent/tools/registry.js\";\nimport {\n agentsStaleFinding,\n binaryBehindFinding,\n agentsStampLine,\n agentsStampVersion,\n restamped,\n versionTrails,\n} from \"../agent/agents-stamp.js\";\nimport { type AgentSurface, type DoctorFinding } from \"./report.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\n\nexport function recordedTools(config: LoamConfig | null): string[] | null {\n const ids = (config?.agentTools ?? []).filter((t) => t in AGENT_TOOLS);\n return ids.length === 0 ? null : ids;\n}\n\n/**\n * Which tools this repo has agent files for, asked of the layout itself: a tool\n * counts when any file it would be given is already there.\n *\n * The fallback for a loam.json written before `agentTools` existed, and the\n * only answer a hand-written one can have — so its absence must never be the\n * thing that reports a problem. This asks a narrower question than `init`'s\n * own scan, and on purpose: init scans for a tool's dot-directory to decide\n * what a repo would LIKE, while doctor grades what loam has already put there.\n * A repo that runs Claude Code and took `--no-commands --no-skills` has chosen\n * to have no loam files, and is not behind the binary for it.\n */\nexport function detectedTools(repoRoot: string): string[] {\n return Object.keys(AGENT_TOOLS).filter((id) =>\n plannedCommandFiles(repoRoot, [id]).some((f) => existsSync(f.path)));\n}\n\n/**\n * Which deliveries this repo actually HOLDS for one tool — a delivery counts\n * when at least one of its files is on disk.\n *\n * `loam init --no-skills` (or `--no-commands`) records the tool in `agentTools`\n * and writes one delivery, so planning both and subtracting what exists reported\n * every skill file as missing on a brand-new repo — under a message saying it\n * \"was initialized by an older loam\" and a `fix` that re-runs init without the\n * flag that was the whole point. Which deliveries a repo asked for is written\n * down nowhere, and must not be: the files ARE the record (there is no state\n * file loam can disagree with). So the question is asked of them.\n *\n * A tool with NO files at all is the other story — recorded, then deleted — and\n * the caller falls back to the full plan there, which is what the finding's\n * \"or they were deleted\" already says.\n */\nexport function heldDeliveries(\n repoRoot: string,\n tool: string,\n profile: AgentProfile = \"full\",\n): readonly Delivery[] {\n const held = DELIVERIES.filter((d) =>\n plannedCommandFiles(repoRoot, [tool], [d], profile).some((f) => existsSync(f.path)));\n return held.length > 0 ? held : DELIVERIES;\n}\n\n/**\n * Of the planned files that ARE here, the ones whose stamp is missing or older\n * than this binary AND whose body is not the one this binary writes.\n *\n * Both halves, and the second is what makes the finding mean something. The\n * stamp sits inside the bytes, so every release moves it in every generated\n * file — and while the stamp was the whole test, every pointer in every repo\n * of a fleet went stale on every upgrade, even when the release had not\n * changed a word of the pointer. A warning that is always on is one people\n * learn to read past, and it then hid the one release where a pointer's body\n * really had moved. So a file whose bytes, its stamp line aside, are exactly\n * what this binary would write is current: it instructs an agent with this\n * binary's text, whichever release happened to write it. Line endings are\n * compared the same way, because a Windows checkout with `core.autocrlf`\n * hands back CRLF for a body nobody touched.\n *\n * A file that differs in its body falls back to the stamp, which is the claim\n * it always was — which loam a person last checked these instructions\n * against. That is why a CURRENT stamp over a customized body stays quiet:\n * somebody reviewed it and said so. Safe refresh is a separate decision made by\n * `init`: the current bytes must match the digest recorded when loam wrote\n * them, which lets an unchanged pointer refresh without turning a differing\n * body into an offer to overwrite it. Same doctrine as AGENTS.md\n * (agents-stamp.ts); a customized file is repaired only when a human reviews it\n * and bumps the stamp or deletes it for `loam init` to recreate.\n *\n * Absence of a stamp counts, because that is what every file written before\n * stamping existed looks like — precisely the \"initialized by an older loam\"\n * population this check is for — and such a file cannot match the current\n * body, which opens with the stamp.\n */\nexport async function staleAgentFiles(\n repoRoot: string,\n planned: Array<{ path: string; content: string }>,\n): Promise<string[]> {\n // Read them together rather than one after another. `planned` is bounded by\n // the tool registry — every delivery of every tool loam knows, a couple of\n // hundred small files — so there is nothing here worth a concurrency cap, and\n // `Promise.all` preserves input order, which is the only property the caller\n // asks of the list it gets back.\n const graded = await Promise.all(planned.map(async (file): Promise<string | null> => {\n let text: string;\n try {\n text = await readFile(file.path, \"utf8\");\n } catch {\n // Unreadable is not stale: it is either the missing-file finding above or\n // a permissions problem, and neither is answered by editing a stamp.\n return null;\n }\n const stamp = agentsStampVersion(text);\n if (stamp !== null && !versionTrails(stamp, LOAM_VERSION)) return null;\n const lf = (s: string): string => s.replace(/\\r\\n/g, \"\\n\");\n if (lf(restamped(text, LOAM_VERSION)) === lf(file.content)) return null;\n return relative(repoRoot, file.path).split(/[\\\\/]/).join(\"/\");\n }));\n return graded.filter((p) => p !== null);\n}\n\n/**\n * What the running binary would write for this repo's tools that is not here,\n * and what is here under an older loam's name.\n *\n * Presence is `existsSync`, the same probe `init` uses to compute its `skipped`\n * list, so doctor cannot disagree with init about which files init would write.\n */\nexport async function inspectAgentSurface(\n repoRoot: string,\n config: LoamConfig | null,\n docsDir: DocsDir | null,\n findings: DoctorFinding[],\n): Promise<AgentSurface> {\n const recorded = recordedTools(config);\n const tools = recorded ?? detectedTools(repoRoot);\n const profile = config?.agentProfile ?? \"full\";\n const planned = tools.flatMap((id) =>\n plannedCommandFiles(repoRoot, [id], heldDeliveries(repoRoot, id, profile), profile));\n const present = planned.filter((f) => existsSync(f.path));\n const missingFiles = planned\n .filter((f) => !existsSync(f.path))\n .map((f) => relative(repoRoot, f.path).split(/[\\\\/]/).join(\"/\"));\n const staleFiles = await staleAgentFiles(repoRoot, present);\n\n if (missingFiles.length > 0) {\n // A warning, never a blocker: an out-of-date command set is a repo whose\n // agents have fewer entry points than they could, not one where anything\n // refuses to run. doctor's blockers are for the second kind.\n //\n // The fix spells `--docs` and `--tools` and deliberately NOT `--service`:\n // `init` spreads the committed config forward, and a `--docs`-less re-run\n // keeps the pointer the repo already commits, so the binding survives\n // untouched. Every interpolation here stands for exactly one argument —\n // test/agent-commands-runnable.test.ts parses this string, and a `${flags}`\n // holding three of them is a command it cannot check.\n findings.push({\n severity: \"warning\",\n code: \"doctor.agent-files-missing\",\n message:\n `${missingFiles.length} of the ${planned.length} command and skill files loam v${LOAM_VERSION} `\n + `lays down for ${tools.join(\", \")} are not in this repo — it was initialized by an older loam, `\n + `or they were deleted: ${missingFiles.join(\", \")}`,\n fix:\n `Re-run \\`loam init --docs ${config?.docsDirAsWritten ?? \"<dir>\"} --tools ${tools.join(\",\")}\\` `\n + \"here — it creates files that are absent and keeps this repo's service binding. Files unchanged \"\n + \"since loam wrote them refresh safely; customized files stay untouched.\",\n });\n }\n\n if (staleFiles.length > 0) {\n // Also a warning, and for a stronger reason than the one above: these files\n // are the team's now. Only a person can say whether their edits still mean\n // what the current binary's tables say, so loam reports and stops.\n findings.push({\n severity: \"warning\",\n code: \"doctor.agent-files-stale\",\n message:\n `${staleFiles.length} of the ${planned.length} command and skill files here differ from the body `\n + `loam v${LOAM_VERSION} writes and carry no \\`${agentsStampLine(LOAM_VERSION)}\\` stamp, or one older `\n + `than this loam — what they instruct an agent with may describe a loam that no longer exists: `\n + `${staleFiles.join(\", \")}`,\n fix:\n \"Read each against the body this loam writes, then set its stamp line to \"\n + `\\`${agentsStampLine(LOAM_VERSION)}\\`; or re-run \\`loam init\\`. A file whose recorded digest `\n + \"still matches is refreshed; a customized file is preserved and remains yours to review.\",\n });\n }\n\n // AGENTS.md lives in the DOCS repo, not here: it travels with the thing it\n // describes. The comparison is `agentsStaleFinding`'s, not a second copy of\n // it — doctor only adds the `fix` column its own findings owe the reader.\n const agentsFile = docsDir === null ? null : agentsPath(docsDir);\n let agentsText: string | null = null;\n if (agentsFile !== null) {\n try {\n agentsText = await readFile(agentsFile, \"utf8\");\n } catch {\n // Unreadable reads as absent, exactly as `validate --all` treats it: no\n // file, no contract to have drifted. An unreadable docs repo is already\n // its own finding.\n }\n }\n const stale = agentsStaleFinding(agentsText, LOAM_VERSION);\n if (stale !== null) {\n findings.push({\n severity: \"warning\",\n code: stale.code,\n message: stale.message,\n fix:\n `Review ${agentsFile} against the current \\`loam --help\\`, then set its stamp line to `\n + `\\`${agentsStampLine(LOAM_VERSION)}\\`. loam never rewrites that file — bumping the stamp IS `\n + \"the record that somebody looked.\",\n });\n }\n // The mirror, and a different fix: nothing is wrong with the documents, the\n // reader is behind them — so this one never points at AGENTS.md.\n const behind = binaryBehindFinding(agentsText, LOAM_VERSION);\n if (behind !== null) {\n findings.push({\n severity: \"warning\",\n code: behind.code,\n message: behind.message,\n fix: \"Upgrade loam (`npm i -g @ybotok/loam`), then re-run. Do not edit AGENTS.md: its stamp is correct and this binary is the older half.\",\n });\n }\n\n return {\n tools,\n profile,\n toolsSource: recorded === null ? \"disk\" : \"config\",\n plannedFiles: planned.length,\n missingFiles,\n staleFiles,\n stamp: {\n path: agentsFile,\n present: agentsText !== null,\n version: agentsText === null ? null : agentsStampVersion(agentsText),\n stale: stale !== null,\n },\n };\n}\n\n/**\n * Grade what a write that did not finish left in the docs repo.\n *\n * Three of the four are blockers, which is unusual for doctor and deliberate:\n * they are not \"you have fewer entry points than you could\", they are \"the\n * living docs may be half-written and the next reader cannot tell\". Before this\n * existed, a SIGKILL between two of archive's renames left `doctor: healthy:\n * true` over merged spec.md + openapi.yaml and an unmerged landscape, and the\n * next `loam archive` reported loam's own half-merge as the author's bug.\n *\n * The temp files are the exception: a `.loam-*.tmp` was never linked into place,\n * so nothing reads it and nothing depends on it. It is litter, not damage.\n */\n"]}