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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (691) hide show
  1. package/CHANGELOG.md +1477 -1
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +40 -22
  5. package/QUICKSTART.md +181 -9
  6. package/README.md +256 -57
  7. package/ROADMAP.md +108 -11
  8. package/SCHEMA.md +721 -130
  9. package/WORKFLOW.md +272 -62
  10. package/dist/cli.js +26 -8
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt/adopt.js +5 -10
  13. package/dist/commands/adopt/adopt.js.map +1 -1
  14. package/dist/commands/archive/archive.js +15 -18
  15. package/dist/commands/archive/archive.js.map +1 -1
  16. package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
  17. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  18. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  19. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/openapi.js +25 -42
  21. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
  23. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  24. package/dist/commands/archive/plan/gate.js +3 -7
  25. package/dist/commands/archive/plan/gate.js.map +1 -1
  26. package/dist/commands/archive/plan/landscape.js +4 -4
  27. package/dist/commands/archive/plan/landscape.js.map +1 -1
  28. package/dist/commands/archive/plan/refusal.js +8 -18
  29. package/dist/commands/archive/plan/refusal.js.map +1 -1
  30. package/dist/commands/archive/plan/specs.js +134 -25
  31. package/dist/commands/archive/plan/specs.js.map +1 -1
  32. package/dist/commands/archive/plan/state.js +1 -0
  33. package/dist/commands/archive/plan/state.js.map +1 -1
  34. package/dist/commands/archive/run.js +82 -33
  35. package/dist/commands/archive/run.js.map +1 -1
  36. package/dist/commands/archive/shipped.js +49 -0
  37. package/dist/commands/archive/shipped.js.map +1 -0
  38. package/dist/commands/context/context.js +4 -7
  39. package/dist/commands/context/context.js.map +1 -1
  40. package/dist/commands/context/print.js +2 -2
  41. package/dist/commands/context/print.js.map +1 -1
  42. package/dist/commands/delta/delta.js +44 -12
  43. package/dist/commands/delta/delta.js.map +1 -1
  44. package/dist/commands/delta/print.js +1 -1
  45. package/dist/commands/delta/print.js.map +1 -1
  46. package/dist/commands/dependencies.js +3 -7
  47. package/dist/commands/dependencies.js.map +1 -1
  48. package/dist/commands/diff/diff.js +20 -27
  49. package/dist/commands/diff/diff.js.map +1 -1
  50. package/dist/commands/explore.js +4 -8
  51. package/dist/commands/explore.js.map +1 -1
  52. package/dist/commands/gate/gate.js +45 -8
  53. package/dist/commands/gate/gate.js.map +1 -1
  54. package/dist/commands/gate/print.js +15 -3
  55. package/dist/commands/gate/print.js.map +1 -1
  56. package/dist/commands/gherkin/commit.js +13 -54
  57. package/dist/commands/gherkin/commit.js.map +1 -1
  58. package/dist/commands/gherkin/gherkin.js +36 -20
  59. package/dist/commands/gherkin/gherkin.js.map +1 -1
  60. package/dist/commands/gherkin/reconcile.js +10 -13
  61. package/dist/commands/gherkin/reconcile.js.map +1 -1
  62. package/dist/commands/init/first-hour.js +44 -0
  63. package/dist/commands/init/first-hour.js.map +1 -1
  64. package/dist/commands/init/init.js +18 -22
  65. package/dist/commands/init/init.js.map +1 -1
  66. package/dist/commands/list/campaign/campaign.js +4 -6
  67. package/dist/commands/list/campaign/campaign.js.map +1 -1
  68. package/dist/commands/list/json.js +0 -4
  69. package/dist/commands/list/json.js.map +1 -1
  70. package/dist/commands/list/list.js +38 -23
  71. package/dist/commands/list/list.js.map +1 -1
  72. package/dist/commands/list/print.js +38 -6
  73. package/dist/commands/list/print.js.map +1 -1
  74. package/dist/commands/list/suites/suites.js +140 -0
  75. package/dist/commands/list/suites/suites.js.map +1 -0
  76. package/dist/commands/list/views.js +28 -33
  77. package/dist/commands/list/views.js.map +1 -1
  78. package/dist/commands/mcp/dispatch.js +91 -36
  79. package/dist/commands/mcp/dispatch.js.map +1 -1
  80. package/dist/commands/mcp/mcp.js +56 -15
  81. package/dist/commands/mcp/mcp.js.map +1 -1
  82. package/dist/commands/mcp/serve.js.map +1 -1
  83. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  84. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  85. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  86. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  87. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  88. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  89. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  90. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  91. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  92. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  93. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  94. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  95. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  96. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  97. package/dist/commands/new/capability-template.js +118 -0
  98. package/dist/commands/new/capability-template.js.map +1 -0
  99. package/dist/commands/new/commit.js +8 -22
  100. package/dist/commands/new/commit.js.map +1 -1
  101. package/dist/commands/new/modifies.js +206 -0
  102. package/dist/commands/new/modifies.js.map +1 -0
  103. package/dist/commands/new/new.js +59 -14
  104. package/dist/commands/new/new.js.map +1 -1
  105. package/dist/commands/new/templates.js +48 -124
  106. package/dist/commands/new/templates.js.map +1 -1
  107. package/dist/commands/open.js +3 -7
  108. package/dist/commands/open.js.map +1 -1
  109. package/dist/commands/policy/gate.js +27 -0
  110. package/dist/commands/policy/gate.js.map +1 -1
  111. package/dist/commands/rebase/plan.js +2 -2
  112. package/dist/commands/rebase/plan.js.map +1 -1
  113. package/dist/commands/rebase/rebase.js +60 -35
  114. package/dist/commands/rebase/rebase.js.map +1 -1
  115. package/dist/commands/seed/commit.js +55 -26
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/plan.js +64 -13
  118. package/dist/commands/seed/plan.js.map +1 -1
  119. package/dist/commands/seed/seed.js +6 -9
  120. package/dist/commands/seed/seed.js.map +1 -1
  121. package/dist/commands/show/show.js +3 -7
  122. package/dist/commands/show/show.js.map +1 -1
  123. package/dist/commands/status/print.js +13 -4
  124. package/dist/commands/status/print.js.map +1 -1
  125. package/dist/commands/status/status.js +3 -7
  126. package/dist/commands/status/status.js.map +1 -1
  127. package/dist/commands/steps/steps.js +3 -7
  128. package/dist/commands/steps/steps.js.map +1 -1
  129. package/dist/commands/subsystem/edit.js +7 -7
  130. package/dist/commands/subsystem/edit.js.map +1 -1
  131. package/dist/commands/subsystem/move.js +60 -25
  132. package/dist/commands/subsystem/move.js.map +1 -1
  133. package/dist/commands/subsystem/query.js +2 -4
  134. package/dist/commands/subsystem/query.js.map +1 -1
  135. package/dist/commands/subsystem/subsystem.js +4 -7
  136. package/dist/commands/subsystem/subsystem.js.map +1 -1
  137. package/dist/commands/subsystem/sync.js +141 -71
  138. package/dist/commands/subsystem/sync.js.map +1 -1
  139. package/dist/commands/subsystem/txn/report.js +55 -5
  140. package/dist/commands/subsystem/txn/report.js.map +1 -1
  141. package/dist/commands/subsystem/txn/txn.js +121 -45
  142. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  143. package/dist/commands/subsystem/txn/views.js +82 -12
  144. package/dist/commands/subsystem/txn/views.js.map +1 -1
  145. package/dist/commands/tasks/render.js +116 -0
  146. package/dist/commands/tasks/render.js.map +1 -0
  147. package/dist/commands/tasks/tasks.js +113 -0
  148. package/dist/commands/tasks/tasks.js.map +1 -0
  149. package/dist/commands/unarchive/manifest.js +6 -9
  150. package/dist/commands/unarchive/manifest.js.map +1 -1
  151. package/dist/commands/unarchive/restore.js +17 -28
  152. package/dist/commands/unarchive/restore.js.map +1 -1
  153. package/dist/commands/unarchive/unarchive.js +22 -30
  154. package/dist/commands/unarchive/unarchive.js.map +1 -1
  155. package/dist/commands/validate/arch-coverage.js +6 -15
  156. package/dist/commands/validate/arch-coverage.js.map +1 -1
  157. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  158. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  159. package/dist/commands/validate/checks/health-deps.js +9 -6
  160. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  161. package/dist/commands/validate/checks/requirements.js +18 -8
  162. package/dist/commands/validate/checks/requirements.js.map +1 -1
  163. package/dist/commands/validate/checks/sources.js +6 -2
  164. package/dist/commands/validate/checks/sources.js.map +1 -1
  165. package/dist/commands/validate/feature.js +23 -19
  166. package/dist/commands/validate/feature.js.map +1 -1
  167. package/dist/commands/validate/fleet/census.js +26 -3
  168. package/dist/commands/validate/fleet/census.js.map +1 -1
  169. package/dist/commands/validate/fleet/landscape.js +35 -32
  170. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  171. package/dist/commands/validate/fleet/map/attest.js +7 -12
  172. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  173. package/dist/commands/validate/fleet/map/bindings.js +16 -5
  174. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  175. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  176. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  177. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  178. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  179. package/dist/commands/validate/fleet/obligations.js +1 -5
  180. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  181. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  182. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  183. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  184. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  185. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  186. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  187. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  188. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  189. package/dist/commands/validate/fleet/views/ids.js +33 -7
  190. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  191. package/dist/commands/validate/fleet/views/root-documents.js +17 -35
  192. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  193. package/dist/commands/validate/fleet/views/stale.js +31 -14
  194. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  195. package/dist/commands/validate/links/corpus.js +1 -4
  196. package/dist/commands/validate/links/corpus.js.map +1 -1
  197. package/dist/commands/validate/links/glossary.js.map +1 -1
  198. package/dist/commands/validate/report.js +5 -4
  199. package/dist/commands/validate/report.js.map +1 -1
  200. package/dist/commands/validate/scope.js +113 -0
  201. package/dist/commands/validate/scope.js.map +1 -0
  202. package/dist/commands/validate/service/api.js +16 -104
  203. package/dist/commands/validate/service/api.js.map +1 -1
  204. package/dist/commands/validate/service/contracts/generated.js +134 -0
  205. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  206. package/dist/commands/validate/service/events/events.js +1 -1
  207. package/dist/commands/validate/service/events/events.js.map +1 -1
  208. package/dist/commands/validate/service/events/message-producers.js +1 -1
  209. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  210. package/dist/commands/validate/service/evidence-pins.js +56 -10
  211. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  212. package/dist/commands/validate/service/service.js +16 -26
  213. package/dist/commands/validate/service/service.js.map +1 -1
  214. package/dist/commands/validate/service/specs.js +31 -38
  215. package/dist/commands/validate/service/specs.js.map +1 -1
  216. package/dist/commands/validate/service/spine.js +8 -11
  217. package/dist/commands/validate/service/spine.js.map +1 -1
  218. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  219. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  220. package/dist/commands/validate/validate.js +44 -26
  221. package/dist/commands/validate/validate.js.map +1 -1
  222. package/dist/commands/verify/cross/diff.js +2 -6
  223. package/dist/commands/verify/cross/diff.js.map +1 -1
  224. package/dist/commands/verify/evidence/contract.js +1 -1
  225. package/dist/commands/verify/evidence/contract.js.map +1 -1
  226. package/dist/commands/verify/evidence/read.js +12 -3
  227. package/dist/commands/verify/evidence/read.js.map +1 -1
  228. package/dist/commands/verify/frozen.js +45 -23
  229. package/dist/commands/verify/frozen.js.map +1 -1
  230. package/dist/commands/verify/record.js +69 -17
  231. package/dist/commands/verify/record.js.map +1 -1
  232. package/dist/commands/verify/report.js +27 -44
  233. package/dist/commands/verify/report.js.map +1 -1
  234. package/dist/commands/verify/results.js +112 -30
  235. package/dist/commands/verify/results.js.map +1 -1
  236. package/dist/commands/verify/verify.js +54 -51
  237. package/dist/commands/verify/verify.js.map +1 -1
  238. package/dist/commands/vouch/pack/pack.js +2 -2
  239. package/dist/commands/vouch/pack/pack.js.map +1 -1
  240. package/dist/commands/vouch/pack/print.js +1 -6
  241. package/dist/commands/vouch/pack/print.js.map +1 -1
  242. package/dist/commands/vouch/pack/sections.js +1 -2
  243. package/dist/commands/vouch/pack/sections.js.map +1 -1
  244. package/dist/commands/vouch/run.js +26 -81
  245. package/dist/commands/vouch/run.js.map +1 -1
  246. package/dist/commands/vouch/sample/plan.js +20 -34
  247. package/dist/commands/vouch/sample/plan.js.map +1 -1
  248. package/dist/commands/vouch/vet/verify.js +39 -7
  249. package/dist/commands/vouch/vet/verify.js.map +1 -1
  250. package/dist/commands/vouch/vouch.js +10 -17
  251. package/dist/commands/vouch/vouch.js.map +1 -1
  252. package/dist/core/agent/agents-md/command-map.js +57 -102
  253. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  254. package/dist/core/agent/agents-md/map/explain.js +4 -3
  255. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  256. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  257. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  258. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  259. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  260. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  261. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  262. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  263. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  264. package/dist/core/agent/agents-md/map/status.js +9 -3
  265. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  266. package/dist/core/agent/agents-md/map/subsystem.js +31 -17
  267. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  268. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  269. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  270. package/dist/core/agent/agents-md/refusals.js +37 -21
  271. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  272. package/dist/core/agent/agents-md.js +25 -10
  273. package/dist/core/agent/agents-md.js.map +1 -1
  274. package/dist/core/agent/agents-stamp.js +10 -0
  275. package/dist/core/agent/agents-stamp.js.map +1 -1
  276. package/dist/core/agent/scaffold.js +10 -15
  277. package/dist/core/agent/scaffold.js.map +1 -1
  278. package/dist/core/agent/tools/dialects.js +1 -0
  279. package/dist/core/agent/tools/dialects.js.map +1 -1
  280. package/dist/core/agent/tools/registry.js +41 -76
  281. package/dist/core/agent/tools/registry.js.map +1 -1
  282. package/dist/core/agent/workflows/adopt.js +5 -3
  283. package/dist/core/agent/workflows/adopt.js.map +1 -1
  284. package/dist/core/agent/workflows/check.js +25 -22
  285. package/dist/core/agent/workflows/check.js.map +1 -1
  286. package/dist/core/agent/workflows/closing.js +54 -21
  287. package/dist/core/agent/workflows/closing.js.map +1 -1
  288. package/dist/core/agent/workflows/feature.js +79 -11
  289. package/dist/core/agent/workflows/feature.js.map +1 -1
  290. package/dist/core/agent/workflows/implement.js +91 -13
  291. package/dist/core/agent/workflows/implement.js.map +1 -1
  292. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  293. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  294. package/dist/core/agent/workflows/reference/codes.js +8 -6
  295. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  296. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  297. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  298. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  299. package/dist/core/agent/workflows/reference/spine.js +62 -24
  300. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  301. package/dist/core/asyncapi/baseline/plan.js +14 -81
  302. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  303. package/dist/core/asyncapi/digest.js +30 -25
  304. package/dist/core/asyncapi/digest.js.map +1 -1
  305. package/dist/core/asyncapi/merge/components.js +5 -4
  306. package/dist/core/asyncapi/merge/components.js.map +1 -1
  307. package/dist/core/asyncapi/merge/error.js +7 -2
  308. package/dist/core/asyncapi/merge/error.js.map +1 -1
  309. package/dist/core/asyncapi/merge/markers.js +30 -50
  310. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  311. package/dist/core/asyncapi/merge/merge.js +31 -53
  312. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  313. package/dist/core/asyncapi/merge/pin.js +10 -36
  314. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  315. package/dist/core/asyncapi/model.js.map +1 -1
  316. package/dist/core/asyncapi/read.js +36 -12
  317. package/dist/core/asyncapi/read.js.map +1 -1
  318. package/dist/core/brief/brief.js +30 -25
  319. package/dist/core/brief/brief.js.map +1 -1
  320. package/dist/core/brief/landscape.js +8 -12
  321. package/dist/core/brief/landscape.js.map +1 -1
  322. package/dist/core/brief/map/extend.js +8 -5
  323. package/dist/core/brief/map/extend.js.map +1 -1
  324. package/dist/core/brief/map/owed.js +8 -26
  325. package/dist/core/brief/map/owed.js.map +1 -1
  326. package/dist/core/brief/targets.js +79 -5
  327. package/dist/core/brief/targets.js.map +1 -1
  328. package/dist/core/brief/unchecked.js +1 -1
  329. package/dist/core/brief/unchecked.js.map +1 -1
  330. package/dist/core/c4/likec4.js +147 -16
  331. package/dist/core/c4/likec4.js.map +1 -1
  332. package/dist/core/c4/locate/unlocated.js +150 -0
  333. package/dist/core/c4/locate/unlocated.js.map +1 -0
  334. package/dist/core/c4/parsed/values.js +54 -1
  335. package/dist/core/c4/parsed/values.js.map +1 -1
  336. package/dist/core/c4/prefetch/fleet.js +1 -9
  337. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  338. package/dist/core/c4/project/architecture.js +31 -9
  339. package/dist/core/c4/project/architecture.js.map +1 -1
  340. package/dist/core/c4/project/documents.js +36 -10
  341. package/dist/core/c4/project/documents.js.map +1 -1
  342. package/dist/core/c4/project/load.js +14 -21
  343. package/dist/core/c4/project/load.js.map +1 -1
  344. package/dist/core/c4/project/staged.js +7 -3
  345. package/dist/core/c4/project/staged.js.map +1 -1
  346. package/dist/core/c4/resolve/external.js +51 -0
  347. package/dist/core/c4/resolve/external.js.map +1 -0
  348. package/dist/core/c4/resolve/service.js +25 -1
  349. package/dist/core/c4/resolve/service.js.map +1 -1
  350. package/dist/core/c4/seed/template.js +15 -1
  351. package/dist/core/c4/seed/template.js.map +1 -1
  352. package/dist/core/c4/service-model/shape.js +2 -22
  353. package/dist/core/c4/service-model/shape.js.map +1 -1
  354. package/dist/core/c4/source-mask.js +288 -4
  355. package/dist/core/c4/source-mask.js.map +1 -1
  356. package/dist/core/c4/source-scan.js +203 -147
  357. package/dist/core/c4/source-scan.js.map +1 -1
  358. package/dist/core/c4/splice/authored-source.js +19 -0
  359. package/dist/core/c4/splice/authored-source.js.map +1 -1
  360. package/dist/core/c4/splice/delta-blocks.js +5 -18
  361. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  362. package/dist/core/c4/splice/landscape-merge.js +13 -8
  363. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  364. package/dist/core/c4/splice/model/merge.js +10 -6
  365. package/dist/core/c4/splice/model/merge.js.map +1 -1
  366. package/dist/core/c4/splice/placement.js +108 -18
  367. package/dist/core/c4/splice/placement.js.map +1 -1
  368. package/dist/core/c4/workspace.js +13 -22
  369. package/dist/core/c4/workspace.js.map +1 -1
  370. package/dist/core/capabilities/capabilities.js +6 -6
  371. package/dist/core/capabilities/capabilities.js.map +1 -1
  372. package/dist/core/capabilities/delta/doc.js +7 -10
  373. package/dist/core/capabilities/delta/doc.js.map +1 -1
  374. package/dist/core/capabilities/findings.js +15 -20
  375. package/dist/core/capabilities/findings.js.map +1 -1
  376. package/dist/core/capabilities/realizes/findings.js +12 -19
  377. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  378. package/dist/core/capabilities/realizes/join.js +0 -20
  379. package/dist/core/capabilities/realizes/join.js.map +1 -1
  380. package/dist/core/capabilities/rollup.js +6 -0
  381. package/dist/core/capabilities/rollup.js.map +1 -1
  382. package/dist/core/capabilities/tree.js +6 -28
  383. package/dist/core/capabilities/tree.js.map +1 -1
  384. package/dist/core/capabilities/usecase-join.js +3 -13
  385. package/dist/core/capabilities/usecase-join.js.map +1 -1
  386. package/dist/core/coherence/authoring/scaffold.js +11 -3
  387. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  388. package/dist/core/coherence/authoring/sentinels.js +2 -1
  389. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  390. package/dist/core/coherence/coherence.js +30 -16
  391. package/dist/core/coherence/coherence.js.map +1 -1
  392. package/dist/core/coherence/consumed/operations.js +70 -0
  393. package/dist/core/coherence/consumed/operations.js.map +1 -0
  394. package/dist/core/coherence/consumed/refusal.js +38 -0
  395. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  396. package/dist/core/coherence/declared.js +60 -34
  397. package/dist/core/coherence/declared.js.map +1 -1
  398. package/dist/core/coherence/events/declared.js +44 -59
  399. package/dist/core/coherence/events/declared.js.map +1 -1
  400. package/dist/core/coherence/events/events.js +119 -8
  401. package/dist/core/coherence/events/events.js.map +1 -1
  402. package/dist/core/coherence/events/grades.js +31 -8
  403. package/dist/core/coherence/events/grades.js.map +1 -1
  404. package/dist/core/coherence/events/lookups.js +14 -82
  405. package/dist/core/coherence/events/lookups.js.map +1 -1
  406. package/dist/core/coherence/lookups.js +18 -78
  407. package/dist/core/coherence/lookups.js.map +1 -1
  408. package/dist/core/coherence/retired/archived.js +285 -0
  409. package/dist/core/coherence/retired/archived.js.map +1 -0
  410. package/dist/core/coherence/retired/event-pair.js +62 -0
  411. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  412. package/dist/core/coherence/retired/merged.js +79 -0
  413. package/dist/core/coherence/retired/merged.js.map +1 -0
  414. package/dist/core/coherence/retired/retirement.js +49 -0
  415. package/dist/core/coherence/retired/retirement.js.map +1 -0
  416. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  417. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  418. package/dist/core/coherence/retired/revival/revived.js +297 -0
  419. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  420. package/dist/core/consumers/after.js +113 -0
  421. package/dist/core/consumers/after.js.map +1 -0
  422. package/dist/core/consumers/consumers.js +296 -0
  423. package/dist/core/consumers/consumers.js.map +1 -0
  424. package/dist/core/consumers/current.js +211 -0
  425. package/dist/core/consumers/current.js.map +1 -0
  426. package/dist/core/consumers/scan.js +46 -0
  427. package/dist/core/consumers/scan.js.map +1 -0
  428. package/dist/core/consumers/state.js +27 -0
  429. package/dist/core/consumers/state.js.map +1 -0
  430. package/dist/core/delta/delta.js +1 -1
  431. package/dist/core/delta/delta.js.map +1 -1
  432. package/dist/core/deployment/delta.js +15 -16
  433. package/dist/core/deployment/delta.js.map +1 -1
  434. package/dist/core/diff/base-git.js +94 -12
  435. package/dist/core/diff/base-git.js.map +1 -1
  436. package/dist/core/diff/base-state.js +28 -16
  437. package/dist/core/diff/base-state.js.map +1 -1
  438. package/dist/core/diff/joins.js +136 -0
  439. package/dist/core/diff/joins.js.map +1 -0
  440. package/dist/core/diff/scope/changed-paths.js +22 -6
  441. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  442. package/dist/core/diff/scope/edges.js +82 -0
  443. package/dist/core/diff/scope/edges.js.map +1 -0
  444. package/dist/core/diff/semantic.js +100 -31
  445. package/dist/core/diff/semantic.js.map +1 -1
  446. package/dist/core/doctor/agents.js +34 -18
  447. package/dist/core/doctor/agents.js.map +1 -1
  448. package/dist/core/doctor/report.js.map +1 -1
  449. package/dist/core/document/apply.js +10 -2
  450. package/dist/core/document/apply.js.map +1 -1
  451. package/dist/core/document/frontmatter.js +55 -0
  452. package/dist/core/document/frontmatter.js.map +1 -1
  453. package/dist/core/document/parse.js +29 -6
  454. package/dist/core/document/parse.js.map +1 -1
  455. package/dist/core/document/scenarios.js +17 -0
  456. package/dist/core/document/scenarios.js.map +1 -1
  457. package/dist/core/document/spec.js +4 -11
  458. package/dist/core/document/spec.js.map +1 -1
  459. package/dist/core/envelope/config.js +10 -0
  460. package/dist/core/envelope/config.js.map +1 -1
  461. package/dist/core/envelope/json.js +34 -0
  462. package/dist/core/envelope/json.js.map +1 -1
  463. package/dist/core/explain/families.js +17 -12
  464. package/dist/core/explain/families.js.map +1 -1
  465. package/dist/core/explain/refusals.js +3 -3
  466. package/dist/core/explain/refusals.js.map +1 -1
  467. package/dist/core/explain/terms.js +18 -9
  468. package/dist/core/explain/terms.js.map +1 -1
  469. package/dist/core/explore/explore.js +2 -7
  470. package/dist/core/explore/explore.js.map +1 -1
  471. package/dist/core/fleet-context.js +34 -95
  472. package/dist/core/fleet-context.js.map +1 -1
  473. package/dist/core/gate/checks.js +10 -3
  474. package/dist/core/gate/checks.js.map +1 -1
  475. package/dist/core/gate/partners.js +64 -19
  476. package/dist/core/gate/partners.js.map +1 -1
  477. package/dist/core/gate/report.js.map +1 -1
  478. package/dist/core/gate/verification.js +75 -12
  479. package/dist/core/gate/verification.js.map +1 -1
  480. package/dist/core/gherkin/stale.js +13 -37
  481. package/dist/core/gherkin/stale.js.map +1 -1
  482. package/dist/core/glossary/tree.js +6 -19
  483. package/dist/core/glossary/tree.js.map +1 -1
  484. package/dist/core/kernel/concurrency.js +19 -0
  485. package/dist/core/kernel/concurrency.js.map +1 -1
  486. package/dist/core/kernel/records.js +11 -0
  487. package/dist/core/kernel/records.js.map +1 -1
  488. package/dist/core/links/backlinks.js +1 -3
  489. package/dist/core/links/backlinks.js.map +1 -1
  490. package/dist/core/links/corpus.js +5 -36
  491. package/dist/core/links/corpus.js.map +1 -1
  492. package/dist/core/mcp/argv.js +101 -0
  493. package/dist/core/mcp/argv.js.map +1 -1
  494. package/dist/core/mcp/author-tools.js +2 -1
  495. package/dist/core/mcp/author-tools.js.map +1 -1
  496. package/dist/core/mcp/protocol.js +2 -2
  497. package/dist/core/mcp/protocol.js.map +1 -1
  498. package/dist/core/mcp/tools.js +43 -33
  499. package/dist/core/mcp/tools.js.map +1 -1
  500. package/dist/core/openapi/baseline/gate.js +53 -12
  501. package/dist/core/openapi/baseline/gate.js.map +1 -1
  502. package/dist/core/openapi/baseline/plan.js +22 -8
  503. package/dist/core/openapi/baseline/plan.js.map +1 -1
  504. package/dist/core/openapi/baseline/record.js +0 -1
  505. package/dist/core/openapi/baseline/record.js.map +1 -1
  506. package/dist/core/openapi/contract-text.js +33 -0
  507. package/dist/core/openapi/contract-text.js.map +1 -0
  508. package/dist/core/openapi/contract-tree.js +56 -0
  509. package/dist/core/openapi/contract-tree.js.map +1 -0
  510. package/dist/core/openapi/depth.js +38 -24
  511. package/dist/core/openapi/depth.js.map +1 -1
  512. package/dist/core/openapi/digest.js +52 -2
  513. package/dist/core/openapi/digest.js.map +1 -1
  514. package/dist/core/openapi/doc.js +12 -4
  515. package/dist/core/openapi/doc.js.map +1 -1
  516. package/dist/core/openapi/merge/components.js +25 -60
  517. package/dist/core/openapi/merge/components.js.map +1 -1
  518. package/dist/core/openapi/merge/error.js +3 -13
  519. package/dist/core/openapi/merge/error.js.map +1 -1
  520. package/dist/core/openapi/merge/markers.js +26 -30
  521. package/dist/core/openapi/merge/markers.js.map +1 -1
  522. package/dist/core/openapi/merge/merge.js +26 -52
  523. package/dist/core/openapi/merge/merge.js.map +1 -1
  524. package/dist/core/openapi/merge/pin.js +15 -35
  525. package/dist/core/openapi/merge/pin.js.map +1 -1
  526. package/dist/core/openspec/decide/verdict.js.map +1 -1
  527. package/dist/core/openspec/inventory.js +4 -5
  528. package/dist/core/openspec/inventory.js.map +1 -1
  529. package/dist/core/openspec/model/mapping.js +30 -28
  530. package/dist/core/openspec/model/mapping.js.map +1 -1
  531. package/dist/core/openspec/model/model.js +12 -7
  532. package/dist/core/openspec/model/model.js.map +1 -1
  533. package/dist/core/openspec/scan/changes.js +10 -4
  534. package/dist/core/openspec/scan/changes.js.map +1 -1
  535. package/dist/core/openspec/scan/config.js +7 -16
  536. package/dist/core/openspec/scan/config.js.map +1 -1
  537. package/dist/core/openspec/scan/shape.js +63 -19
  538. package/dist/core/openspec/scan/shape.js.map +1 -1
  539. package/dist/core/openspec/scan/walk.js +21 -38
  540. package/dist/core/openspec/scan/walk.js.map +1 -1
  541. package/dist/core/pack/living.js +2 -16
  542. package/dist/core/pack/living.js.map +1 -1
  543. package/dist/core/provenance/findings.js +48 -12
  544. package/dist/core/provenance/findings.js.map +1 -1
  545. package/dist/core/provenance/git.js +33 -57
  546. package/dist/core/provenance/git.js.map +1 -1
  547. package/dist/core/provenance/gitq/moves.js +71 -114
  548. package/dist/core/provenance/gitq/moves.js.map +1 -1
  549. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  550. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  551. package/dist/core/provenance/sources.js +23 -6
  552. package/dist/core/provenance/sources.js.map +1 -1
  553. package/dist/core/provenance/stamp.js +5 -17
  554. package/dist/core/provenance/stamp.js.map +1 -1
  555. package/dist/core/repo/entries.js.map +1 -1
  556. package/dist/core/repo/paths.js +34 -29
  557. package/dist/core/repo/paths.js.map +1 -1
  558. package/dist/core/repo/repo.js +24 -14
  559. package/dist/core/repo/repo.js.map +1 -1
  560. package/dist/core/repo/tree/find.js +12 -3
  561. package/dist/core/repo/tree/find.js.map +1 -1
  562. package/dist/core/repo/tree/fs.js +27 -1
  563. package/dist/core/repo/tree/fs.js.map +1 -1
  564. package/dist/core/repo/tree/render/body.js +45 -6
  565. package/dist/core/repo/tree/render/body.js.map +1 -1
  566. package/dist/core/repo/tree/render/service/header.js +125 -0
  567. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  568. package/dist/core/repo/tree/render/service/peers.js +28 -0
  569. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  570. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  571. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  572. package/dist/core/repo/tree/render/service/views.js +78 -0
  573. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  574. package/dist/core/repo/tree/render/stale.js +19 -2
  575. package/dist/core/repo/tree/render/stale.js.map +1 -1
  576. package/dist/core/repo/tree/render/views.js +102 -33
  577. package/dist/core/repo/tree/render/views.js.map +1 -1
  578. package/dist/core/repo/tree/walk.js +8 -30
  579. package/dist/core/repo/tree/walk.js.map +1 -1
  580. package/dist/core/results.js +47 -12
  581. package/dist/core/results.js.map +1 -1
  582. package/dist/core/staging/commit.js +10 -12
  583. package/dist/core/staging/commit.js.map +1 -1
  584. package/dist/core/staging/lock.js +74 -12
  585. package/dist/core/staging/lock.js.map +1 -1
  586. package/dist/core/staging/recovery/intent.js +2 -5
  587. package/dist/core/staging/recovery/intent.js.map +1 -1
  588. package/dist/core/staging/recovery/recover.js +60 -1
  589. package/dist/core/staging/recovery/recover.js.map +1 -1
  590. package/dist/core/staging/snapshot/manifest.js +152 -0
  591. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  592. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  593. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  594. package/dist/core/status/actions/execution.js +11 -0
  595. package/dist/core/status/actions/execution.js.map +1 -1
  596. package/dist/core/status/feature/artifacts.js +2 -2
  597. package/dist/core/status/feature/artifacts.js.map +1 -1
  598. package/dist/core/status/feature/feature.js +5 -11
  599. package/dist/core/status/feature/feature.js.map +1 -1
  600. package/dist/core/status/fleet/fleet.js +6 -2
  601. package/dist/core/status/fleet/fleet.js.map +1 -1
  602. package/dist/core/status/fleet/next.js +26 -3
  603. package/dist/core/status/fleet/next.js.map +1 -1
  604. package/dist/core/status/report.js.map +1 -1
  605. package/dist/core/status/verification.js +30 -15
  606. package/dist/core/status/verification.js.map +1 -1
  607. package/dist/core/tasks/packet.js +174 -0
  608. package/dist/core/tasks/packet.js.map +1 -0
  609. package/dist/core/tasks/record.js +43 -0
  610. package/dist/core/tasks/record.js.map +1 -0
  611. package/dist/core/tasks/sources.js +181 -0
  612. package/dist/core/tasks/sources.js.map +1 -0
  613. package/dist/core/tasks/stamps.js +76 -0
  614. package/dist/core/tasks/stamps.js.map +1 -0
  615. package/dist/core/tasks/unit.js +0 -0
  616. package/dist/core/tasks/unit.js.map +1 -0
  617. package/dist/core/usecases/capability.js +11 -14
  618. package/dist/core/usecases/capability.js.map +1 -1
  619. package/dist/core/usecases/delta/flows.js +5 -7
  620. package/dist/core/usecases/delta/flows.js.map +1 -1
  621. package/dist/core/usecases/fleet.js +178 -31
  622. package/dist/core/usecases/fleet.js.map +1 -1
  623. package/dist/core/usecases/operations.js +5 -4
  624. package/dist/core/usecases/operations.js.map +1 -1
  625. package/dist/core/usecases/service/flows.js +9 -6
  626. package/dist/core/usecases/service/flows.js.map +1 -1
  627. package/dist/core/usecases/suites/participants.js +83 -0
  628. package/dist/core/usecases/suites/participants.js.map +1 -0
  629. package/dist/core/usecases/suites/suites.js +45 -0
  630. package/dist/core/usecases/suites/suites.js.map +1 -0
  631. package/dist/core/verify/answers.js +12 -5
  632. package/dist/core/verify/answers.js.map +1 -1
  633. package/dist/core/verify/build.js +102 -82
  634. package/dist/core/verify/build.js.map +1 -1
  635. package/dist/core/verify/checklist.js +45 -10
  636. package/dist/core/verify/checklist.js.map +1 -1
  637. package/dist/core/verify/location.js +67 -0
  638. package/dist/core/verify/location.js.map +1 -0
  639. package/dist/core/verify/pins/lint.js +3 -2
  640. package/dist/core/verify/pins/lint.js.map +1 -1
  641. package/dist/core/verify/pins/pin.js +1 -1
  642. package/dist/core/verify/pins/pin.js.map +1 -1
  643. package/dist/core/verify/read/file.js +224 -0
  644. package/dist/core/verify/read/file.js.map +1 -0
  645. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  646. package/dist/core/verify/read/shape.js.map +1 -0
  647. package/dist/core/verify/read/union.js +105 -0
  648. package/dist/core/verify/read/union.js.map +1 -0
  649. package/dist/core/verify/record.js +35 -12
  650. package/dist/core/verify/record.js.map +1 -1
  651. package/dist/core/verify/store/commit.js +40 -30
  652. package/dist/core/verify/store/commit.js.map +1 -1
  653. package/dist/core/verify/store/render.js +8 -4
  654. package/dist/core/verify/store/render.js.map +1 -1
  655. package/dist/core/vocabulary/issue.js +1 -0
  656. package/dist/core/vocabulary/issue.js.map +1 -1
  657. package/dist/core/vocabulary/maturity.js +53 -3
  658. package/dist/core/vocabulary/maturity.js.map +1 -1
  659. package/examples/README.md +28 -9
  660. package/examples/docs/architecture/landscape.likec4 +21 -7
  661. package/examples/docs/architecture/services.likec4 +70 -0
  662. package/examples/docs/architecture/subsystems.likec4 +3 -0
  663. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  664. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  665. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  666. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  667. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  668. package/examples/docs/services/order-service/model.likec4 +10 -8
  669. package/examples/docs/services/payment-service/model.likec4 +6 -4
  670. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  671. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  672. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  673. package/package.json +1 -1
  674. package/dist/commands/validate/checks/vocabulary.js +0 -16
  675. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  676. package/dist/core/agent/agents-md/artifacts.js +0 -221
  677. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  678. package/dist/core/agent/agents-md/cycle.js +0 -180
  679. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  680. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  681. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  682. package/dist/core/agent/agents-md/spine.js +0 -40
  683. package/dist/core/agent/agents-md/spine.js.map +0 -1
  684. package/dist/core/coherence/events/merged.js +0 -40
  685. package/dist/core/coherence/events/merged.js.map +0 -1
  686. package/dist/core/diff/current-state.js +0 -96
  687. package/dist/core/diff/current-state.js.map +0 -1
  688. package/dist/core/diff/victims.js +0 -256
  689. package/dist/core/diff/victims.js.map +0 -1
  690. package/dist/core/staging/snapshot.js.map +0 -1
  691. package/dist/core/verify/file.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"glossary.js","sourceRoot":"","sources":["../../../../src/commands/validate/links/glossary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AACnE,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAGlE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgB,EAAE,KAAoB;IAC3E,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;IAC1C,6DAA6D;IAC7D,wEAAwE;IACxE,6EAA6E;IAC7E,2DAA2D;IAC3D,IAAI,CAAC,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEhE,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAClD,MAAM,UAAU,GAAG,CAAC,MAAc,EAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACjG,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK;SAC5B,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;SAC5D,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;SACpH,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;IACjD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAErC,6EAA6E;IAC7E,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,KAAK,CAAC,MAAM,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3E,OAAO;YACL;gBACE,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,mBAAmB;gBACzB,OAAO,EACL,yBAAyB,QAAQ,CAAC,KAAK,CAAC,MAAM,kEAAkE;oBAChH,+EAA+E;oBAC/E,sGAAsG;gBACxG,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;aAC7C;SACF,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;QACzC,QAAQ,EAAE,MAAe;QACzB,IAAI,EAAE,mBAAmB;QACzB,OAAO,EAAE,IAAI,CAAC,EAAE;QAChB,OAAO,EACL,YAAY,IAAI,CAAC,EAAE,kDAAkD;YACrE,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;gBAClB,CAAC,CAAC,sFAAsF;gBACxF,CAAC,CAAC,oGAAoG,CAAC;YACzG,kBAAkB,IAAI,CAAC,EAAE,yBAAyB,IAAI,CAAC,EAAE,mFAAmF;QAC9I,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;KACpD,CAAC,CAAC,CAAC;AACN,CAAC","sourcesContent":["/**\n * `glossary.unlinked` — a defined term that no document outside the glossary\n * cites.\n *\n * The mirror of `permissions.unenforced` and `capability.unrealized`, and it\n * earns its place for their reason: a vocabulary is worth what cites it. A term\n * nobody links to is either a word the fleet never adopted or a definition left\n * behind by a rename — both are drift, and neither is visible from inside the\n * file. WARN, because the honest answer is often \"not written up yet\", and\n * because a glossary must be safe to start: a fleet that defines twenty terms\n * on a Monday and cites four of them is adopting the axis, not failing it.\n *\n * A TERM CITED ONLY BY OTHER TERMS IS STILL UNLINKED, and this is the judgement\n * that keeps the warning worth anything. A glossary is a network of definitions,\n * so `order-line` linking to `order` is normal and correct — and it proves\n * nothing about whether the FLEET uses either word. The same reasoning\n * `capability.requirement-inert-join` applies one axis over: a join written\n * inside the tree is not evidence about the world outside it. The intra-glossary\n * citations are listed in `details` rather than hidden, because they are the\n * first thing a reader will otherwise go looking for.\n *\n * ONE FINDING FOR THE WHOLE GLOSSARY when nothing cites anything, and one per\n * term otherwise. A fleet that has just created `glossary/` and written its\n * first eight definitions would otherwise get eight identical warnings saying\n * the same thing about the same afternoon's work.\n */\nimport { backlinkIndex } from \"../../../core/links/backlinks.js\";\nimport { readGlossary } from \"../../../core/glossary/tree.js\";\nimport { glossaryDir } from \"../../../core/repo/authored/paths.js\";\nimport { repoPath } from \"../../../core/envelope/json.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\nexport async function glossaryFindings(docsDir: DocsDir, fleet?: FleetContext): Promise<Finding[]> {\n const root = glossaryDir(docsDir);\n const glossary = await readGlossary(root);\n // The directory's existence is the axis's opt-in, exactly as\n // `architecture/capabilities.yaml` and `capabilities/` are the business\n // axis's. A fleet without one pays a single `existsSync` and hears nothing —\n // and the whole fleet-wide link walk below is never built.\n if (!glossary.present || glossary.terms.length === 0) return [];\n\n const index = await backlinkIndex(docsDir, fleet);\n const inGlossary = (source: string): boolean => source.startsWith(`${repoPath(docsDir, root)}/`);\n const unlinked = glossary.terms\n .map((term) => ({ term, cited: index.linkersOf(term.path) }))\n .map(({ term, cited }) => ({ term, outside: cited.filter((s) => !inGlossary(s)), within: cited.filter(inGlossary) }))\n .filter(({ outside }) => outside.length === 0);\n if (unlinked.length === 0) return [];\n\n // The whole vocabulary uncited is one fact about one afternoon, not N facts.\n if (unlinked.length === glossary.terms.length && glossary.terms.length > 1) {\n return [\n {\n severity: \"warn\",\n code: \"glossary.unlinked\",\n message:\n `glossary: none of the ${glossary.terms.length} defined term(s) is linked from any document outside glossary/. ` +\n \"A term becomes checkable when a requirement, capability or intent cites it — \" +\n \"`[Order](../../glossary/order.md)` — and until then the definitions are a document nothing joins to.\",\n details: unlinked.map(({ term }) => term.id),\n },\n ];\n }\n return unlinked.map(({ term, within }) => ({\n severity: \"warn\" as const,\n code: \"glossary.unlinked\",\n subject: term.id,\n message:\n `glossary/${term.id}.md is linked from no document outside glossary/` +\n (within.length === 0\n ? \" — either a word the fleet has not adopted, or a definition left behind by a rename.\"\n : \", only from other terms — which says the glossary is consistent, not that the fleet uses the word.\") +\n ` Cite it as \\`[${term.id}](<relative>/glossary/${term.id}.md)\\` from the requirement or capability that uses it, or delete the definition.`,\n ...(within.length === 0 ? {} : { details: within }),\n }));\n}\n"]}
1
+ {"version":3,"file":"glossary.js","sourceRoot":"","sources":["../../../../src/commands/validate/links/glossary.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `glossary.unlinked` — a defined term that no document outside the glossary\n * cites.\n *\n * The mirror of `permissions.unenforced` and `capability.unrealized`, and it\n * earns its place for their reason: a vocabulary is worth what cites it. A term\n * nobody links to is either a word the fleet never adopted or a definition left\n * behind by a rename — both are drift, and neither is visible from inside the\n * file. WARN, because the honest answer is often \"not written up yet\", and\n * because a glossary must be safe to start: a fleet that defines twenty terms\n * on a Monday and cites four of them is adopting the axis, not failing it.\n *\n * A TERM CITED ONLY BY OTHER TERMS IS STILL UNLINKED, and this is the judgement\n * that keeps the warning worth anything. A glossary is a network of definitions,\n * so `order-line` linking to `order` is normal and correct — and it proves\n * nothing about whether the FLEET uses either word. The same reasoning\n * `capability.requirement-inert-join` applies one axis over: a join written\n * inside the tree is not evidence about the world outside it. The intra-glossary\n * citations are listed in `details` rather than hidden, because they are the\n * first thing a reader will otherwise go looking for.\n *\n * ONE FINDING FOR THE WHOLE GLOSSARY when nothing cites anything, and one per\n * term otherwise. A fleet that has just created `glossary/` and written its\n * first eight definitions would otherwise get eight identical warnings saying\n * the same thing about the same afternoon's work.\n */\nimport { backlinkIndex } from \"../../../core/links/backlinks.js\";\nimport { readGlossary } from \"../../../core/glossary/tree.js\";\nimport { glossaryDir } from \"../../../core/repo/authored/paths.js\";\nimport { repoPath } from \"../../../core/envelope/json.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\nexport async function glossaryFindings(docsDir: DocsDir, fleet: FleetContext): Promise<Finding[]> {\n const root = glossaryDir(docsDir);\n const glossary = await readGlossary(root);\n // The directory's existence is the axis's opt-in, exactly as\n // `architecture/capabilities.yaml` and `capabilities/` are the business\n // axis's. A fleet without one pays a single `existsSync` and hears nothing —\n // and the whole fleet-wide link walk below is never built.\n if (!glossary.present || glossary.terms.length === 0) return [];\n\n const index = await backlinkIndex(docsDir, fleet);\n const inGlossary = (source: string): boolean => source.startsWith(`${repoPath(docsDir, root)}/`);\n const unlinked = glossary.terms\n .map((term) => ({ term, cited: index.linkersOf(term.path) }))\n .map(({ term, cited }) => ({ term, outside: cited.filter((s) => !inGlossary(s)), within: cited.filter(inGlossary) }))\n .filter(({ outside }) => outside.length === 0);\n if (unlinked.length === 0) return [];\n\n // The whole vocabulary uncited is one fact about one afternoon, not N facts.\n if (unlinked.length === glossary.terms.length && glossary.terms.length > 1) {\n return [\n {\n severity: \"warn\",\n code: \"glossary.unlinked\",\n message:\n `glossary: none of the ${glossary.terms.length} defined term(s) is linked from any document outside glossary/. ` +\n \"A term becomes checkable when a requirement, capability or intent cites it — \" +\n \"`[Order](../../glossary/order.md)` — and until then the definitions are a document nothing joins to.\",\n details: unlinked.map(({ term }) => term.id),\n },\n ];\n }\n return unlinked.map(({ term, within }) => ({\n severity: \"warn\" as const,\n code: \"glossary.unlinked\",\n subject: term.id,\n message:\n `glossary/${term.id}.md is linked from no document outside glossary/` +\n (within.length === 0\n ? \" — either a word the fleet has not adopted, or a definition left behind by a rename.\"\n : \", only from other terms — which says the glossary is consistent, not that the fleet uses the word.\") +\n ` Cite it as \\`[${term.id}](<relative>/glossary/${term.id}.md)\\` from the requirement or capability that uses it, or delete the definition.`,\n ...(within.length === 0 ? {} : { details: within }),\n }));\n}\n"]}
@@ -32,10 +32,11 @@ export function summary(targets) {
32
32
  * arch.spec.md both name `sources` raises two findings and is one service) is
33
33
  * `subjectsWith`'s, spelled once for every findings-derived rollup — the
34
34
  * fleet scorecard counts through the same function on the same code, so its
35
- * number and this one are structurally the same number. The code is a LITERAL
36
- * rather than checks/vocabulary.ts's UNVERIFIABLE constant because the
37
- * stable-code collector reads counting sites too, and it refuses a slot it
38
- * cannot read — the collector is also what convicts a typo here.
35
+ * number and this one are structurally the same number. The code is a LITERAL,
36
+ * as it is at the emission site (checks/sources.ts), rather than a shared
37
+ * constant, because the stable-code collector reads counting sites too, and it
38
+ * refuses a slot it cannot read — the collector is also what convicts a typo
39
+ * here.
39
40
  */
40
41
  export function unverifiableSubjects(targets) {
41
42
  return subjectsWith(targets, "sources.unverifiable-from-here");
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/commands/validate/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Everything that happens to a `TargetReport` around the checks themselves:\n * how one is built when a target could not be read at all, how a finding's\n * evidence is capped, how the set is summed, and how it prints.\n *\n * A module rather than the tail of validate.ts because the rollup and the\n * renderer have to agree. The `--all` footer and the `--json` summary are the\n * same `summary()`, and the cap reaches both payloads from one place; a second\n * copy of either is how a footer starts printing a number its own findings\n * contradict.\n */\nimport { DocsRepoUnavailableError } from \"../../core/repo/state.js\";\nimport { listCodes } from \"../../core/explain/lookup.js\";\nimport {\n countSeverity,\n subjectsWith,\n SEVERITY_MARK,\n type Finding,\n type TargetReport,\n} from \"../../core/vocabulary/report.js\";\nimport { EXPLAIN_FOOTER, plural } from \"../policy/format.js\";\nimport {\n ADOPTION_AXES,\n groupedWarnCodes,\n type Adoption,\n type AdoptionAxis,\n} from \"./fleet/scorecard/adoption.js\";\n\n/**\n * The rollup `--json` carries and the `--all` footer prints.\n *\n * Four named fields rather than a `Record<string, number>`: the keys ARE the\n * contract, and under an index signature every read of them was an assertion\n * (`s.errors!`) that tsc could not check — rename one and the compiler stayed\n * quiet while the footer printed `undefined errors`.\n */\nexport interface ValidateSummary {\n services: number;\n features: number;\n errors: number;\n warnings: number;\n}\n\nexport function summary(targets: TargetReport[]): ValidateSummary {\n return {\n services: targets.filter((t) => t.kind === \"service\").length,\n features: targets.filter((t) => t.kind === \"feature\").length,\n errors: countSeverity(targets, \"error\"),\n warnings: countSeverity(targets, \"warn\"),\n };\n}\n\n/**\n * How many services this run could not check, for the rollup line and the\n * `--json` payload.\n *\n * Counted off the findings rather than alongside them, so the rollup line\n * and the per-service findings can never disagree about how many services\n * this run could not check. The by-SUBJECT rule (a service whose spec.md and\n * arch.spec.md both name `sources` raises two findings and is one service) is\n * `subjectsWith`'s, spelled once for every findings-derived rollup — the\n * fleet scorecard counts through the same function on the same code, so its\n * number and this one are structurally the same number. The code is a LITERAL\n * rather than checks/vocabulary.ts's UNVERIFIABLE constant because the\n * stable-code collector reads counting sites too, and it refuses a slot it\n * cannot read — the collector is also what convicts a typo here.\n */\nexport function unverifiableSubjects(targets: TargetReport[]): number {\n return subjectsWith(targets, \"sources.unverifiable-from-here\");\n}\n\n/**\n * How many `details` lines any one finding may print before the rest are\n * summarised away.\n *\n * A finding's details are evidence, not a log: LikeC4 reports one syntax error\n * as dozens of cascading diagnostics, and a fleet-sized repo multiplies that by\n * every target that mentions the file. The report is read by a person scrolling\n * a CI log and by an agent with a context window, and neither of them is helped\n * by the four-hundredth copy. The cap is applied to the JSON payload too, on\n * purpose: `--json` is the interface an agent pipes, and an unbounded array is\n * the same denial-of-attention there, just machine-readable.\n */\nconst DETAIL_LIMIT = 10;\n\n/** Truncate every finding's details, marking what was dropped so nothing looks complete when it is not. */\nexport function capDetails(t: TargetReport): TargetReport {\n return {\n ...t,\n findings: t.findings.map((f) => {\n const details = f.details ?? [];\n if (details.length <= DETAIL_LIMIT) return f;\n return {\n ...f,\n details: [...details.slice(0, DETAIL_LIMIT), `… (+${details.length - DETAIL_LIMIT} more)`],\n };\n }),\n };\n}\n\n/**\n * One target's checks, with an IO exception turned into a finding ON that\n * target instead of aborting the run.\n *\n * A fleet gate that dies on the first unreadable file reports nothing about the\n * other ninety-nine services — one bad permission bit, one file that is a\n * dangling symlink, and CI's answer to \"how is the fleet\" becomes a stack\n * trace. The failure is real and it is an error, so the run still exits 1; what\n * changes is that everything else is still graded, and the finding names the\n * service and the path instead of arriving as the `internal` catch-all.\n */\nexport async function guarded(\n target: { kind: \"service\" | \"feature\"; id: string },\n run: () => Promise<TargetReport>,\n): Promise<TargetReport> {\n try {\n return await run();\n } catch (err) {\n // A docs repo that vanished mid-run is not one target's problem — it is the\n // whole run's, and the action's own catch reports it.\n if (err instanceof DocsRepoUnavailableError) throw err;\n const path = (err as NodeJS.ErrnoException).path;\n const reason = err instanceof Error ? err.message : String(err);\n return {\n kind: target.kind,\n id: target.id,\n findings: [\n {\n severity: \"error\",\n code: target.kind === \"service\" ? \"service.unreadable\" : \"feature.unreadable\",\n subject: target.id,\n message:\n `${target.id}: ${path === undefined ? \"an artifact\" : path} could not be read — ` +\n `nothing about this ${target.kind} was checked. ${reason}`,\n },\n ],\n };\n }\n}\n\n/**\n * `target.ambiguous` — the positional named a service AND a feature. The tie is\n * still broken the way it always was (the feature wins), because changing which\n * one is picked would silently re-target every script that relies on it; what\n * is new is that the run says which reading it took and how to force the other.\n */\nexport function ambiguousTarget(arg: string, chosen: \"service\" | \"feature\"): Finding {\n const other = chosen === \"feature\" ? \"service\" : \"feature\";\n return {\n severity: \"warn\",\n code: \"target.ambiguous\",\n subject: arg,\n message:\n `'${arg}' names both a service and a feature — validated as the ${chosen}. ` +\n `Pass --${other} ${arg} for the other reading.`,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Text renderer */\n/* ------------------------------------------------------------------ */\n\n/** What `renderText` needs of the run beyond the graded targets themselves. */\nexport interface RenderOptions {\n all: boolean;\n errorsOnly: boolean;\n /** The fleet's unverifiable-sources subject count, for the one-line rollup. */\n unverifiable: number;\n /**\n * The run's scorecard facts the renderer reads — ONE field, not a nullable\n * adoption beside a defaulted denominator, so \"adoption without its real\n * denominator\" is not constructible. Null on every single-target run and on\n * an `--all` run whose card failed closed, and null renders exactly as\n * before grouping existed: fail-open to today's behaviour, every warning\n * printed individually.\n */\n scorecard: { readonly adoption: Adoption; readonly services: number } | null;\n}\n\n/**\n * `--errors-only` is a RENDERING lever, the way `--strict` is an exit-code\n * lever: neither changes the report, and the `--json` payload is unaffected by\n * both. On a fleet of a hundred services the clean run prints several hundred\n * `ok` confirmations, and the two warnings that matter are somewhere inside it;\n * anyone reading a CI log wants the exceptions, and anyone auditing wants all\n * of it. Both are available, from the same run, and neither is the default.\n *\n * Axis grouping is the same kind of lever, one notch further: warnings whose\n * SOLE cause is a fleet-wide not-started axis (the mechanical, fail-closed\n * rule in fleet/scorecard/adoption.ts — warn severity, listed code, axis at\n * zero) are dropped from the per-target listing and re-stated as one banner\n * per axis after the footer. The report, the footer's counts, `--json` and\n * `--strict` are all computed from the UNFILTERED findings, so nothing a\n * machine or an exit code reads moves by a single bit.\n */\nexport function renderText(targets: TargetReport[], opts: RenderOptions): void {\n const { all, errorsOnly, unverifiable } = opts;\n // When the fleet-wide \"not started\" claim is measurable at all — each guard\n // fails closed to ungrouped rendering. `!all` is structural, not just the\n // call site's promise: the banners print after the footer that only `--all`\n // reaches, so grouping outside `--all` would DROP warnings instead of\n // moving them — the one thing this lever must never do. `services === 0`\n // is the vacuous fleet: with no services every axis is 0 \"fleet-wide\", and\n // a banner over \"0 of 0 services\" would hide the fleet-level warnings of a\n // repo that merely has no fleet yet. And an unreadable service proves\n // nothing about what it started: its all-false participation (contracts.ts)\n // could MANUFACTURE the very N=0 that licenses suppression — beside an\n // error saying that service could not be checked — so one\n // `service.unreadable` subject disables grouping for the whole run.\n const card =\n !all ||\n opts.scorecard === null ||\n opts.scorecard.services === 0 ||\n subjectsWith(targets, \"service.unreadable\") > 0\n ? null\n : opts.scorecard;\n const grouped = card === null ? new Map<string, AdoptionAxis>() : groupedWarnCodes(card.adoption);\n const isGrouped = (f: Finding): boolean => f.severity === \"warn\" && grouped.has(f.code);\n\n // The explain pointer follows any report that printed work — a non-ok\n // finding, listed or folded under an axis banner. Gated on the UNFILTERED\n // findings like every other rollup, so `--errors-only` and grouping cannot\n // print codes the pointer then fails to follow. It is computed HERE rather\n // than beside the two places it prints because the code column below asks\n // the same question and must not walk the findings a second time to answer\n // it differently.\n const pointer = targets.some((t) => t.findings.some((f) => f.severity !== \"ok\"));\n // The lookup key on the line itself.\n //\n // Until now a text report printed messages and no codes, so the footer under\n // it could only say codes were somewhere else — in `--json`, which a reader\n // would have to re-run the whole command to see. The code is what `loam\n // explain` takes, what a CI branch tests and what a bug report quotes, and\n // withholding it from the one surface a person actually reads made the\n // footer's own offer unreachable.\n //\n // Only where a code is genuinely answerable, and that guard is not\n // decoration: a handful of emitted finding codes still have no fix-table row\n // (test/explain.test.ts names them), and printing one beside a footer\n // promising an explanation would send the reader to a refusal. The set is\n // built ONCE per render, and only when something non-ok will be printed:\n // `explainSubject` walks the shipped workflow bodies per code (the cost\n // `fixesFor` measured from the other side, where a handful of lookups is the\n // cheaper shape), while `listCodes()` walks them once for the whole\n // vocabulary — and a fleet report has far more finding lines than the fleet\n // has distinct codes. Measured: 2.7 ms for all 273 codes, once per report.\n const explainable = pointer ? new Set(listCodes().map((listing) => listing.code)) : new Set<string>();\n for (const t of targets) {\n const shown = t.findings.filter(\n (f) => !isGrouped(f) && (!errorsOnly || f.severity !== \"ok\"),\n );\n if (shown.length === 0) continue;\n // A feature announces itself; a service's findings already carry its name.\n if (t.kind === \"feature\") console.log(t.id);\n let header: string | undefined;\n for (const f of shown) {\n const hint = f.text ?? {};\n if (hint.header && hint.header !== header) {\n header = hint.header;\n console.log(` ${header}`);\n }\n // The whole report goes to stdout, in document order. Splitting errors\n // onto stderr meant a piped stdout silently lost them from the middle of\n // the report and 2>&1 could reorder it; the exit code carries failure,\n // and stderr stays reserved for refusals (the fail() path).\n const marker = hint.marker === false ? \"\" : `${SEVERITY_MARK[f.severity]} `;\n // `ok` confirmations stay clean, deliberately. A clean fleet run is\n // several hundred ✓ lines and two warnings, and a code appended to every\n // one of them would cost exactly the signal the exceptions carry — there\n // is nothing to look up about a check that passed.\n const code = f.severity !== \"ok\" && explainable.has(f.code) ? ` (${f.code})` : \"\";\n console.log(`${\" \".repeat(hint.indent ?? 0)}${marker}${f.message}${code}`);\n for (const d of f.details ?? []) console.log(` ${hint.detailPrefix ?? \"\"}${d}`);\n }\n }\n\n if (!all) {\n // Without the --all footer there would be nothing at all to print for a\n // clean single target under --errors-only — and silence is the one output\n // that must never mean \"checked, fine\".\n if (errorsOnly && targets.every((t) => t.findings.every((f) => f.severity === \"ok\"))) {\n console.log(`${targets.map((t) => t.id).join(\", \")}: no errors or warnings`);\n }\n if (pointer) console.log(`\\n${EXPLAIN_FOOTER}`);\n return;\n }\n const s = summary(targets);\n console.log(\n `\\n${plural(s.services, \"service\")}, ${plural(s.features, \"feature\")} — ` +\n `${plural(s.errors, \"error\")}, ${plural(s.warnings, \"warning\")}`,\n );\n // One line for the whole fleet, never one per service: honest about the blind\n // spot without drowning the report in a hundred copies of it.\n if (unverifiable > 0) {\n const whose = unverifiable === 1 ? \"1 service's\" : `${unverifiable} services'`;\n console.log(\n `⚠ sources.unverifiable-from-here: ${whose} sources can only be checked from their own repos`,\n );\n }\n // The axis banners — one per not-started axis that actually grouped a\n // warning, in the axes' fixed order, each naming its dropped codes with\n // counts. Tallied off the UNFILTERED findings (the same walk the footer\n // counts), and printed under --errors-only too: like the footer and the\n // unverifiable line this is a rollup, not a finding.\n if (card !== null && grouped.size > 0) {\n const tally = new Map<string, number>();\n for (const t of targets) {\n for (const f of t.findings) {\n if (isGrouped(f)) tally.set(f.code, (tally.get(f.code) ?? 0) + 1);\n }\n }\n for (const axis of ADOPTION_AXES) {\n // Insertion order of `grouped` is the table's order, so the pairs render\n // deterministically across runs and machines.\n const pairs = [...grouped]\n .filter(([code, a]) => a === axis && (tally.get(code) ?? 0) > 0)\n .map(([code]) => [code, tally.get(code) ?? 0] as const);\n if (pairs.length === 0) continue;\n const total = pairs.reduce((n, [, k]) => n + k, 0);\n console.log(\n `⚠ ${axis} axis not started fleet-wide (0 of ${plural(card.services, \"service\")}), ` +\n `expected during staged adoption — ${total} warning(s) grouped: ` +\n `${pairs.map(([code, k]) => `${code}×${k}`).join(\", \")}; every finding is unchanged in --json`,\n );\n }\n }\n if (pointer) console.log(`\\n${EXPLAIN_FOOTER}`);\n}\n"]}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/commands/validate/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Everything that happens to a `TargetReport` around the checks themselves:\n * how one is built when a target could not be read at all, how a finding's\n * evidence is capped, how the set is summed, and how it prints.\n *\n * A module rather than the tail of validate.ts because the rollup and the\n * renderer have to agree. The `--all` footer and the `--json` summary are the\n * same `summary()`, and the cap reaches both payloads from one place; a second\n * copy of either is how a footer starts printing a number its own findings\n * contradict.\n */\nimport { DocsRepoUnavailableError } from \"../../core/repo/state.js\";\nimport { listCodes } from \"../../core/explain/lookup.js\";\nimport {\n countSeverity,\n subjectsWith,\n SEVERITY_MARK,\n type Finding,\n type TargetReport,\n} from \"../../core/vocabulary/report.js\";\nimport { EXPLAIN_FOOTER, plural } from \"../policy/format.js\";\nimport {\n ADOPTION_AXES,\n groupedWarnCodes,\n type Adoption,\n type AdoptionAxis,\n} from \"./fleet/scorecard/adoption.js\";\n\n/**\n * The rollup `--json` carries and the `--all` footer prints.\n *\n * Four named fields rather than a `Record<string, number>`: the keys ARE the\n * contract, and under an index signature every read of them was an assertion\n * (`s.errors!`) that tsc could not check — rename one and the compiler stayed\n * quiet while the footer printed `undefined errors`.\n */\nexport interface ValidateSummary {\n services: number;\n features: number;\n errors: number;\n warnings: number;\n}\n\nexport function summary(targets: TargetReport[]): ValidateSummary {\n return {\n services: targets.filter((t) => t.kind === \"service\").length,\n features: targets.filter((t) => t.kind === \"feature\").length,\n errors: countSeverity(targets, \"error\"),\n warnings: countSeverity(targets, \"warn\"),\n };\n}\n\n/**\n * How many services this run could not check, for the rollup line and the\n * `--json` payload.\n *\n * Counted off the findings rather than alongside them, so the rollup line\n * and the per-service findings can never disagree about how many services\n * this run could not check. The by-SUBJECT rule (a service whose spec.md and\n * arch.spec.md both name `sources` raises two findings and is one service) is\n * `subjectsWith`'s, spelled once for every findings-derived rollup — the\n * fleet scorecard counts through the same function on the same code, so its\n * number and this one are structurally the same number. The code is a LITERAL,\n * as it is at the emission site (checks/sources.ts), rather than a shared\n * constant, because the stable-code collector reads counting sites too, and it\n * refuses a slot it cannot read — the collector is also what convicts a typo\n * here.\n */\nexport function unverifiableSubjects(targets: TargetReport[]): number {\n return subjectsWith(targets, \"sources.unverifiable-from-here\");\n}\n\n/**\n * How many `details` lines any one finding may print before the rest are\n * summarised away.\n *\n * A finding's details are evidence, not a log: LikeC4 reports one syntax error\n * as dozens of cascading diagnostics, and a fleet-sized repo multiplies that by\n * every target that mentions the file. The report is read by a person scrolling\n * a CI log and by an agent with a context window, and neither of them is helped\n * by the four-hundredth copy. The cap is applied to the JSON payload too, on\n * purpose: `--json` is the interface an agent pipes, and an unbounded array is\n * the same denial-of-attention there, just machine-readable.\n */\nconst DETAIL_LIMIT = 10;\n\n/** Truncate every finding's details, marking what was dropped so nothing looks complete when it is not. */\nexport function capDetails(t: TargetReport): TargetReport {\n return {\n ...t,\n findings: t.findings.map((f) => {\n const details = f.details ?? [];\n if (details.length <= DETAIL_LIMIT) return f;\n return {\n ...f,\n details: [...details.slice(0, DETAIL_LIMIT), `… (+${details.length - DETAIL_LIMIT} more)`],\n };\n }),\n };\n}\n\n/**\n * One target's checks, with an IO exception turned into a finding ON that\n * target instead of aborting the run.\n *\n * A fleet gate that dies on the first unreadable file reports nothing about the\n * other ninety-nine services — one bad permission bit, one file that is a\n * dangling symlink, and CI's answer to \"how is the fleet\" becomes a stack\n * trace. The failure is real and it is an error, so the run still exits 1; what\n * changes is that everything else is still graded, and the finding names the\n * service and the path instead of arriving as the `internal` catch-all.\n */\nexport async function guarded(\n target: { kind: \"service\" | \"feature\"; id: string },\n run: () => Promise<TargetReport>,\n): Promise<TargetReport> {\n try {\n return await run();\n } catch (err) {\n // A docs repo that vanished mid-run is not one target's problem — it is the\n // whole run's, and the action's own catch reports it.\n if (err instanceof DocsRepoUnavailableError) throw err;\n const path = (err as NodeJS.ErrnoException).path;\n const reason = err instanceof Error ? err.message : String(err);\n return {\n kind: target.kind,\n id: target.id,\n findings: [\n {\n severity: \"error\",\n code: target.kind === \"service\" ? \"service.unreadable\" : \"feature.unreadable\",\n subject: target.id,\n message:\n `${target.id}: ${path === undefined ? \"an artifact\" : path} could not be read — ` +\n `nothing about this ${target.kind} was checked. ${reason}`,\n },\n ],\n };\n }\n}\n\n/**\n * `target.ambiguous` — the positional named a service AND a feature. The tie is\n * still broken the way it always was (the feature wins), because changing which\n * one is picked would silently re-target every script that relies on it; what\n * is new is that the run says which reading it took and how to force the other.\n */\nexport function ambiguousTarget(arg: string, chosen: \"service\" | \"feature\"): Finding {\n const other = chosen === \"feature\" ? \"service\" : \"feature\";\n return {\n severity: \"warn\",\n code: \"target.ambiguous\",\n subject: arg,\n message:\n `'${arg}' names both a service and a feature — validated as the ${chosen}. ` +\n `Pass --${other} ${arg} for the other reading.`,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Text renderer */\n/* ------------------------------------------------------------------ */\n\n/** What `renderText` needs of the run beyond the graded targets themselves. */\nexport interface RenderOptions {\n all: boolean;\n errorsOnly: boolean;\n /** The fleet's unverifiable-sources subject count, for the one-line rollup. */\n unverifiable: number;\n /**\n * The run's scorecard facts the renderer reads — ONE field, not a nullable\n * adoption beside a defaulted denominator, so \"adoption without its real\n * denominator\" is not constructible. Null on every single-target run and on\n * an `--all` run whose card failed closed, and null renders exactly as\n * before grouping existed: fail-open to today's behaviour, every warning\n * printed individually.\n */\n scorecard: { readonly adoption: Adoption; readonly services: number } | null;\n}\n\n/**\n * `--errors-only` is a RENDERING lever, the way `--strict` is an exit-code\n * lever: neither changes the report, and the `--json` payload is unaffected by\n * both. On a fleet of a hundred services the clean run prints several hundred\n * `ok` confirmations, and the two warnings that matter are somewhere inside it;\n * anyone reading a CI log wants the exceptions, and anyone auditing wants all\n * of it. Both are available, from the same run, and neither is the default.\n *\n * Axis grouping is the same kind of lever, one notch further: warnings whose\n * SOLE cause is a fleet-wide not-started axis (the mechanical, fail-closed\n * rule in fleet/scorecard/adoption.ts — warn severity, listed code, axis at\n * zero) are dropped from the per-target listing and re-stated as one banner\n * per axis after the footer. The report, the footer's counts, `--json` and\n * `--strict` are all computed from the UNFILTERED findings, so nothing a\n * machine or an exit code reads moves by a single bit.\n */\nexport function renderText(targets: TargetReport[], opts: RenderOptions): void {\n const { all, errorsOnly, unverifiable } = opts;\n // When the fleet-wide \"not started\" claim is measurable at all — each guard\n // fails closed to ungrouped rendering. `!all` is structural, not just the\n // call site's promise: the banners print after the footer that only `--all`\n // reaches, so grouping outside `--all` would DROP warnings instead of\n // moving them — the one thing this lever must never do. `services === 0`\n // is the vacuous fleet: with no services every axis is 0 \"fleet-wide\", and\n // a banner over \"0 of 0 services\" would hide the fleet-level warnings of a\n // repo that merely has no fleet yet. And an unreadable service proves\n // nothing about what it started: its all-false participation (contracts.ts)\n // could MANUFACTURE the very N=0 that licenses suppression — beside an\n // error saying that service could not be checked — so one\n // `service.unreadable` subject disables grouping for the whole run.\n const card =\n !all ||\n opts.scorecard === null ||\n opts.scorecard.services === 0 ||\n subjectsWith(targets, \"service.unreadable\") > 0\n ? null\n : opts.scorecard;\n const grouped = card === null ? new Map<string, AdoptionAxis>() : groupedWarnCodes(card.adoption);\n const isGrouped = (f: Finding): boolean => f.severity === \"warn\" && grouped.has(f.code);\n\n // The explain pointer follows any report that printed work — a non-ok\n // finding, listed or folded under an axis banner. Gated on the UNFILTERED\n // findings like every other rollup, so `--errors-only` and grouping cannot\n // print codes the pointer then fails to follow. It is computed HERE rather\n // than beside the two places it prints because the code column below asks\n // the same question and must not walk the findings a second time to answer\n // it differently.\n const pointer = targets.some((t) => t.findings.some((f) => f.severity !== \"ok\"));\n // The lookup key on the line itself.\n //\n // Until now a text report printed messages and no codes, so the footer under\n // it could only say codes were somewhere else — in `--json`, which a reader\n // would have to re-run the whole command to see. The code is what `loam\n // explain` takes, what a CI branch tests and what a bug report quotes, and\n // withholding it from the one surface a person actually reads made the\n // footer's own offer unreachable.\n //\n // Only where a code is genuinely answerable, and that guard is not\n // decoration: a handful of emitted finding codes still have no fix-table row\n // (test/explain.test.ts names them), and printing one beside a footer\n // promising an explanation would send the reader to a refusal. The set is\n // built ONCE per render, and only when something non-ok will be printed:\n // `explainSubject` walks the shipped workflow bodies per code (the cost\n // `fixesFor` measured from the other side, where a handful of lookups is the\n // cheaper shape), while `listCodes()` walks them once for the whole\n // vocabulary — and a fleet report has far more finding lines than the fleet\n // has distinct codes. Measured: 2.7 ms for all 273 codes, once per report.\n const explainable = pointer ? new Set(listCodes().map((listing) => listing.code)) : new Set<string>();\n for (const t of targets) {\n const shown = t.findings.filter(\n (f) => !isGrouped(f) && (!errorsOnly || f.severity !== \"ok\"),\n );\n if (shown.length === 0) continue;\n // A feature announces itself; a service's findings already carry its name.\n if (t.kind === \"feature\") console.log(t.id);\n let header: string | undefined;\n for (const f of shown) {\n const hint = f.text ?? {};\n if (hint.header && hint.header !== header) {\n header = hint.header;\n console.log(` ${header}`);\n }\n // The whole report goes to stdout, in document order. Splitting errors\n // onto stderr meant a piped stdout silently lost them from the middle of\n // the report and 2>&1 could reorder it; the exit code carries failure,\n // and stderr stays reserved for refusals (the fail() path).\n const marker = hint.marker === false ? \"\" : `${SEVERITY_MARK[f.severity]} `;\n // `ok` confirmations stay clean, deliberately. A clean fleet run is\n // several hundred ✓ lines and two warnings, and a code appended to every\n // one of them would cost exactly the signal the exceptions carry — there\n // is nothing to look up about a check that passed.\n const code = f.severity !== \"ok\" && explainable.has(f.code) ? ` (${f.code})` : \"\";\n console.log(`${\" \".repeat(hint.indent ?? 0)}${marker}${f.message}${code}`);\n for (const d of f.details ?? []) console.log(` ${hint.detailPrefix ?? \"\"}${d}`);\n }\n }\n\n if (!all) {\n // Without the --all footer there would be nothing at all to print for a\n // clean single target under --errors-only — and silence is the one output\n // that must never mean \"checked, fine\".\n if (errorsOnly && targets.every((t) => t.findings.every((f) => f.severity === \"ok\"))) {\n console.log(`${targets.map((t) => t.id).join(\", \")}: no errors or warnings`);\n }\n if (pointer) console.log(`\\n${EXPLAIN_FOOTER}`);\n return;\n }\n const s = summary(targets);\n console.log(\n `\\n${plural(s.services, \"service\")}, ${plural(s.features, \"feature\")} — ` +\n `${plural(s.errors, \"error\")}, ${plural(s.warnings, \"warning\")}`,\n );\n // One line for the whole fleet, never one per service: honest about the blind\n // spot without drowning the report in a hundred copies of it.\n if (unverifiable > 0) {\n const whose = unverifiable === 1 ? \"1 service's\" : `${unverifiable} services'`;\n console.log(\n `⚠ sources.unverifiable-from-here: ${whose} sources can only be checked from their own repos`,\n );\n }\n // The axis banners — one per not-started axis that actually grouped a\n // warning, in the axes' fixed order, each naming its dropped codes with\n // counts. Tallied off the UNFILTERED findings (the same walk the footer\n // counts), and printed under --errors-only too: like the footer and the\n // unverifiable line this is a rollup, not a finding.\n if (card !== null && grouped.size > 0) {\n const tally = new Map<string, number>();\n for (const t of targets) {\n for (const f of t.findings) {\n if (isGrouped(f)) tally.set(f.code, (tally.get(f.code) ?? 0) + 1);\n }\n }\n for (const axis of ADOPTION_AXES) {\n // Insertion order of `grouped` is the table's order, so the pairs render\n // deterministically across runs and machines.\n const pairs = [...grouped]\n .filter(([code, a]) => a === axis && (tally.get(code) ?? 0) > 0)\n .map(([code]) => [code, tally.get(code) ?? 0] as const);\n if (pairs.length === 0) continue;\n const total = pairs.reduce((n, [, k]) => n + k, 0);\n console.log(\n `⚠ ${axis} axis not started fleet-wide (0 of ${plural(card.services, \"service\")}), ` +\n `expected during staged adoption — ${total} warning(s) grouped: ` +\n `${pairs.map(([code, k]) => `${code}×${k}`).join(\", \")}; every finding is unchanged in --json`,\n );\n }\n }\n if (pointer) console.log(`\\n${EXPLAIN_FOOTER}`);\n}\n"]}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * What a `--base` run does to the landscape target, which is the one target
3
+ * whose subject is not a directory.
4
+ *
5
+ * Every other target is graded whole or not at all: a service or a feature the
6
+ * branch touched is graded by exactly the checks `--all` runs on it. The
7
+ * landscape cannot be, for two reasons that pull in opposite directions, and
8
+ * both are defects a docs-repo PR gate actually shipped with.
9
+ *
10
+ * IT GRADED TOO MUCH. The landscape target carries the fleet-wide
11
+ * vocabularies' warnings — a permission nobody requires, a capability nobody
12
+ * realizes, an obligation nobody applies, a glossary term nobody cites — and
13
+ * the AGENTS.md stamp. Every one of those grades a file other than the map, so
14
+ * any `architecture/` branch re-graded all of them, and under `--strict` a
15
+ * one-line map edit on the adopted example fleet went red on four warnings that
16
+ * were all there at the base. Those findings now ride a scoped run only when the
17
+ * file that carries them changed.
18
+ *
19
+ * IT GRADED TOO LITTLE. `spine.op-undefined` is graded on the PROVIDER's
20
+ * service target, so an edge the branch drew or re-spelled on the map was
21
+ * graded by no scoped run unless the provider happened to change too —
22
+ * README's own `authorisePayment` example merged green through both PR gates.
23
+ * Those edges are now graded here, against their providers' contracts, and
24
+ * ONLY those edges: pulling the provider into scope instead would grade every
25
+ * document it already has, which on a spine-first fleet means
26
+ * `service.no-model` reddening exactly the labour that path asks for.
27
+ */
28
+ import { existsSync } from "node:fs";
29
+ import { serviceResolver } from "../../core/c4/resolve/service.js";
30
+ import { edgeKey } from "../../core/diff/scope/edges.js";
31
+ import { touched } from "../../core/diff/scope/changed-paths.js";
32
+ import { repoPath } from "../../core/envelope/json.js";
33
+ import { inOrder } from "../../core/kernel/concurrency.js";
34
+ import { serviceTreePath } from "../../core/kernel/ids/dirs.js";
35
+ import { readOpenapi } from "../../core/openapi/doc.js";
36
+ import { capabilityDocsDir, glossaryDir } from "../../core/repo/authored/paths.js";
37
+ import { agentsPath, capabilitiesPath, obligationsPath, permissionsPath, servicePathsAt } from "../../core/repo/paths.js";
38
+ import { contractOf } from "./service/api.js";
39
+ import { spineFindings } from "./service/spine.js";
40
+ /**
41
+ * The landscape target's findings that grade a file other than the map, with
42
+ * the file — or directory — that carries each.
43
+ *
44
+ * Only the families that are a VERDICT ON THAT FILE are listed: the vocabulary
45
+ * that does not read, and the declaration nothing cites. The obligation codes
46
+ * the MAP causes stay off this table — `obligation.unknown` (a tag naming no
47
+ * rule) and `obligation.uncovered` (an applied rule no requirement covers) are
48
+ * written by an edit to `architecture/`, and a scoped run over that edit owes
49
+ * them. A code on no row rides the landscape target exactly as before.
50
+ *
51
+ * What this costs, said once: the "nothing cites it" grades also move when a
52
+ * CITATION goes away — a branch deleting the last `Requires:` line naming a
53
+ * permission. A branch that touches only a service never graded the landscape
54
+ * at all, so that is not a new blind spot; the unscoped `validate --all` keeps
55
+ * reporting every one of these.
56
+ */
57
+ const CARRIED = [
58
+ { carries: (code) => code.startsWith("permissions."), by: (d) => [permissionsPath(d)] },
59
+ { carries: (code) => code.startsWith("capability."), by: (d) => [capabilitiesPath(d), capabilityDocsDir(d)] },
60
+ { carries: (code) => code === "obligation.invalid" || code === "obligation.unapplied", by: (d) => [obligationsPath(d)] },
61
+ { carries: (code) => code.startsWith("glossary."), by: (d) => [glossaryDir(d)] },
62
+ // The stamp is a fact about which binary wrote the file, and a branch that
63
+ // did not edit the file did not move it — `loam doctor` is where a binary
64
+ // upgrade is noticed, not every map PR that happens to follow one.
65
+ { carries: (code) => code === "agents.stale" || code === "docs.binary-behind", by: (d) => [agentsPath(d)] },
66
+ ];
67
+ /** The landscape target's `findings` as a `--base` run reports them. */
68
+ export async function scopedLandscapeFindings(findings, scoped) {
69
+ const { docsDir, scope } = scoped;
70
+ const kept = findings.filter((f) => {
71
+ const carried = CARRIED.find((row) => row.carries(f.code));
72
+ return carried === undefined || carried.by(docsDir).some((abs) => touched(scope, repoPath(docsDir, abs)));
73
+ });
74
+ return [...kept, ...(await changedEdgeFindings(scoped))];
75
+ }
76
+ /**
77
+ * The spine of the edges this branch wrote, graded per provider by the
78
+ * service target's own `spineFindings` over just those edges — so a changed
79
+ * edge reads exactly as it would on the provider's target, and an unreadable
80
+ * or absent contract proves nothing here for the reason it proves nothing
81
+ * there.
82
+ *
83
+ * A provider the run already grades is skipped: its own target walks every
84
+ * inbound edge, these included, and grading them twice would report one broken
85
+ * join as two.
86
+ */
87
+ async function changedEdgeFindings(scoped) {
88
+ const { docsDir, scope, land, services, fleet } = scoped;
89
+ // A map that does not parse has no edges to read, and `landscape.invalid`
90
+ // on this same target already says so.
91
+ if (land === null || land.errors.length > 0 || scope.edges.size === 0)
92
+ return [];
93
+ const svcOf = serviceResolver(land.elements, new Set(services.map((s) => s.id)));
94
+ const providerOf = (target) => svcOf(target);
95
+ const edges = land.relationships.filter((r) => scope.edges.has(edgeKey(r)));
96
+ const graded = new Set(scope.services.map((s) => s.id));
97
+ const providers = services.filter((s) => !graded.has(s.id) && edges.some((r) => providerOf(r.target) === s.id));
98
+ const perProvider = await inOrder(providers, async (svc) => {
99
+ const paths = servicePathsAt(svc.dir);
100
+ return spineFindings({
101
+ service: svc.id,
102
+ docsDir,
103
+ treePath: serviceTreePath(svc),
104
+ me: svc.id,
105
+ land: { ...land, relationships: edges.filter((r) => providerOf(r.target) === svc.id) },
106
+ landSvcOf: svcOf,
107
+ contract: contractOf(await readOpenapi(paths.openapi, fleet), !existsSync(paths.openapi)),
108
+ landscapeReported: true,
109
+ });
110
+ });
111
+ return perProvider.flat();
112
+ }
113
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../../src/commands/validate/scope.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a `--base` run does to the landscape target, which is the one target\n * whose subject is not a directory.\n *\n * Every other target is graded whole or not at all: a service or a feature the\n * branch touched is graded by exactly the checks `--all` runs on it. The\n * landscape cannot be, for two reasons that pull in opposite directions, and\n * both are defects a docs-repo PR gate actually shipped with.\n *\n * IT GRADED TOO MUCH. The landscape target carries the fleet-wide\n * vocabularies' warnings — a permission nobody requires, a capability nobody\n * realizes, an obligation nobody applies, a glossary term nobody cites — and\n * the AGENTS.md stamp. Every one of those grades a file other than the map, so\n * any `architecture/` branch re-graded all of them, and under `--strict` a\n * one-line map edit on the adopted example fleet went red on four warnings that\n * were all there at the base. Those findings now ride a scoped run only when the\n * file that carries them changed.\n *\n * IT GRADED TOO LITTLE. `spine.op-undefined` is graded on the PROVIDER's\n * service target, so an edge the branch drew or re-spelled on the map was\n * graded by no scoped run unless the provider happened to change too —\n * README's own `authorisePayment` example merged green through both PR gates.\n * Those edges are now graded here, against their providers' contracts, and\n * ONLY those edges: pulling the provider into scope instead would grade every\n * document it already has, which on a spine-first fleet means\n * `service.no-model` reddening exactly the labour that path asks for.\n */\nimport { existsSync } from \"node:fs\";\nimport { serviceResolver } from \"../../core/c4/resolve/service.js\";\nimport type { LoadedDoc } from \"../../core/c4/likec4.js\";\nimport { edgeKey } from \"../../core/diff/scope/edges.js\";\nimport { touched, type ValidateScope } from \"../../core/diff/scope/changed-paths.js\";\nimport { repoPath } from \"../../core/envelope/json.js\";\nimport type { FleetContext } from \"../../core/fleet-context.js\";\nimport { inOrder } from \"../../core/kernel/concurrency.js\";\nimport { serviceTreePath, type DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport { readOpenapi } from \"../../core/openapi/doc.js\";\nimport { capabilityDocsDir, glossaryDir } from \"../../core/repo/authored/paths.js\";\nimport type { ServiceEntry } from \"../../core/repo/entries.js\";\nimport { agentsPath, capabilitiesPath, obligationsPath, permissionsPath, servicePathsAt } from \"../../core/repo/paths.js\";\nimport type { Finding } from \"../../core/vocabulary/report.js\";\nimport { contractOf } from \"./service/api.js\";\nimport { spineFindings } from \"./service/spine.js\";\n\n/**\n * The landscape target's findings that grade a file other than the map, with\n * the file — or directory — that carries each.\n *\n * Only the families that are a VERDICT ON THAT FILE are listed: the vocabulary\n * that does not read, and the declaration nothing cites. The obligation codes\n * the MAP causes stay off this table — `obligation.unknown` (a tag naming no\n * rule) and `obligation.uncovered` (an applied rule no requirement covers) are\n * written by an edit to `architecture/`, and a scoped run over that edit owes\n * them. A code on no row rides the landscape target exactly as before.\n *\n * What this costs, said once: the \"nothing cites it\" grades also move when a\n * CITATION goes away — a branch deleting the last `Requires:` line naming a\n * permission. A branch that touches only a service never graded the landscape\n * at all, so that is not a new blind spot; the unscoped `validate --all` keeps\n * reporting every one of these.\n */\nconst CARRIED: readonly { carries: (code: string) => boolean; by: (docsDir: DocsDir) => string[] }[] = [\n { carries: (code) => code.startsWith(\"permissions.\"), by: (d) => [permissionsPath(d)] },\n { carries: (code) => code.startsWith(\"capability.\"), by: (d) => [capabilitiesPath(d), capabilityDocsDir(d)] },\n { carries: (code) => code === \"obligation.invalid\" || code === \"obligation.unapplied\", by: (d) => [obligationsPath(d)] },\n { carries: (code) => code.startsWith(\"glossary.\"), by: (d) => [glossaryDir(d)] },\n // The stamp is a fact about which binary wrote the file, and a branch that\n // did not edit the file did not move it — `loam doctor` is where a binary\n // upgrade is noticed, not every map PR that happens to follow one.\n { carries: (code) => code === \"agents.stale\" || code === \"docs.binary-behind\", by: (d) => [agentsPath(d)] },\n];\n\n/** What the scoped landscape grade needs from the run. */\nexport interface ScopedLandscape {\n docsDir: DocsDir;\n scope: ValidateScope;\n /** The parsed `architecture/` project, or null when the repo draws no fleet. */\n land: LoadedDoc | null;\n /** The run's FULL service enumeration: a changed edge's provider is found here whether or not it is in scope. */\n services: readonly ServiceEntry[];\n fleet: FleetContext;\n}\n\n/** The landscape target's `findings` as a `--base` run reports them. */\nexport async function scopedLandscapeFindings(findings: Finding[], scoped: ScopedLandscape): Promise<Finding[]> {\n const { docsDir, scope } = scoped;\n const kept = findings.filter((f) => {\n const carried = CARRIED.find((row) => row.carries(f.code));\n return carried === undefined || carried.by(docsDir).some((abs) => touched(scope, repoPath(docsDir, abs)));\n });\n return [...kept, ...(await changedEdgeFindings(scoped))];\n}\n\n/**\n * The spine of the edges this branch wrote, graded per provider by the\n * service target's own `spineFindings` over just those edges — so a changed\n * edge reads exactly as it would on the provider's target, and an unreadable\n * or absent contract proves nothing here for the reason it proves nothing\n * there.\n *\n * A provider the run already grades is skipped: its own target walks every\n * inbound edge, these included, and grading them twice would report one broken\n * join as two.\n */\nasync function changedEdgeFindings(scoped: ScopedLandscape): Promise<Finding[]> {\n const { docsDir, scope, land, services, fleet } = scoped;\n // A map that does not parse has no edges to read, and `landscape.invalid`\n // on this same target already says so.\n if (land === null || land.errors.length > 0 || scope.edges.size === 0) return [];\n const svcOf = serviceResolver(land.elements, new Set(services.map((s) => s.id)));\n const providerOf = (target: string): string => svcOf(target);\n const edges = land.relationships.filter((r) => scope.edges.has(edgeKey(r)));\n const graded = new Set(scope.services.map((s) => s.id));\n const providers = services.filter((s) => !graded.has(s.id) && edges.some((r) => providerOf(r.target) === s.id));\n const perProvider = await inOrder(providers, async (svc) => {\n const paths = servicePathsAt(svc.dir);\n return spineFindings({\n service: svc.id,\n docsDir,\n treePath: serviceTreePath(svc),\n me: svc.id,\n land: { ...land, relationships: edges.filter((r) => providerOf(r.target) === svc.id) },\n landSvcOf: svcOf,\n contract: contractOf(await readOpenapi(paths.openapi, fleet), !existsSync(paths.openapi)),\n landscapeReported: true,\n });\n });\n return perProvider.flat();\n}\n"]}
@@ -10,11 +10,6 @@
10
10
  * is what reads it.
11
11
  */
12
12
  import { existsSync } from "node:fs";
13
- import { readFile } from "node:fs/promises";
14
- import { createHash } from "node:crypto";
15
- import { parse as parseYaml } from "yaml";
16
- import { resolveInside } from "../../../core/kernel/path-safety.js";
17
- import { canonicalJson } from "../../../core/openapi/digest.js";
18
13
  import { closeIds } from "../../../core/c4/arch.js";
19
14
  import {} from "../../../core/c4/likec4.js";
20
15
  import {} from "../../../core/kernel/ids/service.js";
@@ -31,8 +26,10 @@ export async function apiAxisFindings(axis) {
31
26
  const api = await readOpenapi(paths.openapi, fleet);
32
27
  const removeMarkers = api.ops.filter((o) => o.remove);
33
28
  const liveOps = api.ops.filter((o) => !o.remove);
34
- const ops = liveOps.map((o) => o.id);
35
- const deprecatedOps = new Set(liveOps.filter((o) => o.deprecated).map((o) => o.id));
29
+ /** No contract to read: deleted, renamed, or never written. */
30
+ const contractMissing = !existsSync(paths.openapi);
31
+ const contract = contractOf(api, contractMissing);
32
+ const { ops, deprecated: deprecatedOps } = contract;
36
33
  // Inbound calls the landscape can PROVE: op-linked edges whose target
37
34
  // resolves to this service. Null when the landscape proves nothing at all
38
35
  // (absent, or it did not parse) — which is not the same fact as "nobody
@@ -43,8 +40,6 @@ export async function apiAxisFindings(axis) {
43
40
  // The other half of the evidence a contract is owed: the living spec's own
44
41
  // `Operations:` lines. A requirement on its way out governs nothing.
45
42
  const governedOps = livingReqs.flatMap((r) => r.operations);
46
- /** No contract to read: deleted, renamed, or never written. */
47
- const contractMissing = !existsSync(paths.openapi);
48
43
  if (contractMissing) {
49
44
  // A contract that is gone is not a reason to stop grading the API axis —
50
45
  // it IS the axis's answer, and every check below used to live inside the
@@ -236,103 +231,20 @@ export async function apiAxisFindings(axis) {
236
231
  }
237
232
  }
238
233
  }
239
- return {
240
- findings,
241
- contract: { ops, deprecated: deprecatedOps, unreadable: api.unreadable || contractMissing },
242
- };
234
+ return { findings, contract };
243
235
  }
244
236
  /**
245
- * The build's own contract document, compared with the copy the docs repo
246
- * committed — `openapi.generated-stale`, `contracts.source-missing`,
247
- * `contracts.source-invalid`.
248
- *
249
- * WHY THIS IS NOT AN EXTRACTOR. It reads no line of service code and derives no
250
- * meaning: it parses a standard document the team's own build emitted, at a
251
- * path a human wrote into `loam.json`, and compares a digest. That is the same
252
- * category as `verify --results` ingesting a cucumber report, and it is the
253
- * only thing in the product that can notice the assumption SCHEMA quietly makes
254
- * — that the committed `services/<id>/openapi.yaml` is the contract the service
255
- * actually serves. Most fleets generate that document; a fleet that copies it
256
- * by hand in month one is a fleet whose spine checks grade last quarter's
257
- * endpoints by month six, and until now nothing could say so.
258
- *
259
- * IT NEVER WRITES. The copy stays a human `cp`, reviewed in a pull request,
260
- * because that is what makes the docs-repo document a contract somebody agreed
261
- * to rather than a cache of whatever the build last produced. loam reports the
262
- * divergence and stops.
263
- *
264
- * THE DIGEST IS OVER CANONICAL JSON, never raw bytes. Two generator versions
265
- * that order keys differently, or a YAML dumper that re-wraps a description,
266
- * produce identical documents and must not produce a permanent warning nobody
267
- * can clear — the lesson `requirementDigest` already records one package over.
268
- *
269
- * Runs only in the SERVICE repo. From the docs repo there is no build output to
270
- * read and the whole family stays silent, exactly as `sources` provenance does.
237
+ * What the spine may conclude from one contract read — spelled once, because a
238
+ * `--base` run now grades a changed edge against a provider it does not grade
239
+ * as a target (`../scope.ts`), and two spellings of "which operations are
240
+ * live" would be two chances for a removal marker to count as defined.
271
241
  */
272
- export async function generatedContractFindings(service, repoDir, spelled, committedPath) {
273
- if (spelled === undefined)
274
- return [];
275
- let generatedPath;
276
- try {
277
- // Containment is checked HERE rather than at config load, with the service
278
- // in hand so the refusal can name it — and a path that escapes the repo is
279
- // a finding, never a thrown config error that would take down every other
280
- // command in the repo.
281
- generatedPath = resolveInside(repoDir, spelled, `"contracts.openapi"`);
282
- }
283
- catch (err) {
284
- return [{
285
- severity: "error",
286
- code: "contracts.source-invalid",
287
- subject: service,
288
- message: `${service}: loam.json names contracts.openapi '${spelled}', which does not resolve inside this repository — ` +
289
- `${err instanceof Error ? err.message : String(err)}`,
290
- }];
291
- }
292
- if (!existsSync(generatedPath)) {
293
- return [{
294
- severity: "error",
295
- code: "contracts.source-missing",
296
- subject: service,
297
- message: `${service}: loam.json names contracts.openapi '${spelled}', and nothing is there. ` +
298
- "Either the build has not run in this checkout, or the path is wrong — CI that validates before it builds " +
299
- "sees exactly this. Run the build first, or correct the path",
300
- }];
301
- }
302
- const digests = [];
303
- for (const [label, path] of [["generated", generatedPath], ["committed", committedPath]]) {
304
- if (label === "committed" && !existsSync(path))
305
- return [];
306
- let parsed;
307
- try {
308
- parsed = parseYaml(await readFile(path, "utf8"));
309
- }
310
- catch (err) {
311
- // An unreadable GENERATED document is this check's business; an
312
- // unreadable committed one is already `openapi.invalid` and must not be
313
- // reported twice under two codes.
314
- if (label === "committed")
315
- return [];
316
- return [{
317
- severity: "error",
318
- code: "contracts.source-invalid",
319
- subject: service,
320
- message: `${service}: contracts.openapi '${spelled}' does not parse as YAML or JSON — ` +
321
- `${err instanceof Error ? err.message : String(err)}`,
322
- }];
323
- }
324
- digests.push(createHash("sha256").update(canonicalJson(parsed), "utf8").digest("hex"));
325
- }
326
- if (digests[0] === digests[1])
327
- return [];
328
- return [{
329
- severity: "warn",
330
- code: "openapi.generated-stale",
331
- subject: service,
332
- message: `${service}: the committed openapi.yaml differs from what this repository's build produces (${spelled}). ` +
333
- "The docs-repo copy is what every operation join is graded against, so the fleet is being checked against a " +
334
- "contract this service no longer serves. Review the difference and copy the generated document across — " +
335
- "loam never does that for you, because the committed contract is one somebody agreed to, not a cache of the build",
336
- }];
242
+ export function contractOf(api, missing) {
243
+ const live = api.ops.filter((o) => !o.remove);
244
+ return {
245
+ ops: live.map((o) => o.id),
246
+ deprecated: new Set(live.filter((o) => o.deprecated).map((o) => o.id)),
247
+ unreadable: api.unreadable || missing,
248
+ };
337
249
  }
338
250
  //# sourceMappingURL=api.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/api.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The HTTP contract axis: openapi.yaml graded against the living spec.\n *\n * What it decides for everything downstream is whether there is a contract to\n * read at all. A missing or unparseable openapi.yaml is ONE finding about ONE\n * file, and that fact has to travel: grading the fleet map's inbound edges\n * against the empty operation set it leaves behind turns one broken YAML into\n * one `spine.op-undefined` per consumer, each of them accusing the landscape of\n * a fault that is not there. `ServiceContract` is how it travels; `./spine.js`\n * is what reads it.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\nimport { parse as parseYaml } from \"yaml\";\nimport { resolveInside } from \"../../../core/kernel/path-safety.js\";\nimport { canonicalJson } from \"../../../core/openapi/digest.js\";\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type ServicePaths } from \"../../../core/repo/paths.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { responseGovernanceFindings } from \"../checks/requirements.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { readOpenapi } from \"../../../core/openapi/doc.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\n\n/** What the service target hands the HTTP axis. */\nexport interface ApiAxis {\n service: PathableService;\n /** `service` widened for `===` against the resolver's DOCUMENT text — see service.ts. */\n me: string;\n paths: ServicePaths;\n /** Every requirement the living spec declares, REMOVED ones included. */\n reqs: Requirement[];\n /** The ones that still govern something. */\n livingReqs: Requirement[];\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** Its element→service resolver, built once per run; null exactly when `land` is. */\n landSvcOf: ((id: string) => DeclaredService) | null;\n fleet?: FleetContext;\n}\n\n/**\n * What the spine may conclude from this service's contract.\n *\n * `unreadable` covers both shapes of \"there is nothing to read\" — absent and\n * unparseable — because they have the same consequence for an inbound edge:\n * the contract proves neither that the operation exists nor that it does not.\n */\nexport interface ServiceContract {\n /** operationIds the living contract defines, empty when there is nothing to read. */\n ops: string[];\n /** Those of them the contract marks `deprecated: true`. */\n deprecated: ReadonlySet<string>;\n unreadable: boolean;\n}\n\nexport async function apiAxisFindings(\n axis: ApiAxis,\n): Promise<{ findings: Finding[]; contract: ServiceContract }> {\n const { service, me, paths, reqs, livingReqs, land, landSvcOf, fleet } = axis;\n const findings: Finding[] = [];\n\n // API coverage: every operation in openapi.yaml is governed by a requirement.\n const api = await readOpenapi(paths.openapi, fleet);\n const removeMarkers = api.ops.filter((o) => o.remove);\n const liveOps = api.ops.filter((o) => !o.remove);\n const ops = liveOps.map((o) => o.id);\n const deprecatedOps = new Set(liveOps.filter((o) => o.deprecated).map((o) => o.id));\n // Inbound calls the landscape can PROVE: op-linked edges whose target\n // resolves to this service. Null when the landscape proves nothing at all\n // (absent, or it did not parse) — which is not the same fact as \"nobody\n // calls me\", and the two must not be graded alike.\n const inboundOps =\n land === null || land.errors.length > 0\n ? null\n : land.relationships.filter((r) => r.op !== undefined && landSvcOf!(r.target) === me);\n // The other half of the evidence a contract is owed: the living spec's own\n // `Operations:` lines. A requirement on its way out governs nothing.\n const governedOps = livingReqs.flatMap((r) => r.operations);\n /** No contract to read: deleted, renamed, or never written. */\n const contractMissing = !existsSync(paths.openapi);\n if (contractMissing) {\n // A contract that is gone is not a reason to stop grading the API axis —\n // it IS the axis's answer, and every check below used to live inside the\n // file-exists branch, so deleting or misspelling the file turned the whole\n // axis (including the documented `spec-api.op-undefined`) green.\n //\n // What decides the severity is whether anything already written down is\n // holding a join into it: a living `Operations:` line, or an op-linked\n // landscape edge. With one of those, the absence breaks a link somebody\n // authored — an error. With neither, this is a service that legitimately\n // has no HTTP surface (a worker nobody calls), and a fleet mid-rollout\n // must not go red for it — a warning, or, when the landscape PROVES nobody\n // calls it, the documented silence.\n //\n // One finding, not one per dangling link: the fix is a single file, and\n // the links it strands ride along as details.\n const dangling = [...new Set([...governedOps, ...(inboundOps ?? []).map((r) => r.op!)])].sort();\n if (dangling.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"service.no-openapi\",\n subject: service,\n message:\n `No OpenAPI contract at ${paths.openapi}, and ${dangling.length} operation link(s) already point into it — ` +\n `every requirement and landscape edge naming one of them resolves to nothing until the file is back`,\n details: dangling,\n });\n } else if (inboundOps === null) {\n findings.push({\n severity: \"warn\",\n code: \"service.no-openapi\",\n message: `No OpenAPI contract at ${paths.openapi} — API coverage and the landscape spine are unchecked`,\n });\n }\n } else if (api.unreadable) {\n // A contract that EXISTS but does not read is a broken source of truth, not\n // an empty one: swallowing it into zero operations used to grade every\n // inbound landscape edge `spine.op-undefined` — a false diagnosis pointing\n // at the landscape when the truth was this file. So the file is the error,\n // and every check that reads the contract (api.*, the spine's op\n // resolution) is suspended below, the landscape.invalid discipline.\n findings.push({\n severity: \"error\",\n code: \"openapi.invalid\",\n message: `${service}: openapi.yaml does not parse — API coverage and the landscape spine are unchecked`,\n ...(api.error === undefined ? {} : { details: [api.error] }),\n });\n } else {\n if (removeMarkers.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"openapi.remove-marker-living\",\n message: `${service}: living openapi.yaml contains ${removeMarkers.length} x-loam-remove marker(s) (${removeMarkers.map((op) => op.id).join(\", \")}) — removal markers are valid only in feature deltas`,\n });\n }\n // The same marker written at PATH level, beside the methods. `readOpenapi`\n // is keyed by (path, method), which is precisely why the check above cannot\n // see this one and precisely how one reached a living contract in the first\n // place: it addresses no operation, so it retired nothing, and it stayed\n // invisible afterwards. Error, like its method-level sibling — a\n // feature-only key published to every consumer of the fleet, and one that\n // keeps the empty-path cleanup from ever firing. Same code as the archive\n // plan's: one breach, one name, wherever it is found.\n for (const removed of api.pathLevelRemovals) {\n findings.push({\n severity: \"error\",\n code: \"openapi.remove-marker-path-level\",\n subject: service,\n message:\n `${service}: living openapi.yaml carries x-loam-remove at PATH level on '${removed}' — ` +\n `a removal marker names ONE operation, so beside the methods it retires nothing and is not a contract key either, ` +\n `and no id-keyed check can see it. Delete it from the living contract; retire an operation through a feature delta whose marker sits inside the operation.`,\n });\n }\n // Two slots claiming one operationId in a LIVING contract. `readOpenapi`\n // has computed this since the merge needed it, and only the feature path\n // (coherence) ever read it — so the ambiguity was reachable only when some\n // unrelated feature happened to carry a delta for this service, and the\n // fleet gate that actually runs in CI was blind to it. Same code and same\n // sentence as the feature-scope check: one breach, one name.\n for (const id of api.duplicateIds) {\n const slots = api.ops.filter((op) => op.id === id).map((op) => `${op.method} ${op.path}`);\n findings.push({\n severity: \"warn\",\n code: \"openapi.duplicate-operationid\",\n subject: service,\n message: `${service}: the living OpenAPI defines operationId '${id}' at ${slots.join(\" and \")} — every join on the id (a requirement's Operations: line, an edge's metadata { op }, a removal marker) picks one of those slots arbitrarily`,\n });\n }\n // The contract-depth probes. Form validated and depth did not: an\n // operation whose one response is `description: OK`, and a `$ref` to a\n // schema nobody wrote, both read as cleanly as a complete contract while\n // a reader still cannot rebuild its payloads. Presence\n // probes over the document's own keys; nothing reads what a schema says.\n const undescribed = liveOps.filter((op) => op.undescribed === true).map((op) => op.id);\n if (undescribed.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"openapi.response-undescribed\",\n subject: service,\n message:\n `${service}: ${undescribed.length} operation(s) declare no response schema (${undescribed.join(\", \")}) — ` +\n `the contract names the operation and says nothing about what it returns, so a reader still ` +\n `needs the code; declare content with a schema on at least one response (204/304 excepted)`,\n });\n }\n if (api.danglingRefs.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"openapi.ref-unresolved\",\n subject: service,\n message:\n `${service}: openapi.yaml contains ${api.danglingRefs.length} internal $ref(s) that resolve to ` +\n `nothing in the document — the operation validates while its schema is silently absent ` +\n `(the archive plan refuses the same breach in a feature delta, as an error)`,\n details: api.danglingRefs,\n });\n }\n const defined = new Set(ops);\n // `Operations:` on a LIVING requirement, resolved against this service's own\n // contract. Nothing did this before: the same spine is checked inside a\n // feature delta (coherence's spec-api.op-undefined) and then never again, so\n // a typo that shipped, or an operation later renamed out of openapi.yaml,\n // left a living requirement governing an operation that does not exist —\n // green forever, and every downstream join through that id silently empty.\n // Same code and severity as the feature-scope check on purpose: one breach,\n // one name, wherever it is found.\n for (const r of livingReqs) {\n for (const op of r.operations) {\n if (defined.has(op)) continue;\n const close = closeIds(op, ops);\n findings.push({\n severity: \"error\",\n code: \"spec-api.op-undefined\",\n subject: service,\n message:\n `${service}: requirement '${r.name}' governs '${op}', not defined in ${service}'s OpenAPI` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n }\n if (ops.length > 0) {\n const governed = new Set(livingReqs.flatMap((r) => r.operations));\n const orphans = ops.filter((op) => !governed.has(op));\n if (orphans.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"api.covered\",\n // Both counts on purpose: the ratio is the one signal that lets a\n // reviewer smell a thin baseline from the summary — forty behaviours\n // filed under one requirement passes every check, and \"covered\" alone\n // read as done.\n message: `${service}: API covered (${ops.length} operation(s) governed by requirements · ${livingReqs.length} living requirement(s))`,\n });\n } else {\n findings.push({\n severity: \"warn\",\n code: \"api.ungoverned\",\n message: `${service}: ${orphans.length} operation(s) not governed by any requirement — ${orphans.join(\", \")}`,\n });\n }\n findings.push(...responseGovernanceFindings(service, liveOps, livingReqs));\n // The migration-debt case: requirements exist, the API exists, and no\n // `Operations:` line ties them — every cross-axis check above and in\n // feature mode is vacuously green. Once per service, not per operation;\n // with zero requirements the spec (or its absence) is the finding instead.\n if (reqs.length > 0 && reqs.every((r) => r.operations.length === 0)) {\n findings.push({\n severity: \"warn\",\n code: \"api.ops-unlinked\",\n message: `${service}: openapi.yaml defines ${ops.length} operation(s) but no requirement links any — the API axis is unchecked for this service`,\n });\n }\n // Lifecycle: a requirement whose `Operations:` list resolves ONLY to\n // deprecated operations governs behaviour the contract is retiring.\n // Deprecation is the documented first step of removing an op; the explicit\n // feature marker is the final step. Until that delta archives, the op stays\n // live, so the fix is migration or a coordinated retirement. Ops the contract does not define at all\n // prove nothing here and are left to spec-api.op-undefined above.\n for (const r of reqs) {\n const resolved = r.operations.filter((op) => defined.has(op));\n if (resolved.length === 0 || !resolved.every((op) => deprecatedOps.has(op))) continue;\n findings.push({\n severity: \"warn\",\n code: \"api.requirement-deprecated\",\n message: `${service}: requirement '${r.name}' governs only deprecated operation(s) (${resolved.join(\", \")}) — the behaviour it describes is on its way out; migrate it to the replacement operation, or retire it`,\n });\n }\n }\n }\n\n return {\n findings,\n contract: { ops, deprecated: deprecatedOps, unreadable: api.unreadable || contractMissing },\n };\n}\n\n/**\n * The build's own contract document, compared with the copy the docs repo\n * committed — `openapi.generated-stale`, `contracts.source-missing`,\n * `contracts.source-invalid`.\n *\n * WHY THIS IS NOT AN EXTRACTOR. It reads no line of service code and derives no\n * meaning: it parses a standard document the team's own build emitted, at a\n * path a human wrote into `loam.json`, and compares a digest. That is the same\n * category as `verify --results` ingesting a cucumber report, and it is the\n * only thing in the product that can notice the assumption SCHEMA quietly makes\n * — that the committed `services/<id>/openapi.yaml` is the contract the service\n * actually serves. Most fleets generate that document; a fleet that copies it\n * by hand in month one is a fleet whose spine checks grade last quarter's\n * endpoints by month six, and until now nothing could say so.\n *\n * IT NEVER WRITES. The copy stays a human `cp`, reviewed in a pull request,\n * because that is what makes the docs-repo document a contract somebody agreed\n * to rather than a cache of whatever the build last produced. loam reports the\n * divergence and stops.\n *\n * THE DIGEST IS OVER CANONICAL JSON, never raw bytes. Two generator versions\n * that order keys differently, or a YAML dumper that re-wraps a description,\n * produce identical documents and must not produce a permanent warning nobody\n * can clear — the lesson `requirementDigest` already records one package over.\n *\n * Runs only in the SERVICE repo. From the docs repo there is no build output to\n * read and the whole family stays silent, exactly as `sources` provenance does.\n */\nexport async function generatedContractFindings(\n service: string,\n repoDir: string,\n spelled: string | undefined,\n committedPath: string,\n): Promise<Finding[]> {\n if (spelled === undefined) return [];\n let generatedPath: string;\n try {\n // Containment is checked HERE rather than at config load, with the service\n // in hand so the refusal can name it — and a path that escapes the repo is\n // a finding, never a thrown config error that would take down every other\n // command in the repo.\n generatedPath = resolveInside(repoDir, spelled, `\"contracts.openapi\"`);\n } catch (err) {\n return [{\n severity: \"error\",\n code: \"contracts.source-invalid\",\n subject: service,\n message:\n `${service}: loam.json names contracts.openapi '${spelled}', which does not resolve inside this repository — ` +\n `${err instanceof Error ? err.message : String(err)}`,\n }];\n }\n if (!existsSync(generatedPath)) {\n return [{\n severity: \"error\",\n code: \"contracts.source-missing\",\n subject: service,\n message:\n `${service}: loam.json names contracts.openapi '${spelled}', and nothing is there. ` +\n \"Either the build has not run in this checkout, or the path is wrong — CI that validates before it builds \" +\n \"sees exactly this. Run the build first, or correct the path\",\n }];\n }\n const digests: string[] = [];\n for (const [label, path] of [[\"generated\", generatedPath], [\"committed\", committedPath]] as const) {\n if (label === \"committed\" && !existsSync(path)) return [];\n let parsed: unknown;\n try {\n parsed = parseYaml(await readFile(path, \"utf8\"));\n } catch (err) {\n // An unreadable GENERATED document is this check's business; an\n // unreadable committed one is already `openapi.invalid` and must not be\n // reported twice under two codes.\n if (label === \"committed\") return [];\n return [{\n severity: \"error\",\n code: \"contracts.source-invalid\",\n subject: service,\n message:\n `${service}: contracts.openapi '${spelled}' does not parse as YAML or JSON — ` +\n `${err instanceof Error ? err.message : String(err)}`,\n }];\n }\n digests.push(createHash(\"sha256\").update(canonicalJson(parsed), \"utf8\").digest(\"hex\"));\n }\n if (digests[0] === digests[1]) return [];\n return [{\n severity: \"warn\",\n code: \"openapi.generated-stale\",\n subject: service,\n message:\n `${service}: the committed openapi.yaml differs from what this repository's build produces (${spelled}). ` +\n \"The docs-repo copy is what every operation join is graded against, so the fleet is being checked against a \" +\n \"contract this service no longer serves. Review the difference and copy the generated document across — \" +\n \"loam never does that for you, because the committed contract is one somebody agreed to, not a cache of the build\",\n }];\n}\n"]}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../../src/commands/validate/service/api.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The HTTP contract axis: openapi.yaml graded against the living spec.\n *\n * What it decides for everything downstream is whether there is a contract to\n * read at all. A missing or unparseable openapi.yaml is ONE finding about ONE\n * file, and that fact has to travel: grading the fleet map's inbound edges\n * against the empty operation set it leaves behind turns one broken YAML into\n * one `spine.op-undefined` per consumer, each of them accusing the landscape of\n * a fault that is not there. `ServiceContract` is how it travels; `./spine.js`\n * is what reads it.\n */\nimport { existsSync } from \"node:fs\";\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { type ServicePaths } from \"../../../core/repo/paths.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { responseGovernanceFindings } from \"../checks/requirements.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { readOpenapi, type OpenapiDoc } from \"../../../core/openapi/doc.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\n\n/** What the service target hands the HTTP axis. */\nexport interface ApiAxis {\n service: PathableService;\n /** `service` widened for `===` against the resolver's DOCUMENT text — see service.ts. */\n me: string;\n paths: ServicePaths;\n /** Every requirement the living spec declares, REMOVED ones included. */\n reqs: Requirement[];\n /** The ones that still govern something. */\n livingReqs: Requirement[];\n /** The living landscape, or null when the repo has none. */\n land: LoadedDoc | null;\n /** Its element→service resolver, built once per run; null exactly when `land` is. */\n landSvcOf: ((id: string) => DeclaredService) | null;\n fleet: FleetContext;\n}\n\n/**\n * What the spine may conclude from this service's contract.\n *\n * `unreadable` covers both shapes of \"there is nothing to read\" — absent and\n * unparseable — because they have the same consequence for an inbound edge:\n * the contract proves neither that the operation exists nor that it does not.\n */\nexport interface ServiceContract {\n /** operationIds the living contract defines, empty when there is nothing to read. */\n ops: string[];\n /** Those of them the contract marks `deprecated: true`. */\n deprecated: ReadonlySet<string>;\n unreadable: boolean;\n}\n\nexport async function apiAxisFindings(\n axis: ApiAxis,\n): Promise<{ findings: Finding[]; contract: ServiceContract }> {\n const { service, me, paths, reqs, livingReqs, land, landSvcOf, fleet } = axis;\n const findings: Finding[] = [];\n\n // API coverage: every operation in openapi.yaml is governed by a requirement.\n const api = await readOpenapi(paths.openapi, fleet);\n const removeMarkers = api.ops.filter((o) => o.remove);\n const liveOps = api.ops.filter((o) => !o.remove);\n /** No contract to read: deleted, renamed, or never written. */\n const contractMissing = !existsSync(paths.openapi);\n const contract = contractOf(api, contractMissing);\n const { ops, deprecated: deprecatedOps } = contract;\n // Inbound calls the landscape can PROVE: op-linked edges whose target\n // resolves to this service. Null when the landscape proves nothing at all\n // (absent, or it did not parse) — which is not the same fact as \"nobody\n // calls me\", and the two must not be graded alike.\n const inboundOps =\n land === null || land.errors.length > 0\n ? null\n : land.relationships.filter((r) => r.op !== undefined && landSvcOf!(r.target) === me);\n // The other half of the evidence a contract is owed: the living spec's own\n // `Operations:` lines. A requirement on its way out governs nothing.\n const governedOps = livingReqs.flatMap((r) => r.operations);\n if (contractMissing) {\n // A contract that is gone is not a reason to stop grading the API axis —\n // it IS the axis's answer, and every check below used to live inside the\n // file-exists branch, so deleting or misspelling the file turned the whole\n // axis (including the documented `spec-api.op-undefined`) green.\n //\n // What decides the severity is whether anything already written down is\n // holding a join into it: a living `Operations:` line, or an op-linked\n // landscape edge. With one of those, the absence breaks a link somebody\n // authored — an error. With neither, this is a service that legitimately\n // has no HTTP surface (a worker nobody calls), and a fleet mid-rollout\n // must not go red for it — a warning, or, when the landscape PROVES nobody\n // calls it, the documented silence.\n //\n // One finding, not one per dangling link: the fix is a single file, and\n // the links it strands ride along as details.\n const dangling = [...new Set([...governedOps, ...(inboundOps ?? []).map((r) => r.op!)])].sort();\n if (dangling.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"service.no-openapi\",\n subject: service,\n message:\n `No OpenAPI contract at ${paths.openapi}, and ${dangling.length} operation link(s) already point into it — ` +\n `every requirement and landscape edge naming one of them resolves to nothing until the file is back`,\n details: dangling,\n });\n } else if (inboundOps === null) {\n findings.push({\n severity: \"warn\",\n code: \"service.no-openapi\",\n message: `No OpenAPI contract at ${paths.openapi} — API coverage and the landscape spine are unchecked`,\n });\n }\n } else if (api.unreadable) {\n // A contract that EXISTS but does not read is a broken source of truth, not\n // an empty one: swallowing it into zero operations used to grade every\n // inbound landscape edge `spine.op-undefined` — a false diagnosis pointing\n // at the landscape when the truth was this file. So the file is the error,\n // and every check that reads the contract (api.*, the spine's op\n // resolution) is suspended below, the landscape.invalid discipline.\n findings.push({\n severity: \"error\",\n code: \"openapi.invalid\",\n message: `${service}: openapi.yaml does not parse — API coverage and the landscape spine are unchecked`,\n ...(api.error === undefined ? {} : { details: [api.error] }),\n });\n } else {\n if (removeMarkers.length > 0) {\n findings.push({\n severity: \"error\",\n code: \"openapi.remove-marker-living\",\n message: `${service}: living openapi.yaml contains ${removeMarkers.length} x-loam-remove marker(s) (${removeMarkers.map((op) => op.id).join(\", \")}) — removal markers are valid only in feature deltas`,\n });\n }\n // The same marker written at PATH level, beside the methods. `readOpenapi`\n // is keyed by (path, method), which is precisely why the check above cannot\n // see this one and precisely how one reached a living contract in the first\n // place: it addresses no operation, so it retired nothing, and it stayed\n // invisible afterwards. Error, like its method-level sibling — a\n // feature-only key published to every consumer of the fleet, and one that\n // keeps the empty-path cleanup from ever firing. Same code as the archive\n // plan's: one breach, one name, wherever it is found.\n for (const removed of api.pathLevelRemovals) {\n findings.push({\n severity: \"error\",\n code: \"openapi.remove-marker-path-level\",\n subject: service,\n message:\n `${service}: living openapi.yaml carries x-loam-remove at PATH level on '${removed}' — ` +\n `a removal marker names ONE operation, so beside the methods it retires nothing and is not a contract key either, ` +\n `and no id-keyed check can see it. Delete it from the living contract; retire an operation through a feature delta whose marker sits inside the operation.`,\n });\n }\n // Two slots claiming one operationId in a LIVING contract. `readOpenapi`\n // has computed this since the merge needed it, and only the feature path\n // (coherence) ever read it — so the ambiguity was reachable only when some\n // unrelated feature happened to carry a delta for this service, and the\n // fleet gate that actually runs in CI was blind to it. Same code and same\n // sentence as the feature-scope check: one breach, one name.\n for (const id of api.duplicateIds) {\n const slots = api.ops.filter((op) => op.id === id).map((op) => `${op.method} ${op.path}`);\n findings.push({\n severity: \"warn\",\n code: \"openapi.duplicate-operationid\",\n subject: service,\n message: `${service}: the living OpenAPI defines operationId '${id}' at ${slots.join(\" and \")} — every join on the id (a requirement's Operations: line, an edge's metadata { op }, a removal marker) picks one of those slots arbitrarily`,\n });\n }\n // The contract-depth probes. Form validated and depth did not: an\n // operation whose one response is `description: OK`, and a `$ref` to a\n // schema nobody wrote, both read as cleanly as a complete contract while\n // a reader still cannot rebuild its payloads. Presence\n // probes over the document's own keys; nothing reads what a schema says.\n const undescribed = liveOps.filter((op) => op.undescribed === true).map((op) => op.id);\n if (undescribed.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"openapi.response-undescribed\",\n subject: service,\n message:\n `${service}: ${undescribed.length} operation(s) declare no response schema (${undescribed.join(\", \")}) — ` +\n `the contract names the operation and says nothing about what it returns, so a reader still ` +\n `needs the code; declare content with a schema on at least one response (204/304 excepted)`,\n });\n }\n if (api.danglingRefs.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"openapi.ref-unresolved\",\n subject: service,\n message:\n `${service}: openapi.yaml contains ${api.danglingRefs.length} internal $ref(s) that resolve to ` +\n `nothing in the document — the operation validates while its schema is silently absent ` +\n `(the archive plan refuses the same breach in a feature delta, as an error)`,\n details: api.danglingRefs,\n });\n }\n const defined = new Set(ops);\n // `Operations:` on a LIVING requirement, resolved against this service's own\n // contract. Nothing did this before: the same spine is checked inside a\n // feature delta (coherence's spec-api.op-undefined) and then never again, so\n // a typo that shipped, or an operation later renamed out of openapi.yaml,\n // left a living requirement governing an operation that does not exist —\n // green forever, and every downstream join through that id silently empty.\n // Same code and severity as the feature-scope check on purpose: one breach,\n // one name, wherever it is found.\n for (const r of livingReqs) {\n for (const op of r.operations) {\n if (defined.has(op)) continue;\n const close = closeIds(op, ops);\n findings.push({\n severity: \"error\",\n code: \"spec-api.op-undefined\",\n subject: service,\n message:\n `${service}: requirement '${r.name}' governs '${op}', not defined in ${service}'s OpenAPI` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n }\n if (ops.length > 0) {\n const governed = new Set(livingReqs.flatMap((r) => r.operations));\n const orphans = ops.filter((op) => !governed.has(op));\n if (orphans.length === 0) {\n findings.push({\n severity: \"ok\",\n code: \"api.covered\",\n // Both counts on purpose: the ratio is the one signal that lets a\n // reviewer smell a thin baseline from the summary — forty behaviours\n // filed under one requirement passes every check, and \"covered\" alone\n // read as done.\n message: `${service}: API covered (${ops.length} operation(s) governed by requirements · ${livingReqs.length} living requirement(s))`,\n });\n } else {\n findings.push({\n severity: \"warn\",\n code: \"api.ungoverned\",\n message: `${service}: ${orphans.length} operation(s) not governed by any requirement — ${orphans.join(\", \")}`,\n });\n }\n findings.push(...responseGovernanceFindings(service, liveOps, livingReqs));\n // The migration-debt case: requirements exist, the API exists, and no\n // `Operations:` line ties them — every cross-axis check above and in\n // feature mode is vacuously green. Once per service, not per operation;\n // with zero requirements the spec (or its absence) is the finding instead.\n if (reqs.length > 0 && reqs.every((r) => r.operations.length === 0)) {\n findings.push({\n severity: \"warn\",\n code: \"api.ops-unlinked\",\n message: `${service}: openapi.yaml defines ${ops.length} operation(s) but no requirement links any — the API axis is unchecked for this service`,\n });\n }\n // Lifecycle: a requirement whose `Operations:` list resolves ONLY to\n // deprecated operations governs behaviour the contract is retiring.\n // Deprecation is the documented first step of removing an op; the explicit\n // feature marker is the final step. Until that delta archives, the op stays\n // live, so the fix is migration or a coordinated retirement. Ops the contract does not define at all\n // prove nothing here and are left to spec-api.op-undefined above.\n for (const r of reqs) {\n const resolved = r.operations.filter((op) => defined.has(op));\n if (resolved.length === 0 || !resolved.every((op) => deprecatedOps.has(op))) continue;\n findings.push({\n severity: \"warn\",\n code: \"api.requirement-deprecated\",\n message: `${service}: requirement '${r.name}' governs only deprecated operation(s) (${resolved.join(\", \")}) — the behaviour it describes is on its way out; migrate it to the replacement operation, or retire it`,\n });\n }\n }\n }\n\n return { findings, contract };\n}\n\n/**\n * What the spine may conclude from one contract read — spelled once, because a\n * `--base` run now grades a changed edge against a provider it does not grade\n * as a target (`../scope.ts`), and two spellings of \"which operations are\n * live\" would be two chances for a removal marker to count as defined.\n */\nexport function contractOf(api: OpenapiDoc, missing: boolean): ServiceContract {\n const live = api.ops.filter((o) => !o.remove);\n return {\n ops: live.map((o) => o.id),\n deprecated: new Set(live.filter((o) => o.deprecated).map((o) => o.id)),\n unreadable: api.unreadable || missing,\n };\n}\n"]}