@ybotok/loam 0.1.0-beta.3 → 0.2.0-alpha.1

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 (1026) hide show
  1. package/CHANGELOG.md +2548 -86
  2. package/COMPARISON.md +80 -63
  3. package/CONTRIBUTING.md +84 -0
  4. package/MIGRATING-from-OpenSpec.md +213 -54
  5. package/QUICKSTART.md +125 -0
  6. package/README.md +450 -150
  7. package/ROADMAP.md +494 -0
  8. package/SCHEMA.md +2355 -256
  9. package/SECURITY.md +21 -3
  10. package/WORKFLOW.md +701 -0
  11. package/dist/cli.js +113 -19
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/adopt/adopt.js +209 -0
  14. package/dist/commands/adopt/adopt.js.map +1 -0
  15. package/dist/commands/adopt/render.js +268 -0
  16. package/dist/commands/adopt/render.js.map +1 -0
  17. package/dist/commands/archive/archive.js +77 -0
  18. package/dist/commands/archive/archive.js.map +1 -0
  19. package/dist/commands/archive/plan/contracts/asyncapi.js +178 -0
  20. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -0
  21. package/dist/commands/archive/plan/contracts/openapi.js +159 -0
  22. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -0
  23. package/dist/commands/archive/plan/gate.js +317 -0
  24. package/dist/commands/archive/plan/gate.js.map +1 -0
  25. package/dist/commands/archive/plan/landscape.js +161 -0
  26. package/dist/commands/archive/plan/landscape.js.map +1 -0
  27. package/dist/commands/archive/plan/refusal.js +111 -0
  28. package/dist/commands/archive/plan/refusal.js.map +1 -0
  29. package/dist/commands/archive/plan/specs.js +237 -0
  30. package/dist/commands/archive/plan/specs.js.map +1 -0
  31. package/dist/commands/archive/plan/state.js +27 -0
  32. package/dist/commands/archive/plan/state.js.map +1 -0
  33. package/dist/commands/archive/run.js +251 -0
  34. package/dist/commands/archive/run.js.map +1 -0
  35. package/dist/commands/context/context.js +99 -0
  36. package/dist/commands/context/context.js.map +1 -0
  37. package/dist/commands/context/print.js +283 -0
  38. package/dist/commands/context/print.js.map +1 -0
  39. package/dist/commands/delta/delta.js +279 -0
  40. package/dist/commands/delta/delta.js.map +1 -0
  41. package/dist/commands/delta/print.js +205 -0
  42. package/dist/commands/delta/print.js.map +1 -0
  43. package/dist/commands/dependencies.js +9 -7
  44. package/dist/commands/dependencies.js.map +1 -1
  45. package/dist/commands/diff/diff.js +93 -0
  46. package/dist/commands/diff/diff.js.map +1 -0
  47. package/dist/commands/diff/print.js +38 -0
  48. package/dist/commands/diff/print.js.map +1 -0
  49. package/dist/commands/doctor.js +5 -4
  50. package/dist/commands/doctor.js.map +1 -1
  51. package/dist/commands/explain/explain.js +151 -0
  52. package/dist/commands/explain/explain.js.map +1 -0
  53. package/dist/commands/explore.js +29 -24
  54. package/dist/commands/explore.js.map +1 -1
  55. package/dist/commands/gate/gate.js +115 -0
  56. package/dist/commands/gate/gate.js.map +1 -0
  57. package/dist/commands/gate/print.js +54 -0
  58. package/dist/commands/gate/print.js.map +1 -0
  59. package/dist/commands/gherkin/commit.js +146 -0
  60. package/dist/commands/gherkin/commit.js.map +1 -0
  61. package/dist/commands/gherkin/gherkin.js +246 -0
  62. package/dist/commands/gherkin/gherkin.js.map +1 -0
  63. package/dist/commands/gherkin/reconcile.js +149 -0
  64. package/dist/commands/gherkin/reconcile.js.map +1 -0
  65. package/dist/commands/gherkin/render.js +77 -0
  66. package/dist/commands/gherkin/render.js.map +1 -0
  67. package/dist/commands/init/example.js +204 -0
  68. package/dist/commands/init/example.js.map +1 -0
  69. package/dist/commands/init/first-hour.js +28 -0
  70. package/dist/commands/init/first-hour.js.map +1 -0
  71. package/dist/commands/{init.js → init/init.js} +146 -71
  72. package/dist/commands/init/init.js.map +1 -0
  73. package/dist/commands/init/mcp.js +131 -0
  74. package/dist/commands/init/mcp.js.map +1 -0
  75. package/dist/commands/init/options.js +78 -0
  76. package/dist/commands/init/options.js.map +1 -0
  77. package/dist/commands/instructions.js +72 -14
  78. package/dist/commands/instructions.js.map +1 -1
  79. package/dist/commands/list/campaign/campaign.js +65 -0
  80. package/dist/commands/list/campaign/campaign.js.map +1 -0
  81. package/dist/commands/list/campaign/owners.js +65 -0
  82. package/dist/commands/list/campaign/owners.js.map +1 -0
  83. package/dist/commands/list/json.js +157 -0
  84. package/dist/commands/list/json.js.map +1 -0
  85. package/dist/commands/list/list.js +343 -0
  86. package/dist/commands/list/list.js.map +1 -0
  87. package/dist/commands/list/print.js +301 -0
  88. package/dist/commands/list/print.js.map +1 -0
  89. package/dist/commands/list/review.js +120 -0
  90. package/dist/commands/list/review.js.map +1 -0
  91. package/dist/commands/list/views.js +105 -0
  92. package/dist/commands/list/views.js.map +1 -0
  93. package/dist/commands/mcp/dispatch.js +183 -0
  94. package/dist/commands/mcp/dispatch.js.map +1 -0
  95. package/dist/commands/mcp/mcp.js +34 -0
  96. package/dist/commands/mcp/mcp.js.map +1 -0
  97. package/dist/commands/mcp/serve.js +55 -0
  98. package/dist/commands/mcp/serve.js.map +1 -0
  99. package/dist/commands/migrate-openspec/materialize/active.js +170 -0
  100. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -0
  101. package/dist/commands/migrate-openspec/materialize/feature.js +41 -0
  102. package/dist/commands/migrate-openspec/materialize/feature.js.map +1 -0
  103. package/dist/commands/migrate-openspec/materialize/living.js +62 -0
  104. package/dist/commands/migrate-openspec/materialize/living.js.map +1 -0
  105. package/dist/commands/migrate-openspec/materialize/stage.js +142 -0
  106. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -0
  107. package/dist/commands/migrate-openspec/materialize/target.js +219 -0
  108. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -0
  109. package/dist/commands/migrate-openspec/migrate-openspec.js +142 -0
  110. package/dist/commands/migrate-openspec/migrate-openspec.js.map +1 -0
  111. package/dist/commands/migrate-openspec/openspec/decisions.js +91 -0
  112. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -0
  113. package/dist/commands/migrate-openspec/openspec/error.js +19 -0
  114. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -0
  115. package/dist/commands/migrate-openspec/openspec/mapping.js +170 -0
  116. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -0
  117. package/dist/commands/migrate-openspec/openspec/paths.js +43 -0
  118. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -0
  119. package/dist/commands/migrate-openspec/openspec/read.js +49 -0
  120. package/dist/commands/migrate-openspec/openspec/read.js.map +1 -0
  121. package/dist/commands/migrate-openspec/print.js +89 -0
  122. package/dist/commands/migrate-openspec/print.js.map +1 -0
  123. package/dist/commands/new/commit.js +76 -0
  124. package/dist/commands/new/commit.js.map +1 -0
  125. package/dist/commands/new/new.js +283 -0
  126. package/dist/commands/new/new.js.map +1 -0
  127. package/dist/commands/new/templates.js +355 -0
  128. package/dist/commands/new/templates.js.map +1 -0
  129. package/dist/commands/open.js +203 -0
  130. package/dist/commands/open.js.map +1 -0
  131. package/dist/commands/policy/format.js +152 -0
  132. package/dist/commands/policy/format.js.map +1 -0
  133. package/dist/commands/{docs-repo-gate.js → policy/gate.js} +11 -11
  134. package/dist/commands/policy/gate.js.map +1 -0
  135. package/dist/commands/rebase/edit.js +106 -0
  136. package/dist/commands/rebase/edit.js.map +1 -0
  137. package/dist/commands/rebase/living.js +185 -0
  138. package/dist/commands/rebase/living.js.map +1 -0
  139. package/dist/commands/rebase/plan.js +245 -0
  140. package/dist/commands/rebase/plan.js.map +1 -0
  141. package/dist/commands/rebase/rebase.js +275 -0
  142. package/dist/commands/rebase/rebase.js.map +1 -0
  143. package/dist/commands/seed/commit.js +192 -0
  144. package/dist/commands/seed/commit.js.map +1 -0
  145. package/dist/commands/seed/plan.js +133 -0
  146. package/dist/commands/seed/plan.js.map +1 -0
  147. package/dist/commands/seed/seed.js +172 -0
  148. package/dist/commands/seed/seed.js.map +1 -0
  149. package/dist/commands/show/feature.js +178 -0
  150. package/dist/commands/show/feature.js.map +1 -0
  151. package/dist/commands/show/marks.js +24 -0
  152. package/dist/commands/show/marks.js.map +1 -0
  153. package/dist/commands/{show.js → show/service.js} +63 -152
  154. package/dist/commands/show/service.js.map +1 -0
  155. package/dist/commands/show/show.js +77 -0
  156. package/dist/commands/show/show.js.map +1 -0
  157. package/dist/commands/status/json.js +45 -0
  158. package/dist/commands/status/json.js.map +1 -0
  159. package/dist/commands/status/print.js +157 -0
  160. package/dist/commands/status/print.js.map +1 -0
  161. package/dist/commands/status/status.js +119 -0
  162. package/dist/commands/status/status.js.map +1 -0
  163. package/dist/commands/steps/steps.js +189 -0
  164. package/dist/commands/steps/steps.js.map +1 -0
  165. package/dist/commands/subsystem/edit.js +192 -0
  166. package/dist/commands/subsystem/edit.js.map +1 -0
  167. package/dist/commands/subsystem/move.js +265 -0
  168. package/dist/commands/subsystem/move.js.map +1 -0
  169. package/dist/commands/subsystem/query.js +115 -0
  170. package/dist/commands/subsystem/query.js.map +1 -0
  171. package/dist/commands/subsystem/subsystem.js +101 -0
  172. package/dist/commands/subsystem/subsystem.js.map +1 -0
  173. package/dist/commands/subsystem/sync.js +103 -0
  174. package/dist/commands/subsystem/sync.js.map +1 -0
  175. package/dist/commands/subsystem/txn/txn.js +94 -0
  176. package/dist/commands/subsystem/txn/txn.js.map +1 -0
  177. package/dist/commands/subsystem/txn/views.js +70 -0
  178. package/dist/commands/subsystem/txn/views.js.map +1 -0
  179. package/dist/commands/unarchive/manifest.js +160 -0
  180. package/dist/commands/unarchive/manifest.js.map +1 -0
  181. package/dist/commands/{unarchive.js → unarchive/restore.js} +47 -160
  182. package/dist/commands/unarchive/restore.js.map +1 -0
  183. package/dist/commands/unarchive/unarchive.js +67 -0
  184. package/dist/commands/unarchive/unarchive.js.map +1 -0
  185. package/dist/commands/validate/arch-coverage.js +194 -0
  186. package/dist/commands/validate/arch-coverage.js.map +1 -0
  187. package/dist/commands/validate/checks/fleet-shape.js +296 -0
  188. package/dist/commands/validate/checks/fleet-shape.js.map +1 -0
  189. package/dist/commands/validate/checks/health-deps.js +55 -0
  190. package/dist/commands/validate/checks/health-deps.js.map +1 -0
  191. package/dist/commands/validate/checks/requirements.js +254 -0
  192. package/dist/commands/validate/checks/requirements.js.map +1 -0
  193. package/dist/commands/validate/checks/sources.js +76 -0
  194. package/dist/commands/validate/checks/sources.js.map +1 -0
  195. package/dist/commands/validate/checks/vocabulary.js +31 -0
  196. package/dist/commands/validate/checks/vocabulary.js.map +1 -0
  197. package/dist/commands/validate/feature.js +216 -0
  198. package/dist/commands/validate/feature.js.map +1 -0
  199. package/dist/commands/validate/fleet/census.js +69 -0
  200. package/dist/commands/validate/fleet/census.js.map +1 -0
  201. package/dist/commands/validate/fleet/kind-tags.js +39 -0
  202. package/dist/commands/validate/fleet/kind-tags.js.map +1 -0
  203. package/dist/commands/validate/fleet/landscape.js +353 -0
  204. package/dist/commands/validate/fleet/landscape.js.map +1 -0
  205. package/dist/commands/validate/fleet/load.js +52 -0
  206. package/dist/commands/validate/fleet/load.js.map +1 -0
  207. package/dist/commands/validate/fleet/obligations.js +236 -0
  208. package/dist/commands/validate/fleet/obligations.js.map +1 -0
  209. package/dist/commands/validate/fleet/scorecard/adoption.js +102 -0
  210. package/dist/commands/validate/fleet/scorecard/adoption.js.map +1 -0
  211. package/dist/commands/validate/fleet/scorecard/contracts.js +98 -0
  212. package/dist/commands/validate/fleet/scorecard/contracts.js.map +1 -0
  213. package/dist/commands/validate/fleet/scorecard/print.js +63 -0
  214. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -0
  215. package/dist/commands/validate/fleet/scorecard/scorecard.js +156 -0
  216. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -0
  217. package/dist/commands/validate/fleet/usecases/capability-tag.js +94 -0
  218. package/dist/commands/validate/fleet/usecases/capability-tag.js.map +1 -0
  219. package/dist/commands/validate/fleet/usecases/place.js +13 -0
  220. package/dist/commands/validate/fleet/usecases/place.js.map +1 -0
  221. package/dist/commands/validate/fleet/usecases/requirement-tag.js +95 -0
  222. package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +1 -0
  223. package/dist/commands/validate/fleet/usecases/steps.js +197 -0
  224. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -0
  225. package/dist/commands/validate/fleet/usecases/usecases.js +99 -0
  226. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -0
  227. package/dist/commands/validate/fleet/views/ids.js +40 -0
  228. package/dist/commands/validate/fleet/views/ids.js.map +1 -0
  229. package/dist/commands/validate/fleet/views/stale.js +59 -0
  230. package/dist/commands/validate/fleet/views/stale.js.map +1 -0
  231. package/dist/commands/validate/links/corpus.js +92 -0
  232. package/dist/commands/validate/links/corpus.js.map +1 -0
  233. package/dist/commands/validate/links/glossary.js +75 -0
  234. package/dist/commands/validate/links/glossary.js.map +1 -0
  235. package/dist/commands/validate/report.js +269 -0
  236. package/dist/commands/validate/report.js.map +1 -0
  237. package/dist/commands/validate/service/api.js +338 -0
  238. package/dist/commands/validate/service/api.js.map +1 -0
  239. package/dist/commands/validate/service/events/events.js +219 -0
  240. package/dist/commands/validate/service/events/events.js.map +1 -0
  241. package/dist/commands/validate/service/events/message-producers.js +77 -0
  242. package/dist/commands/validate/service/events/message-producers.js.map +1 -0
  243. package/dist/commands/validate/service/evidence-pins.js +131 -0
  244. package/dist/commands/validate/service/evidence-pins.js.map +1 -0
  245. package/dist/commands/validate/service/service.js +250 -0
  246. package/dist/commands/validate/service/service.js.map +1 -0
  247. package/dist/commands/validate/service/specs.js +219 -0
  248. package/dist/commands/validate/service/specs.js.map +1 -0
  249. package/dist/commands/validate/service/spine.js +113 -0
  250. package/dist/commands/validate/service/spine.js.map +1 -0
  251. package/dist/commands/validate/validate.js +338 -0
  252. package/dist/commands/validate/validate.js.map +1 -0
  253. package/dist/commands/verify/cross/diff.js +182 -0
  254. package/dist/commands/verify/cross/diff.js.map +1 -0
  255. package/dist/commands/verify/evidence/contract.js +32 -0
  256. package/dist/commands/verify/evidence/contract.js.map +1 -0
  257. package/dist/commands/verify/evidence/pins.js +52 -0
  258. package/dist/commands/verify/evidence/pins.js.map +1 -0
  259. package/dist/commands/verify/evidence/read.js +72 -0
  260. package/dist/commands/verify/evidence/read.js.map +1 -0
  261. package/dist/commands/verify/frozen.js +241 -0
  262. package/dist/commands/verify/frozen.js.map +1 -0
  263. package/dist/commands/verify/record.js +226 -0
  264. package/dist/commands/verify/record.js.map +1 -0
  265. package/dist/commands/verify/report.js +223 -0
  266. package/dist/commands/verify/report.js.map +1 -0
  267. package/dist/commands/verify/results.js +246 -0
  268. package/dist/commands/verify/results.js.map +1 -0
  269. package/dist/commands/verify/verify.js +217 -0
  270. package/dist/commands/verify/verify.js.map +1 -0
  271. package/dist/commands/vouch/contract.js +11 -0
  272. package/dist/commands/vouch/contract.js.map +1 -0
  273. package/dist/commands/vouch/pack/contract.js +2 -0
  274. package/dist/commands/vouch/pack/contract.js.map +1 -0
  275. package/dist/commands/vouch/pack/pack.js +266 -0
  276. package/dist/commands/vouch/pack/pack.js.map +1 -0
  277. package/dist/commands/vouch/pack/print.js +271 -0
  278. package/dist/commands/vouch/pack/print.js.map +1 -0
  279. package/dist/commands/vouch/pack/sections.js +142 -0
  280. package/dist/commands/vouch/pack/sections.js.map +1 -0
  281. package/dist/commands/vouch/prompt/confirm.js +46 -0
  282. package/dist/commands/vouch/prompt/confirm.js.map +1 -0
  283. package/dist/commands/vouch/report.js +101 -0
  284. package/dist/commands/vouch/report.js.map +1 -0
  285. package/dist/commands/vouch/run.js +285 -0
  286. package/dist/commands/vouch/run.js.map +1 -0
  287. package/dist/commands/vouch/sample/plan.js +125 -0
  288. package/dist/commands/vouch/sample/plan.js.map +1 -0
  289. package/dist/commands/vouch/sample/print.js +109 -0
  290. package/dist/commands/vouch/sample/print.js.map +1 -0
  291. package/dist/commands/vouch/vet/verify.js +123 -0
  292. package/dist/commands/vouch/vet/verify.js.map +1 -0
  293. package/dist/commands/vouch/vouch.js +176 -0
  294. package/dist/commands/vouch/vouch.js.map +1 -0
  295. package/dist/core/agent/agents-md/artifacts.js +220 -0
  296. package/dist/core/agent/agents-md/artifacts.js.map +1 -0
  297. package/dist/core/agent/agents-md/command-map.js +324 -0
  298. package/dist/core/agent/agents-md/command-map.js.map +1 -0
  299. package/dist/core/agent/agents-md/cycle.js +165 -0
  300. package/dist/core/agent/agents-md/cycle.js.map +1 -0
  301. package/dist/core/agent/agents-md/map/explain.js +34 -0
  302. package/dist/core/agent/agents-md/map/explain.js.map +1 -0
  303. package/dist/core/agent/agents-md/map/lenses/context.js +41 -0
  304. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -0
  305. package/dist/core/agent/agents-md/map/lenses/diff.js +46 -0
  306. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -0
  307. package/dist/core/agent/agents-md/map/lenses/gate.js +57 -0
  308. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -0
  309. package/dist/core/agent/agents-md/map/mcp.js +41 -0
  310. package/dist/core/agent/agents-md/map/mcp.js.map +1 -0
  311. package/dist/core/agent/agents-md/map/status.js +62 -0
  312. package/dist/core/agent/agents-md/map/status.js.map +1 -0
  313. package/dist/core/agent/agents-md/map/subsystem.js +32 -0
  314. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -0
  315. package/dist/core/agent/agents-md/map/usecases.js +60 -0
  316. package/dist/core/agent/agents-md/map/usecases.js.map +1 -0
  317. package/dist/core/agent/agents-md/refusals.js +246 -0
  318. package/dist/core/agent/agents-md/refusals.js.map +1 -0
  319. package/dist/core/agent/agents-md/shipped/archive-gate.js +98 -0
  320. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +1 -0
  321. package/dist/core/agent/agents-md/spine.js +40 -0
  322. package/dist/core/agent/agents-md/spine.js.map +1 -0
  323. package/dist/core/agent/agents-md.js +170 -0
  324. package/dist/core/agent/agents-md.js.map +1 -0
  325. package/dist/core/{agents-stamp.js → agent/agents-stamp.js} +50 -0
  326. package/dist/core/agent/agents-stamp.js.map +1 -0
  327. package/dist/core/agent/contract.js +2 -0
  328. package/dist/core/agent/contract.js.map +1 -0
  329. package/dist/core/agent/protocol.js +193 -0
  330. package/dist/core/agent/protocol.js.map +1 -0
  331. package/dist/core/agent/scaffold.js +259 -0
  332. package/dist/core/agent/scaffold.js.map +1 -0
  333. package/dist/core/agent/support/report.js +103 -0
  334. package/dist/core/agent/support/report.js.map +1 -0
  335. package/dist/core/agent/tools/dialects.js +150 -0
  336. package/dist/core/agent/tools/dialects.js.map +1 -0
  337. package/dist/core/agent/tools/registry.js +228 -0
  338. package/dist/core/agent/tools/registry.js.map +1 -0
  339. package/dist/core/agent/workflows/adopt.js +133 -0
  340. package/dist/core/agent/workflows/adopt.js.map +1 -0
  341. package/dist/core/agent/workflows/check.js +334 -0
  342. package/dist/core/agent/workflows/check.js.map +1 -0
  343. package/dist/core/agent/workflows/closing.js +262 -0
  344. package/dist/core/agent/workflows/closing.js.map +1 -0
  345. package/dist/core/agent/workflows/feature.js +105 -0
  346. package/dist/core/agent/workflows/feature.js.map +1 -0
  347. package/dist/core/agent/workflows/implement.js +105 -0
  348. package/dist/core/agent/workflows/implement.js.map +1 -0
  349. package/dist/core/agent/workflows/reference/authoring.js +209 -0
  350. package/dist/core/agent/workflows/reference/authoring.js.map +1 -0
  351. package/dist/core/agent/workflows/reference/codes.js +65 -0
  352. package/dist/core/agent/workflows/reference/codes.js.map +1 -0
  353. package/dist/core/agent/workflows/reference/done-check.js +188 -0
  354. package/dist/core/agent/workflows/reference/done-check.js.map +1 -0
  355. package/dist/core/agent/workflows/reference/reference.js +16 -0
  356. package/dist/core/agent/workflows/reference/reference.js.map +1 -0
  357. package/dist/core/agent/workflows/reference/spine.js +297 -0
  358. package/dist/core/agent/workflows/reference/spine.js.map +1 -0
  359. package/dist/core/asyncapi/baseline/plan.js +110 -0
  360. package/dist/core/asyncapi/baseline/plan.js.map +1 -0
  361. package/dist/core/asyncapi/baseline/surfaces.js +54 -0
  362. package/dist/core/asyncapi/baseline/surfaces.js.map +1 -0
  363. package/dist/core/asyncapi/depth.js +164 -0
  364. package/dist/core/asyncapi/depth.js.map +1 -0
  365. package/dist/core/asyncapi/digest.js +141 -0
  366. package/dist/core/asyncapi/digest.js.map +1 -0
  367. package/dist/core/asyncapi/merge/components.js +91 -0
  368. package/dist/core/asyncapi/merge/components.js.map +1 -0
  369. package/dist/core/asyncapi/merge/error.js +33 -0
  370. package/dist/core/asyncapi/merge/error.js.map +1 -0
  371. package/dist/core/asyncapi/merge/markers.js +308 -0
  372. package/dist/core/asyncapi/merge/markers.js.map +1 -0
  373. package/dist/core/asyncapi/merge/merge.js +215 -0
  374. package/dist/core/asyncapi/merge/merge.js.map +1 -0
  375. package/dist/core/asyncapi/merge/pin.js +107 -0
  376. package/dist/core/asyncapi/merge/pin.js.map +1 -0
  377. package/dist/core/asyncapi/model.js +11 -0
  378. package/dist/core/asyncapi/model.js.map +1 -0
  379. package/dist/core/asyncapi/producers.js +50 -0
  380. package/dist/core/asyncapi/producers.js.map +1 -0
  381. package/dist/core/{asyncapi.js → asyncapi/read.js} +95 -109
  382. package/dist/core/asyncapi/read.js.map +1 -0
  383. package/dist/core/brief/brief.js +225 -0
  384. package/dist/core/brief/brief.js.map +1 -0
  385. package/dist/core/brief/checks.js +250 -0
  386. package/dist/core/brief/checks.js.map +1 -0
  387. package/dist/core/brief/landscape.js +194 -0
  388. package/dist/core/brief/landscape.js.map +1 -0
  389. package/dist/core/brief/targets.js +226 -0
  390. package/dist/core/brief/targets.js.map +1 -0
  391. package/dist/core/brief/unchecked.js +47 -0
  392. package/dist/core/brief/unchecked.js.map +1 -0
  393. package/dist/core/c4/arch.js +282 -0
  394. package/dist/core/c4/arch.js.map +1 -0
  395. package/dist/core/c4/likec4.js +88 -0
  396. package/dist/core/c4/likec4.js.map +1 -0
  397. package/dist/core/c4/parsed/dynamic-views.js +132 -0
  398. package/dist/core/c4/parsed/dynamic-views.js.map +1 -0
  399. package/dist/core/c4/parsed/specification.js +78 -0
  400. package/dist/core/c4/parsed/specification.js.map +1 -0
  401. package/dist/core/c4/parsed/values.js +57 -0
  402. package/dist/core/c4/parsed/values.js.map +1 -0
  403. package/dist/core/c4/parsed/view-ids.js +75 -0
  404. package/dist/core/c4/parsed/view-ids.js.map +1 -0
  405. package/dist/core/c4/project/architecture.js +45 -0
  406. package/dist/core/c4/project/architecture.js.map +1 -0
  407. package/dist/core/c4/project/documents.js +58 -0
  408. package/dist/core/c4/project/documents.js.map +1 -0
  409. package/dist/core/c4/project/load.js +183 -0
  410. package/dist/core/c4/project/load.js.map +1 -0
  411. package/dist/core/c4/resolve/service.js +113 -0
  412. package/dist/core/c4/resolve/service.js.map +1 -0
  413. package/dist/core/c4/seed/fleet-file.js +201 -0
  414. package/dist/core/c4/seed/fleet-file.js.map +1 -0
  415. package/dist/core/c4/seed/items.js +91 -0
  416. package/dist/core/c4/seed/items.js.map +1 -0
  417. package/dist/core/c4/seed/stamp.js +48 -0
  418. package/dist/core/c4/seed/stamp.js.map +1 -0
  419. package/dist/core/c4/seed/template.js +109 -0
  420. package/dist/core/c4/seed/template.js.map +1 -0
  421. package/dist/core/c4/source-mask.js +82 -0
  422. package/dist/core/c4/source-mask.js.map +1 -0
  423. package/dist/core/c4/source-scan.js +227 -0
  424. package/dist/core/c4/source-scan.js.map +1 -0
  425. package/dist/core/c4/splice/authored-source.js +101 -0
  426. package/dist/core/c4/splice/authored-source.js.map +1 -0
  427. package/dist/core/c4/splice/contract.js +10 -0
  428. package/dist/core/c4/splice/contract.js.map +1 -0
  429. package/dist/core/c4/splice/delta-blocks.js +126 -0
  430. package/dist/core/c4/splice/delta-blocks.js.map +1 -0
  431. package/dist/core/c4/splice/identity/edges.js +36 -0
  432. package/dist/core/c4/splice/identity/edges.js.map +1 -0
  433. package/dist/core/c4/splice/landscape-merge.js +269 -0
  434. package/dist/core/c4/splice/landscape-merge.js.map +1 -0
  435. package/dist/core/c4/splice/placement.js +194 -0
  436. package/dist/core/c4/splice/placement.js.map +1 -0
  437. package/dist/core/c4/workspace.js +172 -0
  438. package/dist/core/c4/workspace.js.map +1 -0
  439. package/dist/core/capabilities/authoring/notes.js +141 -0
  440. package/dist/core/capabilities/authoring/notes.js.map +1 -0
  441. package/dist/core/capabilities/capabilities.js +124 -0
  442. package/dist/core/capabilities/capabilities.js.map +1 -0
  443. package/dist/core/capabilities/delta/doc.js +50 -0
  444. package/dist/core/capabilities/delta/doc.js.map +1 -0
  445. package/dist/core/capabilities/delta/overlay.js +129 -0
  446. package/dist/core/capabilities/delta/overlay.js.map +1 -0
  447. package/dist/core/capabilities/delta/summary.js +57 -0
  448. package/dist/core/capabilities/delta/summary.js.map +1 -0
  449. package/dist/core/capabilities/delta/tree.js +43 -0
  450. package/dist/core/capabilities/delta/tree.js.map +1 -0
  451. package/dist/core/capabilities/delta/uncovered.js +222 -0
  452. package/dist/core/capabilities/delta/uncovered.js.map +1 -0
  453. package/dist/core/capabilities/findings.js +342 -0
  454. package/dist/core/capabilities/findings.js.map +1 -0
  455. package/dist/core/capabilities/realizes/findings.js +181 -0
  456. package/dist/core/capabilities/realizes/findings.js.map +1 -0
  457. package/dist/core/capabilities/realizes/join.js +143 -0
  458. package/dist/core/capabilities/realizes/join.js.map +1 -0
  459. package/dist/core/capabilities/rollup.js +199 -0
  460. package/dist/core/capabilities/rollup.js.map +1 -0
  461. package/dist/core/capabilities/tree.js +126 -0
  462. package/dist/core/capabilities/tree.js.map +1 -0
  463. package/dist/core/capabilities/usecase-join.js +277 -0
  464. package/dist/core/capabilities/usecase-join.js.map +1 -0
  465. package/dist/core/coherence/authoring/scaffold.js +177 -0
  466. package/dist/core/coherence/authoring/scaffold.js.map +1 -0
  467. package/dist/core/coherence/authoring/sentinels.js +74 -0
  468. package/dist/core/coherence/authoring/sentinels.js.map +1 -0
  469. package/dist/core/coherence/coherence.js +251 -0
  470. package/dist/core/coherence/coherence.js.map +1 -0
  471. package/dist/core/coherence/declared.js +269 -0
  472. package/dist/core/coherence/declared.js.map +1 -0
  473. package/dist/core/coherence/events/declared.js +206 -0
  474. package/dist/core/coherence/events/declared.js.map +1 -0
  475. package/dist/core/coherence/events/events.js +204 -0
  476. package/dist/core/coherence/events/events.js.map +1 -0
  477. package/dist/core/coherence/events/grades.js +166 -0
  478. package/dist/core/coherence/events/grades.js.map +1 -0
  479. package/dist/core/coherence/events/lookups.js +122 -0
  480. package/dist/core/coherence/events/lookups.js.map +1 -0
  481. package/dist/core/coherence/events/merged.js +40 -0
  482. package/dist/core/coherence/events/merged.js.map +1 -0
  483. package/dist/core/coherence/living.js +198 -0
  484. package/dist/core/coherence/living.js.map +1 -0
  485. package/dist/core/coherence/lookups.js +155 -0
  486. package/dist/core/coherence/lookups.js.map +1 -0
  487. package/dist/core/coherence/pending.js +39 -0
  488. package/dist/core/coherence/pending.js.map +1 -0
  489. package/dist/core/conflict-markers.js +60 -0
  490. package/dist/core/conflict-markers.js.map +1 -0
  491. package/dist/core/delta/claims.js +73 -0
  492. package/dist/core/delta/claims.js.map +1 -0
  493. package/dist/core/delta/delta.js +296 -0
  494. package/dist/core/delta/delta.js.map +1 -0
  495. package/dist/core/delta/document.js +177 -0
  496. package/dist/core/delta/document.js.map +1 -0
  497. package/dist/core/delta/scope.js +48 -0
  498. package/dist/core/delta/scope.js.map +1 -0
  499. package/dist/core/delta/select.js +237 -0
  500. package/dist/core/delta/select.js.map +1 -0
  501. package/dist/core/dependencies/dependencies.js +141 -0
  502. package/dist/core/dependencies/dependencies.js.map +1 -0
  503. package/dist/core/dependencies/facts.js +165 -0
  504. package/dist/core/dependencies/facts.js.map +1 -0
  505. package/dist/core/dependencies/fanin.js +88 -0
  506. package/dist/core/dependencies/fanin.js.map +1 -0
  507. package/dist/core/dependencies/graph.js +106 -0
  508. package/dist/core/dependencies/graph.js.map +1 -0
  509. package/dist/core/diff/base-git.js +148 -0
  510. package/dist/core/diff/base-git.js.map +1 -0
  511. package/dist/core/diff/base-state.js +209 -0
  512. package/dist/core/diff/base-state.js.map +1 -0
  513. package/dist/core/diff/current-state.js +96 -0
  514. package/dist/core/diff/current-state.js.map +1 -0
  515. package/dist/core/diff/scope/changed-paths.js +188 -0
  516. package/dist/core/diff/scope/changed-paths.js.map +1 -0
  517. package/dist/core/diff/semantic.js +243 -0
  518. package/dist/core/diff/semantic.js.map +1 -0
  519. package/dist/core/diff/victims.js +247 -0
  520. package/dist/core/diff/victims.js.map +1 -0
  521. package/dist/core/docs.js +7 -64
  522. package/dist/core/docs.js.map +1 -1
  523. package/dist/core/doctor/agents.js +222 -0
  524. package/dist/core/doctor/agents.js.map +1 -0
  525. package/dist/core/doctor/config.js +135 -0
  526. package/dist/core/doctor/config.js.map +1 -0
  527. package/dist/core/doctor/doctor.js +275 -0
  528. package/dist/core/doctor/doctor.js.map +1 -0
  529. package/dist/core/doctor/report.js +13 -0
  530. package/dist/core/doctor/report.js.map +1 -0
  531. package/dist/core/doctor/residue.js +110 -0
  532. package/dist/core/doctor/residue.js.map +1 -0
  533. package/dist/core/document/apply.js +77 -0
  534. package/dist/core/document/apply.js.map +1 -0
  535. package/dist/core/{frontmatter.js → document/frontmatter.js} +48 -2
  536. package/dist/core/document/frontmatter.js.map +1 -0
  537. package/dist/core/document/grammar/directives.js +143 -0
  538. package/dist/core/document/grammar/directives.js.map +1 -0
  539. package/dist/core/document/parse.js +263 -0
  540. package/dist/core/document/parse.js.map +1 -0
  541. package/dist/core/document/scenarios.js +221 -0
  542. package/dist/core/document/scenarios.js.map +1 -0
  543. package/dist/core/document/spec.js +241 -0
  544. package/dist/core/document/spec.js.map +1 -0
  545. package/dist/core/{config.js → envelope/config.js} +79 -18
  546. package/dist/core/envelope/config.js.map +1 -0
  547. package/dist/core/envelope/json.js +175 -0
  548. package/dist/core/envelope/json.js.map +1 -0
  549. package/dist/core/{version.js → envelope/version.js} +3 -3
  550. package/dist/core/envelope/version.js.map +1 -0
  551. package/dist/core/explain/families.js +175 -0
  552. package/dist/core/explain/families.js.map +1 -0
  553. package/dist/core/explain/fix-tables.js +178 -0
  554. package/dist/core/explain/fix-tables.js.map +1 -0
  555. package/dist/core/explain/lookup.js +181 -0
  556. package/dist/core/explain/lookup.js.map +1 -0
  557. package/dist/core/explain/refusals.js +49 -0
  558. package/dist/core/explain/refusals.js.map +1 -0
  559. package/dist/core/explain/terms.js +144 -0
  560. package/dist/core/explain/terms.js.map +1 -0
  561. package/dist/core/explore/describe.js +105 -0
  562. package/dist/core/explore/describe.js.map +1 -0
  563. package/dist/core/{explore.js → explore/explore.js} +50 -101
  564. package/dist/core/explore/explore.js.map +1 -0
  565. package/dist/core/fleet-context.js +120 -78
  566. package/dist/core/fleet-context.js.map +1 -1
  567. package/dist/core/gate/checks.js +201 -0
  568. package/dist/core/gate/checks.js.map +1 -0
  569. package/dist/core/gate/partners.js +205 -0
  570. package/dist/core/gate/partners.js.map +1 -0
  571. package/dist/core/gate/report.js +47 -0
  572. package/dist/core/gate/report.js.map +1 -0
  573. package/dist/core/gate/verification.js +113 -0
  574. package/dist/core/gate/verification.js.map +1 -0
  575. package/dist/core/gherkin/digest.js +60 -0
  576. package/dist/core/gherkin/digest.js.map +1 -0
  577. package/dist/core/gherkin/emit.js +177 -0
  578. package/dist/core/gherkin/emit.js.map +1 -0
  579. package/dist/core/gherkin/read.js +76 -0
  580. package/dist/core/gherkin/read.js.map +1 -0
  581. package/dist/core/gherkin/stale.js +234 -0
  582. package/dist/core/gherkin/stale.js.map +1 -0
  583. package/dist/core/gherkin/stamp.js +77 -0
  584. package/dist/core/gherkin/stamp.js.map +1 -0
  585. package/dist/core/gherkin/steps/catalogue.js +154 -0
  586. package/dist/core/gherkin/steps/catalogue.js.map +1 -0
  587. package/dist/core/gherkin/steps/inventory.js +98 -0
  588. package/dist/core/gherkin/steps/inventory.js.map +1 -0
  589. package/dist/core/gherkin/steps/phrase.js +72 -0
  590. package/dist/core/gherkin/steps/phrase.js.map +1 -0
  591. package/dist/core/glossary/delta.js +106 -0
  592. package/dist/core/glossary/delta.js.map +1 -0
  593. package/dist/core/glossary/tree.js +86 -0
  594. package/dist/core/glossary/tree.js.map +1 -0
  595. package/dist/core/kernel/concurrency.js.map +1 -0
  596. package/dist/core/{document-bytes.js → kernel/document-bytes.js} +1 -1
  597. package/dist/core/kernel/document-bytes.js.map +1 -0
  598. package/dist/core/kernel/fences.js +42 -0
  599. package/dist/core/kernel/fences.js.map +1 -0
  600. package/dist/core/kernel/ids/capability.js +72 -0
  601. package/dist/core/kernel/ids/capability.js.map +1 -0
  602. package/dist/core/kernel/ids/dirs.js +33 -0
  603. package/dist/core/kernel/ids/dirs.js.map +1 -0
  604. package/dist/core/kernel/ids/feature.js +75 -0
  605. package/dist/core/kernel/ids/feature.js.map +1 -0
  606. package/dist/core/kernel/ids/service.js +156 -0
  607. package/dist/core/kernel/ids/service.js.map +1 -0
  608. package/dist/core/kernel/ids/subsystem.js +63 -0
  609. package/dist/core/kernel/ids/subsystem.js.map +1 -0
  610. package/dist/core/{path-safety.js → kernel/path-safety.js} +18 -4
  611. package/dist/core/kernel/path-safety.js.map +1 -0
  612. package/dist/core/kernel/records.js.map +1 -0
  613. package/dist/core/links/backlinks.js +68 -0
  614. package/dist/core/links/backlinks.js.map +1 -0
  615. package/dist/core/links/case.js +74 -0
  616. package/dist/core/links/case.js.map +1 -0
  617. package/dist/core/links/corpus.js +104 -0
  618. package/dist/core/links/corpus.js.map +1 -0
  619. package/dist/core/links/findings.js +110 -0
  620. package/dist/core/links/findings.js.map +1 -0
  621. package/dist/core/links/parse.js +236 -0
  622. package/dist/core/links/parse.js.map +1 -0
  623. package/dist/core/mcp/argv.js +111 -0
  624. package/dist/core/mcp/argv.js.map +1 -0
  625. package/dist/core/mcp/author-tools.js +72 -0
  626. package/dist/core/mcp/author-tools.js.map +1 -0
  627. package/dist/core/mcp/framing.js +46 -0
  628. package/dist/core/mcp/framing.js.map +1 -0
  629. package/dist/core/mcp/protocol.js +280 -0
  630. package/dist/core/mcp/protocol.js.map +1 -0
  631. package/dist/core/mcp/tools.js +314 -0
  632. package/dist/core/mcp/tools.js.map +1 -0
  633. package/dist/core/obligations/obligations.js +106 -0
  634. package/dist/core/obligations/obligations.js.map +1 -0
  635. package/dist/core/openapi/baseline/gate.js +166 -0
  636. package/dist/core/openapi/baseline/gate.js.map +1 -0
  637. package/dist/core/openapi/baseline/plan.js +112 -0
  638. package/dist/core/openapi/baseline/plan.js.map +1 -0
  639. package/dist/core/openapi/baseline/record.js +175 -0
  640. package/dist/core/openapi/baseline/record.js.map +1 -0
  641. package/dist/core/openapi/depth.js +122 -0
  642. package/dist/core/openapi/depth.js.map +1 -0
  643. package/dist/core/openapi/digest.js +189 -0
  644. package/dist/core/openapi/digest.js.map +1 -0
  645. package/dist/core/{openapi.js → openapi/doc.js} +44 -106
  646. package/dist/core/openapi/doc.js.map +1 -0
  647. package/dist/core/openapi/merge/components.js +234 -0
  648. package/dist/core/openapi/merge/components.js.map +1 -0
  649. package/dist/core/openapi/merge/error.js +26 -0
  650. package/dist/core/openapi/merge/error.js.map +1 -0
  651. package/dist/core/openapi/merge/markers.js +208 -0
  652. package/dist/core/openapi/merge/markers.js.map +1 -0
  653. package/dist/core/openapi/merge/merge.js +256 -0
  654. package/dist/core/openapi/merge/merge.js.map +1 -0
  655. package/dist/core/openapi/merge/pin.js +128 -0
  656. package/dist/core/openapi/merge/pin.js.map +1 -0
  657. package/dist/core/openspec/decide/capabilities.js +244 -0
  658. package/dist/core/openspec/decide/capabilities.js.map +1 -0
  659. package/dist/core/openspec/decide/dispositions.js +63 -0
  660. package/dist/core/openspec/decide/dispositions.js.map +1 -0
  661. package/dist/core/openspec/decide/renames.js +179 -0
  662. package/dist/core/openspec/decide/renames.js.map +1 -0
  663. package/dist/core/openspec/decide/verdict.js +84 -0
  664. package/dist/core/openspec/decide/verdict.js.map +1 -0
  665. package/dist/core/openspec/inventory.js +221 -0
  666. package/dist/core/openspec/inventory.js.map +1 -0
  667. package/dist/core/openspec/model/mapping.js +76 -0
  668. package/dist/core/openspec/model/mapping.js.map +1 -0
  669. package/dist/core/openspec/model/model.js +65 -0
  670. package/dist/core/openspec/model/model.js.map +1 -0
  671. package/dist/core/openspec/scan/changes.js +235 -0
  672. package/dist/core/openspec/scan/changes.js.map +1 -0
  673. package/dist/core/openspec/scan/config.js +200 -0
  674. package/dist/core/openspec/scan/config.js.map +1 -0
  675. package/dist/core/openspec/scan/shape.js +260 -0
  676. package/dist/core/openspec/scan/shape.js.map +1 -0
  677. package/dist/core/openspec/scan/walk.js +133 -0
  678. package/dist/core/openspec/scan/walk.js.map +1 -0
  679. package/dist/core/openspec/scan/workspace.js +71 -0
  680. package/dist/core/openspec/scan/workspace.js.map +1 -0
  681. package/dist/core/owners/codeowners.js +141 -0
  682. package/dist/core/owners/codeowners.js.map +1 -0
  683. package/dist/core/pack/features.js +120 -0
  684. package/dist/core/pack/features.js.map +1 -0
  685. package/dist/core/pack/joins.js +112 -0
  686. package/dist/core/pack/joins.js.map +1 -0
  687. package/dist/core/pack/living.js +118 -0
  688. package/dist/core/pack/living.js.map +1 -0
  689. package/dist/core/pack/pack.js +76 -0
  690. package/dist/core/pack/pack.js.map +1 -0
  691. package/dist/core/permissions/permissions.js +87 -0
  692. package/dist/core/permissions/permissions.js.map +1 -0
  693. package/dist/core/projection/api.js +84 -0
  694. package/dist/core/projection/api.js.map +1 -0
  695. package/dist/core/projection/arch-slice.js +63 -0
  696. package/dist/core/projection/arch-slice.js.map +1 -0
  697. package/dist/core/projection/events.js +34 -0
  698. package/dist/core/projection/events.js.map +1 -0
  699. package/dist/core/provenance/findings.js +208 -0
  700. package/dist/core/provenance/findings.js.map +1 -0
  701. package/dist/core/provenance/git.js +225 -0
  702. package/dist/core/provenance/git.js.map +1 -0
  703. package/dist/core/provenance/gitq/moves.js +182 -0
  704. package/dist/core/provenance/gitq/moves.js.map +1 -0
  705. package/dist/core/provenance/gitq/vouched-ancestor.js +179 -0
  706. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -0
  707. package/dist/core/provenance/sample/scope.js +238 -0
  708. package/dist/core/provenance/sample/scope.js.map +1 -0
  709. package/dist/core/provenance/sample/sections.js +61 -0
  710. package/dist/core/provenance/sample/sections.js.map +1 -0
  711. package/dist/core/provenance/sources.js +263 -0
  712. package/dist/core/provenance/sources.js.map +1 -0
  713. package/dist/core/provenance/stamp.js +232 -0
  714. package/dist/core/provenance/stamp.js.map +1 -0
  715. package/dist/core/provenance/walk.js +157 -0
  716. package/dist/core/provenance/walk.js.map +1 -0
  717. package/dist/core/repo/authored/paths.js +192 -0
  718. package/dist/core/repo/authored/paths.js.map +1 -0
  719. package/dist/core/repo/entries.js +126 -0
  720. package/dist/core/repo/entries.js.map +1 -0
  721. package/dist/core/repo/paths.js +236 -0
  722. package/dist/core/repo/paths.js.map +1 -0
  723. package/dist/core/repo/repo.js +248 -0
  724. package/dist/core/repo/repo.js.map +1 -0
  725. package/dist/core/repo/service-target.js +153 -0
  726. package/dist/core/repo/service-target.js.map +1 -0
  727. package/dist/core/repo/state.js +58 -0
  728. package/dist/core/repo/state.js.map +1 -0
  729. package/dist/core/repo/tree/find.js +52 -0
  730. package/dist/core/repo/tree/find.js.map +1 -0
  731. package/dist/core/repo/tree/fs.js +64 -0
  732. package/dist/core/repo/tree/fs.js.map +1 -0
  733. package/dist/core/repo/tree/marker.js +61 -0
  734. package/dist/core/repo/tree/marker.js.map +1 -0
  735. package/dist/core/repo/tree/views.js +172 -0
  736. package/dist/core/repo/tree/views.js.map +1 -0
  737. package/dist/core/repo/tree/walk.js +234 -0
  738. package/dist/core/repo/tree/walk.js.map +1 -0
  739. package/dist/core/results.js +99 -2
  740. package/dist/core/results.js.map +1 -1
  741. package/dist/core/scaffold/landscape.js +131 -0
  742. package/dist/core/scaffold/landscape.js.map +1 -0
  743. package/dist/core/scaffold/readme.js +84 -0
  744. package/dist/core/scaffold/readme.js.map +1 -0
  745. package/dist/core/staging/commit.js +241 -0
  746. package/dist/core/staging/commit.js.map +1 -0
  747. package/dist/core/staging/interrupted.js +96 -0
  748. package/dist/core/staging/interrupted.js.map +1 -0
  749. package/dist/core/staging/lock.js +206 -0
  750. package/dist/core/staging/lock.js.map +1 -0
  751. package/dist/core/staging/recovery/finding.js +35 -0
  752. package/dist/core/staging/recovery/finding.js.map +1 -0
  753. package/dist/core/staging/recovery/intent.js +98 -0
  754. package/dist/core/staging/recovery/intent.js.map +1 -0
  755. package/dist/core/staging/recovery/recover.js +126 -0
  756. package/dist/core/staging/recovery/recover.js.map +1 -0
  757. package/dist/core/staging/recovery/residue.js +62 -0
  758. package/dist/core/staging/recovery/residue.js.map +1 -0
  759. package/dist/core/staging/snapshot.js +202 -0
  760. package/dist/core/staging/snapshot.js.map +1 -0
  761. package/dist/core/staging/txn/forward.js +171 -0
  762. package/dist/core/staging/txn/forward.js.map +1 -0
  763. package/dist/core/staging/txn/journal.js +139 -0
  764. package/dist/core/staging/txn/journal.js.map +1 -0
  765. package/dist/core/staging/txn/transaction.js +75 -0
  766. package/dist/core/staging/txn/transaction.js.map +1 -0
  767. package/dist/core/staging/writes.js +113 -0
  768. package/dist/core/staging/writes.js.map +1 -0
  769. package/dist/core/status/actions/execution.js +49 -0
  770. package/dist/core/status/actions/execution.js.map +1 -0
  771. package/dist/core/status/contracts.js +102 -0
  772. package/dist/core/status/contracts.js.map +1 -0
  773. package/dist/core/status/feature/artifacts.js +171 -0
  774. package/dist/core/status/feature/artifacts.js.map +1 -0
  775. package/dist/core/status/feature/feature.js +207 -0
  776. package/dist/core/status/feature/feature.js.map +1 -0
  777. package/dist/core/status/feature/next.js +150 -0
  778. package/dist/core/status/feature/next.js.map +1 -0
  779. package/dist/core/status/feature/state.js +21 -0
  780. package/dist/core/status/feature/state.js.map +1 -0
  781. package/dist/core/status/fleet/fleet.js +187 -0
  782. package/dist/core/status/fleet/fleet.js.map +1 -0
  783. package/dist/core/status/fleet/next.js +207 -0
  784. package/dist/core/status/fleet/next.js.map +1 -0
  785. package/dist/core/status/interrupted.js +97 -0
  786. package/dist/core/status/interrupted.js.map +1 -0
  787. package/dist/core/status/report.js +31 -0
  788. package/dist/core/status/report.js.map +1 -0
  789. package/dist/core/status/scan.js +41 -0
  790. package/dist/core/status/scan.js.map +1 -0
  791. package/dist/core/status/verification.js +201 -0
  792. package/dist/core/status/verification.js.map +1 -0
  793. package/dist/core/usecases/capability.js +169 -0
  794. package/dist/core/usecases/capability.js.map +1 -0
  795. package/dist/core/usecases/delta/claims.js +116 -0
  796. package/dist/core/usecases/delta/claims.js.map +1 -0
  797. package/dist/core/usecases/delta/flows.js +103 -0
  798. package/dist/core/usecases/delta/flows.js.map +1 -0
  799. package/dist/core/usecases/delta/overlay.js +235 -0
  800. package/dist/core/usecases/delta/overlay.js.map +1 -0
  801. package/dist/core/usecases/fleet.js +137 -0
  802. package/dist/core/usecases/fleet.js.map +1 -0
  803. package/dist/core/usecases/operations.js +111 -0
  804. package/dist/core/usecases/operations.js.map +1 -0
  805. package/dist/core/usecases/place.js +44 -0
  806. package/dist/core/usecases/place.js.map +1 -0
  807. package/dist/core/usecases/touch.js +101 -0
  808. package/dist/core/usecases/touch.js.map +1 -0
  809. package/dist/core/verify/answers.js +173 -0
  810. package/dist/core/verify/answers.js.map +1 -0
  811. package/dist/core/verify/build.js +150 -0
  812. package/dist/core/verify/build.js.map +1 -0
  813. package/dist/core/verify/checklist.js +201 -0
  814. package/dist/core/verify/checklist.js.map +1 -0
  815. package/dist/core/verify/claims/identity.js +61 -0
  816. package/dist/core/verify/claims/identity.js.map +1 -0
  817. package/dist/core/verify/cross/diff.js +97 -0
  818. package/dist/core/verify/cross/diff.js.map +1 -0
  819. package/dist/core/verify/evidence/contract.js +169 -0
  820. package/dist/core/verify/evidence/contract.js.map +1 -0
  821. package/dist/core/verify/file.js +257 -0
  822. package/dist/core/verify/file.js.map +1 -0
  823. package/dist/core/verify/pins/lint.js +157 -0
  824. package/dist/core/verify/pins/lint.js.map +1 -0
  825. package/dist/core/verify/pins/pin.js +95 -0
  826. package/dist/core/verify/pins/pin.js.map +1 -0
  827. package/dist/core/verify/record.js +142 -0
  828. package/dist/core/verify/record.js.map +1 -0
  829. package/dist/core/verify/store/commit.js +89 -0
  830. package/dist/core/verify/store/commit.js.map +1 -0
  831. package/dist/core/verify/store/render.js +57 -0
  832. package/dist/core/verify/store/render.js.map +1 -0
  833. package/dist/core/vocabulary/health.js +91 -0
  834. package/dist/core/vocabulary/health.js.map +1 -0
  835. package/dist/core/vocabulary/issue.js +55 -0
  836. package/dist/core/vocabulary/issue.js.map +1 -0
  837. package/dist/core/vocabulary/maturity.js +145 -0
  838. package/dist/core/vocabulary/maturity.js.map +1 -0
  839. package/dist/core/{report.js → vocabulary/report.js} +30 -2
  840. package/dist/core/vocabulary/report.js.map +1 -0
  841. package/dist/core/vocabulary/steps.js +266 -0
  842. package/dist/core/vocabulary/steps.js.map +1 -0
  843. package/dist/core/workspace/discover.js +239 -0
  844. package/dist/core/workspace/discover.js.map +1 -0
  845. package/dist/core/workspace/render.js +52 -0
  846. package/dist/core/workspace/render.js.map +1 -0
  847. package/examples/README.md +189 -0
  848. package/examples/docs/architecture/adrs/0001-transactional-outbox.md +33 -0
  849. package/examples/docs/architecture/capabilities.yaml +31 -0
  850. package/examples/docs/architecture/landscape.likec4 +280 -0
  851. package/examples/docs/architecture/obligations.yaml +23 -0
  852. package/examples/docs/architecture/permissions.yaml +50 -0
  853. package/examples/docs/architecture/subsystems.likec4 +11 -0
  854. package/examples/docs/architecture/usecases/order-notification.likec4 +65 -0
  855. package/examples/docs/capabilities/checkout/spec.md +55 -0
  856. package/examples/docs/capabilities/identity/tokens/spec.md +42 -0
  857. package/examples/docs/capabilities/order-notifications/spec.md +43 -0
  858. package/examples/docs/features/FEAT-101-payment-splitting/adrs/0001-dedicated-split-service.md +18 -0
  859. package/examples/docs/features/FEAT-101-payment-splitting/delta.likec4 +94 -0
  860. package/examples/docs/features/FEAT-101-payment-splitting/intent.md +44 -0
  861. package/examples/docs/features/FEAT-101-payment-splitting/specs/checkout-web/spec.md +21 -0
  862. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +129 -0
  863. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/spec.md +36 -0
  864. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/arch.spec.md +40 -0
  865. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +116 -0
  866. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/spec.md +55 -0
  867. package/examples/docs/features/FEAT-112-retire-order-v1/intent.md +35 -0
  868. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +28 -0
  869. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/spec.md +17 -0
  870. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/architecture/landscape.likec4 +253 -0
  871. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/order-service/spec.md +121 -0
  872. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/openapi.yaml +111 -0
  873. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/files/services/payment-service/spec.md +58 -0
  874. package/examples/docs/features/archive/FEAT-088-refunds/.loam-before/manifest.json +32 -0
  875. package/examples/docs/features/archive/FEAT-088-refunds/adrs/0001-refunds-stay-in-payment-service.md +26 -0
  876. package/examples/docs/features/archive/FEAT-088-refunds/delta.likec4 +28 -0
  877. package/examples/docs/features/archive/FEAT-088-refunds/intent.md +26 -0
  878. package/examples/docs/features/archive/FEAT-088-refunds/specs/order-service/spec.md +37 -0
  879. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +70 -0
  880. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/spec.md +23 -0
  881. package/examples/docs/features/archive/FEAT-088-refunds/verification.yaml +76 -0
  882. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/architecture/landscape.likec4 +275 -0
  883. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/asyncapi.yaml +96 -0
  884. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/payment-service/spec.md +82 -0
  885. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/asyncapi.yaml +164 -0
  886. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/files/services/platform/notification-service/spec.md +91 -0
  887. package/examples/docs/features/archive/FEAT-120-refund-notification/.loam-before/manifest.json +46 -0
  888. package/examples/docs/features/archive/FEAT-120-refund-notification/delta.likec4 +36 -0
  889. package/examples/docs/features/archive/FEAT-120-refund-notification/intent.md +22 -0
  890. package/examples/docs/features/archive/FEAT-120-refund-notification/scenario-report.json +10 -0
  891. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/asyncapi.yaml +208 -0
  892. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/notification-service/spec.md +24 -0
  893. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/asyncapi.yaml +137 -0
  894. package/examples/docs/features/archive/FEAT-120-refund-notification/specs/payment-service/spec.md +31 -0
  895. package/examples/docs/features/archive/FEAT-120-refund-notification/verification.yaml +84 -0
  896. package/examples/docs/glossary/order.md +15 -0
  897. package/examples/docs/glossary/payments/authorization.md +14 -0
  898. package/examples/docs/likec4.config.json +9 -0
  899. package/examples/docs/loam.json +3 -0
  900. package/examples/docs/services/checkout-web/model.likec4 +109 -0
  901. package/examples/docs/services/checkout-web/spec.md +91 -0
  902. package/examples/docs/services/checkout-web/ui/pages/checkout.page.yaml +40 -0
  903. package/examples/docs/services/checkout-web/ui/pages/order-status.page.yaml +24 -0
  904. package/examples/docs/services/checkout-web/ui/pages/split-payment.page.yaml +26 -0
  905. package/examples/docs/services/order-service/adrs/0001-orchestrated-order-lifecycle.md +52 -0
  906. package/examples/docs/services/order-service/arch.spec.md +127 -0
  907. package/examples/docs/services/order-service/asyncapi.yaml +108 -0
  908. package/examples/docs/services/order-service/health.yaml +57 -0
  909. package/examples/docs/services/order-service/model.likec4 +138 -0
  910. package/examples/docs/services/order-service/openapi.yaml +257 -0
  911. package/examples/docs/services/order-service/runbook.md +62 -0
  912. package/examples/docs/services/order-service/spec.md +134 -0
  913. package/examples/docs/services/payment-service/adrs/0001-payments-outbox.md +18 -0
  914. package/examples/docs/services/payment-service/adrs/0002-idempotency-keys-on-authorization.md +27 -0
  915. package/examples/docs/services/payment-service/arch.spec.md +73 -0
  916. package/examples/docs/services/payment-service/asyncapi.yaml +130 -0
  917. package/examples/docs/services/payment-service/health.yaml +39 -0
  918. package/examples/docs/services/payment-service/model.likec4 +67 -0
  919. package/examples/docs/services/payment-service/openapi.yaml +151 -0
  920. package/examples/docs/services/payment-service/runbook.md +37 -0
  921. package/examples/docs/services/payment-service/spec.md +88 -0
  922. package/examples/docs/services/platform/identity-service/adrs/0001-token-introspection-over-local-validation.md +60 -0
  923. package/examples/docs/services/platform/identity-service/arch.spec.md +117 -0
  924. package/examples/docs/services/platform/identity-service/health.yaml +70 -0
  925. package/examples/docs/services/platform/identity-service/model.likec4 +83 -0
  926. package/examples/docs/services/platform/identity-service/openapi.yaml +197 -0
  927. package/examples/docs/services/platform/identity-service/runbook.md +95 -0
  928. package/examples/docs/services/platform/identity-service/spec.md +119 -0
  929. package/examples/docs/services/platform/notification-service/asyncapi.yaml +199 -0
  930. package/examples/docs/services/platform/notification-service/health.yaml +35 -0
  931. package/examples/docs/services/platform/notification-service/model.likec4 +118 -0
  932. package/examples/docs/services/platform/notification-service/runbook.md +48 -0
  933. package/examples/docs/services/platform/notification-service/spec.md +112 -0
  934. package/examples/docs/services/platform/subsystem.yaml +2 -0
  935. package/examples/fleet.yaml +57 -0
  936. package/package.json +19 -6
  937. package/dist/commands/adopt.js +0 -240
  938. package/dist/commands/adopt.js.map +0 -1
  939. package/dist/commands/archive.js +0 -1313
  940. package/dist/commands/archive.js.map +0 -1
  941. package/dist/commands/delta.js +0 -388
  942. package/dist/commands/delta.js.map +0 -1
  943. package/dist/commands/docs-repo-gate.js.map +0 -1
  944. package/dist/commands/format.js +0 -17
  945. package/dist/commands/format.js.map +0 -1
  946. package/dist/commands/gherkin.js +0 -309
  947. package/dist/commands/gherkin.js.map +0 -1
  948. package/dist/commands/init.js.map +0 -1
  949. package/dist/commands/list.js +0 -295
  950. package/dist/commands/list.js.map +0 -1
  951. package/dist/commands/migrate-openspec.js +0 -950
  952. package/dist/commands/migrate-openspec.js.map +0 -1
  953. package/dist/commands/new.js +0 -413
  954. package/dist/commands/new.js.map +0 -1
  955. package/dist/commands/rebase.js +0 -356
  956. package/dist/commands/rebase.js.map +0 -1
  957. package/dist/commands/show.js.map +0 -1
  958. package/dist/commands/status.js +0 -262
  959. package/dist/commands/status.js.map +0 -1
  960. package/dist/commands/unarchive.js.map +0 -1
  961. package/dist/commands/validate.js +0 -1773
  962. package/dist/commands/validate.js.map +0 -1
  963. package/dist/commands/verify.js +0 -775
  964. package/dist/commands/verify.js.map +0 -1
  965. package/dist/commands/vouch.js +0 -421
  966. package/dist/commands/vouch.js.map +0 -1
  967. package/dist/core/agent.js +0 -2173
  968. package/dist/core/agent.js.map +0 -1
  969. package/dist/core/agents-stamp.js.map +0 -1
  970. package/dist/core/arch.js +0 -130
  971. package/dist/core/arch.js.map +0 -1
  972. package/dist/core/asyncapi.js.map +0 -1
  973. package/dist/core/brief.js +0 -755
  974. package/dist/core/brief.js.map +0 -1
  975. package/dist/core/coherence.js +0 -577
  976. package/dist/core/coherence.js.map +0 -1
  977. package/dist/core/concurrency.js.map +0 -1
  978. package/dist/core/config.js.map +0 -1
  979. package/dist/core/delta.js +0 -476
  980. package/dist/core/delta.js.map +0 -1
  981. package/dist/core/dependencies.js +0 -356
  982. package/dist/core/dependencies.js.map +0 -1
  983. package/dist/core/doctor.js +0 -596
  984. package/dist/core/doctor.js.map +0 -1
  985. package/dist/core/document-bytes.js.map +0 -1
  986. package/dist/core/explore.js.map +0 -1
  987. package/dist/core/frontmatter.js.map +0 -1
  988. package/dist/core/gherkin.js +0 -463
  989. package/dist/core/gherkin.js.map +0 -1
  990. package/dist/core/health.js +0 -67
  991. package/dist/core/health.js.map +0 -1
  992. package/dist/core/ids.js +0 -122
  993. package/dist/core/ids.js.map +0 -1
  994. package/dist/core/issue.js +0 -9
  995. package/dist/core/issue.js.map +0 -1
  996. package/dist/core/json.js +0 -74
  997. package/dist/core/json.js.map +0 -1
  998. package/dist/core/likec4.js +0 -489
  999. package/dist/core/likec4.js.map +0 -1
  1000. package/dist/core/maturity.js +0 -66
  1001. package/dist/core/maturity.js.map +0 -1
  1002. package/dist/core/openapi-merge.js +0 -552
  1003. package/dist/core/openapi-merge.js.map +0 -1
  1004. package/dist/core/openapi.js.map +0 -1
  1005. package/dist/core/openspec-inventory.js +0 -1440
  1006. package/dist/core/openspec-inventory.js.map +0 -1
  1007. package/dist/core/path-safety.js.map +0 -1
  1008. package/dist/core/provenance.js +0 -787
  1009. package/dist/core/provenance.js.map +0 -1
  1010. package/dist/core/records.js.map +0 -1
  1011. package/dist/core/repo.js +0 -438
  1012. package/dist/core/repo.js.map +0 -1
  1013. package/dist/core/report.js.map +0 -1
  1014. package/dist/core/spec.js +0 -505
  1015. package/dist/core/spec.js.map +0 -1
  1016. package/dist/core/staging.js +0 -890
  1017. package/dist/core/staging.js.map +0 -1
  1018. package/dist/core/status.js +0 -1132
  1019. package/dist/core/status.js.map +0 -1
  1020. package/dist/core/steps.js +0 -72
  1021. package/dist/core/steps.js.map +0 -1
  1022. package/dist/core/verify.js +0 -807
  1023. package/dist/core/verify.js.map +0 -1
  1024. package/dist/core/version.js.map +0 -1
  1025. /package/dist/core/{concurrency.js → kernel/concurrency.js} +0 -0
  1026. /package/dist/core/{records.js → kernel/records.js} +0 -0
@@ -1,1773 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import { agentsStaleFinding } from "../core/agents-stamp.js";
4
- import { inOrder } from "../core/concurrency.js";
5
- import { loadConfig } from "../core/config.js";
6
- import { listField, readFrontmatter } from "../core/frontmatter.js";
7
- import { emitJson, fail, NO_SERVICE_MESSAGE, reportNoConfig } from "../core/json.js";
8
- import { elementService, loadFile, serviceResolver, } from "../core/likec4.js";
9
- import { DocsRepoUnavailableError, agentsPath as agentsFile, docsRepoState, featurePaths, featureSpecPaths, featureSpecServices, landscapePath as landscapeFile, listFeatures, listServices, missingFeatureMessage, resolveFeature, serviceIdFindings, servicePaths, SPEC_AXES, } from "../core/repo.js";
10
- import { countSeverity, reportValid, SEVERITY_MARK, targetJson, targetValid, } from "../core/report.js";
11
- import { parseRequirements, requirementIdProblems, requirementsMissingScenarios, steplessFindings, } from "../core/spec.js";
12
- import { readOpenapi } from "../core/openapi.js";
13
- import { producersByMessage, readAsyncapi, slotsOf } from "../core/asyncapi.js";
14
- import { deltaServiceUnknownFinding, featureCoherence } from "../core/coherence.js";
15
- import { gatesArchive } from "../core/issue.js";
16
- import { emptySourcesMessage, expandSourceFiles, featureProvenance, missingSources, patternSources, serviceProvenance, unsafeSources, } from "../core/provenance.js";
17
- import { closeIds, coversCandidates, coversEdge, coversElement, entryResolves, parseCoversEntry, } from "../core/arch.js";
18
- import { gherkinFindings } from "../core/gherkin.js";
19
- import { readHealth } from "../core/health.js";
20
- import { LOAM_VERSION } from "../core/version.js";
21
- import { FleetContext, documentConflictFinding, landscapeConflictFinding, } from "../core/fleet-context.js";
22
- import { docsRepoReady, reportDocsRepoError, reportRepositoryUnavailable, } from "./docs-repo-gate.js";
23
- import { plural } from "./format.js";
24
- export function registerValidate(program) {
25
- program
26
- .command("validate")
27
- .argument("[target]", "service or feature id (a feature wins when both match; --service/--feature force the reading)")
28
- .description("Validate a service (C4 + requirement coverage) or a feature (delta + coverage)")
29
- .option("--service <id>", "service to validate (defaults to the configured service)")
30
- .option("--feature <id>", "validate a feature delta instead of a service")
31
- .option("--all", "validate every service and every active feature")
32
- .option("--strict", "exit 1 on any warning too — a per-invocation CI lever; the report and --json payload do not change")
33
- .option("--errors-only", "print only errors and warnings — a rendering lever; the --json payload does not change")
34
- .option("--json", "emit the machine contract instead of the human view")
35
- .action(async (target, opts) => {
36
- const json = opts.json === true;
37
- if (target !== undefined && (opts.all || opts.service || opts.feature)) {
38
- fail(json, "invalid-option", `'${target}' already names the target; drop --all/--service/--feature.`);
39
- return;
40
- }
41
- if (opts.all && (opts.service || opts.feature)) {
42
- fail(json, "invalid-option", "--all validates everything; drop --service/--feature.");
43
- return;
44
- }
45
- // Two targets is no target: silently validating only the feature taught
46
- // callers that --service had been honoured when it had been dropped.
47
- if (opts.service && opts.feature) {
48
- fail(json, "invalid-option", "--service and --feature name different targets; pass one or the other.");
49
- return;
50
- }
51
- const config = await loadConfig();
52
- if (!config) {
53
- reportNoConfig(json);
54
- return;
55
- }
56
- const { docsDir } = config;
57
- // Before anything is enumerated: a docsDir that is not a docs repo must
58
- // refuse, not report an empty fleet. See docsRepoReady. `--feature` is the
59
- // one mode that never enumerates services, so it asks for less.
60
- if (!docsRepoReady(json, docsDir, opts.feature ? "docs" : "services"))
61
- return;
62
- // One invocation, one filesystem snapshot. Nothing is global: a later
63
- // command gets a fresh index and therefore cannot observe stale files.
64
- const fleet = new FleetContext();
65
- // `sources` are paths into a service's own repository, so they only mean
66
- // something when loam is standing in that repository — which is exactly
67
- // what loam.json's `service` records.
68
- const repoOf = (service) => config.service === service ? process.cwd() : undefined;
69
- const targets = [];
70
- /** What the positional argument turned out to name, for the JSON payload. */
71
- let resolvedKind;
72
- try {
73
- if (opts.all) {
74
- // Parse the living landscape ONCE for the whole run: loadFile spins up
75
- // a fresh LikeC4 workspace per call, and paying that per service makes
76
- // the fleet's main CI command O(services) re-parses of the same file.
77
- const lp = landscapeFile(docsDir);
78
- const land = existsSync(lp) ? await readLandscape(() => fleet.loadLikeC4(lp)) : null;
79
- // The fleet-level cross-check first: it frames everything below it, and a
80
- // service nobody drew is worth knowing before its own findings scroll past.
81
- targets.push(await validateLandscape(docsDir, land, fleet));
82
- const landscape = targets[0];
83
- // The agent contract check, --all only: AGENTS.md is written once and
84
- // never refreshed (the ownership contract), so the one thing the
85
- // docs-repo-wide mode owes it is detection — a stamp older than the
86
- // binary means agents are branching on tables the binary no longer
87
- // honours. It grades the repo, not any service, so it rides on the
88
- // landscape target.
89
- const agentsPath = agentsFile(docsDir);
90
- const agents = agentsStaleFinding(existsSync(agentsPath) ? await readFile(agentsPath, "utf8") : null, LOAM_VERSION);
91
- if (agents !== null)
92
- landscape.findings.push(agents);
93
- targets.push(...(await inOrder(await listServices(docsDir, fleet), (svc) => guarded({ kind: "service", id: svc.id }, () => validateService({
94
- docsDir,
95
- service: svc.id,
96
- repoDir: repoOf(svc.id),
97
- preloaded: land,
98
- gherkinDir: config.gherkinDir,
99
- fleet,
100
- landscapeReported: true,
101
- })))));
102
- targets.push(...(await inOrder(await listFeatures(docsDir, {}, fleet), (feat) => guarded({ kind: "feature", id: feat.id }, () => validateFeature(docsDir, feat, land, fleet)))));
103
- }
104
- else if (opts.feature) {
105
- const feature = await resolveFeature(docsDir, opts.feature, "exclude", fleet);
106
- if (!feature) {
107
- fail(json, "unknown-target", await missingFeatureMessage(docsDir, opts.feature, fleet));
108
- return;
109
- }
110
- targets.push(await guarded({ kind: "feature", id: feature.id }, () => validateFeature(docsDir, feature, undefined, fleet)));
111
- }
112
- else if (target !== undefined) {
113
- // The positional reads the way `show` reads one: try the feature first
114
- // (ids like FEAT-101 are distinctive, service names are arbitrary), then
115
- // the service. --service/--feature stay as the explicit spellings for a
116
- // name that could be both — and when both readings exist, the run says
117
- // so out loud instead of silently taking one (`target.ambiguous`).
118
- const feature = await resolveFeature(docsDir, target, "exclude", fleet);
119
- const isService = (await listServices(docsDir, fleet)).some((s) => s.id === target);
120
- if (feature) {
121
- resolvedKind = "feature";
122
- const report = await guarded({ kind: "feature", id: feature.id }, () => validateFeature(docsDir, feature, undefined, fleet));
123
- if (isService)
124
- report.findings.unshift(ambiguousTarget(target, "feature"));
125
- targets.push(report);
126
- }
127
- else {
128
- // Neither reading exists. An archived feature is its own diagnosis
129
- // ("already archived", not "no such thing"); anything else reads as a
130
- // service, so the did-you-mean hints in service.unknown fire.
131
- if (!isService && (await resolveFeature(docsDir, target, "only", fleet)) !== null) {
132
- fail(json, "unknown-target", await missingFeatureMessage(docsDir, target, fleet));
133
- return;
134
- }
135
- resolvedKind = "service";
136
- targets.push(await guarded({ kind: "service", id: target }, () => validateService({
137
- docsDir,
138
- service: target,
139
- repoDir: repoOf(target),
140
- gherkinDir: config.gherkinDir,
141
- fleet,
142
- })));
143
- }
144
- }
145
- else {
146
- const service = opts.service ?? config.service;
147
- if (!service) {
148
- fail(json, "invalid-option", NO_SERVICE_MESSAGE);
149
- return;
150
- }
151
- targets.push(await guarded({ kind: "service", id: service }, () => validateService({
152
- docsDir,
153
- service,
154
- repoDir: repoOf(service),
155
- gherkinDir: config.gherkinDir,
156
- fleet,
157
- })));
158
- }
159
- }
160
- catch (err) {
161
- // The docs repo went away between the gate above and the enumeration.
162
- if (err instanceof DocsRepoUnavailableError) {
163
- reportDocsRepoError(json, err);
164
- return;
165
- }
166
- // An IO failure that `guarded` could not localise to one target — it
167
- // happened in the ENUMERATION, which reads every living spec's
168
- // frontmatter to build the service list. There is no partial report to
169
- // give: without the list, no service was checked, including the ones
170
- // that are fine. Say which file, and refuse; the one thing that must
171
- // not happen is a stack trace on stdout's sibling stream and an
172
- // `internal` envelope that names nothing.
173
- reportRepositoryUnavailable(json, err, "the service list itself is unknown and nothing was validated", docsDir);
174
- return;
175
- }
176
- // Counted off the findings rather than alongside them, so the rollup line
177
- // and the per-service findings can never disagree about how many services
178
- // this run could not check.
179
- //
180
- // Counted by SUBJECT, not by finding: a service whose spec.md and
181
- // arch.spec.md both name `sources` raises two findings — each document
182
- // has its own list and its own answer — but it is one service nobody
183
- // here can check, and "2 services' sources" over a fleet of one is a
184
- // number that sends the reader looking for a service that does not exist.
185
- const unverifiable = new Set(targets.flatMap((t) => t.findings.filter((f) => f.code === UNVERIFIABLE).map((f) => f.subject ?? t.id))).size;
186
- const valid = reportValid(targets);
187
- const capped = targets.map(capDetails);
188
- if (json) {
189
- emitJson({
190
- valid,
191
- summary: summary(targets),
192
- ...(resolvedKind === undefined ? {} : { resolvedKind }),
193
- // Emitted in every mode now that it is derived per service: a
194
- // single-service run knows exactly as much about its own blind spot
195
- // as `--all` knows about the fleet's.
196
- sourcesUnverifiableFromHere: unverifiable,
197
- targets: capped.map(targetJson),
198
- });
199
- }
200
- else {
201
- renderText(capped, opts.all === true, unverifiable, opts.errorsOnly === true);
202
- }
203
- // --strict is a per-invocation CI lever: it fails the run on any error or
204
- // warning (ok-severity confirmations never trip it — virtually every
205
- // clean target emits some), and changes nothing else — `valid` keeps meaning "no
206
- // errors" in text and JSON alike, so two pipelines reading the same repo
207
- // may grade the same report differently and both are telling the truth.
208
- const strictFailed = opts.strict === true &&
209
- countSeverity(targets, "error") + countSeverity(targets, "warn") > 0;
210
- if (!valid || strictFailed)
211
- process.exitCode = 1;
212
- });
213
- }
214
- function summary(targets) {
215
- return {
216
- services: targets.filter((t) => t.kind === "service").length,
217
- features: targets.filter((t) => t.kind === "feature").length,
218
- errors: countSeverity(targets, "error"),
219
- warnings: countSeverity(targets, "warn"),
220
- };
221
- }
222
- /** The one code the rollup line counts; spelled once so the two cannot drift. */
223
- const UNVERIFIABLE = "sources.unverifiable-from-here";
224
- /**
225
- * How many `details` lines any one finding may print before the rest are
226
- * summarised away.
227
- *
228
- * A finding's details are evidence, not a log: LikeC4 reports one syntax error
229
- * as dozens of cascading diagnostics, and a fleet-sized repo multiplies that by
230
- * every target that mentions the file. The report is read by a person scrolling
231
- * a CI log and by an agent with a context window, and neither of them is helped
232
- * by the four-hundredth copy. The cap is applied to the JSON payload too, on
233
- * purpose: `--json` is the interface an agent pipes, and an unbounded array is
234
- * the same denial-of-attention there, just machine-readable.
235
- */
236
- const DETAIL_LIMIT = 10;
237
- /** Truncate every finding's details, marking what was dropped so nothing looks complete when it is not. */
238
- function capDetails(t) {
239
- return {
240
- ...t,
241
- findings: t.findings.map((f) => {
242
- const details = f.details ?? [];
243
- if (details.length <= DETAIL_LIMIT)
244
- return f;
245
- return {
246
- ...f,
247
- details: [...details.slice(0, DETAIL_LIMIT), `… (+${details.length - DETAIL_LIMIT} more)`],
248
- };
249
- }),
250
- };
251
- }
252
- /**
253
- * One target's checks, with an IO exception turned into a finding ON that
254
- * target instead of aborting the run.
255
- *
256
- * A fleet gate that dies on the first unreadable file reports nothing about the
257
- * other ninety-nine services — one bad permission bit, one file that is a
258
- * dangling symlink, and CI's answer to "how is the fleet" becomes a stack
259
- * trace. The failure is real and it is an error, so the run still exits 1; what
260
- * changes is that everything else is still graded, and the finding names the
261
- * service and the path instead of arriving as the `internal` catch-all.
262
- */
263
- async function guarded(target, run) {
264
- try {
265
- return await run();
266
- }
267
- catch (err) {
268
- // A docs repo that vanished mid-run is not one target's problem — it is the
269
- // whole run's, and the action's own catch reports it.
270
- if (err instanceof DocsRepoUnavailableError)
271
- throw err;
272
- const path = err.path;
273
- const reason = err instanceof Error ? err.message : String(err);
274
- return {
275
- kind: target.kind,
276
- id: target.id,
277
- findings: [
278
- {
279
- severity: "error",
280
- code: target.kind === "service" ? "service.unreadable" : "feature.unreadable",
281
- subject: target.id,
282
- message: `${target.id}: ${path === undefined ? "an artifact" : path} could not be read — ` +
283
- `nothing about this ${target.kind} was checked. ${reason}`,
284
- },
285
- ],
286
- };
287
- }
288
- }
289
- /**
290
- * `target.ambiguous` — the positional named a service AND a feature. The tie is
291
- * still broken the way it always was (the feature wins), because changing which
292
- * one is picked would silently re-target every script that relies on it; what
293
- * is new is that the run says which reading it took and how to force the other.
294
- */
295
- function ambiguousTarget(arg, chosen) {
296
- const other = chosen === "feature" ? "service" : "feature";
297
- return {
298
- severity: "warn",
299
- code: "target.ambiguous",
300
- subject: arg,
301
- message: `'${arg}' names both a service and a feature — validated as the ${chosen}. ` +
302
- `Pass --${other} ${arg} for the other reading.`,
303
- };
304
- }
305
- /* ------------------------------------------------------------------ */
306
- /* Checks — every one produces findings, none of them print */
307
- /* ------------------------------------------------------------------ */
308
- /** C4 kinds that model people. A person is never a service directory. */
309
- const ACTOR_KINDS = new Set(["person", "actor", "user"]);
310
- /** Tag marking an element as somebody else's system — undocumented on purpose. */
311
- const EXTERNAL_TAG = "external";
312
- /**
313
- * A landscape that could not be READ, shaped as one that did not PARSE.
314
- *
315
- * The landscape is the one artifact no target owns: it is read once for the
316
- * whole run and it is graded on a target of its own, which runs OUTSIDE
317
- * `guarded`. So a landscape.likec4 that is a directory, or that carries a
318
- * permission bit this process cannot open, escaped every per-target catch and
319
- * became the whole run's `repository-unavailable` — one file, and a fleet gate
320
- * that said nothing about the ninety-nine services that are fine. A DANGLING
321
- * symlink is not one of those shapes and never was: every read of the file is
322
- * gated on an `existsSync` that follows the link, so a broken one resolves as
323
- * `landscape.missing` long before anything opens it.
324
- *
325
- * What this contains is the FLEET-level reads, and only those: the `--all`
326
- * preload, and both reads inside `validateLandscape` — the conflict-marker
327
- * `readFile` and the parse. `loam validate --service <id>` and
328
- * `loam validate --feature <id>` hand in no preloaded doc, so `validateService`
329
- * and `validateFeature` open the same file again on demand, unwrapped and
330
- * INSIDE `guarded`: an EISDIR there is still reported as `service.unreadable`
331
- * or `feature.unreadable`, which files the fleet map's failure against the one
332
- * target the caller happened to name. That is the wrong subject on a finding
333
- * that does at least carry the offending path, and it costs those runs nothing
334
- * further — they grade a single target either way, so there is no report left
335
- * unwritten — which is why the remainder was left for the change that gives the
336
- * landscape one load path instead of three.
337
- *
338
- * `guarded` is deliberately NOT widened to cover the landscape target instead.
339
- * Its code ternary knows only services and features, so it would file the fleet
340
- * map's failure as `feature.unreadable`; and a guarded failure yields no
341
- * document, so all N services would go on to re-open the same broken file
342
- * inside their own guards and emit N copies of it. Containing the IO here needs
343
- * no new code and no new sentence: "could not be read" and "did not parse" have
344
- * the same consequence — nothing may be concluded from this file — and
345
- * `landscape.invalid` is already how that is said.
346
- */
347
- function unreadableLandscape(err) {
348
- return {
349
- errors: [{ message: err instanceof Error ? err.message : String(err) }],
350
- elements: [],
351
- relationships: [],
352
- };
353
- }
354
- /** One landscape load, answering with `unreadableLandscape` rather than throwing. */
355
- async function readLandscape(load) {
356
- try {
357
- return await load();
358
- }
359
- catch (err) {
360
- return unreadableLandscape(err);
361
- }
362
- }
363
- /**
364
- * The fleet cross-check: `services/` and the landscape both claim to name the
365
- * fleet, and nothing used to compare them. A directory nobody drew and an element
366
- * with nothing behind it were equally invisible.
367
- *
368
- * The two directions are graded differently because the evidence differs. A
369
- * directory that exists is a fact, so a landscape missing it is an error — every
370
- * view derived from that landscape is then incomplete. An element with no
371
- * directory may legitimately be someone else's system, so it warns, and
372
- * `#external` says "deliberately not ours" and silences it. An explicit
373
- * `metadata { service '<id>' }` naming nothing is an error either way: a binding
374
- * is a claim about this repo, not a guess at one.
375
- *
376
- * An ABSENT landscape is a finding, not a skipped check. It used to return null
377
- * — no target, no findings, and a fleet gate that went green over a docs repo
378
- * with no fleet map at all, which is the single artifact every derived view and
379
- * every spine check is computed from. It is graded by what its absence proves:
380
- * with services in `services/` it is an ERROR (a fleet that exists is undrawn),
381
- * with none it is a WARNING (a docs repo before its first adopt legitimately
382
- * has nothing to draw, but the file still belongs there — `loam init` scaffolds
383
- * it, and a repo missing it will silently accept never getting one).
384
- *
385
- * `preloaded` is the already-parsed landscape under --all — the same doc every
386
- * service check gets.
387
- */
388
- async function validateLandscape(docsDir, preloaded, fleet) {
389
- const path = landscapeFile(docsDir);
390
- const findings = [];
391
- const report = { kind: "landscape", id: "landscape", findings };
392
- // The SET of service directories is this target's other subject, so it is
393
- // graded before the map is even opened: `service.id-invalid` is a fact about
394
- // `services/` that holds whether or not a landscape exists or parses, and
395
- // both of those return early below. Emitted here and nowhere else — the
396
- // enumeration is what makes the id a question, and one finding per fleet is
397
- // what the rename fixes.
398
- const entries = await listServices(docsDir, fleet);
399
- findings.push(...serviceIdFindings(entries));
400
- if (!existsSync(path)) {
401
- const count = entries.length;
402
- findings.push({
403
- severity: count > 0 ? "error" : "warn",
404
- code: "landscape.missing",
405
- message: `landscape: architecture/landscape.likec4 does not exist — ` +
406
- (count > 0
407
- ? `${count} service(s) are adopted and nothing draws the fleet. `
408
- : "nothing draws the fleet. ") +
409
- "It is the one map every derived view and the C4↔API spine are computed from: " +
410
- "write a `specification { element softwareSystem }` + `model { … }` document there " +
411
- "with one element per services/<id>/, bound with `metadata { service '<id>' }`.",
412
- });
413
- return report;
414
- }
415
- // Conflict markers before the parse — `loam doctor`'s order, and for its
416
- // reason: the markers are the cause and the parse errors are the cascade.
417
- // Nothing may be concluded from a file that is two halves of two different
418
- // maps, least of all that a service nobody drew is unmodelled, so this
419
- // returns the way `landscape.invalid` does. An error, gating by the default
420
- // rule and deliberately not carrying `gates` — that field is coherence's,
421
- // and the archive gate has to ask this question itself (it does not yet).
422
- // Read plainly rather than through FleetContext: this target runs outside
423
- // `guarded`, and a document refused for its encoding would surface here as
424
- // the whole run's `repository-unavailable` instead of one finding.
425
- //
426
- // Both reads are contained for that same reason — see `unreadableLandscape`.
427
- // The conflict-marker read is the FIRST touch of the file, so it is the one
428
- // that fails when the file cannot be opened at all, and it runs before
429
- // `preloaded ??` can spare it.
430
- let land;
431
- try {
432
- const conflict = landscapeConflictFinding("architecture/landscape.likec4", await readFile(path, "utf8"));
433
- if (conflict !== null) {
434
- findings.push(conflict);
435
- return report;
436
- }
437
- land = preloaded ?? (fleet === undefined ? await loadFile(path) : await fleet.loadLikeC4(path));
438
- }
439
- catch (err) {
440
- land = unreadableLandscape(err);
441
- }
442
- if (land.errors.length > 0) {
443
- // Nothing may be concluded from a document that did not parse — in particular
444
- // not that every service is unmodelled.
445
- findings.push({
446
- severity: "error",
447
- code: "landscape.invalid",
448
- message: `landscape: architecture/landscape.likec4 has ${land.errors.length} error(s) — cross-check with services/ impossible`,
449
- details: land.errors.map(errorText),
450
- });
451
- return report;
452
- }
453
- const services = new Set(entries.map((s) => s.id));
454
- // Depth is not a fact about a service. This used to keep only top-level
455
- // elements (`!e.id.includes(".")`), which ordinary C4 breaks the moment it
456
- // groups services under a parent: every nested element was thrown away, so a
457
- // bound service read as unmodelled — an ERROR, on every service in the fleet
458
- // — and a binding written one level down was never checked at all. The tree
459
- // is walked instead, the way `serviceResolver` walks it for edges: a binding
460
- // wins at any depth, then a title naming a real services/<id>/, and what sits
461
- // INSIDE one of those is that service's container, not a service of its own.
462
- const byId = new Map(land.elements.map((e) => [e.id, e]));
463
- /** Declared ancestors of an element, nearest first. */
464
- const ancestorsOf = (e) => {
465
- const out = [];
466
- for (let dot = e.id.lastIndexOf("."); dot !== -1; dot = e.id.lastIndexOf(".", dot - 1)) {
467
- const parent = byId.get(e.id.slice(0, dot));
468
- if (parent !== undefined)
469
- out.push(parent);
470
- }
471
- return out;
472
- };
473
- const standsForService = (e) => e.service !== undefined || services.has(e.title);
474
- /** Service-LEVEL elements: everything not drawn inside something that is already a service. */
475
- const drawn = land.elements.filter((e) => !ancestorsOf(e).some(standsForService));
476
- // Which services the landscape models, answered by the same resolver every
477
- // edge join uses — so "modelled" here and "inbound edge" in the spine check
478
- // can never disagree about what an element stands for.
479
- const landSvcOf = serviceResolver(land.elements, services);
480
- const modelled = new Set(land.elements.map((e) => landSvcOf(e.id)));
481
- // Two boxes standing for one service directory. Every join in loam is
482
- // `element -> service`, computed by picking the FIRST element that resolves —
483
- // so with two of them, which one wins is readdir order, and the edges of the
484
- // loser are attributed to a service they do not belong to. Silent until now,
485
- // and unfixable by staring at either element on its own.
486
- const perService = new Map();
487
- for (const e of drawn) {
488
- if (e.tags.includes(EXTERNAL_TAG))
489
- continue;
490
- const id = elementService(e);
491
- perService.set(id, [...(perService.get(id) ?? []), e]);
492
- }
493
- for (const [id, elems] of perService) {
494
- if (elems.length < 2)
495
- continue;
496
- // A collision only matters where it decides something: a real directory to
497
- // attribute to, or a binding somebody wrote down on purpose.
498
- if (!services.has(id) && !elems.some((e) => e.service !== undefined))
499
- continue;
500
- findings.push({
501
- severity: "warn",
502
- code: "landscape.binding-duplicate",
503
- subject: id,
504
- message: `landscape: ${elems.length} elements resolve to service '${id}' (${elems.map((e) => e.id).join(", ")}) — every element→service join picks one of them arbitrarily, so the others' edges are filed under a service that does not own them; keep one element per services/<id>/`,
505
- });
506
- }
507
- for (const id of services) {
508
- if (modelled.has(id))
509
- continue;
510
- findings.push({
511
- severity: "error",
512
- code: "landscape.service-unmodelled",
513
- subject: id,
514
- message: `landscape: services/${id}/ exists but nothing in architecture/landscape.likec4 models it — add an element, or bind one with metadata { service '${id}' }`,
515
- });
516
- }
517
- // A binding is a claim about this repo wherever it is written — including
518
- // inside another element, which the old top-level filter never looked at, so
519
- // a typo one level down bound an edge to a service that does not exist and
520
- // nothing said so. Every element with a binding answers for it, at any depth.
521
- for (const e of land.elements) {
522
- if (e.tags.includes(EXTERNAL_TAG) || e.service === undefined)
523
- continue;
524
- if (services.has(e.service))
525
- continue;
526
- findings.push({
527
- severity: "error",
528
- code: "landscape.binding-unknown",
529
- subject: e.service,
530
- message: `landscape: '${e.title}' binds to service '${e.service}', but services/${e.service}/ does not exist`,
531
- });
532
- }
533
- for (const e of drawn) {
534
- if (e.tags.includes(EXTERNAL_TAG))
535
- continue;
536
- if (e.service !== undefined)
537
- continue; // graded by the binding pass above
538
- if (ACTOR_KINDS.has(e.kind.toLowerCase()))
539
- continue;
540
- if (services.has(e.title))
541
- continue;
542
- // An element that CONTAINS a service is a grouping — a domain, a boundary,
543
- // an enterprise — not a service nobody adopted. There is nothing to bind on
544
- // it and nothing to tag #external, so asking for either would be a warning
545
- // with no correct fix; the services under it answer for themselves.
546
- if (land.elements.some((c) => c.id.startsWith(`${e.id}.`) && standsForService(c)))
547
- continue;
548
- findings.push({
549
- severity: "warn",
550
- code: "landscape.service-undocumented",
551
- subject: e.title,
552
- message: `landscape: '${e.title}' has no services/${e.title}/ — bind it with metadata { service '<id>' }, or tag it #${EXTERNAL_TAG} if it is not ours`,
553
- });
554
- }
555
- if (findings.length === 0) {
556
- findings.push({
557
- severity: "ok",
558
- code: "landscape.matched",
559
- message: `landscape: ${services.size} service(s) modelled — architecture/landscape.likec4 and services/ agree`,
560
- });
561
- }
562
- return report;
563
- }
564
- async function validateService(check) {
565
- const { docsDir, service, repoDir, preloaded, gherkinDir, fleet } = check;
566
- const findings = [];
567
- const report = { kind: "service", id: service, findings };
568
- const paths = servicePaths(docsDir, service);
569
- // A directory that does not exist is a different fact from a directory with
570
- // everything missing: validating a typo must say "typo", not "unadopted".
571
- if (!existsSync(paths.dir)) {
572
- const close = closeIds(service, (await listServices(docsDir, fleet)).map((s) => s.id));
573
- findings.push({
574
- severity: "error",
575
- code: "service.unknown",
576
- message: `No service directory at ${paths.dir}.` +
577
- (close.length > 0
578
- ? ` Did you mean: ${close.join(", ")}?`
579
- : " `loam list services` shows what exists."),
580
- text: { marker: false },
581
- });
582
- return report;
583
- }
584
- // C4 model. Its absence is an error — this is where `adopt` comes in — but it
585
- // must NOT silence the rest of the gate stack: `loam archive` of a feature
586
- // introducing a new service creates exactly this state (spec.md, arch.spec.md,
587
- // openapi.yaml, no model.likec4), and an early return here suspended arch
588
- // coverage, health.uncovered, provenance (content_digest included) and the
589
- // gherkin staleness chain for the very services vouch had just promised them
590
- // to. So the finding is emitted and the walk CONTINUES; only the checks that
591
- // read the model itself are guarded.
592
- const hasModel = existsSync(paths.model);
593
- let elements = [];
594
- let relationships = [];
595
- if (!hasModel) {
596
- findings.push({
597
- severity: "error",
598
- code: "service.no-model",
599
- message: `No C4 model at ${paths.model}. Run \`loam adopt\` for '${service}' first.`,
600
- text: { marker: false },
601
- });
602
- }
603
- else {
604
- const model = fleet === undefined ? await loadFile(paths.model) : await fleet.loadLikeC4(paths.model);
605
- elements = model.elements;
606
- relationships = model.relationships;
607
- if (model.errors.length > 0) {
608
- findings.push({
609
- severity: "error",
610
- code: "c4.invalid",
611
- message: `${service}: C4 model has ${model.errors.length} error(s)`,
612
- details: model.errors.map(errorText),
613
- });
614
- }
615
- else {
616
- findings.push({
617
- severity: "ok",
618
- code: "c4.valid",
619
- message: `${service}: C4 model valid (${elements.length} elements · ${relationships.length} relationships)`,
620
- });
621
- }
622
- }
623
- // The living landscape, parsed at most once per run: under --all the caller
624
- // hands in the doc it already loaded, single-service runs load on demand. It
625
- // serves two checks below — the no-openapi grace and the spine.
626
- const land = preloaded ??
627
- (existsSync(landscapeFile(docsDir))
628
- ? fleet === undefined
629
- ? await loadFile(landscapeFile(docsDir))
630
- : await fleet.loadLikeC4(landscapeFile(docsDir))
631
- : null);
632
- // Which service directories actually exist — the positive evidence
633
- // `serviceOf` needs to resolve an edge drawn into a modelled CONTAINER
634
- // (`paymentService.api`) back to the service that owns it. Without it every
635
- // such edge resolves to the container's own title, i.e. to a service nobody
636
- // has ever adopted, and drops out of the spine unnoticed.
637
- const known = new Set((await listServices(docsDir, fleet)).map((s) => s.id));
638
- // Requirement coverage.
639
- let reqs = [];
640
- if (existsSync(paths.spec)) {
641
- const specText = fleet === undefined ? await readFile(paths.spec, "utf8") : await fleet.readText(paths.spec);
642
- reqs = fleet === undefined ? parseRequirements(specText) : await fleet.readRequirements(paths.spec);
643
- // Conflict markers first, because they say that nothing below this line is
644
- // anyone's text. An error, gating by the default rule and deliberately
645
- // without `gates`: that field is coherence's (issue.ts) and archive does
646
- // not read validate's findings, so claiming it here would be a promise
647
- // about a gate that is not wired. What makes it an error rather than a
648
- // warning is what the next merge does — `loam archive` rewrites the
649
- // requirements section of this very file and takes whichever marker lines
650
- // fall inside it, turning a conflict anyone can see into a document nobody
651
- // can tell is wrong.
652
- const conflict = documentConflictFinding(`${service}: spec.md`, service, specText);
653
- if (conflict !== null)
654
- findings.push(conflict);
655
- // A living spec with no `### Requirement:` block at all is the baseline
656
- // `loam adopt` scaffolds and nobody ever filled in. "requirements covered
657
- // (0 requirements, all with scenarios)" is true of it and says the opposite
658
- // of what a reader needs: every cross-axis check downstream — API
659
- // governance, the Operations: spine, gherkin generation — is vacuous here,
660
- // and the green tick is what let a whole fleet score `sourced` over empty
661
- // files.
662
- if (reqs.length === 0) {
663
- findings.push({
664
- severity: "warn",
665
- code: "spec.no-requirements",
666
- subject: service,
667
- message: `${service}: spec.md holds no '### Requirement:' blocks — every requirement-driven check below is vacuous, and nothing here can go out of date because nothing is written down`,
668
- });
669
- }
670
- else {
671
- findings.push(coverageFinding(`${service}: requirements`, reqs));
672
- }
673
- // The other half of the coverage question, and an error for the same
674
- // reason the first half is: the whole claim of coverage is that a
675
- // requirement has a scenario something can RUN. A heading with no steps
676
- // satisfies the count and tests nothing — cucumber runs it vacuously green
677
- // and `loam verify --results` can never confirm it — so a fleet gate that
678
- // called it covered was certifying the absence of a test.
679
- findings.push(...steplessFindings(`${service}: requirements`, service, reqs));
680
- findings.push(...duplicateRequirementFindings(reqs, `${service}: spec.md`, service));
681
- findings.push(...requirementIdFindings(reqs, `${service}: spec.md`, service));
682
- findings.push(...repeatedListLineFindings(reqs, `${service}: spec.md`, service));
683
- }
684
- else {
685
- findings.push({
686
- severity: "warn",
687
- code: "service.no-spec",
688
- message: `No living spec at ${paths.spec} — requirement coverage and API governance are unchecked`,
689
- });
690
- }
691
- // The requirements that still govern anything. A REMOVED requirement is on
692
- // its way out together with the operations it names: it makes no claim on the
693
- // contract and governs nothing once the retiring feature archives — the same
694
- // position coherence.ts takes on the delta side.
695
- //
696
- // Spelled once because it was spelled three times and dropped on the fourth:
697
- // `governedOps` filtered, `spec-api.op-undefined` filtered, and the
698
- // `api.covered` join did not — so an operation whose only requirement was
699
- // REMOVED counted as governed, and the one operation about to be left without
700
- // a requirement was the one the report called covered.
701
- const livingReqs = reqs.filter((r) => r.kind !== "REMOVED");
702
- // API coverage: every operation in openapi.yaml is governed by a requirement.
703
- const api = await readOpenapi(paths.openapi, fleet);
704
- const removeMarkers = api.ops.filter((o) => o.remove);
705
- const liveOps = api.ops.filter((o) => !o.remove);
706
- const ops = liveOps.map((o) => o.id);
707
- const deprecatedOps = new Set(liveOps.filter((o) => o.deprecated).map((o) => o.id));
708
- // The living landscape's element→service resolver, container-aware and
709
- // memoized. Every question below that asks "does this edge point at me?"
710
- // asks it through this one function, so the no-openapi grace and the spine
711
- // check can never disagree about which edges are inbound.
712
- const landSvcOf = land === null ? null : serviceResolver(land.elements, known);
713
- // Inbound calls the landscape can PROVE: op-linked edges whose target
714
- // resolves to this service. Null when the landscape proves nothing at all
715
- // (absent, or it did not parse) — which is not the same fact as "nobody
716
- // calls me", and the two must not be graded alike.
717
- const inboundOps = land === null || land.errors.length > 0
718
- ? null
719
- : land.relationships.filter((r) => r.op !== undefined && landSvcOf(r.target) === service);
720
- // The other half of the evidence a contract is owed: the living spec's own
721
- // `Operations:` lines. A requirement on its way out governs nothing.
722
- const governedOps = livingReqs.flatMap((r) => r.operations);
723
- /** No contract to read: deleted, renamed, or never written. */
724
- const contractMissing = !existsSync(paths.openapi);
725
- if (contractMissing) {
726
- // A contract that is gone is not a reason to stop grading the API axis —
727
- // it IS the axis's answer, and every check below used to live inside the
728
- // file-exists branch, so deleting or misspelling the file turned the whole
729
- // axis (including the documented `spec-api.op-undefined`) green.
730
- //
731
- // What decides the severity is whether anything already written down is
732
- // holding a join into it: a living `Operations:` line, or an op-linked
733
- // landscape edge. With one of those, the absence breaks a link somebody
734
- // authored — an error. With neither, this is a service that legitimately
735
- // has no HTTP surface (a worker nobody calls), and a fleet mid-rollout
736
- // must not go red for it — a warning, or, when the landscape PROVES nobody
737
- // calls it, the documented silence.
738
- //
739
- // One finding, not one per dangling link: the fix is a single file, and
740
- // the links it strands ride along as details.
741
- const dangling = [...new Set([...governedOps, ...(inboundOps ?? []).map((r) => r.op)])].sort();
742
- if (dangling.length > 0) {
743
- findings.push({
744
- severity: "error",
745
- code: "service.no-openapi",
746
- subject: service,
747
- message: `No OpenAPI contract at ${paths.openapi}, and ${dangling.length} operation link(s) already point into it — ` +
748
- `every requirement and landscape edge naming one of them resolves to nothing until the file is back`,
749
- details: dangling,
750
- });
751
- }
752
- else if (inboundOps === null) {
753
- findings.push({
754
- severity: "warn",
755
- code: "service.no-openapi",
756
- message: `No OpenAPI contract at ${paths.openapi} — API coverage and the landscape spine are unchecked`,
757
- });
758
- }
759
- }
760
- else if (api.unreadable) {
761
- // A contract that EXISTS but does not read is a broken source of truth, not
762
- // an empty one: swallowing it into zero operations used to grade every
763
- // inbound landscape edge `spine.op-undefined` — a false diagnosis pointing
764
- // at the landscape when the truth was this file. So the file is the error,
765
- // and every check that reads the contract (api.*, the spine's op
766
- // resolution) is suspended below, the landscape.invalid discipline.
767
- findings.push({
768
- severity: "error",
769
- code: "openapi.invalid",
770
- message: `${service}: openapi.yaml does not parse — API coverage and the landscape spine are unchecked`,
771
- ...(api.error === undefined ? {} : { details: [api.error] }),
772
- });
773
- }
774
- else {
775
- if (removeMarkers.length > 0) {
776
- findings.push({
777
- severity: "error",
778
- code: "openapi.remove-marker-living",
779
- 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`,
780
- });
781
- }
782
- // The same marker written at PATH level, beside the methods. `readOpenapi`
783
- // is keyed by (path, method), which is precisely why the check above cannot
784
- // see this one and precisely how one reached a living contract in the first
785
- // place: it addresses no operation, so it retired nothing, and it stayed
786
- // invisible afterwards. Error, like its method-level sibling — a
787
- // feature-only key published to every consumer of the fleet, and one that
788
- // keeps the empty-path cleanup from ever firing. Same code as the archive
789
- // plan's: one breach, one name, wherever it is found.
790
- for (const removed of api.pathLevelRemovals) {
791
- findings.push({
792
- severity: "error",
793
- code: "openapi.remove-marker-path-level",
794
- subject: service,
795
- message: `${service}: living openapi.yaml carries x-loam-remove at PATH level on '${removed}' — ` +
796
- `a removal marker names ONE operation, so beside the methods it retires nothing and is not a contract key either, ` +
797
- `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.`,
798
- });
799
- }
800
- // Two slots claiming one operationId in a LIVING contract. `readOpenapi`
801
- // has computed this since the merge needed it, and only the feature path
802
- // (coherence) ever read it — so the ambiguity was reachable only when some
803
- // unrelated feature happened to carry a delta for this service, and the
804
- // fleet gate that actually runs in CI was blind to it. Same code and same
805
- // sentence as the feature-scope check: one breach, one name.
806
- for (const id of api.duplicateIds) {
807
- const slots = api.ops.filter((op) => op.id === id).map((op) => `${op.method} ${op.path}`);
808
- findings.push({
809
- severity: "warn",
810
- code: "openapi.duplicate-operationid",
811
- subject: service,
812
- 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`,
813
- });
814
- }
815
- const defined = new Set(ops);
816
- // `Operations:` on a LIVING requirement, resolved against this service's own
817
- // contract. Nothing did this before: the same spine is checked inside a
818
- // feature delta (coherence's spec-api.op-undefined) and then never again, so
819
- // a typo that shipped, or an operation later renamed out of openapi.yaml,
820
- // left a living requirement governing an operation that does not exist —
821
- // green forever, and every downstream join through that id silently empty.
822
- // Same code and severity as the feature-scope check on purpose: one breach,
823
- // one name, wherever it is found.
824
- for (const r of livingReqs) {
825
- for (const op of r.operations) {
826
- if (defined.has(op))
827
- continue;
828
- const close = closeIds(op, ops);
829
- findings.push({
830
- severity: "error",
831
- code: "spec-api.op-undefined",
832
- subject: service,
833
- message: `${service}: requirement '${r.name}' governs '${op}', not defined in ${service}'s OpenAPI` +
834
- (close.length > 0 ? `. Did you mean: ${close.join(", ")}?` : ""),
835
- });
836
- }
837
- }
838
- if (ops.length > 0) {
839
- const governed = new Set(livingReqs.flatMap((r) => r.operations));
840
- const orphans = ops.filter((op) => !governed.has(op));
841
- if (orphans.length === 0) {
842
- findings.push({
843
- severity: "ok",
844
- code: "api.covered",
845
- message: `${service}: API covered (${ops.length} operation(s) governed by requirements)`,
846
- });
847
- }
848
- else {
849
- findings.push({
850
- severity: "warn",
851
- code: "api.ungoverned",
852
- message: `${service}: ${orphans.length} operation(s) not governed by any requirement — ${orphans.join(", ")}`,
853
- });
854
- }
855
- // The migration-debt case: requirements exist, the API exists, and no
856
- // `Operations:` line ties them — every cross-axis check above and in
857
- // feature mode is vacuously green. Once per service, not per operation;
858
- // with zero requirements the spec (or its absence) is the finding instead.
859
- if (reqs.length > 0 && reqs.every((r) => r.operations.length === 0)) {
860
- findings.push({
861
- severity: "warn",
862
- code: "api.ops-unlinked",
863
- message: `${service}: openapi.yaml defines ${ops.length} operation(s) but no requirement links any — the API axis is unchecked for this service`,
864
- });
865
- }
866
- // Lifecycle: a requirement whose `Operations:` list resolves ONLY to
867
- // deprecated operations governs behaviour the contract is retiring.
868
- // Deprecation is the documented first step of removing an op; the explicit
869
- // feature marker is the final step. Until that delta archives, the op stays
870
- // live, so the fix is migration or a coordinated retirement. Ops the contract does not define at all
871
- // prove nothing here and are left to spec-api.op-undefined above.
872
- for (const r of reqs) {
873
- const resolved = r.operations.filter((op) => defined.has(op));
874
- if (resolved.length === 0 || !resolved.every((op) => deprecatedOps.has(op)))
875
- continue;
876
- findings.push({
877
- severity: "warn",
878
- code: "api.requirement-deprecated",
879
- 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`,
880
- });
881
- }
882
- }
883
- }
884
- // Landscape spine: cross-system edges calling THIS service must resolve to a real
885
- // operation in its OpenAPI — the C4↔API contract, checked in the living landscape,
886
- // not only in feature mode. Catches dangling / de-linked op edges.
887
- if (land !== null) {
888
- if (land.errors.length > 0) {
889
- // A living landscape that does not parse disables the C4↔API spine check —
890
- // that is a broken source of truth, not a skippable detail.
891
- //
892
- // The parser's own output is attached ONCE per run, and not here when the
893
- // run has a landscape target to carry it. One syntax error in one file
894
- // becomes N copies of a dozen cascading diagnostics on a fleet of N
895
- // services — the report stops being readable at exactly the moment
896
- // somebody needs to read it, and the fix is one file either way.
897
- findings.push({
898
- severity: "error",
899
- code: "spine.landscape-invalid",
900
- subject: service,
901
- message: `${service}: landscape.likec4 has ${land.errors.length} error(s) — spine check impossible` +
902
- (check.landscapeReported === true
903
- ? "; the parser output is reported once, on the landscape target"
904
- : ""),
905
- ...(check.landscapeReported === true ? {} : { details: land.errors.map(errorText) }),
906
- });
907
- }
908
- else {
909
- // Which element IS this service is the binding's call, then a title that
910
- // names a real services/<id>/, and an edge into a modelled container
911
- // counts as an edge into its service — matching the exact id alone meant a
912
- // container edge left the spine without a word.
913
- const svcOf = landSvcOf;
914
- const opset = new Set(ops);
915
- let checked = 0;
916
- let broken = 0;
917
- for (const r of land.relationships) {
918
- if (svcOf(r.target) !== service)
919
- continue;
920
- if (r.op !== undefined) {
921
- // A contract that cannot be read — broken, or not there at all —
922
- // proves nothing about this edge, neither broken nor resolved.
923
- // Grading the edges against an empty operation set turned ONE root
924
- // cause (the file) into one `spine.op-undefined` per inbound edge, so
925
- // a service with twelve consumers reported twelve landscape defects
926
- // and never named the file. `openapi.invalid` / `service.no-openapi`
927
- // already did; only op-link-missing (which never reads the contract)
928
- // stays live, and `checked` stays 0 so no false spine.resolved is
929
- // claimed either.
930
- if (api.unreadable || contractMissing)
931
- continue;
932
- checked += 1;
933
- if (!opset.has(r.op)) {
934
- broken += 1;
935
- findings.push({
936
- severity: "error",
937
- code: "spine.op-undefined",
938
- message: `${service}: landscape edge ${svcOf(r.source)} → ${service} calls '${r.op}', not defined in ${service}'s OpenAPI`,
939
- });
940
- }
941
- else if (deprecatedOps.has(r.op)) {
942
- // The contract holds — the op is defined — but it is marked
943
- // `deprecated: true`: the consumer is standing on a contract being
944
- // retired, and should be migrating off it. Warn per inbound edge;
945
- // a deprecated op nobody calls raises no spine finding at all.
946
- findings.push({
947
- severity: "warn",
948
- code: "spine.op-deprecated",
949
- message: `${service}: landscape edge ${svcOf(r.source)} → ${service} calls '${r.op}', which ${service}'s OpenAPI marks deprecated — the consumer should migrate off it`,
950
- });
951
- }
952
- }
953
- else if ((r.title ?? "").toLowerCase().startsWith("call")) {
954
- findings.push({
955
- severity: "warn",
956
- code: "spine.op-link-missing",
957
- message: `${service}: landscape edge ${svcOf(r.source)} → ${service} ("${r.title}") has no operation link (metadata { op })`,
958
- });
959
- }
960
- }
961
- if (broken === 0 && checked > 0) {
962
- findings.push({
963
- severity: "ok",
964
- code: "spine.resolved",
965
- message: `${service}: landscape spine (${checked} inbound call(s) resolve to OpenAPI)`,
966
- });
967
- }
968
- }
969
- }
970
- /* ------------------------------------------------------------------ */
971
- /* The async contract axis — AsyncAPI messages on the landscape spine */
972
- /* ------------------------------------------------------------------ */
973
- //
974
- // The HTTP spine above and this one are mirror images, and the mirror is the
975
- // whole reason this is a second axis rather than a second `metadata` key. On
976
- // the HTTP axis the PROVIDER owns the contract: an edge names an operation and
977
- // the target's openapi.yaml settles it, locally, in one file. Here the
978
- // PRODUCER owns the message and the consumer joins to it from another
979
- // repository — so "is this message declared" is local, while "does anybody
980
- // publish it" is a fleet question with no local answer at all.
981
- // The architecture spec, read HERE rather than with the rest of its axis
982
- // below, because the event links are a requirement-level join and both
983
- // requirement files carry them. Events are architecture as often as they are
984
- // business — the outbox requirement in a service's arch.spec.md is the
985
- // canonical place a `Publishes:` line belongs — so reading only spec.md would
986
- // silently ignore the lines most authors write.
987
- const archText = existsSync(paths.archSpec)
988
- ? fleet === undefined
989
- ? await readFile(paths.archSpec, "utf8")
990
- : await fleet.readText(paths.archSpec)
991
- : null;
992
- const archReqs = archText === null
993
- ? []
994
- : fleet === undefined
995
- ? parseRequirements(archText)
996
- : await fleet.readRequirements(paths.archSpec);
997
- // Where this axis started, so the closing ok-finding can be "nothing here
998
- // broke" rather than a second, independently-computed claim about the same
999
- // facts. A positive finding derived from the same walk cannot disagree with it.
1000
- const eventFindingsAt = findings.length;
1001
- const events = await readAsyncapi(paths.asyncapi, fleet);
1002
- const sentHere = new Set(events.sent);
1003
- const receivedHere = new Set(events.received);
1004
- // Every edge that binds THIS service to a message: `publishes` binds the
1005
- // edge's source (a service produces what it declares an `action: send` for),
1006
- // `consumes` binds its target. One edge may legitimately carry both — a relay
1007
- // that reads one topic and writes another is a single arrow in most fleet maps.
1008
- const eventEdges = land === null || land.errors.length > 0
1009
- ? []
1010
- : land.relationships.flatMap((r) => {
1011
- const bound = [];
1012
- if (r.publishes !== undefined && landSvcOf(r.source) === service) {
1013
- bound.push({ direction: "publishes", message: r.publishes, other: landSvcOf(r.target) });
1014
- }
1015
- if (r.consumes !== undefined && landSvcOf(r.target) === service) {
1016
- bound.push({ direction: "consumes", message: r.consumes, other: landSvcOf(r.source) });
1017
- }
1018
- return bound;
1019
- });
1020
- // Both requirement namespaces, and a REMOVED requirement links nothing —
1021
- // the `livingReqs` rule the API axis already follows, applied to the pair.
1022
- const eventLinks = [...livingReqs, ...archReqs.filter((r) => r.kind !== "REMOVED")].flatMap((r) => [
1023
- ...r.publishes.map((m) => ({ direction: "publishes", message: m, requirement: r.name })),
1024
- ...r.consumes.map((m) => ({ direction: "consumes", message: m, requirement: r.name })),
1025
- ]);
1026
- if (!existsSync(paths.asyncapi)) {
1027
- // Deliberately TWO grades, not the three `service.no-openapi` uses. Its
1028
- // middle grade — warn when the landscape cannot prove nobody calls this
1029
- // service — rests on HTTP being the default: most services expose one, so
1030
- // "I could not look" is worth saying. An event contract is genuinely
1031
- // optional; most services in a legacy fleet touch no topic at all. Warning
1032
- // every one of them the moment a landscape fails to parse would put a
1033
- // finding on the whole fleet that names a file nobody owes.
1034
- //
1035
- // So: something already joins into the absent file, or silence.
1036
- const dangling = [
1037
- ...new Set([...eventEdges.map((e) => e.message), ...eventLinks.map((e) => e.message)]),
1038
- ].sort();
1039
- if (dangling.length > 0) {
1040
- findings.push({
1041
- severity: "error",
1042
- code: "service.no-asyncapi",
1043
- subject: service,
1044
- message: `No async contract at ${paths.asyncapi}, and ${dangling.length} message link(s) already point into it — ` +
1045
- `every landscape edge and requirement naming one of them resolves to nothing until the file is back`,
1046
- details: dangling,
1047
- });
1048
- }
1049
- }
1050
- else if (events.unreadable) {
1051
- // The openapi.invalid discipline: a contract that EXISTS but does not read
1052
- // is a broken source of truth, not an empty one. Grading the links against
1053
- // an empty message set would turn one file into one `spine.message-undefined`
1054
- // per edge, every one of them pointing at the landscape.
1055
- findings.push({
1056
- severity: "error",
1057
- code: "asyncapi.invalid",
1058
- subject: service,
1059
- message: `${service}: asyncapi.yaml does not parse — the event spine is unchecked for this service`,
1060
- ...(events.error === undefined ? {} : { details: [events.error] }),
1061
- });
1062
- }
1063
- else {
1064
- for (const name of events.duplicateNames) {
1065
- findings.push({
1066
- severity: "warn",
1067
- code: "asyncapi.duplicate-message",
1068
- subject: service,
1069
- message: `${service}: asyncapi.yaml declares message '${name}' in ${slotsOf(events, name).join(" and ")} — every join on the name (an edge's metadata { publishes }, a requirement's Publishes: line) picks one of those slots arbitrarily`,
1070
- });
1071
- }
1072
- // Local resolution, both directions and both sources of a claim. An edge and
1073
- // a requirement making the same broken claim get the same sentence under
1074
- // different codes, matching how `spine.op-undefined` and
1075
- // `spec-api.op-undefined` split the HTTP axis: one is the fleet map's
1076
- // mistake, the other is this document's, and the fix is in a different file.
1077
- const declares = (direction, message) => direction === "publishes" ? sentHere.has(message) : receivedHere.has(message);
1078
- const action = (direction) => direction === "publishes" ? "send" : "receive";
1079
- for (const e of eventEdges) {
1080
- if (declares(e.direction, e.message))
1081
- continue;
1082
- findings.push({
1083
- severity: "error",
1084
- code: "spine.message-undefined",
1085
- subject: service,
1086
- message: `${service}: landscape edge ${e.direction === "publishes" ? `${service} → ${e.other}` : `${e.other} → ${service}`} ` +
1087
- `${e.direction} '${e.message}', but ${service}'s asyncapi.yaml declares no operation with action: ${action(e.direction)} for it`,
1088
- });
1089
- }
1090
- for (const e of eventLinks) {
1091
- if (declares(e.direction, e.message))
1092
- continue;
1093
- findings.push({
1094
- severity: "error",
1095
- code: "spec-event.message-undefined",
1096
- subject: service,
1097
- message: `${service}: requirement '${e.requirement}' ${e.direction} '${e.message}', but ${service}'s asyncapi.yaml declares no operation with action: ${action(e.direction)} for it`,
1098
- });
1099
- }
1100
- // The migration-debt case, `api.ops-unlinked`'s twin: a contract and
1101
- // requirements that never name each other leave this axis vacuously green.
1102
- if (events.messages.length > 0 && reqs.length + archReqs.length > 0 && eventLinks.length === 0) {
1103
- findings.push({
1104
- severity: "warn",
1105
- code: "event.messages-unlinked",
1106
- subject: service,
1107
- message: `${service}: asyncapi.yaml declares ${events.messages.length} message(s) but no requirement links any — the event axis is unchecked for this service`,
1108
- });
1109
- }
1110
- }
1111
- // The fleet question, and the one check on either spine that a single
1112
- // repository cannot answer: a consumer joins to a message whose schema lives
1113
- // in the producer's contract, in the producer's repo. Paid only when something
1114
- // actually consumes — a fleet where nobody has adopted the axis walks nothing.
1115
- const consumed = [
1116
- ...new Set([
1117
- ...eventEdges.filter((e) => e.direction === "consumes").map((e) => e.message),
1118
- ...eventLinks.filter((e) => e.direction === "consumes").map((e) => e.message),
1119
- ]),
1120
- ].sort();
1121
- if (consumed.length > 0) {
1122
- const producers = await producersByMessage(docsDir, [...known], fleet);
1123
- for (const message of consumed) {
1124
- const who = producers.byMessage.get(message) ?? [];
1125
- // "Nobody produces this" is an argument from absence, and it is only
1126
- // sound over a fleet loam could actually read. One unreadable contract
1127
- // anywhere suspends it — that service may be the producer, and the
1128
- // alternative is blaming every consumer for somebody else's broken YAML.
1129
- // The contested answer below rests on positive evidence and needs no such
1130
- // guard.
1131
- if (who.length === 0 && producers.unreadable.length === 0) {
1132
- findings.push({
1133
- severity: "error",
1134
- code: "spine.message-unproduced",
1135
- subject: service,
1136
- message: `${service}: consumes '${message}', but no service in the fleet declares an operation with action: send for it — the message has no producer, so nothing defines its payload`,
1137
- });
1138
- }
1139
- else if (who.length > 1) {
1140
- findings.push({
1141
- severity: "warn",
1142
- code: "asyncapi.message-contested",
1143
- subject: service,
1144
- message: `${service}: consumes '${message}', which ${who.length} services declare they send (${who.join(", ")}) — every consumer's join picks one of them arbitrarily, so the payload this service reads is whichever one happens to win`,
1145
- });
1146
- }
1147
- }
1148
- }
1149
- // Positive confirmation, on the same rule `spine.resolved` follows: claimed
1150
- // only where the axis actually checked something. A service that touches no
1151
- // topic says nothing here — an "event axis clean" on every worker in the fleet
1152
- // would be a green tick for work nobody did.
1153
- const eventChecks = eventEdges.length + eventLinks.length;
1154
- if (eventChecks > 0 && findings.length === eventFindingsAt) {
1155
- findings.push({
1156
- severity: "ok",
1157
- code: "event.covered",
1158
- message: `${service}: event spine (${eventChecks} message link(s) resolve to asyncapi.yaml)`,
1159
- });
1160
- }
1161
- // The architecture spec axis — the obligations a business spec never carries
1162
- // (the transactional outbox, retries, metrics, alerts). arch.spec.md is
1163
- // optional and its ABSENCE is not a finding (partial adoption is supported);
1164
- // what exists must hold together: every requirement needs a scenario, every
1165
- // `Covers:` entry must resolve (covers.unknown), and every alert/SLI that
1166
- // health.yaml declares wants a covering requirement (health.uncovered) — the
1167
- // moment health.yaml stops being inert. Warnings, never gates: `--strict` is
1168
- // the CI escalation.
1169
- const health = await readHealth(paths.health);
1170
- if (archText !== null) {
1171
- // The arch axis is advisory in what it ASKS for (covers.unknown,
1172
- // health.uncovered are warnings) but not in whether the file is readable:
1173
- // both breaches below are about the document itself, and they are graded
1174
- // exactly as they are on the business axis — one file, one rule.
1175
- const conflict = documentConflictFinding(`${service}: arch.spec.md`, service, archText);
1176
- if (conflict !== null)
1177
- findings.push(conflict);
1178
- findings.push(coverageFinding(`${service}: arch requirements`, archReqs));
1179
- findings.push(...steplessFindings(`${service}: arch requirements`, service, archReqs));
1180
- findings.push(...duplicateRequirementFindings(archReqs, `${service}: arch.spec.md`, service));
1181
- findings.push(...requirementIdFindings(archReqs, `${service}: arch.spec.md`, service));
1182
- findings.push(...repeatedListLineFindings(archReqs, `${service}: arch.spec.md`, service));
1183
- }
1184
- // A health.yaml that exists but does not read is reported once, and the
1185
- // checks that read it go quiet BOTH ways: its ids are unknown, so no
1186
- // health.uncovered obligation exists (the empty id set below is already
1187
- // silent), and no `Covers: alert:/sli:` entry may be graded a typo against
1188
- // ids nobody could read (coversUnknownFindings mutes on the flag). A warn,
1189
- // not an error: the axis it feeds is advisory end to end.
1190
- if (health.unreadable) {
1191
- findings.push({
1192
- severity: "warn",
1193
- code: "health.invalid",
1194
- subject: service,
1195
- message: `${service}: health.yaml does not parse — alert/SLI ids are unreadable, so Covers: alert:/sli: entries and health coverage are unchecked`,
1196
- ...(health.error === undefined ? {} : { details: [health.error] }),
1197
- });
1198
- }
1199
- const landParses = land !== null && land.errors.length === 0 ? land : null;
1200
- const scope = {
1201
- elements: [...elements, ...(landParses?.elements ?? [])],
1202
- relationships: [...relationships, ...(landParses?.relationships ?? [])],
1203
- health: health.ids,
1204
- };
1205
- findings.push(...coversUnknownFindings(archReqs, `${service}: arch.spec.md`, service, scope, health.unreadable));
1206
- const activeCovers = coversEntries(archReqs);
1207
- for (const { form, ids } of [
1208
- { form: "alert", ids: health.ids.alerts },
1209
- { form: "sli", ids: health.ids.slis },
1210
- ]) {
1211
- for (const id of ids) {
1212
- if (activeCovers.some((e) => e.form === form && e.id === id))
1213
- continue;
1214
- findings.push({
1215
- severity: "warn",
1216
- code: "health.uncovered",
1217
- subject: service,
1218
- message: `${service}: health.yaml declares ${form === "alert" ? "alert" : "SLI"} '${id}' but no arch.spec.md requirement covers it — write one with 'Covers: ${form}:${id}', or the signal ships with nothing testing it`,
1219
- });
1220
- }
1221
- }
1222
- // Provenance last: who vouched for this, and what code it was written from.
1223
- findings.push(...(await serviceProvenance(docsDir, service, { repoDir })));
1224
- findings.push(...(await sourceScopeFindings(docsDir, service, repoDir)));
1225
- // The generated-gherkin freshness chain, service-repo-scoped like sources.*:
1226
- // it needs the repo (the suite lives there), and it stays quiet until
1227
- // <gherkinDir>/loam/ exists — a service that never generated has not opted in.
1228
- findings.push(...(await gherkinFindings({ docsDir, service, repoDir, gherkinDir, fleet })));
1229
- return report;
1230
- }
1231
- /** The parsed Covers entries of every requirement that will live (REMOVED covers nothing). */
1232
- function coversEntries(reqs) {
1233
- return reqs.filter((r) => r.kind !== "REMOVED").flatMap((r) => r.covers.map(parseCoversEntry));
1234
- }
1235
- /**
1236
- * `spec.duplicate-requirement` — two `### Requirement:` blocks with one name in
1237
- * one LIVING document. Nothing else catches it, and the merge algebra
1238
- * (applyRequirementDelta) matches by name and edits only the FIRST match: a
1239
- * later archive rewrites one copy and the other survives as a stale snapshot
1240
- * of whatever the requirement used to say. Per file on purpose — spec.md and
1241
- * arch.spec.md are separate requirement namespaces (their merges never cross
1242
- * files), so one name appearing in both is legal and unflagged.
1243
- */
1244
- function duplicateRequirementFindings(reqs, where, subject) {
1245
- const counts = new Map();
1246
- for (const r of reqs)
1247
- counts.set(r.name, (counts.get(r.name) ?? 0) + 1);
1248
- return [...counts]
1249
- .filter(([, n]) => n > 1)
1250
- .map(([name, n]) => ({
1251
- severity: "error",
1252
- code: "spec.duplicate-requirement",
1253
- subject,
1254
- message: `${where}: requirement '${name}' is defined ${n} times — a merge edits only the first, every other copy lives on stale; keep exactly one`,
1255
- }));
1256
- }
1257
- /** Stable IDs are optional, but once authored they must select exactly one requirement. */
1258
- function requirementIdFindings(reqs, where, subject) {
1259
- return requirementIdProblems(reqs).map((problem) => {
1260
- if (problem.kind === "invalid") {
1261
- return {
1262
- severity: "error",
1263
- code: "spec.requirement-id-invalid",
1264
- subject,
1265
- message: `${where}: requirement '${problem.requirement}' has invalid Requirement-ID '${problem.value}' — use 1-128 characters matching [A-Za-z][A-Za-z0-9._-]*`,
1266
- };
1267
- }
1268
- if (problem.kind === "repeated") {
1269
- return {
1270
- severity: "error",
1271
- code: "spec.requirement-id-repeated",
1272
- subject,
1273
- message: `${where}: requirement '${problem.requirement}' declares Requirement-ID ${problem.values.length} times — identity must be declared exactly once`,
1274
- };
1275
- }
1276
- return {
1277
- severity: "error",
1278
- code: "spec.requirement-id-duplicate",
1279
- subject,
1280
- message: `${where}: Requirement-ID '${problem.id}' is shared by ${problem.requirements.map((name) => `'${name}'`).join(", ")} — one ID may identify only one requirement`,
1281
- };
1282
- });
1283
- }
1284
- /**
1285
- * The two list lines of the requirement grammar, exactly as core/spec.ts spells
1286
- * them (mirrored here, not exported from there, because the parser's grammar is
1287
- * its own; a drift shows up as this check counting differently than the parser
1288
- * assigns). A SECOND matching line in one requirement body REPLACES the first —
1289
- * assignment, not append, the documented keep-last quirk — so the author's
1290
- * "long list in two lines" pattern silently loses its first line.
1291
- */
1292
- const OPERATIONS_LINE_RE = /^\s*Operations?:\s*(.+?)\s*$/i;
1293
- const COVERS_LINE_RE = /^\s*Covers?:\s*(.+?)\s*$/i;
1294
- /**
1295
- * `spec.repeated-operations` / `spec.repeated-covers` — warn on the silent
1296
- * loss, keep the keep-last semantics (changing them would re-read every spec
1297
- * in the fleet). Scenario bodies never count: the parser only assigns from the
1298
- * requirement's own body lines, and `Requirement.text` is exactly those.
1299
- * REMOVED requirements are exempt the way coversEntries exempts them — content
1300
- * on its way out obliges nothing.
1301
- */
1302
- function repeatedListLineFindings(reqs, where, subject) {
1303
- const out = [];
1304
- for (const r of reqs) {
1305
- if (r.kind === "REMOVED")
1306
- continue;
1307
- for (const { re, label, code } of [
1308
- { re: OPERATIONS_LINE_RE, label: "Operations:", code: "spec.repeated-operations" },
1309
- { re: COVERS_LINE_RE, label: "Covers:", code: "spec.repeated-covers" },
1310
- ]) {
1311
- const n = r.text.filter((line) => re.test(line)).length;
1312
- if (n < 2)
1313
- continue;
1314
- out.push({
1315
- severity: "warn",
1316
- code,
1317
- subject,
1318
- message: `${where}: requirement '${r.name}' has ${n} '${label}' lines — the last REPLACES the others (assignment, not append), the earlier list is silently lost; merge them into one comma-separated line`,
1319
- });
1320
- }
1321
- }
1322
- return out;
1323
- }
1324
- /**
1325
- * `covers.unknown` — the typo guard on the Covers: line. Warn, not error: the
1326
- * axis is advisory end to end, and a wrong id already costs its author the
1327
- * coverage they wrote the line for. The hint offers only real ids (closeIds's
1328
- * rule), and says where resolution looked when there is nothing close.
1329
- * `healthUnreadable` mutes the alert:/sli: forms only: against a health.yaml
1330
- * nobody could read, "did you mean" is a false diagnosis of a typo —
1331
- * health.invalid (emitted by the service target) is the honest one.
1332
- */
1333
- function coversUnknownFindings(reqs, where, subject, scope, healthUnreadable = false) {
1334
- const out = [];
1335
- for (const r of reqs) {
1336
- if (r.kind === "REMOVED")
1337
- continue;
1338
- for (const raw of r.covers) {
1339
- const entry = parseCoversEntry(raw);
1340
- if (healthUnreadable && (entry.form === "alert" || entry.form === "sli"))
1341
- continue;
1342
- if (entryResolves(entry, scope))
1343
- continue;
1344
- const close = coversCandidates(entry, scope);
1345
- out.push({
1346
- severity: "warn",
1347
- code: "covers.unknown",
1348
- subject,
1349
- message: `${where}: requirement '${r.name}' — Covers: '${raw}' resolves to nothing` +
1350
- (close.length > 0
1351
- ? `. Did you mean: ${close.join(", ")}?`
1352
- : " in the model, the landscape or health.yaml"),
1353
- });
1354
- }
1355
- }
1356
- return out;
1357
- }
1358
- async function validateFeature(docsDir, feature, preloadedLand, fleet) {
1359
- const findings = [];
1360
- const featureDir = feature.dir;
1361
- const featureId = feature.id;
1362
- // delta.likec4 parse + collect tagged edges. The loaded doc is kept and
1363
- // handed to featureCoherence below — loading it is a Langium workspace spin,
1364
- // and paying it twice per feature was the dominant cost of `validate --all`.
1365
- let taggedEls = [];
1366
- let taggedRels = [];
1367
- let elements = [];
1368
- let deltaRels = [];
1369
- let deltaDoc;
1370
- const deltaPath = featurePaths(featureDir).delta;
1371
- if (existsSync(deltaPath)) {
1372
- const res = fleet === undefined ? await loadFile(deltaPath) : await fleet.loadLikeC4(deltaPath);
1373
- deltaDoc = res;
1374
- if (res.errors.length > 0) {
1375
- findings.push({
1376
- severity: "error",
1377
- code: "delta.invalid",
1378
- message: `delta.likec4 has ${res.errors.length} error(s)`,
1379
- details: res.errors.map(errorText),
1380
- });
1381
- }
1382
- else {
1383
- elements = res.elements;
1384
- deltaRels = res.relationships;
1385
- taggedEls = res.elements.filter((e) => e.tags.includes(featureId));
1386
- taggedRels = res.relationships.filter((r) => r.tags.includes(featureId));
1387
- findings.push({
1388
- severity: "ok",
1389
- code: "delta.valid",
1390
- message: `delta.likec4 valid (${res.elements.length} elements · ${res.relationships.length} relationships)`,
1391
- });
1392
- }
1393
- }
1394
- findings.push(...(await featureProvenance(featureDir, featureId)));
1395
- // Who this feature is allowed to address. `specs/<svc>/` is what the archive
1396
- // materialises `services/<svc>/` from, and nothing used to ask whether that
1397
- // name means anything: one wrong character in `--touches` passed
1398
- // `validate --all` with zero errors, and archive then created the phantom
1399
- // directory. A delta may legitimately name a service that does not exist yet
1400
- // — but only one it INTRODUCES itself, in its own tagged C4. A delta that did
1401
- // not parse proves neither (`delta.invalid` is that finding), so the question
1402
- // is suspended there rather than answered by guessing.
1403
- //
1404
- // `services/<svc>/` is asked for directly rather than through the
1405
- // enumeration: `validate --feature` is allowed to run in a docs repo with no
1406
- // services/ at all (repo.ts takes the same position), where enumerating is a
1407
- // refusal, not an answer.
1408
- const featureServices = await featureSpecServices(featureDir, fleet);
1409
- const introduces = new Set(taggedEls.map(elementService));
1410
- const deltaReadable = deltaDoc === undefined || deltaDoc.errors.length === 0;
1411
- const unknownServices = deltaReadable
1412
- ? featureServices.filter((svc) => !existsSync(servicePaths(docsDir, svc).dir) && !introduces.has(svc))
1413
- : [];
1414
- // The near-miss hint, on the same rule `service.unknown` uses — a typo is
1415
- // only diagnosable against the ids that DO exist.
1416
- const closeTo = unknownServices.length > 0 && docsRepoState(docsDir).kind === "ok"
1417
- ? (await listServices(docsDir, fleet)).map((s) => s.id)
1418
- : [];
1419
- // The finding is coherence.ts's — the same words archive refuses with, because
1420
- // it is the same conclusion about the same directory.
1421
- for (const svc of unknownServices)
1422
- findings.push(deltaServiceUnknownFinding(svc, closeTo));
1423
- // Requirement coverage across every per-service delta — the business spec and
1424
- // the arch spec through the same check — and collect scenario text.
1425
- let scenarioText = "";
1426
- const archDeltas = [];
1427
- for (const svc of featureServices) {
1428
- const p = featureSpecPaths(featureDir, svc);
1429
- if (existsSync(p.spec)) {
1430
- const raw = fleet === undefined ? await readFile(p.spec, "utf8") : await fleet.readText(p.spec);
1431
- scenarioText += "\n" + raw.toLowerCase();
1432
- const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.spec);
1433
- // Both document-level breaches carry into the living spec through the
1434
- // merge, so a delta is graded for them exactly as a living document is:
1435
- // conflict markers merge as prose under someone's requirement, and a
1436
- // stepless scenario merges as a requirement the coverage rule calls
1437
- // covered forever after.
1438
- const conflict = documentConflictFinding(`${svc}: spec.md`, svc, raw);
1439
- if (conflict !== null)
1440
- findings.push(conflict);
1441
- findings.push({ ...coverageFinding(`${svc}: requirements`, reqs), subject: svc });
1442
- findings.push(...steplessFindings(`${svc}: requirements`, svc, reqs));
1443
- // The keep-last quirk loses lines in a delta exactly as in a living spec
1444
- // — and a delta's lost Operations: line then merges into the living one.
1445
- findings.push(...repeatedListLineFindings(reqs, `${svc}: spec.md`, svc));
1446
- }
1447
- if (existsSync(p.archSpec)) {
1448
- const raw = fleet === undefined ? await readFile(p.archSpec, "utf8") : await fleet.readText(p.archSpec);
1449
- scenarioText += "\n" + raw.toLowerCase();
1450
- const reqs = fleet === undefined ? parseRequirements(raw) : await fleet.readRequirements(p.archSpec);
1451
- archDeltas.push({ service: svc, reqs });
1452
- const conflict = documentConflictFinding(`${svc}: arch.spec.md`, svc, raw);
1453
- if (conflict !== null)
1454
- findings.push(conflict);
1455
- findings.push({ ...coverageFinding(`${svc}: arch requirements`, reqs), subject: svc });
1456
- findings.push(...steplessFindings(`${svc}: arch requirements`, svc, reqs));
1457
- findings.push(...repeatedListLineFindings(reqs, `${svc}: arch.spec.md`, svc));
1458
- }
1459
- }
1460
- // The delta's own element→service resolver, built once for the whole feature.
1461
- // `serviceOf` is a one-shot wrapper that rebuilds its index on every call, and
1462
- // the two loops below ask it up to five times per tagged edge — so a delta
1463
- // over a large model paid for one Map of every element per question asked.
1464
- // Identical answers by construction: `serviceOf(elements, id)` IS
1465
- // `serviceResolver(elements)(id)`, and nothing reassigns `elements` after the
1466
- // delta parse.
1467
- const svcOf = serviceResolver(elements);
1468
- // Arch-edge coverage (heuristic, warn-only): each new tagged edge should be named by a scenario.
1469
- for (const r of taggedRels) {
1470
- const target = svcOf(r.target);
1471
- const covered = edgeCovered(target, r.title, scenarioText);
1472
- findings.push({
1473
- severity: covered ? "ok" : "warn",
1474
- code: covered ? "archedge.covered" : "archedge.uncovered",
1475
- subject: target,
1476
- message: `${svcOf(r.source)} → ${target} "${r.title ?? ""}"${covered ? "" : " — no scenario names it"}`,
1477
- text: { indent: 4, header: "arch-edge coverage (heuristic):" },
1478
- });
1479
- }
1480
- // The architecture spec axis, feature scope — the mechanical counterpart of
1481
- // the heuristic above. Every NEW tagged element and edge in the delta wants a
1482
- // `Covers:` line in one of the feature's arch.spec.md deltas (c4.uncovered):
1483
- // this is where agent-built code cuts its corners — the outbox, the retries,
1484
- // the alerts — because no business scenario was ever going to mention them.
1485
- // Grouping-only elements follow the landscape checks' exemptions (person
1486
- // kinds, #external). Warnings, never archive gates; `--strict` escalates.
1487
- //
1488
- // Only requirements the archive will MERGE grant coverage here. In a delta,
1489
- // BASE means "the living state, quoted": it merges nothing, emits no
1490
- // .feature, and yields no scenario.tested claim — so a Covers: line under a
1491
- // plain `## Requirements` quote is an obligation that ships nowhere, and
1492
- // counting it silenced c4.uncovered for free. (The service-scope pass keeps
1493
- // the unfiltered call: a LIVING spec is legitimately all BASE.)
1494
- const activeCovers = archDeltas.flatMap(({ reqs }) => coversEntries(reqs.filter((r) => r.kind === "ADDED" || r.kind === "MODIFIED")));
1495
- // What the living landscape ALREADY holds. A delta has to re-declare the
1496
- // elements its new edges attach to, and authors tag those re-declarations
1497
- // along with everything else — so a requirements-only feature that touches an
1498
- // existing service was told to write `Covers:` lines for architecture it is
1499
- // not adding. c4.uncovered is an obligation on NEW architecture; an element
1500
- // the living landscape already resolves is not new, whatever the tag says.
1501
- // Loaded lazily: a delta with nothing tagged never pays for the parse.
1502
- let living = preloadedLand;
1503
- const livingLandscape = async () => {
1504
- if (living === undefined) {
1505
- const lp = landscapeFile(docsDir);
1506
- living = existsSync(lp)
1507
- ? fleet === undefined
1508
- ? await loadFile(lp)
1509
- : await fleet.loadLikeC4(lp)
1510
- : null;
1511
- }
1512
- return living;
1513
- };
1514
- const alreadyLiving = async () => {
1515
- if (taggedEls.length === 0 && taggedRels.length === 0)
1516
- return null;
1517
- const doc = await livingLandscape();
1518
- return doc !== null && doc.errors.length === 0 ? doc : null;
1519
- };
1520
- const base = await alreadyLiving();
1521
- const baseSvcOf = base === null ? null : serviceResolver(base.elements);
1522
- const baseIds = new Set(base?.elements.map((e) => e.id) ?? []);
1523
- const baseServices = new Set((base?.elements ?? []).map(elementService));
1524
- // How a service→service pair is keyed, spelled ONCE. The two sides used to
1525
- // join with different separators — a NUL where the set was built, a space
1526
- // where it was queried — so the exemption never matched anything, and every
1527
- // edge a delta re-declares verbatim (which it must, to attach anything to
1528
- // it) was reported as new architecture nobody covers. The join is structural
1529
- // rather than a separator character: the last one was a NUL, and a raw NUL in
1530
- // a template literal makes the source read as binary to `file` and invisible
1531
- // to `grep` — which is how a one-character mismatch survived review.
1532
- const edgeKey = (source, target) => JSON.stringify([source, target]);
1533
- const baseEdges = new Set((base?.relationships ?? []).map((r) => edgeKey(baseSvcOf(r.source), baseSvcOf(r.target))));
1534
- for (const e of taggedEls) {
1535
- if (ACTOR_KINDS.has(e.kind.toLowerCase()) || e.tags.includes(EXTERNAL_TAG))
1536
- continue;
1537
- if (baseIds.has(e.id) || baseServices.has(elementService(e)))
1538
- continue;
1539
- if (activeCovers.some((c) => coversElement(c, e)))
1540
- continue;
1541
- findings.push({
1542
- severity: "warn",
1543
- code: "c4.uncovered",
1544
- subject: elementService(e),
1545
- message: `delta adds '${e.title}' (${e.id}) but no arch requirement covers it — add 'Covers: ${e.id}' to a specs/<svc>/arch.spec.md delta, or its architectural obligations ship unchecked`,
1546
- });
1547
- }
1548
- for (const r of taggedRels) {
1549
- if (baseEdges.has(edgeKey(svcOf(r.source), svcOf(r.target))))
1550
- continue;
1551
- if (activeCovers.some((c) => coversEdge(c, r, elements)))
1552
- continue;
1553
- findings.push({
1554
- severity: "warn",
1555
- code: "c4.uncovered",
1556
- subject: svcOf(r.target),
1557
- message: `delta adds edge ${svcOf(r.source)} → ${svcOf(r.target)} ("${r.title ?? ""}") but no arch requirement covers it — add 'Covers: ${r.source} -> ${r.target}' to a specs/<svc>/arch.spec.md delta`,
1558
- });
1559
- }
1560
- // covers.unknown, feature scope. Resolution looks at the delta itself, the
1561
- // living landscape, the service's own model and its health.yaml — a delta's
1562
- // arch requirement may cover an element it adds, one that already exists, or
1563
- // an alert the service declares. The landscape and each model are loaded
1564
- // lazily, and only when an entry fails against what is already in hand: the
1565
- // clean path never pays for a workspace spin.
1566
- if (archDeltas.some(({ reqs }) => coversEntries(reqs).length > 0)) {
1567
- const land = await livingLandscape();
1568
- const landParses = land !== null && land.errors.length === 0 ? land : null;
1569
- const baseElements = [...elements, ...(landParses?.elements ?? [])];
1570
- const baseRels = [...deltaRels, ...(landParses?.relationships ?? [])];
1571
- for (const { service: svc, reqs } of archDeltas) {
1572
- // An unreadable living health.yaml mutes the alert:/sli: entries here
1573
- // exactly as in service scope — the health.invalid finding itself
1574
- // belongs to the service target, which owns the file's diagnosis.
1575
- const health = await readHealth(servicePaths(docsDir, svc).health);
1576
- let scope = { elements: baseElements, relationships: baseRels, health: health.ids };
1577
- const unresolved = coversUnknownFindings(reqs, `${svc}: arch.spec.md`, svc, scope, health.unreadable);
1578
- if (unresolved.length > 0) {
1579
- const modelPath = servicePaths(docsDir, svc).model;
1580
- const model = existsSync(modelPath)
1581
- ? fleet === undefined
1582
- ? await loadFile(modelPath)
1583
- : await fleet.loadLikeC4(modelPath)
1584
- : null;
1585
- if (model !== null && model.errors.length === 0) {
1586
- scope = {
1587
- elements: [...baseElements, ...model.elements],
1588
- relationships: [...baseRels, ...model.relationships],
1589
- health: health.ids,
1590
- };
1591
- }
1592
- findings.push(...coversUnknownFindings(reqs, `${svc}: arch.spec.md`, svc, scope, health.unreadable));
1593
- }
1594
- }
1595
- }
1596
- // Coherence — cross-axis consistency (C4 ↔ requirements ↔ OpenAPI).
1597
- const issues = await featureCoherence(docsDir, featureDir, featureId, deltaDoc, fleet);
1598
- if (issues.length === 0) {
1599
- findings.push({
1600
- severity: "ok",
1601
- code: "coherence.ok",
1602
- message: "coherence: ✓ C4 · requirements · OpenAPI agree",
1603
- text: { indent: 2, marker: false },
1604
- });
1605
- }
1606
- else {
1607
- for (const i of issues) {
1608
- findings.push({
1609
- severity: i.severity,
1610
- code: i.code,
1611
- gates: gatesArchive(i),
1612
- ...(i.subject === undefined ? {} : { subject: i.subject }),
1613
- message: i.message,
1614
- text: { indent: 4, header: "coherence:" },
1615
- });
1616
- }
1617
- }
1618
- return { kind: "feature", id: featureId, findings };
1619
- }
1620
- function coverageFinding(label, reqs) {
1621
- const missing = requirementsMissingScenarios(reqs);
1622
- if (missing.length === 0) {
1623
- return {
1624
- severity: "ok",
1625
- code: "requirements.covered",
1626
- message: `${label} covered (${reqs.length} requirement${reqs.length === 1 ? "" : "s"}, all with scenarios)`,
1627
- };
1628
- }
1629
- return {
1630
- severity: "error",
1631
- code: "requirements.missing-scenarios",
1632
- message: `${label}: ${missing.length} requirement(s) without a scenario`,
1633
- details: missing.map((r) => r.name),
1634
- text: { detailPrefix: "- " },
1635
- };
1636
- }
1637
- /* ------------------------------------------------------------------ */
1638
- /* Text renderer */
1639
- /* ------------------------------------------------------------------ */
1640
- /**
1641
- * `--errors-only` is a RENDERING lever, the way `--strict` is an exit-code
1642
- * lever: neither changes the report, and the `--json` payload is unaffected by
1643
- * both. On a fleet of a hundred services the clean run prints several hundred
1644
- * `ok` confirmations, and the two warnings that matter are somewhere inside it;
1645
- * anyone reading a CI log wants the exceptions, and anyone auditing wants all
1646
- * of it. Both are available, from the same run, and neither is the default.
1647
- */
1648
- function renderText(targets, all, unverifiable, errorsOnly) {
1649
- for (const t of targets) {
1650
- const shown = errorsOnly ? t.findings.filter((f) => f.severity !== "ok") : t.findings;
1651
- if (shown.length === 0)
1652
- continue;
1653
- // A feature announces itself; a service's findings already carry its name.
1654
- if (t.kind === "feature")
1655
- console.log(t.id);
1656
- let header;
1657
- for (const f of shown) {
1658
- const hint = f.text ?? {};
1659
- if (hint.header && hint.header !== header) {
1660
- header = hint.header;
1661
- console.log(` ${header}`);
1662
- }
1663
- // The whole report goes to stdout, in document order. Splitting errors
1664
- // onto stderr meant a piped stdout silently lost them from the middle of
1665
- // the report and 2>&1 could reorder it; the exit code carries failure,
1666
- // and stderr stays reserved for refusals (the fail() path).
1667
- const marker = hint.marker === false ? "" : `${SEVERITY_MARK[f.severity]} `;
1668
- console.log(`${" ".repeat(hint.indent ?? 0)}${marker}${f.message}`);
1669
- for (const d of f.details ?? [])
1670
- console.log(` ${hint.detailPrefix ?? ""}${d}`);
1671
- }
1672
- }
1673
- if (!all) {
1674
- // Without the --all footer there would be nothing at all to print for a
1675
- // clean single target under --errors-only — and silence is the one output
1676
- // that must never mean "checked, fine".
1677
- if (errorsOnly && targets.every((t) => t.findings.every((f) => f.severity === "ok"))) {
1678
- console.log(`${targets.map((t) => t.id).join(", ")}: no errors or warnings`);
1679
- }
1680
- return;
1681
- }
1682
- const s = summary(targets);
1683
- console.log(`\n${plural(s.services, "service")}, ${plural(s.features, "feature")} — ` +
1684
- `${plural(s.errors, "error")}, ${plural(s.warnings, "warning")}`);
1685
- // One line for the whole fleet, never one per service: honest about the blind
1686
- // spot without drowning the report in a hundred copies of it.
1687
- if (unverifiable > 0) {
1688
- const whose = unverifiable === 1 ? "1 service's" : `${unverifiable} services'`;
1689
- console.log(`⚠ sources.unverifiable-from-here: ${whose} sources can only be checked from their own repos`);
1690
- }
1691
- }
1692
- /* ------------------------------------------------------------------ */
1693
- /**
1694
- * The two things a `sources` list can be that `serviceProvenance` cannot say.
1695
- *
1696
- * `sources.unverifiable-from-here` — the spec names sources and loam is NOT in
1697
- * that service's repository, so every sources check (existence, digest,
1698
- * staleness) is skipped. serviceProvenance returns an empty list in that case
1699
- * and the silence read as "checked and fine". It used to be counted only under
1700
- * `--all` and printed as one rollup line, which meant `validate --service X`
1701
- * run from the docs repo — the single most common way anyone looks at one
1702
- * service — reported nothing at all about its own blind spot. Severity `ok`,
1703
- * deliberately: nothing is WRONG with the docs, the check simply cannot run
1704
- * here, and grading it a warning would make a correctly-adopted fleet
1705
- * permanently yellow and `--strict` permanently red in the docs repo's CI.
1706
- *
1707
- * `sources.empty` — the paths exist, and expand to no files at all: an empty
1708
- * directory, or a tree the repository itself ignores. A digest over nothing
1709
- * never changes, so the stamp would read as current forever. `loam vouch`
1710
- * already refuses to stamp it; until now `validate` said nothing, so an author
1711
- * got a green run followed by a refusal, two commands contradicting each other
1712
- * about one document. The sentence comes from `emptySourcesMessage`, the same
1713
- * definition vouch refuses with, under the label vouch uses.
1714
- */
1715
- async function sourceScopeFindings(docsDir, service, repoDir) {
1716
- const paths = servicePaths(docsDir, service);
1717
- const out = [];
1718
- // The axis pair is SPEC_AXES', not this function's: `serviceProvenance` grades
1719
- // the same two files from the same list, and a scope check that walked a
1720
- // shorter list than the grading would go quiet on the axis it forgot.
1721
- for (const { path, file } of SPEC_AXES.map((axis) => ({ path: paths[axis.key], file: axis.file }))) {
1722
- if (!existsSync(path))
1723
- continue;
1724
- const sources = listField(await readFrontmatter(path), "sources");
1725
- if (sources.length === 0)
1726
- continue;
1727
- // vouch's own labelling: a bare service id for spec.md, qualified for the
1728
- // arch axis. The refusal and the finding must be the same sentence.
1729
- const label = file === "spec.md" ? service : `${service}: ${file}`;
1730
- if (repoDir === undefined) {
1731
- out.push({
1732
- severity: "ok",
1733
- code: UNVERIFIABLE,
1734
- subject: service,
1735
- message: `${label}: ${sources.length} source(s) declared, but this is not ${service}'s repository — nothing here can resolve them. Run \`loam validate --service ${service}\` from inside it.`,
1736
- });
1737
- continue;
1738
- }
1739
- // Every other shape of broken list is serviceProvenance's to grade, and
1740
- // grading them twice would send an author fixing one thing from two
1741
- // findings. "Covers no files" is only meaningful once the paths are real.
1742
- if (patternSources(sources).length > 0 ||
1743
- unsafeSources(repoDir, sources).length > 0 ||
1744
- missingSources(repoDir, sources).length > 0) {
1745
- continue;
1746
- }
1747
- const expansion = await expandSourceFiles(repoDir, sources, label);
1748
- if (expansion.files.length > 0)
1749
- continue;
1750
- out.push({
1751
- severity: "warn",
1752
- code: "sources.empty",
1753
- subject: service,
1754
- message: expansion.empty ?? emptySourcesMessage(label, sources),
1755
- });
1756
- }
1757
- return out;
1758
- }
1759
- /** Heuristic: an edge is "covered" if a scenario names the target or a keyword from the edge title. */
1760
- function edgeCovered(target, title, scenarioText) {
1761
- if (scenarioText.includes(target.toLowerCase()))
1762
- return true;
1763
- for (const token of (title ?? "").split(/[^A-Za-z0-9]+/)) {
1764
- if (token.length >= 5 && scenarioText.includes(token.toLowerCase()))
1765
- return true;
1766
- }
1767
- return false;
1768
- }
1769
- function errorText(e) {
1770
- return typeof e.line === "number" ? `L${e.line}: ${e.message}` : e.message;
1771
- }
1772
- export { targetValid };
1773
- //# sourceMappingURL=validate.js.map