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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (691) hide show
  1. package/CHANGELOG.md +1477 -1
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +40 -22
  5. package/QUICKSTART.md +181 -9
  6. package/README.md +256 -57
  7. package/ROADMAP.md +108 -11
  8. package/SCHEMA.md +721 -130
  9. package/WORKFLOW.md +272 -62
  10. package/dist/cli.js +26 -8
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt/adopt.js +5 -10
  13. package/dist/commands/adopt/adopt.js.map +1 -1
  14. package/dist/commands/archive/archive.js +15 -18
  15. package/dist/commands/archive/archive.js.map +1 -1
  16. package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
  17. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  18. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  19. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/openapi.js +25 -42
  21. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
  23. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  24. package/dist/commands/archive/plan/gate.js +3 -7
  25. package/dist/commands/archive/plan/gate.js.map +1 -1
  26. package/dist/commands/archive/plan/landscape.js +4 -4
  27. package/dist/commands/archive/plan/landscape.js.map +1 -1
  28. package/dist/commands/archive/plan/refusal.js +8 -18
  29. package/dist/commands/archive/plan/refusal.js.map +1 -1
  30. package/dist/commands/archive/plan/specs.js +134 -25
  31. package/dist/commands/archive/plan/specs.js.map +1 -1
  32. package/dist/commands/archive/plan/state.js +1 -0
  33. package/dist/commands/archive/plan/state.js.map +1 -1
  34. package/dist/commands/archive/run.js +82 -33
  35. package/dist/commands/archive/run.js.map +1 -1
  36. package/dist/commands/archive/shipped.js +49 -0
  37. package/dist/commands/archive/shipped.js.map +1 -0
  38. package/dist/commands/context/context.js +4 -7
  39. package/dist/commands/context/context.js.map +1 -1
  40. package/dist/commands/context/print.js +2 -2
  41. package/dist/commands/context/print.js.map +1 -1
  42. package/dist/commands/delta/delta.js +44 -12
  43. package/dist/commands/delta/delta.js.map +1 -1
  44. package/dist/commands/delta/print.js +1 -1
  45. package/dist/commands/delta/print.js.map +1 -1
  46. package/dist/commands/dependencies.js +3 -7
  47. package/dist/commands/dependencies.js.map +1 -1
  48. package/dist/commands/diff/diff.js +20 -27
  49. package/dist/commands/diff/diff.js.map +1 -1
  50. package/dist/commands/explore.js +4 -8
  51. package/dist/commands/explore.js.map +1 -1
  52. package/dist/commands/gate/gate.js +45 -8
  53. package/dist/commands/gate/gate.js.map +1 -1
  54. package/dist/commands/gate/print.js +15 -3
  55. package/dist/commands/gate/print.js.map +1 -1
  56. package/dist/commands/gherkin/commit.js +13 -54
  57. package/dist/commands/gherkin/commit.js.map +1 -1
  58. package/dist/commands/gherkin/gherkin.js +36 -20
  59. package/dist/commands/gherkin/gherkin.js.map +1 -1
  60. package/dist/commands/gherkin/reconcile.js +10 -13
  61. package/dist/commands/gherkin/reconcile.js.map +1 -1
  62. package/dist/commands/init/first-hour.js +44 -0
  63. package/dist/commands/init/first-hour.js.map +1 -1
  64. package/dist/commands/init/init.js +18 -22
  65. package/dist/commands/init/init.js.map +1 -1
  66. package/dist/commands/list/campaign/campaign.js +4 -6
  67. package/dist/commands/list/campaign/campaign.js.map +1 -1
  68. package/dist/commands/list/json.js +0 -4
  69. package/dist/commands/list/json.js.map +1 -1
  70. package/dist/commands/list/list.js +38 -23
  71. package/dist/commands/list/list.js.map +1 -1
  72. package/dist/commands/list/print.js +38 -6
  73. package/dist/commands/list/print.js.map +1 -1
  74. package/dist/commands/list/suites/suites.js +140 -0
  75. package/dist/commands/list/suites/suites.js.map +1 -0
  76. package/dist/commands/list/views.js +28 -33
  77. package/dist/commands/list/views.js.map +1 -1
  78. package/dist/commands/mcp/dispatch.js +91 -36
  79. package/dist/commands/mcp/dispatch.js.map +1 -1
  80. package/dist/commands/mcp/mcp.js +56 -15
  81. package/dist/commands/mcp/mcp.js.map +1 -1
  82. package/dist/commands/mcp/serve.js.map +1 -1
  83. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  84. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  85. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  86. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  87. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  88. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  89. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  90. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  91. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  92. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  93. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  94. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  95. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  96. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  97. package/dist/commands/new/capability-template.js +118 -0
  98. package/dist/commands/new/capability-template.js.map +1 -0
  99. package/dist/commands/new/commit.js +8 -22
  100. package/dist/commands/new/commit.js.map +1 -1
  101. package/dist/commands/new/modifies.js +206 -0
  102. package/dist/commands/new/modifies.js.map +1 -0
  103. package/dist/commands/new/new.js +59 -14
  104. package/dist/commands/new/new.js.map +1 -1
  105. package/dist/commands/new/templates.js +48 -124
  106. package/dist/commands/new/templates.js.map +1 -1
  107. package/dist/commands/open.js +3 -7
  108. package/dist/commands/open.js.map +1 -1
  109. package/dist/commands/policy/gate.js +27 -0
  110. package/dist/commands/policy/gate.js.map +1 -1
  111. package/dist/commands/rebase/plan.js +2 -2
  112. package/dist/commands/rebase/plan.js.map +1 -1
  113. package/dist/commands/rebase/rebase.js +60 -35
  114. package/dist/commands/rebase/rebase.js.map +1 -1
  115. package/dist/commands/seed/commit.js +55 -26
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/plan.js +64 -13
  118. package/dist/commands/seed/plan.js.map +1 -1
  119. package/dist/commands/seed/seed.js +6 -9
  120. package/dist/commands/seed/seed.js.map +1 -1
  121. package/dist/commands/show/show.js +3 -7
  122. package/dist/commands/show/show.js.map +1 -1
  123. package/dist/commands/status/print.js +13 -4
  124. package/dist/commands/status/print.js.map +1 -1
  125. package/dist/commands/status/status.js +3 -7
  126. package/dist/commands/status/status.js.map +1 -1
  127. package/dist/commands/steps/steps.js +3 -7
  128. package/dist/commands/steps/steps.js.map +1 -1
  129. package/dist/commands/subsystem/edit.js +7 -7
  130. package/dist/commands/subsystem/edit.js.map +1 -1
  131. package/dist/commands/subsystem/move.js +60 -25
  132. package/dist/commands/subsystem/move.js.map +1 -1
  133. package/dist/commands/subsystem/query.js +2 -4
  134. package/dist/commands/subsystem/query.js.map +1 -1
  135. package/dist/commands/subsystem/subsystem.js +4 -7
  136. package/dist/commands/subsystem/subsystem.js.map +1 -1
  137. package/dist/commands/subsystem/sync.js +141 -71
  138. package/dist/commands/subsystem/sync.js.map +1 -1
  139. package/dist/commands/subsystem/txn/report.js +55 -5
  140. package/dist/commands/subsystem/txn/report.js.map +1 -1
  141. package/dist/commands/subsystem/txn/txn.js +121 -45
  142. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  143. package/dist/commands/subsystem/txn/views.js +82 -12
  144. package/dist/commands/subsystem/txn/views.js.map +1 -1
  145. package/dist/commands/tasks/render.js +116 -0
  146. package/dist/commands/tasks/render.js.map +1 -0
  147. package/dist/commands/tasks/tasks.js +113 -0
  148. package/dist/commands/tasks/tasks.js.map +1 -0
  149. package/dist/commands/unarchive/manifest.js +6 -9
  150. package/dist/commands/unarchive/manifest.js.map +1 -1
  151. package/dist/commands/unarchive/restore.js +17 -28
  152. package/dist/commands/unarchive/restore.js.map +1 -1
  153. package/dist/commands/unarchive/unarchive.js +22 -30
  154. package/dist/commands/unarchive/unarchive.js.map +1 -1
  155. package/dist/commands/validate/arch-coverage.js +6 -15
  156. package/dist/commands/validate/arch-coverage.js.map +1 -1
  157. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  158. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  159. package/dist/commands/validate/checks/health-deps.js +9 -6
  160. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  161. package/dist/commands/validate/checks/requirements.js +18 -8
  162. package/dist/commands/validate/checks/requirements.js.map +1 -1
  163. package/dist/commands/validate/checks/sources.js +6 -2
  164. package/dist/commands/validate/checks/sources.js.map +1 -1
  165. package/dist/commands/validate/feature.js +23 -19
  166. package/dist/commands/validate/feature.js.map +1 -1
  167. package/dist/commands/validate/fleet/census.js +26 -3
  168. package/dist/commands/validate/fleet/census.js.map +1 -1
  169. package/dist/commands/validate/fleet/landscape.js +35 -32
  170. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  171. package/dist/commands/validate/fleet/map/attest.js +7 -12
  172. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  173. package/dist/commands/validate/fleet/map/bindings.js +16 -5
  174. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  175. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  176. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  177. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  178. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  179. package/dist/commands/validate/fleet/obligations.js +1 -5
  180. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  181. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  182. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  183. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  184. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  185. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  186. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  187. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  188. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  189. package/dist/commands/validate/fleet/views/ids.js +33 -7
  190. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  191. package/dist/commands/validate/fleet/views/root-documents.js +17 -35
  192. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  193. package/dist/commands/validate/fleet/views/stale.js +31 -14
  194. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  195. package/dist/commands/validate/links/corpus.js +1 -4
  196. package/dist/commands/validate/links/corpus.js.map +1 -1
  197. package/dist/commands/validate/links/glossary.js.map +1 -1
  198. package/dist/commands/validate/report.js +5 -4
  199. package/dist/commands/validate/report.js.map +1 -1
  200. package/dist/commands/validate/scope.js +113 -0
  201. package/dist/commands/validate/scope.js.map +1 -0
  202. package/dist/commands/validate/service/api.js +16 -104
  203. package/dist/commands/validate/service/api.js.map +1 -1
  204. package/dist/commands/validate/service/contracts/generated.js +134 -0
  205. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  206. package/dist/commands/validate/service/events/events.js +1 -1
  207. package/dist/commands/validate/service/events/events.js.map +1 -1
  208. package/dist/commands/validate/service/events/message-producers.js +1 -1
  209. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  210. package/dist/commands/validate/service/evidence-pins.js +56 -10
  211. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  212. package/dist/commands/validate/service/service.js +16 -26
  213. package/dist/commands/validate/service/service.js.map +1 -1
  214. package/dist/commands/validate/service/specs.js +31 -38
  215. package/dist/commands/validate/service/specs.js.map +1 -1
  216. package/dist/commands/validate/service/spine.js +8 -11
  217. package/dist/commands/validate/service/spine.js.map +1 -1
  218. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  219. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  220. package/dist/commands/validate/validate.js +44 -26
  221. package/dist/commands/validate/validate.js.map +1 -1
  222. package/dist/commands/verify/cross/diff.js +2 -6
  223. package/dist/commands/verify/cross/diff.js.map +1 -1
  224. package/dist/commands/verify/evidence/contract.js +1 -1
  225. package/dist/commands/verify/evidence/contract.js.map +1 -1
  226. package/dist/commands/verify/evidence/read.js +12 -3
  227. package/dist/commands/verify/evidence/read.js.map +1 -1
  228. package/dist/commands/verify/frozen.js +45 -23
  229. package/dist/commands/verify/frozen.js.map +1 -1
  230. package/dist/commands/verify/record.js +69 -17
  231. package/dist/commands/verify/record.js.map +1 -1
  232. package/dist/commands/verify/report.js +27 -44
  233. package/dist/commands/verify/report.js.map +1 -1
  234. package/dist/commands/verify/results.js +112 -30
  235. package/dist/commands/verify/results.js.map +1 -1
  236. package/dist/commands/verify/verify.js +54 -51
  237. package/dist/commands/verify/verify.js.map +1 -1
  238. package/dist/commands/vouch/pack/pack.js +2 -2
  239. package/dist/commands/vouch/pack/pack.js.map +1 -1
  240. package/dist/commands/vouch/pack/print.js +1 -6
  241. package/dist/commands/vouch/pack/print.js.map +1 -1
  242. package/dist/commands/vouch/pack/sections.js +1 -2
  243. package/dist/commands/vouch/pack/sections.js.map +1 -1
  244. package/dist/commands/vouch/run.js +26 -81
  245. package/dist/commands/vouch/run.js.map +1 -1
  246. package/dist/commands/vouch/sample/plan.js +20 -34
  247. package/dist/commands/vouch/sample/plan.js.map +1 -1
  248. package/dist/commands/vouch/vet/verify.js +39 -7
  249. package/dist/commands/vouch/vet/verify.js.map +1 -1
  250. package/dist/commands/vouch/vouch.js +10 -17
  251. package/dist/commands/vouch/vouch.js.map +1 -1
  252. package/dist/core/agent/agents-md/command-map.js +57 -102
  253. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  254. package/dist/core/agent/agents-md/map/explain.js +4 -3
  255. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  256. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  257. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  258. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  259. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  260. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  261. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  262. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  263. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  264. package/dist/core/agent/agents-md/map/status.js +9 -3
  265. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  266. package/dist/core/agent/agents-md/map/subsystem.js +31 -17
  267. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  268. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  269. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  270. package/dist/core/agent/agents-md/refusals.js +37 -21
  271. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  272. package/dist/core/agent/agents-md.js +25 -10
  273. package/dist/core/agent/agents-md.js.map +1 -1
  274. package/dist/core/agent/agents-stamp.js +10 -0
  275. package/dist/core/agent/agents-stamp.js.map +1 -1
  276. package/dist/core/agent/scaffold.js +10 -15
  277. package/dist/core/agent/scaffold.js.map +1 -1
  278. package/dist/core/agent/tools/dialects.js +1 -0
  279. package/dist/core/agent/tools/dialects.js.map +1 -1
  280. package/dist/core/agent/tools/registry.js +41 -76
  281. package/dist/core/agent/tools/registry.js.map +1 -1
  282. package/dist/core/agent/workflows/adopt.js +5 -3
  283. package/dist/core/agent/workflows/adopt.js.map +1 -1
  284. package/dist/core/agent/workflows/check.js +25 -22
  285. package/dist/core/agent/workflows/check.js.map +1 -1
  286. package/dist/core/agent/workflows/closing.js +54 -21
  287. package/dist/core/agent/workflows/closing.js.map +1 -1
  288. package/dist/core/agent/workflows/feature.js +79 -11
  289. package/dist/core/agent/workflows/feature.js.map +1 -1
  290. package/dist/core/agent/workflows/implement.js +91 -13
  291. package/dist/core/agent/workflows/implement.js.map +1 -1
  292. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  293. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  294. package/dist/core/agent/workflows/reference/codes.js +8 -6
  295. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  296. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  297. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  298. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  299. package/dist/core/agent/workflows/reference/spine.js +62 -24
  300. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  301. package/dist/core/asyncapi/baseline/plan.js +14 -81
  302. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  303. package/dist/core/asyncapi/digest.js +30 -25
  304. package/dist/core/asyncapi/digest.js.map +1 -1
  305. package/dist/core/asyncapi/merge/components.js +5 -4
  306. package/dist/core/asyncapi/merge/components.js.map +1 -1
  307. package/dist/core/asyncapi/merge/error.js +7 -2
  308. package/dist/core/asyncapi/merge/error.js.map +1 -1
  309. package/dist/core/asyncapi/merge/markers.js +30 -50
  310. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  311. package/dist/core/asyncapi/merge/merge.js +31 -53
  312. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  313. package/dist/core/asyncapi/merge/pin.js +10 -36
  314. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  315. package/dist/core/asyncapi/model.js.map +1 -1
  316. package/dist/core/asyncapi/read.js +36 -12
  317. package/dist/core/asyncapi/read.js.map +1 -1
  318. package/dist/core/brief/brief.js +30 -25
  319. package/dist/core/brief/brief.js.map +1 -1
  320. package/dist/core/brief/landscape.js +8 -12
  321. package/dist/core/brief/landscape.js.map +1 -1
  322. package/dist/core/brief/map/extend.js +8 -5
  323. package/dist/core/brief/map/extend.js.map +1 -1
  324. package/dist/core/brief/map/owed.js +8 -26
  325. package/dist/core/brief/map/owed.js.map +1 -1
  326. package/dist/core/brief/targets.js +79 -5
  327. package/dist/core/brief/targets.js.map +1 -1
  328. package/dist/core/brief/unchecked.js +1 -1
  329. package/dist/core/brief/unchecked.js.map +1 -1
  330. package/dist/core/c4/likec4.js +147 -16
  331. package/dist/core/c4/likec4.js.map +1 -1
  332. package/dist/core/c4/locate/unlocated.js +150 -0
  333. package/dist/core/c4/locate/unlocated.js.map +1 -0
  334. package/dist/core/c4/parsed/values.js +54 -1
  335. package/dist/core/c4/parsed/values.js.map +1 -1
  336. package/dist/core/c4/prefetch/fleet.js +1 -9
  337. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  338. package/dist/core/c4/project/architecture.js +31 -9
  339. package/dist/core/c4/project/architecture.js.map +1 -1
  340. package/dist/core/c4/project/documents.js +36 -10
  341. package/dist/core/c4/project/documents.js.map +1 -1
  342. package/dist/core/c4/project/load.js +14 -21
  343. package/dist/core/c4/project/load.js.map +1 -1
  344. package/dist/core/c4/project/staged.js +7 -3
  345. package/dist/core/c4/project/staged.js.map +1 -1
  346. package/dist/core/c4/resolve/external.js +51 -0
  347. package/dist/core/c4/resolve/external.js.map +1 -0
  348. package/dist/core/c4/resolve/service.js +25 -1
  349. package/dist/core/c4/resolve/service.js.map +1 -1
  350. package/dist/core/c4/seed/template.js +15 -1
  351. package/dist/core/c4/seed/template.js.map +1 -1
  352. package/dist/core/c4/service-model/shape.js +2 -22
  353. package/dist/core/c4/service-model/shape.js.map +1 -1
  354. package/dist/core/c4/source-mask.js +288 -4
  355. package/dist/core/c4/source-mask.js.map +1 -1
  356. package/dist/core/c4/source-scan.js +203 -147
  357. package/dist/core/c4/source-scan.js.map +1 -1
  358. package/dist/core/c4/splice/authored-source.js +19 -0
  359. package/dist/core/c4/splice/authored-source.js.map +1 -1
  360. package/dist/core/c4/splice/delta-blocks.js +5 -18
  361. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  362. package/dist/core/c4/splice/landscape-merge.js +13 -8
  363. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  364. package/dist/core/c4/splice/model/merge.js +10 -6
  365. package/dist/core/c4/splice/model/merge.js.map +1 -1
  366. package/dist/core/c4/splice/placement.js +108 -18
  367. package/dist/core/c4/splice/placement.js.map +1 -1
  368. package/dist/core/c4/workspace.js +13 -22
  369. package/dist/core/c4/workspace.js.map +1 -1
  370. package/dist/core/capabilities/capabilities.js +6 -6
  371. package/dist/core/capabilities/capabilities.js.map +1 -1
  372. package/dist/core/capabilities/delta/doc.js +7 -10
  373. package/dist/core/capabilities/delta/doc.js.map +1 -1
  374. package/dist/core/capabilities/findings.js +15 -20
  375. package/dist/core/capabilities/findings.js.map +1 -1
  376. package/dist/core/capabilities/realizes/findings.js +12 -19
  377. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  378. package/dist/core/capabilities/realizes/join.js +0 -20
  379. package/dist/core/capabilities/realizes/join.js.map +1 -1
  380. package/dist/core/capabilities/rollup.js +6 -0
  381. package/dist/core/capabilities/rollup.js.map +1 -1
  382. package/dist/core/capabilities/tree.js +6 -28
  383. package/dist/core/capabilities/tree.js.map +1 -1
  384. package/dist/core/capabilities/usecase-join.js +3 -13
  385. package/dist/core/capabilities/usecase-join.js.map +1 -1
  386. package/dist/core/coherence/authoring/scaffold.js +11 -3
  387. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  388. package/dist/core/coherence/authoring/sentinels.js +2 -1
  389. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  390. package/dist/core/coherence/coherence.js +30 -16
  391. package/dist/core/coherence/coherence.js.map +1 -1
  392. package/dist/core/coherence/consumed/operations.js +70 -0
  393. package/dist/core/coherence/consumed/operations.js.map +1 -0
  394. package/dist/core/coherence/consumed/refusal.js +38 -0
  395. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  396. package/dist/core/coherence/declared.js +60 -34
  397. package/dist/core/coherence/declared.js.map +1 -1
  398. package/dist/core/coherence/events/declared.js +44 -59
  399. package/dist/core/coherence/events/declared.js.map +1 -1
  400. package/dist/core/coherence/events/events.js +119 -8
  401. package/dist/core/coherence/events/events.js.map +1 -1
  402. package/dist/core/coherence/events/grades.js +31 -8
  403. package/dist/core/coherence/events/grades.js.map +1 -1
  404. package/dist/core/coherence/events/lookups.js +14 -82
  405. package/dist/core/coherence/events/lookups.js.map +1 -1
  406. package/dist/core/coherence/lookups.js +18 -78
  407. package/dist/core/coherence/lookups.js.map +1 -1
  408. package/dist/core/coherence/retired/archived.js +285 -0
  409. package/dist/core/coherence/retired/archived.js.map +1 -0
  410. package/dist/core/coherence/retired/event-pair.js +62 -0
  411. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  412. package/dist/core/coherence/retired/merged.js +79 -0
  413. package/dist/core/coherence/retired/merged.js.map +1 -0
  414. package/dist/core/coherence/retired/retirement.js +49 -0
  415. package/dist/core/coherence/retired/retirement.js.map +1 -0
  416. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  417. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  418. package/dist/core/coherence/retired/revival/revived.js +297 -0
  419. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  420. package/dist/core/consumers/after.js +113 -0
  421. package/dist/core/consumers/after.js.map +1 -0
  422. package/dist/core/consumers/consumers.js +296 -0
  423. package/dist/core/consumers/consumers.js.map +1 -0
  424. package/dist/core/consumers/current.js +211 -0
  425. package/dist/core/consumers/current.js.map +1 -0
  426. package/dist/core/consumers/scan.js +46 -0
  427. package/dist/core/consumers/scan.js.map +1 -0
  428. package/dist/core/consumers/state.js +27 -0
  429. package/dist/core/consumers/state.js.map +1 -0
  430. package/dist/core/delta/delta.js +1 -1
  431. package/dist/core/delta/delta.js.map +1 -1
  432. package/dist/core/deployment/delta.js +15 -16
  433. package/dist/core/deployment/delta.js.map +1 -1
  434. package/dist/core/diff/base-git.js +94 -12
  435. package/dist/core/diff/base-git.js.map +1 -1
  436. package/dist/core/diff/base-state.js +28 -16
  437. package/dist/core/diff/base-state.js.map +1 -1
  438. package/dist/core/diff/joins.js +136 -0
  439. package/dist/core/diff/joins.js.map +1 -0
  440. package/dist/core/diff/scope/changed-paths.js +22 -6
  441. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  442. package/dist/core/diff/scope/edges.js +82 -0
  443. package/dist/core/diff/scope/edges.js.map +1 -0
  444. package/dist/core/diff/semantic.js +100 -31
  445. package/dist/core/diff/semantic.js.map +1 -1
  446. package/dist/core/doctor/agents.js +34 -18
  447. package/dist/core/doctor/agents.js.map +1 -1
  448. package/dist/core/doctor/report.js.map +1 -1
  449. package/dist/core/document/apply.js +10 -2
  450. package/dist/core/document/apply.js.map +1 -1
  451. package/dist/core/document/frontmatter.js +55 -0
  452. package/dist/core/document/frontmatter.js.map +1 -1
  453. package/dist/core/document/parse.js +29 -6
  454. package/dist/core/document/parse.js.map +1 -1
  455. package/dist/core/document/scenarios.js +17 -0
  456. package/dist/core/document/scenarios.js.map +1 -1
  457. package/dist/core/document/spec.js +4 -11
  458. package/dist/core/document/spec.js.map +1 -1
  459. package/dist/core/envelope/config.js +10 -0
  460. package/dist/core/envelope/config.js.map +1 -1
  461. package/dist/core/envelope/json.js +34 -0
  462. package/dist/core/envelope/json.js.map +1 -1
  463. package/dist/core/explain/families.js +17 -12
  464. package/dist/core/explain/families.js.map +1 -1
  465. package/dist/core/explain/refusals.js +3 -3
  466. package/dist/core/explain/refusals.js.map +1 -1
  467. package/dist/core/explain/terms.js +18 -9
  468. package/dist/core/explain/terms.js.map +1 -1
  469. package/dist/core/explore/explore.js +2 -7
  470. package/dist/core/explore/explore.js.map +1 -1
  471. package/dist/core/fleet-context.js +34 -95
  472. package/dist/core/fleet-context.js.map +1 -1
  473. package/dist/core/gate/checks.js +10 -3
  474. package/dist/core/gate/checks.js.map +1 -1
  475. package/dist/core/gate/partners.js +64 -19
  476. package/dist/core/gate/partners.js.map +1 -1
  477. package/dist/core/gate/report.js.map +1 -1
  478. package/dist/core/gate/verification.js +75 -12
  479. package/dist/core/gate/verification.js.map +1 -1
  480. package/dist/core/gherkin/stale.js +13 -37
  481. package/dist/core/gherkin/stale.js.map +1 -1
  482. package/dist/core/glossary/tree.js +6 -19
  483. package/dist/core/glossary/tree.js.map +1 -1
  484. package/dist/core/kernel/concurrency.js +19 -0
  485. package/dist/core/kernel/concurrency.js.map +1 -1
  486. package/dist/core/kernel/records.js +11 -0
  487. package/dist/core/kernel/records.js.map +1 -1
  488. package/dist/core/links/backlinks.js +1 -3
  489. package/dist/core/links/backlinks.js.map +1 -1
  490. package/dist/core/links/corpus.js +5 -36
  491. package/dist/core/links/corpus.js.map +1 -1
  492. package/dist/core/mcp/argv.js +101 -0
  493. package/dist/core/mcp/argv.js.map +1 -1
  494. package/dist/core/mcp/author-tools.js +2 -1
  495. package/dist/core/mcp/author-tools.js.map +1 -1
  496. package/dist/core/mcp/protocol.js +2 -2
  497. package/dist/core/mcp/protocol.js.map +1 -1
  498. package/dist/core/mcp/tools.js +43 -33
  499. package/dist/core/mcp/tools.js.map +1 -1
  500. package/dist/core/openapi/baseline/gate.js +53 -12
  501. package/dist/core/openapi/baseline/gate.js.map +1 -1
  502. package/dist/core/openapi/baseline/plan.js +22 -8
  503. package/dist/core/openapi/baseline/plan.js.map +1 -1
  504. package/dist/core/openapi/baseline/record.js +0 -1
  505. package/dist/core/openapi/baseline/record.js.map +1 -1
  506. package/dist/core/openapi/contract-text.js +33 -0
  507. package/dist/core/openapi/contract-text.js.map +1 -0
  508. package/dist/core/openapi/contract-tree.js +56 -0
  509. package/dist/core/openapi/contract-tree.js.map +1 -0
  510. package/dist/core/openapi/depth.js +38 -24
  511. package/dist/core/openapi/depth.js.map +1 -1
  512. package/dist/core/openapi/digest.js +52 -2
  513. package/dist/core/openapi/digest.js.map +1 -1
  514. package/dist/core/openapi/doc.js +12 -4
  515. package/dist/core/openapi/doc.js.map +1 -1
  516. package/dist/core/openapi/merge/components.js +25 -60
  517. package/dist/core/openapi/merge/components.js.map +1 -1
  518. package/dist/core/openapi/merge/error.js +3 -13
  519. package/dist/core/openapi/merge/error.js.map +1 -1
  520. package/dist/core/openapi/merge/markers.js +26 -30
  521. package/dist/core/openapi/merge/markers.js.map +1 -1
  522. package/dist/core/openapi/merge/merge.js +26 -52
  523. package/dist/core/openapi/merge/merge.js.map +1 -1
  524. package/dist/core/openapi/merge/pin.js +15 -35
  525. package/dist/core/openapi/merge/pin.js.map +1 -1
  526. package/dist/core/openspec/decide/verdict.js.map +1 -1
  527. package/dist/core/openspec/inventory.js +4 -5
  528. package/dist/core/openspec/inventory.js.map +1 -1
  529. package/dist/core/openspec/model/mapping.js +30 -28
  530. package/dist/core/openspec/model/mapping.js.map +1 -1
  531. package/dist/core/openspec/model/model.js +12 -7
  532. package/dist/core/openspec/model/model.js.map +1 -1
  533. package/dist/core/openspec/scan/changes.js +10 -4
  534. package/dist/core/openspec/scan/changes.js.map +1 -1
  535. package/dist/core/openspec/scan/config.js +7 -16
  536. package/dist/core/openspec/scan/config.js.map +1 -1
  537. package/dist/core/openspec/scan/shape.js +63 -19
  538. package/dist/core/openspec/scan/shape.js.map +1 -1
  539. package/dist/core/openspec/scan/walk.js +21 -38
  540. package/dist/core/openspec/scan/walk.js.map +1 -1
  541. package/dist/core/pack/living.js +2 -16
  542. package/dist/core/pack/living.js.map +1 -1
  543. package/dist/core/provenance/findings.js +48 -12
  544. package/dist/core/provenance/findings.js.map +1 -1
  545. package/dist/core/provenance/git.js +33 -57
  546. package/dist/core/provenance/git.js.map +1 -1
  547. package/dist/core/provenance/gitq/moves.js +71 -114
  548. package/dist/core/provenance/gitq/moves.js.map +1 -1
  549. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  550. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  551. package/dist/core/provenance/sources.js +23 -6
  552. package/dist/core/provenance/sources.js.map +1 -1
  553. package/dist/core/provenance/stamp.js +5 -17
  554. package/dist/core/provenance/stamp.js.map +1 -1
  555. package/dist/core/repo/entries.js.map +1 -1
  556. package/dist/core/repo/paths.js +34 -29
  557. package/dist/core/repo/paths.js.map +1 -1
  558. package/dist/core/repo/repo.js +24 -14
  559. package/dist/core/repo/repo.js.map +1 -1
  560. package/dist/core/repo/tree/find.js +12 -3
  561. package/dist/core/repo/tree/find.js.map +1 -1
  562. package/dist/core/repo/tree/fs.js +27 -1
  563. package/dist/core/repo/tree/fs.js.map +1 -1
  564. package/dist/core/repo/tree/render/body.js +45 -6
  565. package/dist/core/repo/tree/render/body.js.map +1 -1
  566. package/dist/core/repo/tree/render/service/header.js +125 -0
  567. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  568. package/dist/core/repo/tree/render/service/peers.js +28 -0
  569. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  570. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  571. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  572. package/dist/core/repo/tree/render/service/views.js +78 -0
  573. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  574. package/dist/core/repo/tree/render/stale.js +19 -2
  575. package/dist/core/repo/tree/render/stale.js.map +1 -1
  576. package/dist/core/repo/tree/render/views.js +102 -33
  577. package/dist/core/repo/tree/render/views.js.map +1 -1
  578. package/dist/core/repo/tree/walk.js +8 -30
  579. package/dist/core/repo/tree/walk.js.map +1 -1
  580. package/dist/core/results.js +47 -12
  581. package/dist/core/results.js.map +1 -1
  582. package/dist/core/staging/commit.js +10 -12
  583. package/dist/core/staging/commit.js.map +1 -1
  584. package/dist/core/staging/lock.js +74 -12
  585. package/dist/core/staging/lock.js.map +1 -1
  586. package/dist/core/staging/recovery/intent.js +2 -5
  587. package/dist/core/staging/recovery/intent.js.map +1 -1
  588. package/dist/core/staging/recovery/recover.js +60 -1
  589. package/dist/core/staging/recovery/recover.js.map +1 -1
  590. package/dist/core/staging/snapshot/manifest.js +152 -0
  591. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  592. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  593. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  594. package/dist/core/status/actions/execution.js +11 -0
  595. package/dist/core/status/actions/execution.js.map +1 -1
  596. package/dist/core/status/feature/artifacts.js +2 -2
  597. package/dist/core/status/feature/artifacts.js.map +1 -1
  598. package/dist/core/status/feature/feature.js +5 -11
  599. package/dist/core/status/feature/feature.js.map +1 -1
  600. package/dist/core/status/fleet/fleet.js +6 -2
  601. package/dist/core/status/fleet/fleet.js.map +1 -1
  602. package/dist/core/status/fleet/next.js +26 -3
  603. package/dist/core/status/fleet/next.js.map +1 -1
  604. package/dist/core/status/report.js.map +1 -1
  605. package/dist/core/status/verification.js +30 -15
  606. package/dist/core/status/verification.js.map +1 -1
  607. package/dist/core/tasks/packet.js +174 -0
  608. package/dist/core/tasks/packet.js.map +1 -0
  609. package/dist/core/tasks/record.js +43 -0
  610. package/dist/core/tasks/record.js.map +1 -0
  611. package/dist/core/tasks/sources.js +181 -0
  612. package/dist/core/tasks/sources.js.map +1 -0
  613. package/dist/core/tasks/stamps.js +76 -0
  614. package/dist/core/tasks/stamps.js.map +1 -0
  615. package/dist/core/tasks/unit.js +0 -0
  616. package/dist/core/tasks/unit.js.map +1 -0
  617. package/dist/core/usecases/capability.js +11 -14
  618. package/dist/core/usecases/capability.js.map +1 -1
  619. package/dist/core/usecases/delta/flows.js +5 -7
  620. package/dist/core/usecases/delta/flows.js.map +1 -1
  621. package/dist/core/usecases/fleet.js +178 -31
  622. package/dist/core/usecases/fleet.js.map +1 -1
  623. package/dist/core/usecases/operations.js +5 -4
  624. package/dist/core/usecases/operations.js.map +1 -1
  625. package/dist/core/usecases/service/flows.js +9 -6
  626. package/dist/core/usecases/service/flows.js.map +1 -1
  627. package/dist/core/usecases/suites/participants.js +83 -0
  628. package/dist/core/usecases/suites/participants.js.map +1 -0
  629. package/dist/core/usecases/suites/suites.js +45 -0
  630. package/dist/core/usecases/suites/suites.js.map +1 -0
  631. package/dist/core/verify/answers.js +12 -5
  632. package/dist/core/verify/answers.js.map +1 -1
  633. package/dist/core/verify/build.js +102 -82
  634. package/dist/core/verify/build.js.map +1 -1
  635. package/dist/core/verify/checklist.js +45 -10
  636. package/dist/core/verify/checklist.js.map +1 -1
  637. package/dist/core/verify/location.js +67 -0
  638. package/dist/core/verify/location.js.map +1 -0
  639. package/dist/core/verify/pins/lint.js +3 -2
  640. package/dist/core/verify/pins/lint.js.map +1 -1
  641. package/dist/core/verify/pins/pin.js +1 -1
  642. package/dist/core/verify/pins/pin.js.map +1 -1
  643. package/dist/core/verify/read/file.js +224 -0
  644. package/dist/core/verify/read/file.js.map +1 -0
  645. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  646. package/dist/core/verify/read/shape.js.map +1 -0
  647. package/dist/core/verify/read/union.js +105 -0
  648. package/dist/core/verify/read/union.js.map +1 -0
  649. package/dist/core/verify/record.js +35 -12
  650. package/dist/core/verify/record.js.map +1 -1
  651. package/dist/core/verify/store/commit.js +40 -30
  652. package/dist/core/verify/store/commit.js.map +1 -1
  653. package/dist/core/verify/store/render.js +8 -4
  654. package/dist/core/verify/store/render.js.map +1 -1
  655. package/dist/core/vocabulary/issue.js +1 -0
  656. package/dist/core/vocabulary/issue.js.map +1 -1
  657. package/dist/core/vocabulary/maturity.js +53 -3
  658. package/dist/core/vocabulary/maturity.js.map +1 -1
  659. package/examples/README.md +28 -9
  660. package/examples/docs/architecture/landscape.likec4 +21 -7
  661. package/examples/docs/architecture/services.likec4 +70 -0
  662. package/examples/docs/architecture/subsystems.likec4 +3 -0
  663. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  664. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  665. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  666. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  667. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  668. package/examples/docs/services/order-service/model.likec4 +10 -8
  669. package/examples/docs/services/payment-service/model.likec4 +6 -4
  670. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  671. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  672. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  673. package/package.json +1 -1
  674. package/dist/commands/validate/checks/vocabulary.js +0 -16
  675. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  676. package/dist/core/agent/agents-md/artifacts.js +0 -221
  677. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  678. package/dist/core/agent/agents-md/cycle.js +0 -180
  679. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  680. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  681. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  682. package/dist/core/agent/agents-md/spine.js +0 -40
  683. package/dist/core/agent/agents-md/spine.js.map +0 -1
  684. package/dist/core/coherence/events/merged.js +0 -40
  685. package/dist/core/coherence/events/merged.js.map +0 -1
  686. package/dist/core/diff/current-state.js +0 -96
  687. package/dist/core/diff/current-state.js.map +0 -1
  688. package/dist/core/diff/victims.js +0 -256
  689. package/dist/core/diff/victims.js.map +0 -1
  690. package/dist/core/staging/snapshot.js.map +0 -1
  691. package/dist/core/verify/file.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"arch-coverage.js","sourceRoot":"","sources":["../../../src/commands/validate/arch-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Whether the architecture a delta INTRODUCES is covered by a requirement that\n * will actually merge.\n *\n * Three findings, one question asked three ways: `archedge.uncovered` (a\n * scenario names the new edge — heuristic, warn-only), `c4.uncovered` (an\n * arch.spec.md requirement covers the new element or edge — mechanical), and\n * `covers.unknown` (the `Covers:` line resolves to something real). This is\n * where agent-built code cuts its corners — the outbox, the retries, the alerts\n * — because no business scenario was ever going to mention them.\n *\n * Split out of `validateFeature` because it is the only part of the feature\n * target that needs the LIVING landscape: everything else there reads the\n * feature directory alone. Keeping the lazy load and its two exemption sets in\n * one module is what stops \"new\" being decided differently in three places.\n */\nimport { existsSync } from \"node:fs\";\nimport { loadFile, type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile } from \"../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../core/repo/service-target.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { type Finding } from \"../../core/vocabulary/report.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { coversEdge, coversElement, type CoverageScope } from \"../../core/c4/arch.js\";\nimport { readHealth } from \"../../core/vocabulary/health.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { ACTOR_KINDS, EXTERNAL_TAG } from \"../../core/vocabulary/maturity.js\";\nimport { coversEntries, coversUnknownFindings } from \"./checks/requirements.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\n/** One feature delta's architecture, and everything needed to decide what in it is new. */\nexport interface DeltaArch {\n docsDir: DocsDir;\n /** Every element the delta declares — the resolver's index, and `coversEdge`'s scope. */\n elements: Elem[];\n /** Every relationship the delta declares. */\n relationships: Rel[];\n /** The delta's own additions: elements and edges carrying the feature id as a tag. */\n taggedEls: Elem[];\n taggedRels: Rel[];\n /** Each addressed service's arch.spec.md delta, in enumeration order. */\n archDeltas: Array<{ service: PathableService; reqs: Requirement[] }>;\n /**\n * Every service the feature's `specs/` addresses — not just the ones with an\n * arch delta. The resolver's fleet set unions these in because a service the\n * feature INTRODUCES has no `services/` directory yet, and without its name\n * an edge into that service's own modelled container resolved to the\n * container's title — a phantom the fleet half alone cannot rule out.\n */\n featureServices: PathableService[];\n /** Every scenario the feature's deltas carry, lowercased — the heuristic's haystack. */\n scenarioText: string;\n /** The living landscape under --all; undefined means \"load it if you need it\", null \"there is none\". */\n preloadedLand?: LoadedDoc | null;\n fleet?: FleetContext;\n}\n\nexport async function deltaArchCoverage(delta: DeltaArch): Promise<Finding[]> {\n const {\n docsDir,\n elements,\n relationships,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n } = delta;\n const findings: Finding[] = [];\n // The read index the service models below are read through. A feature target\n // may arrive without one (`validate --feature` builds no fleet), and it gets\n // its own for `validateService`'s reason: `serviceModel` needs the enumeration\n // and a document memo to answer at all.\n const context = fleet ?? new FleetContext();\n\n // The delta's own element→service resolver, built once for the whole feature.\n // `serviceOf` is a one-shot wrapper that rebuilds its index on every call, and\n // the two loops below ask it up to five times per tagged edge — so a delta\n // over a large model paid for one Map of every element per question asked.\n // The enumerated fleet — `services/` plus the feature's own `specs/` names,\n // where a service it introduces lives — rides into it, and into `baseSvcOf`\n // below, which MUST resolve the same way or the already-living exemption\n // keys never match: so an edge drawn into a modelled container\n // (`payment.api`) grades against the service that owns it instead of a\n // service called \"api\" that has never existed. An unenumerable services/\n // degrades to the feature's own names, exactly what every caller got before\n // the set existed.\n const known: ReadonlySet<string> = new Set<string>([\n ...(await enumeratedServiceIds(docsDir, fleet)),\n ...featureServices,\n ]);\n const svcOf = serviceResolver(elements, known);\n\n // Arch-edge coverage (heuristic, warn-only): each new tagged edge should be named by a scenario.\n for (const r of taggedRels) {\n const target = svcOf(r.target);\n const covered = edgeCovered(target, r.title, scenarioText);\n findings.push({\n severity: covered ? \"ok\" : \"warn\",\n code: covered ? \"archedge.covered\" : \"archedge.uncovered\",\n subject: target,\n message: `${svcOf(r.source)} → ${target} \"${r.title ?? \"\"}\"${covered ? \"\" : \" — no scenario names it\"}`,\n text: { indent: 4, header: \"arch-edge coverage (heuristic):\" },\n });\n }\n\n // The architecture spec axis, feature scope — the mechanical counterpart of\n // the heuristic above. Every NEW tagged element and edge in the delta wants a\n // `Covers:` line in one of the feature's arch.spec.md deltas (c4.uncovered):\n // this is where agent-built code cuts its corners — the outbox, the retries,\n // the alerts — because no business scenario was ever going to mention them.\n // Grouping-only elements take the same exemptions the fleet map applies\n // (person kinds, #external) — one set, in checks/vocabulary.ts, so \"not a\n // service\" cannot mean two things. Warnings, never archive gates; `--strict`\n // escalates.\n //\n // Only requirements the archive will MERGE grant coverage here. In a delta,\n // BASE means \"the living state, quoted\": it merges nothing, emits no\n // .feature, and yields no scenario.tested claim — so a Covers: line under a\n // plain `## Requirements` quote is an obligation that ships nowhere, and\n // counting it silenced c4.uncovered for free. (The service-scope pass keeps\n // the unfiltered call: a LIVING spec is legitimately all BASE.)\n const activeCovers = archDeltas.flatMap(({ reqs }) =>\n coversEntries(reqs.filter((r) => r.kind === \"ADDED\" || r.kind === \"MODIFIED\")),\n );\n\n // What the living landscape ALREADY holds. A delta has to re-declare the\n // elements its new edges attach to, and authors tag those re-declarations\n // along with everything else — so a requirements-only feature that touches an\n // existing service was told to write `Covers:` lines for architecture it is\n // not adding. c4.uncovered is an obligation on NEW architecture; an element\n // the living landscape already resolves is not new, whatever the tag says.\n // Loaded lazily: a delta with nothing tagged never pays for the parse.\n let living: LoadedDoc | null | undefined = preloadedLand;\n const livingLandscape = async (): Promise<LoadedDoc | null> => {\n if (living === undefined) {\n const lp = landscapeFile(docsDir);\n living = existsSync(lp)\n ? fleet === undefined\n ? await loadFile(lp)\n : await fleet.loadLikeC4(lp)\n : null;\n }\n return living;\n };\n const alreadyLiving = async (): Promise<LoadedDoc | null> => {\n if (taggedEls.length === 0 && taggedRels.length === 0) return null;\n const doc = await livingLandscape();\n return doc !== null && doc.errors.length === 0 ? doc : null;\n };\n const base = await alreadyLiving();\n const baseSvcOf = base === null ? null : serviceResolver(base.elements, known);\n const baseIds = new Set(base?.elements.map((e) => e.id) ?? []);\n const baseServices = new Set((base?.elements ?? []).map(elementService));\n // How a service→service pair is keyed, spelled ONCE. The two sides used to\n // join with different separators — a NUL where the set was built, a space\n // where it was queried — so the exemption never matched anything, and every\n // edge a delta re-declares verbatim (which it must, to attach anything to\n // it) was reported as new architecture nobody covers. The join is structural\n // rather than a separator character: the last one was a NUL, and a raw NUL in\n // a template literal makes the source read as binary to `file` and invisible\n // to `grep` — which is how a one-character mismatch survived review.\n const edgeKey = (source: string, target: string): string => JSON.stringify([source, target]);\n const baseEdges = new Set(\n (base?.relationships ?? []).map((r) => edgeKey(baseSvcOf!(r.source), baseSvcOf!(r.target))),\n );\n\n for (const e of taggedEls) {\n if (ACTOR_KINDS.has(e.kind.toLowerCase()) || e.tags.includes(EXTERNAL_TAG)) continue;\n if (baseIds.has(e.id) || baseServices.has(elementService(e))) continue;\n if (activeCovers.some((c) => coversElement(c, e))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: elementService(e),\n 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`,\n });\n }\n for (const r of taggedRels) {\n if (baseEdges.has(edgeKey(svcOf(r.source), svcOf(r.target)))) continue;\n if (activeCovers.some((c) => coversEdge(c, r, elements, known))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: svcOf(r.target),\n 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`,\n });\n }\n\n // covers.unknown, feature scope. Resolution looks at the delta itself, the\n // living landscape, the service's own model and its health.yaml — a delta's\n // arch requirement may cover an element it adds, one that already exists, or\n // an alert the service declares. The landscape and each model are loaded\n // lazily, and only when an entry fails against what is already in hand: the\n // clean path never pays for a workspace spin.\n if (archDeltas.some(({ reqs }) => coversEntries(reqs).length > 0)) {\n const land = await livingLandscape();\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const baseElements = [...elements, ...(landParses?.elements ?? [])];\n const baseRels = [...relationships, ...(landParses?.relationships ?? [])];\n // A feature never brings topology through `delta.likec4` — a `deployment { }`\n // block in one is refused by the merge — so the living map is the whole\n // scope for a `node:` entry in a delta, exactly as it is in service scope.\n const baseDeployment = landParses?.deployment;\n for (const { service: svc, reqs } of archDeltas) {\n // An unreadable living health.yaml mutes the alert:/sli: entries here\n // exactly as in service scope — the health.invalid finding itself\n // belongs to the service target, which owns the file's diagnosis.\n const health = await readHealth((await locateServicePaths(docsDir, svc, fleet)).health);\n let scope: CoverageScope = {\n elements: baseElements,\n relationships: baseRels,\n ...(baseDeployment === undefined ? {} : { deployment: baseDeployment }),\n health: health.ids,\n known,\n };\n const unresolved = coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable);\n if (unresolved.length > 0) {\n // Through the ONE reader of `model.likec4`: a model that EXTENDS the map\n // is not a file this could open, and reading it as one would answer with\n // a pile of parse errors — which this arm reads as \"no widening\", so\n // every `Covers:` line naming a container of a migrated service would be\n // convicted as a typo. `mapUnreadable` is the same silence for the same\n // reason: nothing may widen a scope out of a document nobody could read.\n const svcPaths = await locateServicePaths(docsDir, svc, fleet);\n const model = existsSync(svcPaths.model) ? await context.serviceModel(docsDir, svcPaths) : null;\n if (model !== null && !model.mapUnreadable && model.doc.errors.length === 0) {\n scope = {\n elements: [...baseElements, ...model.doc.elements],\n relationships: [...baseRels, ...model.doc.relationships],\n health: health.ids,\n known,\n };\n }\n findings.push(...coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable));\n }\n }\n }\n\n return findings;\n}\n\n/** Heuristic: an edge is \"covered\" if a scenario names the target or a keyword from the edge title. */\nfunction edgeCovered(target: string, title: string | undefined, scenarioText: string): boolean {\n if (scenarioText.includes(target.toLowerCase())) return true;\n for (const token of (title ?? \"\").split(/[^A-Za-z0-9]+/)) {\n if (token.length >= 5 && scenarioText.includes(token.toLowerCase())) return true;\n }\n return false;\n}\n"]}
1
+ {"version":3,"file":"arch-coverage.js","sourceRoot":"","sources":["../../../src/commands/validate/arch-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Whether the architecture a delta INTRODUCES is covered by a requirement that\n * will actually merge.\n *\n * Three findings, one question asked three ways: `archedge.uncovered` (a\n * scenario names the new edge — heuristic, warn-only), `c4.uncovered` (an\n * arch.spec.md requirement covers the new element or edge — mechanical), and\n * `covers.unknown` (the `Covers:` line resolves to something real). This is\n * where agent-built code cuts its corners — the outbox, the retries, the alerts\n * — because no business scenario was ever going to mention them.\n *\n * Split out of `validateFeature` because it is the only part of the feature\n * target that needs the LIVING landscape: everything else there reads the\n * feature directory alone. Keeping the lazy load and its two exemption sets in\n * one module is what stops \"new\" being decided differently in three places.\n */\nimport { existsSync } from \"node:fs\";\nimport { type Elem, type LoadedDoc, type Rel } from \"../../core/c4/likec4.js\";\nimport { elementService, serviceResolver } from \"../../core/c4/resolve/service.js\";\nimport { type PathableService } from \"../../core/kernel/ids/service.js\";\nimport { landscapePath as landscapeFile } from \"../../core/repo/paths.js\";\nimport { locateServicePaths } from \"../../core/repo/service-target.js\";\nimport { enumeratedServiceIds } from \"../../core/repo/service-target.js\";\nimport { type Finding } from \"../../core/vocabulary/report.js\";\nimport { type Requirement } from \"../../core/document/spec.js\";\nimport { coversEdge, coversElement, type CoverageScope } from \"../../core/c4/arch.js\";\nimport { readHealth } from \"../../core/vocabulary/health.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { ACTOR_KINDS, EXTERNAL_TAG } from \"../../core/vocabulary/maturity.js\";\nimport { coversEntries, coversUnknownFindings } from \"./checks/requirements.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\n/** One feature delta's architecture, and everything needed to decide what in it is new. */\nexport interface DeltaArch {\n docsDir: DocsDir;\n /** Every element the delta declares — the resolver's index, and `coversEdge`'s scope. */\n elements: Elem[];\n /** Every relationship the delta declares. */\n relationships: Rel[];\n /** The delta's own additions: elements and edges carrying the feature id as a tag. */\n taggedEls: Elem[];\n taggedRels: Rel[];\n /** Each addressed service's arch.spec.md delta, in enumeration order. */\n archDeltas: Array<{ service: PathableService; reqs: Requirement[] }>;\n /**\n * Every service the feature's `specs/` addresses — not just the ones with an\n * arch delta. The resolver's fleet set unions these in because a service the\n * feature INTRODUCES has no `services/` directory yet, and without its name\n * an edge into that service's own modelled container resolved to the\n * container's title — a phantom the fleet half alone cannot rule out.\n */\n featureServices: PathableService[];\n /** Every scenario the feature's deltas carry, lowercased — the heuristic's haystack. */\n scenarioText: string;\n /** The living landscape under --all; undefined means \"load it if you need it\", null \"there is none\". */\n preloadedLand?: LoadedDoc | null;\n /**\n * The read index the service models below are read through. Required, for\n * `validateService`'s reason: `serviceModel` needs the enumeration and a\n * document memo to answer at all. Every validate mode builds one before it\n * grades anything — `--feature` included (`validate.ts`) — so there is no\n * second, unmemoised reading rule here for a run that arrived without one.\n */\n fleet: FleetContext;\n}\n\nexport async function deltaArchCoverage(delta: DeltaArch): Promise<Finding[]> {\n const {\n docsDir,\n elements,\n relationships,\n taggedEls,\n taggedRels,\n archDeltas,\n featureServices,\n scenarioText,\n preloadedLand,\n fleet,\n } = delta;\n const findings: Finding[] = [];\n\n // The delta's own element→service resolver, built once for the whole feature.\n // `serviceOf` is a one-shot wrapper that rebuilds its index on every call, and\n // the two loops below ask it up to five times per tagged edge — so a delta\n // over a large model paid for one Map of every element per question asked.\n // The enumerated fleet — `services/` plus the feature's own `specs/` names,\n // where a service it introduces lives — rides into it, and into `baseSvcOf`\n // below, which MUST resolve the same way or the already-living exemption\n // keys never match: so an edge drawn into a modelled container\n // (`payment.api`) grades against the service that owns it instead of a\n // service called \"api\" that has never existed. An unenumerable services/\n // degrades to the feature's own names, exactly what every caller got before\n // the set existed.\n const known: ReadonlySet<string> = new Set<string>([\n ...(await enumeratedServiceIds(docsDir, fleet)),\n ...featureServices,\n ]);\n const svcOf = serviceResolver(elements, known);\n\n // Arch-edge coverage (heuristic, warn-only): each new tagged edge should be named by a scenario.\n for (const r of taggedRels) {\n const target = svcOf(r.target);\n const covered = edgeCovered(target, r.title, scenarioText);\n findings.push({\n severity: covered ? \"ok\" : \"warn\",\n code: covered ? \"archedge.covered\" : \"archedge.uncovered\",\n subject: target,\n message: `${svcOf(r.source)} → ${target} \"${r.title ?? \"\"}\"${covered ? \"\" : \" — no scenario names it\"}`,\n text: { indent: 4, header: \"arch-edge coverage (heuristic):\" },\n });\n }\n\n // The architecture spec axis, feature scope — the mechanical counterpart of\n // the heuristic above. Every NEW tagged element and edge in the delta wants a\n // `Covers:` line in one of the feature's arch.spec.md deltas (c4.uncovered):\n // this is where agent-built code cuts its corners — the outbox, the retries,\n // the alerts — because no business scenario was ever going to mention them.\n // Grouping-only elements take the same exemptions the fleet map applies\n // (person kinds, #external) — one set, in core/vocabulary/maturity.ts, so\n // \"not a service\" cannot mean two things. Warnings, never archive gates;\n // `--strict` escalates.\n //\n // Only requirements the archive will MERGE grant coverage here. In a delta,\n // BASE means \"the living state, quoted\": it merges nothing, emits no\n // .feature, and yields no scenario.tested claim — so a Covers: line under a\n // plain `## Requirements` quote is an obligation that ships nowhere, and\n // counting it silenced c4.uncovered for free. (The service-scope pass keeps\n // the unfiltered call: a LIVING spec is legitimately all BASE.)\n const activeCovers = archDeltas.flatMap(({ reqs }) =>\n coversEntries(reqs.filter((r) => r.kind === \"ADDED\" || r.kind === \"MODIFIED\")),\n );\n\n // What the living landscape ALREADY holds. A delta has to re-declare the\n // elements its new edges attach to, and authors tag those re-declarations\n // along with everything else — so a requirements-only feature that touches an\n // existing service was told to write `Covers:` lines for architecture it is\n // not adding. c4.uncovered is an obligation on NEW architecture; an element\n // the living landscape already resolves is not new, whatever the tag says.\n // Loaded lazily: a delta with nothing tagged never pays for the parse.\n let living: LoadedDoc | null | undefined = preloadedLand;\n const livingLandscape = async (): Promise<LoadedDoc | null> => {\n if (living === undefined) {\n const lp = landscapeFile(docsDir);\n living = existsSync(lp) ? await fleet.loadLikeC4(lp) : null;\n }\n return living;\n };\n const alreadyLiving = async (): Promise<LoadedDoc | null> => {\n if (taggedEls.length === 0 && taggedRels.length === 0) return null;\n const doc = await livingLandscape();\n return doc !== null && doc.errors.length === 0 ? doc : null;\n };\n const base = await alreadyLiving();\n const baseSvcOf = base === null ? null : serviceResolver(base.elements, known);\n const baseIds = new Set(base?.elements.map((e) => e.id) ?? []);\n const baseServices = new Set((base?.elements ?? []).map(elementService));\n // How a service→service pair is keyed, spelled ONCE. The two sides used to\n // join with different separators — a NUL where the set was built, a space\n // where it was queried — so the exemption never matched anything, and every\n // edge a delta re-declares verbatim (which it must, to attach anything to\n // it) was reported as new architecture nobody covers. The join is structural\n // rather than a separator character: the last one was a NUL, and a raw NUL in\n // a template literal makes the source read as binary to `file` and invisible\n // to `grep` — which is how a one-character mismatch survived review.\n const edgeKey = (source: string, target: string): string => JSON.stringify([source, target]);\n const baseEdges = new Set(\n (base?.relationships ?? []).map((r) => edgeKey(baseSvcOf!(r.source), baseSvcOf!(r.target))),\n );\n\n for (const e of taggedEls) {\n if (ACTOR_KINDS.has(e.kind.toLowerCase()) || e.tags.includes(EXTERNAL_TAG)) continue;\n if (baseIds.has(e.id) || baseServices.has(elementService(e))) continue;\n if (activeCovers.some((c) => coversElement(c, e))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: elementService(e),\n 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`,\n });\n }\n for (const r of taggedRels) {\n if (baseEdges.has(edgeKey(svcOf(r.source), svcOf(r.target)))) continue;\n if (activeCovers.some((c) => coversEdge(c, r, elements, known))) continue;\n findings.push({\n severity: \"warn\",\n code: \"c4.uncovered\",\n subject: svcOf(r.target),\n 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`,\n });\n }\n\n // covers.unknown, feature scope. Resolution looks at the delta itself, the\n // living landscape, the service's own model and its health.yaml — a delta's\n // arch requirement may cover an element it adds, one that already exists, or\n // an alert the service declares. The landscape and each model are loaded\n // lazily, and only when an entry fails against what is already in hand: the\n // clean path never pays for a workspace spin.\n if (archDeltas.some(({ reqs }) => coversEntries(reqs).length > 0)) {\n const land = await livingLandscape();\n const landParses = land !== null && land.errors.length === 0 ? land : null;\n const baseElements = [...elements, ...(landParses?.elements ?? [])];\n const baseRels = [...relationships, ...(landParses?.relationships ?? [])];\n // A feature never brings topology through `delta.likec4` — a `deployment { }`\n // block in one is refused by the merge — so the living map is the whole\n // scope for a `node:` entry in a delta, exactly as it is in service scope.\n const baseDeployment = landParses?.deployment;\n for (const { service: svc, reqs } of archDeltas) {\n // An unreadable living health.yaml mutes the alert:/sli: entries here\n // exactly as in service scope — the health.invalid finding itself\n // belongs to the service target, which owns the file's diagnosis.\n const health = await readHealth((await locateServicePaths(docsDir, svc, fleet)).health);\n let scope: CoverageScope = {\n elements: baseElements,\n relationships: baseRels,\n ...(baseDeployment === undefined ? {} : { deployment: baseDeployment }),\n health: health.ids,\n known,\n };\n const unresolved = coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable);\n if (unresolved.length > 0) {\n // Through the ONE reader of `model.likec4`: a model that EXTENDS the map\n // is not a file this could open, and reading it as one would answer with\n // a pile of parse errors — which this arm reads as \"no widening\", so\n // every `Covers:` line naming a container of a migrated service would be\n // convicted as a typo. `mapUnreadable` is the same silence for the same\n // reason: nothing may widen a scope out of a document nobody could read.\n const svcPaths = await locateServicePaths(docsDir, svc, fleet);\n const model = existsSync(svcPaths.model) ? await fleet.serviceModel(docsDir, svcPaths) : null;\n if (model !== null && !model.mapUnreadable && model.doc.errors.length === 0) {\n scope = {\n elements: [...baseElements, ...model.doc.elements],\n relationships: [...baseRels, ...model.doc.relationships],\n health: health.ids,\n known,\n };\n }\n findings.push(...coversUnknownFindings(reqs, { where: `${svc}: arch.spec.md`, subject: svc }, scope, health.unreadable));\n }\n }\n }\n\n return findings;\n}\n\n/** Heuristic: an edge is \"covered\" if a scenario names the target or a keyword from the edge title. */\nfunction edgeCovered(target: string, title: string | undefined, scenarioText: string): boolean {\n if (scenarioText.includes(target.toLowerCase())) return true;\n for (const token of (title ?? \"\").split(/[^A-Za-z0-9]+/)) {\n if (token.length >= 5 && scenarioText.includes(token.toLowerCase())) return true;\n }\n return false;\n}\n"]}
@@ -21,20 +21,15 @@
21
21
  * as platform — this warning only names a shape that is usually wrong.
22
22
  */
23
23
  import { existsSync } from "node:fs";
24
- import { readFile } from "node:fs/promises";
25
24
  import {} from "../../../core/c4/likec4.js";
26
- import {} from "../../../core/kernel/ids/service.js";
27
25
  import {} from "../../../core/vocabulary/report.js";
28
26
  import {} from "../../../core/repo/entries.js";
29
27
  import { permissionsPath, servicePathsAt } from "../../../core/repo/paths.js";
30
28
  import { readVocabulary } from "../../../core/permissions/permissions.js";
31
- import { readCapabilityVocabulary } from "../../../core/capabilities/capabilities.js";
32
29
  import { capabilityDocFindings, capabilityRequirementIndex, docMissingFindings, gradableCapabilityIds, invalidVocabularyFinding, unrealizedFindings, } from "../../../core/capabilities/findings.js";
33
30
  import { useCaseRequirementClaims } from "../../../core/usecases/capability.js";
34
31
  import { capabilityRollup, usedCapabilities } from "../../../core/capabilities/rollup.js";
35
32
  import { requirementUnrealizedFindings } from "../../../core/capabilities/realizes/findings.js";
36
- import { parseRequirements } from "../../../core/document/parse.js";
37
- import {} from "../../../core/document/spec.js";
38
33
  import { FleetContext } from "../../../core/fleet-context.js";
39
34
  import { EXTERNAL_TAG, PLATFORM_TAG } from "../../../core/vocabulary/maturity.js";
40
35
  /**
@@ -71,18 +66,6 @@ export function fleetShapeFindings(shape) {
71
66
  }
72
67
  return findings;
73
68
  }
74
- /**
75
- * The #external element that declares it PUBLISHES this message, or null —
76
- * the landscape-side half of the event spine's fleet question, beside the
77
- * other checks that read tags off the drawn map.
78
- *
79
- * Positive evidence only: an edge carrying `metadata { publishes '<msg>' }`
80
- * whose source element — itself or a declared ancestor, since the edge may
81
- * point out of a topic nested inside the broker — is tagged #external and
82
- * resolves to no known service directory. The resolution guard is what keeps
83
- * a mis-tagged internal service reading as unproduced: a tag is cheap to
84
- * write, and a services/<id>/ directory outranks it.
85
- */
86
69
  /**
87
70
  * The authorization vocabulary graded against the fleet that uses it — the half
88
71
  * of the axis no single service can answer.
@@ -118,14 +101,14 @@ export async function permissionFindings(docsDir, services, fleet) {
118
101
  for (const entry of services) {
119
102
  const paths = servicePathsAt(entry.dir);
120
103
  for (const path of [paths.spec, paths.archSpec]) {
121
- // Existence first, and for BOTH readers: arch.spec.md is optional (most
104
+ // Existence first, and for BOTH documents: arch.spec.md is optional (most
122
105
  // of a legacy fleet has none) and spec.md is missing on a half-adopted
123
106
  // service. `FleetContext.readRequirements` throws ENOENT, which surfaces
124
107
  // as `repository-unavailable` and takes the whole `--all` run down — an
125
108
  // absent optional artifact must never be able to do that.
126
109
  if (!existsSync(path))
127
110
  continue;
128
- const reqs = fleet === undefined ? await readRequirementsAt(path) : await fleet.readRequirements(path);
111
+ const reqs = await fleet.readRequirements(path);
129
112
  for (const r of reqs) {
130
113
  if (r.kind !== "REMOVED")
131
114
  for (const p of r.requires)
@@ -145,12 +128,6 @@ export async function permissionFindings(docsDir, services, fleet) {
145
128
  },
146
129
  ];
147
130
  }
148
- /** The uncached read, for the `--service` path where no fleet context exists. */
149
- async function readRequirementsAt(path) {
150
- if (!existsSync(path))
151
- return [];
152
- return parseRequirements(await readFile(path, "utf8"));
153
- }
154
131
  /**
155
132
  * The capability vocabulary graded against the fleet that realizes it — the
156
133
  * axis's fleet half, beside permissionFindings and shaped by the same two
@@ -180,10 +157,10 @@ async function readRequirementsAt(path) {
180
157
  * nobody read.
181
158
  */
182
159
  export async function capabilityFleetFindings(docsDir, services, fleet, flows) {
183
- const vocab = fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);
160
+ const vocab = await fleet.capabilities(docsDir);
184
161
  if (!vocab.present)
185
162
  return [];
186
- const read = fleet === undefined ? readRequirementsAt : (p) => fleet.readRequirements(p);
163
+ const read = (p) => fleet.readRequirements(p);
187
164
  const authored = docMissingFindings(vocab.tree);
188
165
  for (const doc of vocab.tree.docs)
189
166
  authored.push(...capabilityDocFindings(await read(doc.spec), doc.id));
@@ -224,26 +201,4 @@ export async function capabilityFleetFindings(docsDir, services, fleet, flows) {
224
201
  ...requirementUnrealizedFindings(unrealizedRequirements),
225
202
  ];
226
203
  }
227
- export function externalProducerOf(message, land, landSvcOf, known) {
228
- if (land === null || land.errors.length > 0 || landSvcOf === null)
229
- return null;
230
- const knownIds = known;
231
- const byId = new Map(land.elements.map((e) => [e.id, e]));
232
- for (const r of land.relationships) {
233
- if (r.publishes !== message)
234
- continue;
235
- if (knownIds.has(landSvcOf(r.source)))
236
- continue;
237
- for (let id = r.source;;) {
238
- const e = byId.get(id);
239
- if (e !== undefined && e.tags.includes(EXTERNAL_TAG))
240
- return e.title;
241
- const dot = id.lastIndexOf(".");
242
- if (dot === -1)
243
- break;
244
- id = id.slice(0, dot);
245
- }
246
- }
247
- return null;
248
- }
249
204
  //# sourceMappingURL=fleet-shape.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fleet-shape.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/fleet-shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fleet-shape advisories over the parsed landscape: the way a fleet map stops\n * answering questions as it grows, caught while the map is already in memory.\n *\n * The shape becomes visible at service three rather than service two.\n * Ubiquitous infrastructure — logging Kafka, an identity provider, service\n * discovery — takes one inbound edge per modelled service until the map is a\n * hairball around it; deleting the element fixes the picture and loses \"who\n * depends on the Identity Provider\", which is the question the map exists to\n * answer during an incident. Tagging keeps both: the fleet view excludes\n * `#platform`, and a platform view keeps the dependents.\n *\n * The DATASTORE half of this pair moved to `../fleet/map/consumers.ts` when its\n * consumer count stopped being a fact about the map alone: a private store now\n * lives nested inside its owner's element, and the services reaching it are\n * attested in their own models (E3/R1). The census both checks read is built\n * there and handed in here as `consumers`, so a hub and a store can never\n * disagree about who consumes what.\n *\n * loam still reads no code and decides no policy: the team chooses what counts\n * as platform — this warning only names a shape that is usually wrong.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { type Elem, type LoadedDoc } from \"../../../core/c4/likec4.js\";\nimport { type DeclaredService, type RawServiceId } from \"../../../core/kernel/ids/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { permissionsPath, servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport { readCapabilityVocabulary } from \"../../../core/capabilities/capabilities.js\";\nimport {\n capabilityDocFindings,\n capabilityRequirementIndex,\n docMissingFindings,\n gradableCapabilityIds,\n invalidVocabularyFinding,\n unrealizedFindings,\n} from \"../../../core/capabilities/findings.js\";\nimport { useCaseRequirementClaims } from \"../../../core/usecases/capability.js\";\nimport type { ParsedView } from \"../../../core/c4/parsed/dynamic-views.js\";\nimport { capabilityRollup, usedCapabilities } from \"../../../core/capabilities/rollup.js\";\nimport { requirementUnrealizedFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { EXTERNAL_TAG, PLATFORM_TAG } from \"../../../core/vocabulary/maturity.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * Consumers at which an untagged external hub starts to warn. Three, not two:\n * two services sharing a dependency is a fact about those two services, the\n * same edge from three is a pattern about the element — and three is where\n * the shared hub starts making the map unreadable.\n */\nexport const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;\n\nexport interface FleetShape {\n /** Service-LEVEL elements — the fleet map's own boxes (landscape.ts `drawn`). */\n drawn: Elem[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /**\n * Distinct consumer SERVICES of an element, its nested children included —\n * `../fleet/map/consumers.ts` owns the derivation and both checks that read a\n * consumer count share this one instance of it.\n */\n consumers: (elementId: string) => string[];\n}\n\nexport function fleetShapeFindings(shape: FleetShape): Finding[] {\n const { drawn, services } = shape;\n const findings: Finding[] = [];\n\n for (const e of drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape: many consumers is what a well-used service looks like.\n if (e.service !== undefined || services.has(e.title)) continue;\n if (!e.tags.includes(EXTERNAL_TAG) || e.tags.includes(PLATFORM_TAG)) continue;\n const consumers = shape.consumers(e.id);\n if (consumers.length < PLATFORM_CANDIDATE_MIN_CONSUMERS) continue;\n findings.push({\n severity: \"warn\",\n code: \"landscape.platform-candidate\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(\", \")}) ` +\n `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +\n `the fleet view is unreadable. Declare \\`tag platform\\` in the specification block and tag the ` +\n `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +\n `excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps ` +\n `\"who depends on it\" answerable`,\n });\n }\n return findings;\n}\n\n/**\n * The #external element that declares it PUBLISHES this message, or null —\n * the landscape-side half of the event spine's fleet question, beside the\n * other checks that read tags off the drawn map.\n *\n * Positive evidence only: an edge carrying `metadata { publishes '<msg>' }`\n * whose source element — itself or a declared ancestor, since the edge may\n * point out of a topic nested inside the broker — is tagged #external and\n * resolves to no known service directory. The resolution guard is what keeps\n * a mis-tagged internal service reading as unproduced: a tag is cheap to\n * write, and a services/<id>/ directory outranks it.\n */\n/**\n * The authorization vocabulary graded against the fleet that uses it — the half\n * of the axis no single service can answer.\n *\n * `permissions.invalid` is an error and it is the only one reported when it\n * fires: a vocabulary nobody can read resolves nothing, so every `Requires:`\n * line in the fleet would follow it as `permissions.unknown`, and a hundred\n * findings about one broken file is a cascade, not a diagnosis.\n *\n * `permissions.unenforced` is the mirror of `api.ungoverned`, and it earns its\n * place for the same reason: a vocabulary is only worth what cites it. A\n * permission nothing requires is either a rule that was removed and left its\n * word behind, or a word nobody adopted — both are drift, and neither is\n * visible from inside the file. Warn, because the honest answer is sometimes\n * \"not modelled yet\".\n */\nexport async function permissionFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n): Promise<Finding[]> {\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n return [\n {\n severity: \"error\",\n code: \"permissions.invalid\",\n message:\n `landscape: architecture/permissions.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `Requires:` line in the fleet resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `subjects: {<kind>: {description}}` then `permissions: {<kind>: {<name>: {description, owned_by, enforced_by}}}`.\",\n },\n ];\n }\n const used = new Set<string>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n // Existence first, and for BOTH readers: arch.spec.md is optional (most\n // of a legacy fleet has none) and spec.md is missing on a half-adopted\n // service. `FleetContext.readRequirements` throws ENOENT, which surfaces\n // as `repository-unavailable` and takes the whole `--all` run down — an\n // absent optional artifact must never be able to do that.\n if (!existsSync(path)) continue;\n const reqs = fleet === undefined ? await readRequirementsAt(path) : await fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind !== \"REMOVED\") for (const p of r.requires) used.add(p);\n }\n }\n }\n const unenforced = [...vocabulary.byId.keys()].filter((id) => !used.has(id));\n if (unenforced.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"permissions.unenforced\",\n message: `landscape: ${unenforced.length} declared permission(s) that no requirement's \\`Requires:\\` line names — write the requirement that gates on each, or drop the declaration`,\n details: unenforced,\n },\n ];\n}\n\n/** The uncached read, for the `--service` path where no fleet context exists. */\nasync function readRequirementsAt(path: string): Promise<Requirement[]> {\n if (!existsSync(path)) return [];\n return parseRequirements(await readFile(path, \"utf8\"));\n}\n\n/**\n * The capability vocabulary graded against the fleet that realizes it — the\n * axis's fleet half, beside permissionFindings and shaped by the same two\n * verdicts. `capability.invalid` is a `validate --all` run's ONE finding about\n * an unreadable file — fleet scope, so single-target runs stay silent about\n * it — and it suppresses the rest of the family (the unknown\n * grades at every service target return [] for the same vocabulary — one\n * breach, one finding, never a cascade). `capability.unrealized` is one warn\n * PER declared-but-unnamed capability, subject = the id, because the roadmap's\n * criterion is 'one warning per capability, not one per service'. Holding\n * NEITHER `architecture/capabilities.yaml` nor `capabilities/` is total\n * silence: the fleet's own files are this axis's opt-in, either of them.\n *\n * The used set comes through capabilityRollup with the fleet's own cached\n * reader, so under --all the walk re-parses nothing the service targets have\n * not already paid for — and the rollup, the warn and `loam list capabilities`\n * can never disagree about what \"realized\" means.\n *\n * The AUTHORED documents are graded here too, and they ride this function\n * rather than a target of their own for `viewsStaleFindings`' reason one tree\n * over: a capability belongs to no service, so only the fleet run has the whole\n * vocabulary and the enumerated tree in view at once. Their reads are NOT\n * wrapped, deliberately — a `capabilities/<id>/spec.md` that is not UTF-8 takes\n * the run down exactly as a `services/<id>/spec.md` does, because a document\n * loam cannot decode is a refusal everywhere else in the product and a quiet\n * \"zero requirements\" here would grade a whole capability green on bytes\n * nobody read.\n */\nexport async function capabilityFleetFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext | undefined,\n flows: readonly ParsedView[] | null,\n): Promise<Finding[]> {\n const vocab = fleet === undefined ? await readCapabilityVocabulary(docsDir) : await fleet.capabilities(docsDir);\n if (!vocab.present) return [];\n const read = fleet === undefined ? readRequirementsAt : (p: string) => fleet.readRequirements(p);\n const authored: Finding[] = docMissingFindings(vocab.tree);\n for (const doc of vocab.tree.docs) authored.push(...capabilityDocFindings(await read(doc.spec), doc.id));\n\n const invalid = invalidVocabularyFinding(vocab);\n if (invalid !== null) return [invalid, ...authored];\n // A USE CASE IS THE OTHER WAY TO KEEP A PROMISE, and it is the only carrier a\n // cross-service criterion has: \"I enter a login and a password and I am in\"\n // belongs to no single service's spec, so a fleet could satisfy it perfectly\n // through a `#cap-`/`#req-` tagged flow and still be told nobody realizes it.\n //\n // The claims go INTO the rollup rather than beside it, so both unrealized\n // grades and `loam list capabilities` read one set of rows. `flows === null`\n // means loam could not READ the flows (no preload, or an `architecture/` that\n // did not parse); the key is then OMITTED rather than empty, and every\n // `keptBy` below is `undefined` — the three-state contract `rollup.ts` states.\n const index = await capabilityRequirementIndex(vocab, read);\n const keptByFlows =\n flows === null\n ? undefined\n : useCaseRequirementClaims(flows, gradableCapabilityIds(vocab), (c) => index.byCapability.get(c));\n const rows = await capabilityRollup({ services, vocab, read, ...(keptByFlows === undefined ? {} : { keptByFlows }) });\n\n // The two unrealized grades read the SAME rollup, which is what keeps them\n // from disagreeing about the word: a capability is unrealized when nothing\n // names it by any join, and one of its requirements is unrealized when nothing\n // keeps that promise. Both are warnings, and the second is the one that\n // survives a healthy-looking row — a capability with four requirements and\n // three kept reports nothing at all through the first.\n //\n // The suspension falls out of the DATA rather than a branch: `keptBy` absent\n // means nobody looked, and a promise nobody looked for is not a promise\n // reported unkept.\n const unrealizedRequirements = rows.flatMap((row) =>\n (row.requirements ?? [])\n .filter((req) => req.realizedBy.length === 0 && req.keptBy?.length === 0)\n .map((req) => ({ capability: row.id, id: req.id, name: req.name })),\n );\n return [\n ...authored,\n ...unrealizedFindings(vocab, usedCapabilities(rows)),\n ...requirementUnrealizedFindings(unrealizedRequirements),\n ];\n}\n\nexport function externalProducerOf(\n message: string,\n land: LoadedDoc | null,\n landSvcOf: ((id: string) => DeclaredService) | null,\n known: ReadonlySet<RawServiceId>,\n): string | null {\n if (land === null || land.errors.length > 0 || landSvcOf === null) return null;\n const knownIds: ReadonlySet<string> = known;\n const byId = new Map(land.elements.map((e) => [e.id, e]));\n for (const r of land.relationships) {\n if (r.publishes !== message) continue;\n if (knownIds.has(landSvcOf(r.source))) continue;\n for (let id = r.source; ; ) {\n const e = byId.get(id);\n if (e !== undefined && e.tags.includes(EXTERNAL_TAG)) return e.title;\n const dot = id.lastIndexOf(\".\");\n if (dot === -1) break;\n id = id.slice(0, dot);\n }\n }\n return null;\n}\n"]}
1
+ {"version":3,"file":"fleet-shape.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/fleet-shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fleet-shape advisories over the parsed landscape: the way a fleet map stops\n * answering questions as it grows, caught while the map is already in memory.\n *\n * The shape becomes visible at service three rather than service two.\n * Ubiquitous infrastructure — logging Kafka, an identity provider, service\n * discovery — takes one inbound edge per modelled service until the map is a\n * hairball around it; deleting the element fixes the picture and loses \"who\n * depends on the Identity Provider\", which is the question the map exists to\n * answer during an incident. Tagging keeps both: the fleet view excludes\n * `#platform`, and a platform view keeps the dependents.\n *\n * The DATASTORE half of this pair moved to `../fleet/map/consumers.ts` when its\n * consumer count stopped being a fact about the map alone: a private store now\n * lives nested inside its owner's element, and the services reaching it are\n * attested in their own models (E3/R1). The census both checks read is built\n * there and handed in here as `consumers`, so a hub and a store can never\n * disagree about who consumes what.\n *\n * loam still reads no code and decides no policy: the team chooses what counts\n * as platform — this warning only names a shape that is usually wrong.\n */\nimport { existsSync } from \"node:fs\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { type ServiceEntry } from \"../../../core/repo/entries.js\";\nimport { permissionsPath, servicePathsAt } from \"../../../core/repo/paths.js\";\nimport { readVocabulary } from \"../../../core/permissions/permissions.js\";\nimport {\n capabilityDocFindings,\n capabilityRequirementIndex,\n docMissingFindings,\n gradableCapabilityIds,\n invalidVocabularyFinding,\n unrealizedFindings,\n} from \"../../../core/capabilities/findings.js\";\nimport { useCaseRequirementClaims } from \"../../../core/usecases/capability.js\";\nimport type { ParsedView } from \"../../../core/c4/parsed/dynamic-views.js\";\nimport { capabilityRollup, usedCapabilities } from \"../../../core/capabilities/rollup.js\";\nimport { requirementUnrealizedFindings } from \"../../../core/capabilities/realizes/findings.js\";\nimport { FleetContext } from \"../../../core/fleet-context.js\";\nimport { EXTERNAL_TAG, PLATFORM_TAG } from \"../../../core/vocabulary/maturity.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * Consumers at which an untagged external hub starts to warn. Three, not two:\n * two services sharing a dependency is a fact about those two services, the\n * same edge from three is a pattern about the element — and three is where\n * the shared hub starts making the map unreadable.\n */\nexport const PLATFORM_CANDIDATE_MIN_CONSUMERS = 3;\n\nexport interface FleetShape {\n /** Service-LEVEL elements — the fleet map's own boxes (landscape.ts `drawn`). */\n drawn: Elem[];\n /** The services/<id>/ directories that exist. */\n services: ReadonlySet<string>;\n /**\n * Distinct consumer SERVICES of an element, its nested children included —\n * `../fleet/map/consumers.ts` owns the derivation and both checks that read a\n * consumer count share this one instance of it.\n */\n consumers: (elementId: string) => string[];\n}\n\nexport function fleetShapeFindings(shape: FleetShape): Finding[] {\n const { drawn, services } = shape;\n const findings: Finding[] = [];\n\n for (const e of drawn) {\n // An element that stands for a real service is the fleet itself, not its\n // shape: many consumers is what a well-used service looks like.\n if (e.service !== undefined || services.has(e.title)) continue;\n if (!e.tags.includes(EXTERNAL_TAG) || e.tags.includes(PLATFORM_TAG)) continue;\n const consumers = shape.consumers(e.id);\n if (consumers.length < PLATFORM_CANDIDATE_MIN_CONSUMERS) continue;\n findings.push({\n severity: \"warn\",\n code: \"landscape.platform-candidate\",\n subject: e.title,\n message:\n `landscape: '${e.title}' is consumed by ${consumers.length} services (${consumers.join(\", \")}) ` +\n `and is not tagged #platform — a hub like this takes one more edge per modelled service until ` +\n `the fleet view is unreadable. Declare \\`tag platform\\` in the specification block and tag the ` +\n `element (LikeC4 refuses an undeclared tag, so both steps are needed): the fleet view then ` +\n `excludes it, and a platform view over \\`include * -> element.tag = #platform\\` keeps ` +\n `\"who depends on it\" answerable`,\n });\n }\n return findings;\n}\n\n/**\n * The authorization vocabulary graded against the fleet that uses it — the half\n * of the axis no single service can answer.\n *\n * `permissions.invalid` is an error and it is the only one reported when it\n * fires: a vocabulary nobody can read resolves nothing, so every `Requires:`\n * line in the fleet would follow it as `permissions.unknown`, and a hundred\n * findings about one broken file is a cascade, not a diagnosis.\n *\n * `permissions.unenforced` is the mirror of `api.ungoverned`, and it earns its\n * place for the same reason: a vocabulary is only worth what cites it. A\n * permission nothing requires is either a rule that was removed and left its\n * word behind, or a word nobody adopted — both are drift, and neither is\n * visible from inside the file. Warn, because the honest answer is sometimes\n * \"not modelled yet\".\n */\nexport async function permissionFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext,\n): Promise<Finding[]> {\n const vocabulary = await readVocabulary(permissionsPath(docsDir));\n if (!vocabulary.present) return [];\n if (vocabulary.invalid !== undefined) {\n return [\n {\n severity: \"error\",\n code: \"permissions.invalid\",\n message:\n `landscape: architecture/permissions.yaml does not read as a vocabulary — ${vocabulary.invalid}. ` +\n \"Every `Requires:` line in the fleet resolves against this file, so none of them can be graded until it parses. \" +\n \"The shape is `subjects: {<kind>: {description}}` then `permissions: {<kind>: {<name>: {description, owned_by, enforced_by}}}`.\",\n },\n ];\n }\n const used = new Set<string>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const path of [paths.spec, paths.archSpec]) {\n // Existence first, and for BOTH documents: arch.spec.md is optional (most\n // of a legacy fleet has none) and spec.md is missing on a half-adopted\n // service. `FleetContext.readRequirements` throws ENOENT, which surfaces\n // as `repository-unavailable` and takes the whole `--all` run down — an\n // absent optional artifact must never be able to do that.\n if (!existsSync(path)) continue;\n const reqs = await fleet.readRequirements(path);\n for (const r of reqs) {\n if (r.kind !== \"REMOVED\") for (const p of r.requires) used.add(p);\n }\n }\n }\n const unenforced = [...vocabulary.byId.keys()].filter((id) => !used.has(id));\n if (unenforced.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"permissions.unenforced\",\n message: `landscape: ${unenforced.length} declared permission(s) that no requirement's \\`Requires:\\` line names — write the requirement that gates on each, or drop the declaration`,\n details: unenforced,\n },\n ];\n}\n\n/**\n * The capability vocabulary graded against the fleet that realizes it — the\n * axis's fleet half, beside permissionFindings and shaped by the same two\n * verdicts. `capability.invalid` is a `validate --all` run's ONE finding about\n * an unreadable file — fleet scope, so single-target runs stay silent about\n * it — and it suppresses the rest of the family (the unknown\n * grades at every service target return [] for the same vocabulary — one\n * breach, one finding, never a cascade). `capability.unrealized` is one warn\n * PER declared-but-unnamed capability, subject = the id, because the roadmap's\n * criterion is 'one warning per capability, not one per service'. Holding\n * NEITHER `architecture/capabilities.yaml` nor `capabilities/` is total\n * silence: the fleet's own files are this axis's opt-in, either of them.\n *\n * The used set comes through capabilityRollup with the fleet's own cached\n * reader, so under --all the walk re-parses nothing the service targets have\n * not already paid for — and the rollup, the warn and `loam list capabilities`\n * can never disagree about what \"realized\" means.\n *\n * The AUTHORED documents are graded here too, and they ride this function\n * rather than a target of their own for `viewsStaleFindings`' reason one tree\n * over: a capability belongs to no service, so only the fleet run has the whole\n * vocabulary and the enumerated tree in view at once. Their reads are NOT\n * wrapped, deliberately — a `capabilities/<id>/spec.md` that is not UTF-8 takes\n * the run down exactly as a `services/<id>/spec.md` does, because a document\n * loam cannot decode is a refusal everywhere else in the product and a quiet\n * \"zero requirements\" here would grade a whole capability green on bytes\n * nobody read.\n */\nexport async function capabilityFleetFindings(\n docsDir: DocsDir,\n services: ServiceEntry[],\n fleet: FleetContext,\n flows: readonly ParsedView[] | null,\n): Promise<Finding[]> {\n const vocab = await fleet.capabilities(docsDir);\n if (!vocab.present) return [];\n const read = (p: string) => fleet.readRequirements(p);\n const authored: Finding[] = docMissingFindings(vocab.tree);\n for (const doc of vocab.tree.docs) authored.push(...capabilityDocFindings(await read(doc.spec), doc.id));\n\n const invalid = invalidVocabularyFinding(vocab);\n if (invalid !== null) return [invalid, ...authored];\n // A USE CASE IS THE OTHER WAY TO KEEP A PROMISE, and it is the only carrier a\n // cross-service criterion has: \"I enter a login and a password and I am in\"\n // belongs to no single service's spec, so a fleet could satisfy it perfectly\n // through a `#cap-`/`#req-` tagged flow and still be told nobody realizes it.\n //\n // The claims go INTO the rollup rather than beside it, so both unrealized\n // grades and `loam list capabilities` read one set of rows. `flows === null`\n // means loam could not READ the flows (no preload, or an `architecture/` that\n // did not parse); the key is then OMITTED rather than empty, and every\n // `keptBy` below is `undefined` — the three-state contract `rollup.ts` states.\n const index = await capabilityRequirementIndex(vocab, read);\n const keptByFlows =\n flows === null\n ? undefined\n : useCaseRequirementClaims(flows, gradableCapabilityIds(vocab), (c) => index.byCapability.get(c));\n const rows = await capabilityRollup({ services, vocab, read, ...(keptByFlows === undefined ? {} : { keptByFlows }) });\n\n // The two unrealized grades read the SAME rollup, which is what keeps them\n // from disagreeing about the word: a capability is unrealized when nothing\n // names it by any join, and one of its requirements is unrealized when nothing\n // keeps that promise. Both are warnings, and the second is the one that\n // survives a healthy-looking row — a capability with four requirements and\n // three kept reports nothing at all through the first.\n //\n // The suspension falls out of the DATA rather than a branch: `keptBy` absent\n // means nobody looked, and a promise nobody looked for is not a promise\n // reported unkept.\n const unrealizedRequirements = rows.flatMap((row) =>\n (row.requirements ?? [])\n .filter((req) => req.realizedBy.length === 0 && req.keptBy?.length === 0)\n .map((req) => ({ capability: row.id, id: req.id, name: req.name })),\n );\n return [\n ...authored,\n ...unrealizedFindings(vocab, usedCapabilities(rows)),\n ...requirementUnrealizedFindings(unrealizedRequirements),\n ];\n}\n"]}
@@ -10,11 +10,13 @@
10
10
  * concerned.
11
11
  *
12
12
  * Resolution is against the SERVICE'S OWN model — deliberately never the
13
- * landscape. A private datastore lives nested inside the service element
14
- * (`landscape.datastore-private` is the check pushing it there), so the
15
- * landscape carries only what crosses the service boundary, while the model
16
- * carries everything the service touches — and that is the set the on-call
17
- * file has to agree with. An id resolves by the same names every other join
13
+ * landscape. A store one service owns may be nested inside that service's
14
+ * element or drawn as a fleet-level peer, whichever the team chose (loam grades
15
+ * neither: `landscape.datastore-private`, which used to push it into the first,
16
+ * was retired 2026-09-07). Only the nested placement puts it in the landscape's
17
+ * interior at all, so the landscape is not a set this check could rely on, while
18
+ * the model carries everything the service touches — and that is the set the
19
+ * on-call file has to agree with. An id resolves by the same names every other join
18
20
  * uses: the element's likec4 id, its `metadata { service }` binding, or its
19
21
  * title. Exact match, no case folding — a fold would be a permanent
20
22
  * equivalence semantic no other loam join has, able to silently join two
@@ -45,7 +47,8 @@ export function healthDependencyFindings(deps) {
45
47
  message: `${service}: health.yaml declares dependency '${dep}' but nothing in ` +
46
48
  `${modelPath} answers to that name — either a dependency nobody ` +
47
49
  `modelled, or one that no longer exists. Model it (an external system as its own element ` +
48
- `with the edge this service actually has; a private store as a nested container), or ` +
50
+ `with the edge this service actually has; a store this service owns as a nested container ` +
51
+ `or as its own element, whichever the map already does), or ` +
49
52
  `delete the entry` +
50
53
  (close.length > 0 ? `. Did you mean: ${close.join(", ")}?` : ""),
51
54
  });
@@ -1 +1 @@
1
- {"version":3,"file":"health-deps.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/health-deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAkBlE,MAAM,UAAU,wBAAwB,CAAC,IAAgB;IACvD,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5D,yEAAyE;IACzE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;YAAE,SAAS;QAC9E,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,8BAA8B;YACpC,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,OAAO,sCAAsC,GAAG,mBAAmB;gBACtE,GAAG,SAAS,qDAAqD;gBACjE,0FAA0F;gBAC1F,sFAAsF;gBACtF,kBAAkB;gBAClB,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * The health.yaml → model reconciliation: every id the `dependencies:` block\n * declares must be something this service's OWN model answers to.\n *\n * Without this reconciliation, a disabled optional service registry can leave\n * the model, landscape, arch spec and runbook corrected while health.yaml still\n * declares `serviceRegistry` as a `critical: startup` dependency under a green\n * validator — in the file an on-call engineer reaches for first. Its\n * dependencies block was otherwise free text as far as validation was\n * concerned.\n *\n * Resolution is against the SERVICE'S OWN model — deliberately never the\n * landscape. A private datastore lives nested inside the service element\n * (`landscape.datastore-private` is the check pushing it there), so the\n * landscape carries only what crosses the service boundary, while the model\n * carries everything the service touches — and that is the set the on-call\n * file has to agree with. An id resolves by the same names every other join\n * uses: the element's likec4 id, its `metadata { service }` binding, or its\n * title. Exact match, no case folding — a fold would be a permanent\n * equivalence semantic no other loam join has, able to silently join two\n * genuinely different names; the did-you-mean hint bridges the typo case\n * instead.\n */\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { elementService } from \"../../../core/c4/resolve/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\n\nexport interface HealthDeps {\n service: string;\n /**\n * The resolved path of the service's own `model.likec4` — the file this\n * check reads and the one its message names. Handed in rather than joined\n * from the id: `services/<id>/model.likec4` is right only for an unfiled\n * service, and every sibling finding in this command prints the resolved\n * path of the file it means.\n */\n modelPath: string;\n /** `dependencies:` ids, already muted by the caller when health.yaml is unreadable. */\n dependencies: string[];\n /** The service's own parsed model — empty when absent or invalid, which mutes the check. */\n elements: Elem[];\n}\n\nexport function healthDependencyFindings(deps: HealthDeps): Finding[] {\n const { service, dependencies, elements, modelPath } = deps;\n // An absent or unparsable model already has its own finding, and grading\n // health.yaml against a model nobody could read would manufacture warns out\n // of the wrong file's breakage.\n if (elements.length === 0) return [];\n const findings: Finding[] = [];\n for (const dep of dependencies) {\n if (elements.some((e) => e.id === dep || elementService(e) === dep)) continue;\n const pool = [...new Set(elements.flatMap((e) => [e.id, elementService(e) as string]))];\n const close = closeIds(dep, pool);\n findings.push({\n severity: \"warn\",\n code: \"health.dependency-unmodelled\",\n subject: service,\n message:\n `${service}: health.yaml declares dependency '${dep}' but nothing in ` +\n `${modelPath} answers to that name — either a dependency nobody ` +\n `modelled, or one that no longer exists. Model it (an external system as its own element ` +\n `with the edge this service actually has; a private store as a nested container), or ` +\n `delete the entry` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n return findings;\n}\n"]}
1
+ {"version":3,"file":"health-deps.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/health-deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAkBlE,MAAM,UAAU,wBAAwB,CAAC,IAAgB;IACvD,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5D,yEAAyE;IACzE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;YAAE,SAAS;QAC9E,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,8BAA8B;YACpC,OAAO,EAAE,OAAO;YAChB,OAAO,EACL,GAAG,OAAO,sCAAsC,GAAG,mBAAmB;gBACtE,GAAG,SAAS,qDAAqD;gBACjE,0FAA0F;gBAC1F,2FAA2F;gBAC3F,6DAA6D;gBAC7D,kBAAkB;gBAClB,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * The health.yaml → model reconciliation: every id the `dependencies:` block\n * declares must be something this service's OWN model answers to.\n *\n * Without this reconciliation, a disabled optional service registry can leave\n * the model, landscape, arch spec and runbook corrected while health.yaml still\n * declares `serviceRegistry` as a `critical: startup` dependency under a green\n * validator — in the file an on-call engineer reaches for first. Its\n * dependencies block was otherwise free text as far as validation was\n * concerned.\n *\n * Resolution is against the SERVICE'S OWN model — deliberately never the\n * landscape. A store one service owns may be nested inside that service's\n * element or drawn as a fleet-level peer, whichever the team chose (loam grades\n * neither: `landscape.datastore-private`, which used to push it into the first,\n * was retired 2026-09-07). Only the nested placement puts it in the landscape's\n * interior at all, so the landscape is not a set this check could rely on, while\n * the model carries everything the service touches — and that is the set the\n * on-call file has to agree with. An id resolves by the same names every other join\n * uses: the element's likec4 id, its `metadata { service }` binding, or its\n * title. Exact match, no case folding — a fold would be a permanent\n * equivalence semantic no other loam join has, able to silently join two\n * genuinely different names; the did-you-mean hint bridges the typo case\n * instead.\n */\nimport { closeIds } from \"../../../core/c4/arch.js\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { elementService } from \"../../../core/c4/resolve/service.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\n\nexport interface HealthDeps {\n service: string;\n /**\n * The resolved path of the service's own `model.likec4` — the file this\n * check reads and the one its message names. Handed in rather than joined\n * from the id: `services/<id>/model.likec4` is right only for an unfiled\n * service, and every sibling finding in this command prints the resolved\n * path of the file it means.\n */\n modelPath: string;\n /** `dependencies:` ids, already muted by the caller when health.yaml is unreadable. */\n dependencies: string[];\n /** The service's own parsed model — empty when absent or invalid, which mutes the check. */\n elements: Elem[];\n}\n\nexport function healthDependencyFindings(deps: HealthDeps): Finding[] {\n const { service, dependencies, elements, modelPath } = deps;\n // An absent or unparsable model already has its own finding, and grading\n // health.yaml against a model nobody could read would manufacture warns out\n // of the wrong file's breakage.\n if (elements.length === 0) return [];\n const findings: Finding[] = [];\n for (const dep of dependencies) {\n if (elements.some((e) => e.id === dep || elementService(e) === dep)) continue;\n const pool = [...new Set(elements.flatMap((e) => [e.id, elementService(e) as string]))];\n const close = closeIds(dep, pool);\n findings.push({\n severity: \"warn\",\n code: \"health.dependency-unmodelled\",\n subject: service,\n message:\n `${service}: health.yaml declares dependency '${dep}' but nothing in ` +\n `${modelPath} answers to that name — either a dependency nobody ` +\n `modelled, or one that no longer exists. Model it (an external system as its own element ` +\n `with the edge this service actually has; a store this service owns as a nested container ` +\n `or as its own element, whichever the map already does), or ` +\n `delete the entry` +\n (close.length > 0 ? `. Did you mean: ${close.join(\", \")}?` : \"\"),\n });\n }\n return findings;\n}\n"]}
@@ -13,7 +13,7 @@
13
13
  * requirements and answer in findings.
14
14
  */
15
15
  import {} from "../../../core/vocabulary/report.js";
16
- import { requirementsMissingScenarios } from "../../../core/document/scenarios.js";
16
+ import { assertionlessFindings, examplesFindings, missingScenariosFinding, requirementsMissingScenarios, steplessFindings, } from "../../../core/document/scenarios.js";
17
17
  import { requirementIdProblems } from "../../../core/document/spec.js";
18
18
  import { closeIds, coversCandidates, entryResolves, parseCoversEntry, } from "../../../core/c4/arch.js";
19
19
  import {} from "../../../core/permissions/permissions.js";
@@ -243,12 +243,22 @@ export function coverageFinding(label, reqs) {
243
243
  message: `${label} covered (${reqs.length} requirement${reqs.length === 1 ? "" : "s"}, all with scenarios)`,
244
244
  };
245
245
  }
246
- return {
247
- severity: "error",
248
- code: "requirements.missing-scenarios",
249
- message: `${label}: ${missing.length} requirement(s) without a scenario`,
250
- details: missing.map((r) => r.name),
251
- text: { detailPrefix: "- " },
252
- };
246
+ return { ...missingScenariosFinding(label, missing), text: { detailPrefix: "- " } };
247
+ }
248
+ /**
249
+ * The scenario-shape run of the battery every requirement document gets, in
250
+ * report order — stepless, assertionless, examples — spelled once for the four
251
+ * documents the header names, so they cannot drift apart. What differs between
252
+ * the four stays at each call site: a delta's coverage finding carries its
253
+ * service as `subject`, a living spec.md with no requirements reports
254
+ * `spec.no-requirements` in coverage's place, and duplicate names and
255
+ * requirement ids are graded on LIVING documents (`duplicateRequirementFindings`).
256
+ */
257
+ export function scenarioShapeFindings(label, subject, reqs) {
258
+ return [
259
+ ...steplessFindings(label, subject, reqs),
260
+ ...assertionlessFindings(label, subject, reqs),
261
+ ...examplesFindings(label, subject, reqs),
262
+ ];
253
263
  }
254
264
  //# sourceMappingURL=requirements.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"requirements.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/requirements.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a requirement document owes, checked the same way wherever it lives.\n *\n * The same `### Requirement:` grammar is authored in four places — a service's\n * spec.md and arch.spec.md, and a feature's per-service deltas of both — and\n * every breach below carries from a delta into the living document through the\n * archive merge. So the checks are one set called from both target kinds\n * (`service/` and `feature/`): grading a delta more leniently than the file it\n * is about to become is how a defect acquires a signature saying it was\n * reviewed.\n *\n * None of these read the filesystem and none of them print — they take parsed\n * requirements and answer in findings.\n */\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { requirementsMissingScenarios } from \"../../../core/document/scenarios.js\";\nimport { requirementIdProblems, type Requirement } from \"../../../core/document/spec.js\";\nimport {\n closeIds,\n coversCandidates,\n entryResolves,\n parseCoversEntry,\n type CoverageScope,\n type CoversEntry,\n} from \"../../../core/c4/arch.js\";\nimport { type Vocabulary } from \"../../../core/permissions/permissions.js\";\nimport { type Operation } from \"../../../core/openapi/doc.js\";\n\n/** The parsed Covers entries of every requirement that will live (REMOVED covers nothing). */\nexport function coversEntries(reqs: Requirement[]): CoversEntry[] {\n return reqs.filter((r) => r.kind !== \"REMOVED\").flatMap((r) => r.covers.map(parseCoversEntry));\n}\n\n/**\n * `spec.duplicate-requirement` — two `### Requirement:` blocks with one name in\n * one LIVING document. Nothing else catches it, and the merge algebra\n * (applyRequirementDelta) matches by name and edits only the FIRST match: a\n * later archive rewrites one copy and the other survives as a stale snapshot\n * of whatever the requirement used to say. Per file on purpose — spec.md and\n * arch.spec.md are separate requirement namespaces (their merges never cross\n * files), so one name appearing in both is legal and unflagged.\n */\nexport function duplicateRequirementFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n const counts = new Map<string, number>();\n for (const r of reqs) counts.set(r.name, (counts.get(r.name) ?? 0) + 1);\n return [...counts]\n .filter(([, n]) => n > 1)\n .map(([name, n]) => ({\n severity: \"error\" as const,\n code: \"spec.duplicate-requirement\",\n subject,\n message: `${where}: requirement '${name}' is defined ${n} times — a merge edits only the first, every other copy lives on stale; keep exactly one`,\n }));\n}\n\n/** Stable IDs are optional, but once authored they must select exactly one requirement. */\nexport function requirementIdFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n return requirementIdProblems(reqs).map((problem) => {\n if (problem.kind === \"invalid\") {\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-invalid\",\n subject,\n message: `${where}: requirement '${problem.requirement}' has invalid Requirement-ID '${problem.value}' — use 1-128 characters matching [A-Za-z][A-Za-z0-9._-]*`,\n };\n }\n if (problem.kind === \"repeated\") {\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-repeated\",\n subject,\n message: `${where}: requirement '${problem.requirement}' declares Requirement-ID ${problem.values.length} times — identity must be declared exactly once`,\n };\n }\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-duplicate\",\n subject,\n message: `${where}: Requirement-ID '${problem.id}' is shared by ${problem.requirements.map((name) => `'${name}'`).join(\", \")} — one ID may identify only one requirement`,\n };\n });\n}\n\n/**\n * The two list lines of the requirement grammar, exactly as core/document/spec.ts spells\n * them (mirrored here, not exported from there, because the parser's grammar is\n * its own; a drift shows up as this check counting differently than the parser\n * assigns). A SECOND matching line in one requirement body REPLACES the first —\n * assignment, not append, the documented keep-last quirk — so the author's\n * \"long list in two lines\" pattern silently loses its first line.\n */\nconst OPERATIONS_LINE_RE = /^\\s*Operations?:\\s*(.+?)\\s*$/i;\nconst COVERS_LINE_RE = /^\\s*Covers?:\\s*(.+?)\\s*$/i;\n\n/**\n * `spec.repeated-operations` / `spec.repeated-covers` — warn on the silent\n * loss, keep the keep-last semantics (changing them would re-read every spec\n * in the fleet). Scenario bodies never count: the parser only assigns from the\n * requirement's own body lines, and `Requirement.text` is exactly those.\n * REMOVED requirements are exempt the way coversEntries exempts them — content\n * on its way out obliges nothing.\n */\nexport function repeatedListLineFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const { re, label, code } of [\n { re: OPERATIONS_LINE_RE, label: \"Operations:\", code: \"spec.repeated-operations\" },\n { re: COVERS_LINE_RE, label: \"Covers:\", code: \"spec.repeated-covers\" },\n ]) {\n const n = r.text.filter((line) => re.test(line)).length;\n if (n < 2) continue;\n out.push({\n severity: \"warn\",\n code,\n subject,\n 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`,\n });\n }\n }\n return out;\n}\n\n/**\n * `covers.unknown` — the typo guard on the Covers: line. Warn, not error: the\n * axis is advisory end to end, and a wrong id already costs its author the\n * coverage they wrote the line for. The hint offers only real ids (closeIds's\n * rule), and says where resolution looked when there is nothing close.\n * `healthUnreadable` mutes the alert:/sli: forms only: against a health.yaml\n * nobody could read, \"did you mean\" is a false diagnosis of a typo —\n * health.invalid (emitted by the service target) is the honest one.\n */\n/** Which document is being graded, and under whose name the finding is filed. */\nexport interface CoverageTarget {\n where: string;\n subject: string;\n}\n\nexport function coversUnknownFindings(\n reqs: Requirement[],\n target: CoverageTarget,\n scope: CoverageScope,\n healthUnreadable = false,\n): Finding[] {\n const { where, subject } = target;\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const raw of r.covers) {\n const entry = parseCoversEntry(raw);\n if (healthUnreadable && (entry.form === \"alert\" || entry.form === \"sli\")) continue;\n if (entryResolves(entry, scope)) continue;\n const close = coversCandidates(entry, scope);\n out.push({\n severity: \"warn\",\n code: \"covers.unknown\",\n subject,\n message:\n `${where}: requirement '${r.name}' — Covers: '${raw}' resolves to nothing` +\n (close.length > 0\n ? `. Did you mean: ${close.join(\", \")}?`\n : \" in the model, the landscape or health.yaml\"),\n });\n }\n }\n return out;\n}\n\n/**\n * `permissions.unknown` — the typo guard on the `Requires:` line, and an ERROR\n * where its `Covers:` sibling is a warning.\n *\n * The two differ because what they promise differs. `Covers:` is advisory end\n * to end: a wrong id costs its author the coverage they wrote the line for and\n * nothing else. `Requires:` names authorization the service actually enforces,\n * and the failure it guards is the one an unaided reader cannot see — an\n * invented permission reads exactly like a real one, in the requirement, in the\n * generated scenario, and in the test somebody then writes against it. Its\n * grade is `Operations:`', not `Covers:`'.\n *\n * Silent on ONE case only: an unreadable vocabulary, where\n * `permissions.invalid` is the honest finding and grading every entry against a\n * file nobody could parse is the false cascade `healthUnreadable` exists to\n * stop one axis over. A MISSING vocabulary is not that case — the `Requires:`\n * line is itself the opt-in, and a line that resolves against nothing while\n * reading like a join is the failure this whole axis exists to prevent.\n */\nexport function requiresUnknownFindings(\n reqs: Requirement[],\n target: CoverageTarget,\n vocabulary: Vocabulary,\n): Finding[] {\n const { where, subject } = target;\n if (vocabulary.invalid !== undefined) return [];\n const known = [...vocabulary.byId.keys()];\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const raw of r.requires) {\n if (vocabulary.byId.has(raw)) continue;\n const close = closeIds(raw, known);\n out.push({\n severity: \"error\",\n code: \"permissions.unknown\",\n subject,\n message:\n `${where}: requirement '${r.name}' — Requires: '${raw}' is not declared in architecture/permissions.yaml` +\n (!vocabulary.present\n ? \", which does not exist. The `Requires:` line is the opt-in: create the fleet vocabulary with this permission under its subject kind, or drop the line\"\n : close.length > 0\n ? `. Did you mean: ${close.join(\", \")}?`\n : \". Entries are '<subject>/<permission>'; declare it there, or fix the spelling\"),\n });\n }\n }\n return out;\n}\n\n/**\n * `api.response-ungoverned` — a declared 4xx/5xx response no scenario reaches.\n *\n * The gap this closes is the one a whole adoption can fall into: `api.ungoverned`\n * grades OPERATIONS against requirements, so an endpoint with one happy-path\n * requirement and twelve declared failure codes is fully governed by every\n * check loam had. The refusals are where a service's decision layer surfaces —\n * a permission denied, a field combination rejected, a state that forbids the\n * transition — and the contract is the only artifact in the corpus that already\n * enumerates them.\n *\n * A scenario \"reaches\" a code when the code appears as a standalone number\n * anywhere in its body, `Examples` rows included — which is why this arrived\n * with outline support and not before: a status column is exactly where a\n * matrix puts its codes, and before it those rows were prose in a description.\n * The match is textual and therefore generous by design. It answers \"did\n * anybody write this case down\", not \"is this scenario correct\", and the\n * failure it is built to catch is the code nobody mentioned at all. Warn, like\n * its sibling: a service may legitimately declare a code its scenarios describe\n * in words, and an error would teach authors to paste numbers into prose.\n *\n * Operations no requirement governs are skipped: `api.ungoverned` already\n * names those, and reporting each of their codes as well is one defect\n * counted twice.\n */\nexport function responseGovernanceFindings(\n service: string,\n ops: Operation[],\n livingReqs: Requirement[],\n): Finding[] {\n const ungoverned: string[] = [];\n for (const op of ops) {\n if (op.failureCodes.length === 0) continue;\n const governing = livingReqs.filter((r) => r.kind !== \"REMOVED\" && r.operations.includes(op.id));\n if (governing.length === 0) continue;\n const body = governing.flatMap((r) => r.scenarios.flatMap((s) => s.lines)).join(\"\\n\");\n const missing = op.failureCodes.filter((code) => !new RegExp(`(?<!\\\\d)${code}(?!\\\\d)`).test(body));\n if (missing.length > 0) ungoverned.push(`${op.id}: ${missing.join(\", \")}`);\n }\n if (ungoverned.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"api.response-ungoverned\",\n subject: service,\n message: `${service}: ${ungoverned.length} operation(s) declare a failure response no scenario reaches — write the refusal as a scenario, or as a row in one`,\n details: ungoverned,\n },\n ];\n}\n\nexport function coverageFinding(label: string, reqs: Requirement[]): Finding {\n const missing = requirementsMissingScenarios(reqs);\n if (missing.length === 0) {\n return {\n severity: \"ok\",\n code: \"requirements.covered\",\n message: `${label} covered (${reqs.length} requirement${reqs.length === 1 ? \"\" : \"s\"}, all with scenarios)`,\n };\n }\n return {\n severity: \"error\",\n code: \"requirements.missing-scenarios\",\n message: `${label}: ${missing.length} requirement(s) without a scenario`,\n details: missing.map((r) => r.name),\n text: { detailPrefix: \"- \" },\n };\n}\n"]}
1
+ {"version":3,"file":"requirements.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/requirements.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a requirement document owes, checked the same way wherever it lives.\n *\n * The same `### Requirement:` grammar is authored in four places — a service's\n * spec.md and arch.spec.md, and a feature's per-service deltas of both — and\n * every breach below carries from a delta into the living document through the\n * archive merge. So the checks are one set called from both target kinds\n * (`service/` and `feature/`): grading a delta more leniently than the file it\n * is about to become is how a defect acquires a signature saying it was\n * reviewed.\n *\n * None of these read the filesystem and none of them print — they take parsed\n * requirements and answer in findings.\n */\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport {\n assertionlessFindings,\n examplesFindings,\n missingScenariosFinding,\n requirementsMissingScenarios,\n steplessFindings,\n} from \"../../../core/document/scenarios.js\";\nimport { requirementIdProblems, type Requirement } from \"../../../core/document/spec.js\";\nimport {\n closeIds,\n coversCandidates,\n entryResolves,\n parseCoversEntry,\n type CoverageScope,\n type CoversEntry,\n} from \"../../../core/c4/arch.js\";\nimport { type Vocabulary } from \"../../../core/permissions/permissions.js\";\nimport { type Operation } from \"../../../core/openapi/doc.js\";\n\n/** The parsed Covers entries of every requirement that will live (REMOVED covers nothing). */\nexport function coversEntries(reqs: Requirement[]): CoversEntry[] {\n return reqs.filter((r) => r.kind !== \"REMOVED\").flatMap((r) => r.covers.map(parseCoversEntry));\n}\n\n/**\n * `spec.duplicate-requirement` — two `### Requirement:` blocks with one name in\n * one LIVING document. Nothing else catches it, and the merge algebra\n * (applyRequirementDelta) matches by name and edits only the FIRST match: a\n * later archive rewrites one copy and the other survives as a stale snapshot\n * of whatever the requirement used to say. Per file on purpose — spec.md and\n * arch.spec.md are separate requirement namespaces (their merges never cross\n * files), so one name appearing in both is legal and unflagged.\n */\nexport function duplicateRequirementFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n const counts = new Map<string, number>();\n for (const r of reqs) counts.set(r.name, (counts.get(r.name) ?? 0) + 1);\n return [...counts]\n .filter(([, n]) => n > 1)\n .map(([name, n]) => ({\n severity: \"error\" as const,\n code: \"spec.duplicate-requirement\",\n subject,\n message: `${where}: requirement '${name}' is defined ${n} times — a merge edits only the first, every other copy lives on stale; keep exactly one`,\n }));\n}\n\n/** Stable IDs are optional, but once authored they must select exactly one requirement. */\nexport function requirementIdFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n return requirementIdProblems(reqs).map((problem) => {\n if (problem.kind === \"invalid\") {\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-invalid\",\n subject,\n message: `${where}: requirement '${problem.requirement}' has invalid Requirement-ID '${problem.value}' — use 1-128 characters matching [A-Za-z][A-Za-z0-9._-]*`,\n };\n }\n if (problem.kind === \"repeated\") {\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-repeated\",\n subject,\n message: `${where}: requirement '${problem.requirement}' declares Requirement-ID ${problem.values.length} times — identity must be declared exactly once`,\n };\n }\n return {\n severity: \"error\" as const,\n code: \"spec.requirement-id-duplicate\",\n subject,\n message: `${where}: Requirement-ID '${problem.id}' is shared by ${problem.requirements.map((name) => `'${name}'`).join(\", \")} — one ID may identify only one requirement`,\n };\n });\n}\n\n/**\n * The two list lines of the requirement grammar, exactly as core/document/spec.ts spells\n * them (mirrored here, not exported from there, because the parser's grammar is\n * its own; a drift shows up as this check counting differently than the parser\n * assigns). A SECOND matching line in one requirement body REPLACES the first —\n * assignment, not append, the documented keep-last quirk — so the author's\n * \"long list in two lines\" pattern silently loses its first line.\n */\nconst OPERATIONS_LINE_RE = /^\\s*Operations?:\\s*(.+?)\\s*$/i;\nconst COVERS_LINE_RE = /^\\s*Covers?:\\s*(.+?)\\s*$/i;\n\n/**\n * `spec.repeated-operations` / `spec.repeated-covers` — warn on the silent\n * loss, keep the keep-last semantics (changing them would re-read every spec\n * in the fleet). Scenario bodies never count: the parser only assigns from the\n * requirement's own body lines, and `Requirement.text` is exactly those.\n * REMOVED requirements are exempt the way coversEntries exempts them — content\n * on its way out obliges nothing.\n */\nexport function repeatedListLineFindings(reqs: Requirement[], where: string, subject: string): Finding[] {\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const { re, label, code } of [\n { re: OPERATIONS_LINE_RE, label: \"Operations:\", code: \"spec.repeated-operations\" },\n { re: COVERS_LINE_RE, label: \"Covers:\", code: \"spec.repeated-covers\" },\n ]) {\n const n = r.text.filter((line) => re.test(line)).length;\n if (n < 2) continue;\n out.push({\n severity: \"warn\",\n code,\n subject,\n 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`,\n });\n }\n }\n return out;\n}\n\n/**\n * `covers.unknown` — the typo guard on the Covers: line. Warn, not error: the\n * axis is advisory end to end, and a wrong id already costs its author the\n * coverage they wrote the line for. The hint offers only real ids (closeIds's\n * rule), and says where resolution looked when there is nothing close.\n * `healthUnreadable` mutes the alert:/sli: forms only: against a health.yaml\n * nobody could read, \"did you mean\" is a false diagnosis of a typo —\n * health.invalid (emitted by the service target) is the honest one.\n */\n/** Which document is being graded, and under whose name the finding is filed. */\nexport interface CoverageTarget {\n where: string;\n subject: string;\n}\n\nexport function coversUnknownFindings(\n reqs: Requirement[],\n target: CoverageTarget,\n scope: CoverageScope,\n healthUnreadable = false,\n): Finding[] {\n const { where, subject } = target;\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const raw of r.covers) {\n const entry = parseCoversEntry(raw);\n if (healthUnreadable && (entry.form === \"alert\" || entry.form === \"sli\")) continue;\n if (entryResolves(entry, scope)) continue;\n const close = coversCandidates(entry, scope);\n out.push({\n severity: \"warn\",\n code: \"covers.unknown\",\n subject,\n message:\n `${where}: requirement '${r.name}' — Covers: '${raw}' resolves to nothing` +\n (close.length > 0\n ? `. Did you mean: ${close.join(\", \")}?`\n : \" in the model, the landscape or health.yaml\"),\n });\n }\n }\n return out;\n}\n\n/**\n * `permissions.unknown` — the typo guard on the `Requires:` line, and an ERROR\n * where its `Covers:` sibling is a warning.\n *\n * The two differ because what they promise differs. `Covers:` is advisory end\n * to end: a wrong id costs its author the coverage they wrote the line for and\n * nothing else. `Requires:` names authorization the service actually enforces,\n * and the failure it guards is the one an unaided reader cannot see — an\n * invented permission reads exactly like a real one, in the requirement, in the\n * generated scenario, and in the test somebody then writes against it. Its\n * grade is `Operations:`', not `Covers:`'.\n *\n * Silent on ONE case only: an unreadable vocabulary, where\n * `permissions.invalid` is the honest finding and grading every entry against a\n * file nobody could parse is the false cascade `healthUnreadable` exists to\n * stop one axis over. A MISSING vocabulary is not that case — the `Requires:`\n * line is itself the opt-in, and a line that resolves against nothing while\n * reading like a join is the failure this whole axis exists to prevent.\n */\nexport function requiresUnknownFindings(\n reqs: Requirement[],\n target: CoverageTarget,\n vocabulary: Vocabulary,\n): Finding[] {\n const { where, subject } = target;\n if (vocabulary.invalid !== undefined) return [];\n const known = [...vocabulary.byId.keys()];\n const out: Finding[] = [];\n for (const r of reqs) {\n if (r.kind === \"REMOVED\") continue;\n for (const raw of r.requires) {\n if (vocabulary.byId.has(raw)) continue;\n const close = closeIds(raw, known);\n out.push({\n severity: \"error\",\n code: \"permissions.unknown\",\n subject,\n message:\n `${where}: requirement '${r.name}' — Requires: '${raw}' is not declared in architecture/permissions.yaml` +\n (!vocabulary.present\n ? \", which does not exist. The `Requires:` line is the opt-in: create the fleet vocabulary with this permission under its subject kind, or drop the line\"\n : close.length > 0\n ? `. Did you mean: ${close.join(\", \")}?`\n : \". Entries are '<subject>/<permission>'; declare it there, or fix the spelling\"),\n });\n }\n }\n return out;\n}\n\n/**\n * `api.response-ungoverned` — a declared 4xx/5xx response no scenario reaches.\n *\n * The gap this closes is the one a whole adoption can fall into: `api.ungoverned`\n * grades OPERATIONS against requirements, so an endpoint with one happy-path\n * requirement and twelve declared failure codes is fully governed by every\n * check loam had. The refusals are where a service's decision layer surfaces —\n * a permission denied, a field combination rejected, a state that forbids the\n * transition — and the contract is the only artifact in the corpus that already\n * enumerates them.\n *\n * A scenario \"reaches\" a code when the code appears as a standalone number\n * anywhere in its body, `Examples` rows included — which is why this arrived\n * with outline support and not before: a status column is exactly where a\n * matrix puts its codes, and before it those rows were prose in a description.\n * The match is textual and therefore generous by design. It answers \"did\n * anybody write this case down\", not \"is this scenario correct\", and the\n * failure it is built to catch is the code nobody mentioned at all. Warn, like\n * its sibling: a service may legitimately declare a code its scenarios describe\n * in words, and an error would teach authors to paste numbers into prose.\n *\n * Operations no requirement governs are skipped: `api.ungoverned` already\n * names those, and reporting each of their codes as well is one defect\n * counted twice.\n */\nexport function responseGovernanceFindings(\n service: string,\n ops: Operation[],\n livingReqs: Requirement[],\n): Finding[] {\n const ungoverned: string[] = [];\n for (const op of ops) {\n if (op.failureCodes.length === 0) continue;\n const governing = livingReqs.filter((r) => r.kind !== \"REMOVED\" && r.operations.includes(op.id));\n if (governing.length === 0) continue;\n const body = governing.flatMap((r) => r.scenarios.flatMap((s) => s.lines)).join(\"\\n\");\n const missing = op.failureCodes.filter((code) => !new RegExp(`(?<!\\\\d)${code}(?!\\\\d)`).test(body));\n if (missing.length > 0) ungoverned.push(`${op.id}: ${missing.join(\", \")}`);\n }\n if (ungoverned.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"api.response-ungoverned\",\n subject: service,\n message: `${service}: ${ungoverned.length} operation(s) declare a failure response no scenario reaches — write the refusal as a scenario, or as a row in one`,\n details: ungoverned,\n },\n ];\n}\n\nexport function coverageFinding(label: string, reqs: Requirement[]): Finding {\n const missing = requirementsMissingScenarios(reqs);\n if (missing.length === 0) {\n return {\n severity: \"ok\",\n code: \"requirements.covered\",\n message: `${label} covered (${reqs.length} requirement${reqs.length === 1 ? \"\" : \"s\"}, all with scenarios)`,\n };\n }\n return { ...missingScenariosFinding(label, missing), text: { detailPrefix: \"- \" } };\n}\n\n/**\n * The scenario-shape run of the battery every requirement document gets, in\n * report order — stepless, assertionless, examples — spelled once for the four\n * documents the header names, so they cannot drift apart. What differs between\n * the four stays at each call site: a delta's coverage finding carries its\n * service as `subject`, a living spec.md with no requirements reports\n * `spec.no-requirements` in coverage's place, and duplicate names and\n * requirement ids are graded on LIVING documents (`duplicateRequirementFindings`).\n */\nexport function scenarioShapeFindings(label: string, subject: string, reqs: Requirement[]): Finding[] {\n return [\n ...steplessFindings(label, subject, reqs),\n ...assertionlessFindings(label, subject, reqs),\n ...examplesFindings(label, subject, reqs),\n ];\n}\n"]}
@@ -6,7 +6,6 @@ import { listField, readFrontmatter } from "../../../core/document/frontmatter.j
6
6
  import {} from "../../../core/vocabulary/report.js";
7
7
  import { missingSources, patternSources, unsafeSources } from "../../../core/provenance/sources.js";
8
8
  import { emptySourcesMessage, expandSourceFiles } from "../../../core/provenance/stamp.js";
9
- import { UNVERIFIABLE } from "./vocabulary.js";
10
9
  /**
11
10
  * The two things a `sources` list can be that `serviceProvenance` cannot say.
12
11
  *
@@ -45,9 +44,14 @@ export async function sourceScopeFindings(docsDir, service, repoDir, fleet) {
45
44
  // arch axis. The refusal and the finding must be the same sentence.
46
45
  const label = file === "spec.md" ? service : `${service}: ${file}`;
47
46
  if (repoDir === undefined) {
47
+ // A literal, as at the rollup's counting site (../report.ts), and for
48
+ // the reason given there: the stable-code collector reads only literal
49
+ // `code:` slots. This used to be a shared constant, which kept the two
50
+ // spellings equal but hid the emission itself from the collector — the
51
+ // code was guarded only because the rollup's text marker spelled it.
48
52
  out.push({
49
53
  severity: "ok",
50
- code: UNVERIFIABLE,
54
+ code: "sources.unverifiable-from-here",
51
55
  subject: service,
52
56
  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.`,
53
57
  });
@@ -1 +1 @@
1
- {"version":3,"file":"sources.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/sources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAwB,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAExD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACpG,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3F,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAgB,EAChB,OAAwB,EACxB,OAA2B,EAC3B,KAAoB;IAEpB,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAChE,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,+EAA+E;IAC/E,yEAAyE;IACzE,sEAAsE;IACtE,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QACnG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QAChC,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,eAAe,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;QAClE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACnC,0EAA0E;QAC1E,oEAAoE;QACpE,MAAM,KAAK,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,IAAI,EAAE,CAAC;QACnE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,GAAG,CAAC,IAAI,CAAC;gBACP,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,YAAY;gBAClB,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,KAAK,KAAK,OAAO,CAAC,MAAM,wCAAwC,OAAO,gFAAgF,OAAO,oBAAoB;aAC/L,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,wEAAwE;QACxE,oEAAoE;QACpE,0EAA0E;QAC1E,IACE,cAAc,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC;YAClC,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC;YAC1C,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,EAC3C,CAAC;YACD,SAAS;QACX,CAAC;QACD,MAAM,SAAS,GAAG,MAAM,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QACnE,IAAI,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QACzC,GAAG,CAAC,IAAI,CAAC;YACP,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,SAAS,CAAC,KAAK,IAAI,mBAAmB,CAAC,KAAK,EAAE,OAAO,CAAC;SAChE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { SPEC_AXES } from \"../../../core/repo/paths.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { listField, readFrontmatter } from \"../../../core/document/frontmatter.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { missingSources, patternSources, unsafeSources } from \"../../../core/provenance/sources.js\";\nimport { emptySourcesMessage, expandSourceFiles } from \"../../../core/provenance/stamp.js\";\nimport { UNVERIFIABLE } from \"./vocabulary.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * The two things a `sources` list can be that `serviceProvenance` cannot say.\n *\n * `sources.unverifiable-from-here` — the spec names sources and loam is NOT in\n * that service's repository, so every sources check (existence, digest,\n * staleness) is skipped. serviceProvenance returns an empty list in that case\n * and the silence read as \"checked and fine\". It used to be counted only under\n * `--all` and printed as one rollup line, which meant `validate --service X`\n * run from the docs repo — the single most common way anyone looks at one\n * service — reported nothing at all about its own blind spot. Severity `ok`,\n * deliberately: nothing is WRONG with the docs, the check simply cannot run\n * here, and grading it a warning would make a fully documented fleet\n * permanently yellow and `--strict` permanently red in the docs repo's CI.\n *\n * `sources.empty` — the paths exist, and expand to no files at all: an empty\n * directory, or a tree the repository itself ignores. A digest over nothing\n * never changes, so the stamp would read as current forever. `loam vouch`\n * already refuses to stamp it; until now `validate` said nothing, so an author\n * got a green run followed by a refusal, two commands contradicting each other\n * about one document. The sentence comes from `emptySourcesMessage`, the same\n * definition vouch refuses with, under the label vouch uses.\n */\nexport async function sourceScopeFindings(\n docsDir: DocsDir,\n service: PathableService,\n repoDir: string | undefined,\n fleet?: FleetContext,\n): Promise<Finding[]> {\n const paths = await locateServicePaths(docsDir, service, fleet);\n const out: Finding[] = [];\n // The axis pair is SPEC_AXES', not this function's: `serviceProvenance` grades\n // the same two files from the same list, and a scope check that walked a\n // shorter list than the grading would go quiet on the axis it forgot.\n for (const { path, file } of SPEC_AXES.map((axis) => ({ path: paths[axis.key], file: axis.file }))) {\n if (!existsSync(path)) continue;\n const sources = listField(await readFrontmatter(path), \"sources\");\n if (sources.length === 0) continue;\n // vouch's own labelling: a bare service id for spec.md, qualified for the\n // arch axis. The refusal and the finding must be the same sentence.\n const label = file === \"spec.md\" ? service : `${service}: ${file}`;\n if (repoDir === undefined) {\n out.push({\n severity: \"ok\",\n code: UNVERIFIABLE,\n subject: service,\n 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.`,\n });\n continue;\n }\n // Every other shape of broken list is serviceProvenance's to grade, and\n // grading them twice would send an author fixing one thing from two\n // findings. \"Covers no files\" is only meaningful once the paths are real.\n if (\n patternSources(sources).length > 0 ||\n unsafeSources(repoDir, sources).length > 0 ||\n missingSources(repoDir, sources).length > 0\n ) {\n continue;\n }\n const expansion = await expandSourceFiles(repoDir, sources, label);\n if (expansion.files.length > 0) continue;\n out.push({\n severity: \"warn\",\n code: \"sources.empty\",\n subject: service,\n message: expansion.empty ?? emptySourcesMessage(label, sources),\n });\n }\n return out;\n}\n"]}
1
+ {"version":3,"file":"sources.js","sourceRoot":"","sources":["../../../../src/commands/validate/checks/sources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAwB,MAAM,qCAAqC,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAExD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,EAAgB,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACpG,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAG3F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAgB,EAChB,OAAwB,EACxB,OAA2B,EAC3B,KAAmB;IAEnB,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAChE,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,+EAA+E;IAC/E,yEAAyE;IACzE,sEAAsE;IACtE,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QACnG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QAChC,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,eAAe,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;QAClE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACnC,0EAA0E;QAC1E,oEAAoE;QACpE,MAAM,KAAK,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,IAAI,EAAE,CAAC;QACnE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,sEAAsE;YACtE,uEAAuE;YACvE,uEAAuE;YACvE,uEAAuE;YACvE,qEAAqE;YACrE,GAAG,CAAC,IAAI,CAAC;gBACP,QAAQ,EAAE,IAAI;gBACd,IAAI,EAAE,gCAAgC;gBACtC,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,GAAG,KAAK,KAAK,OAAO,CAAC,MAAM,wCAAwC,OAAO,gFAAgF,OAAO,oBAAoB;aAC/L,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,wEAAwE;QACxE,oEAAoE;QACpE,0EAA0E;QAC1E,IACE,cAAc,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC;YAClC,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC;YAC1C,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,EAC3C,CAAC;YACD,SAAS;QACX,CAAC;QACD,MAAM,SAAS,GAAG,MAAM,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QACnE,IAAI,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QACzC,GAAG,CAAC,IAAI,CAAC;YACP,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,SAAS,CAAC,KAAK,IAAI,mBAAmB,CAAC,KAAK,EAAE,OAAO,CAAC;SAChE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { type PathableService } from \"../../../core/kernel/ids/service.js\";\nimport { SPEC_AXES } from \"../../../core/repo/paths.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { listField, readFrontmatter } from \"../../../core/document/frontmatter.js\";\nimport { type Finding } from \"../../../core/vocabulary/report.js\";\nimport { missingSources, patternSources, unsafeSources } from \"../../../core/provenance/sources.js\";\nimport { emptySourcesMessage, expandSourceFiles } from \"../../../core/provenance/stamp.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\n\n/**\n * The two things a `sources` list can be that `serviceProvenance` cannot say.\n *\n * `sources.unverifiable-from-here` — the spec names sources and loam is NOT in\n * that service's repository, so every sources check (existence, digest,\n * staleness) is skipped. serviceProvenance returns an empty list in that case\n * and the silence read as \"checked and fine\". It used to be counted only under\n * `--all` and printed as one rollup line, which meant `validate --service X`\n * run from the docs repo — the single most common way anyone looks at one\n * service — reported nothing at all about its own blind spot. Severity `ok`,\n * deliberately: nothing is WRONG with the docs, the check simply cannot run\n * here, and grading it a warning would make a fully documented fleet\n * permanently yellow and `--strict` permanently red in the docs repo's CI.\n *\n * `sources.empty` — the paths exist, and expand to no files at all: an empty\n * directory, or a tree the repository itself ignores. A digest over nothing\n * never changes, so the stamp would read as current forever. `loam vouch`\n * already refuses to stamp it; until now `validate` said nothing, so an author\n * got a green run followed by a refusal, two commands contradicting each other\n * about one document. The sentence comes from `emptySourcesMessage`, the same\n * definition vouch refuses with, under the label vouch uses.\n */\nexport async function sourceScopeFindings(\n docsDir: DocsDir,\n service: PathableService,\n repoDir: string | undefined,\n fleet: FleetContext,\n): Promise<Finding[]> {\n const paths = await locateServicePaths(docsDir, service, fleet);\n const out: Finding[] = [];\n // The axis pair is SPEC_AXES', not this function's: `serviceProvenance` grades\n // the same two files from the same list, and a scope check that walked a\n // shorter list than the grading would go quiet on the axis it forgot.\n for (const { path, file } of SPEC_AXES.map((axis) => ({ path: paths[axis.key], file: axis.file }))) {\n if (!existsSync(path)) continue;\n const sources = listField(await readFrontmatter(path), \"sources\");\n if (sources.length === 0) continue;\n // vouch's own labelling: a bare service id for spec.md, qualified for the\n // arch axis. The refusal and the finding must be the same sentence.\n const label = file === \"spec.md\" ? service : `${service}: ${file}`;\n if (repoDir === undefined) {\n // A literal, as at the rollup's counting site (../report.ts), and for\n // the reason given there: the stable-code collector reads only literal\n // `code:` slots. This used to be a shared constant, which kept the two\n // spellings equal but hid the emission itself from the collector — the\n // code was guarded only because the rollup's text marker spelled it.\n out.push({\n severity: \"ok\",\n code: \"sources.unverifiable-from-here\",\n subject: service,\n 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.`,\n });\n continue;\n }\n // Every other shape of broken list is serviceProvenance's to grade, and\n // grading them twice would send an author fixing one thing from two\n // findings. \"Covers no files\" is only meaningful once the paths are real.\n if (\n patternSources(sources).length > 0 ||\n unsafeSources(repoDir, sources).length > 0 ||\n missingSources(repoDir, sources).length > 0\n ) {\n continue;\n }\n const expansion = await expandSourceFiles(repoDir, sources, label);\n if (expansion.files.length > 0) continue;\n out.push({\n severity: \"warn\",\n code: \"sources.empty\",\n subject: service,\n message: expansion.empty ?? emptySourcesMessage(label, sources),\n });\n }\n return out;\n}\n"]}