@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
@@ -0,0 +1,181 @@
1
+ /**
2
+ * The feature's own delta documents, read once: the requirement axes, the join
3
+ * from a scenario claim back to the words it was minted from, and whether the
4
+ * architecture and contract documents could be read at all.
5
+ *
6
+ * Everything here reads; nothing grades. The readability answers travel beside
7
+ * the counts for `core/projection/api.ts`'s reason, one axis over: a delta that
8
+ * does not parse yields an EMPTY change list, which is the same answer as a
9
+ * delta that genuinely changes nothing — and a packet is consumed as "what is
10
+ * left to do", where an empty answer over a YAML error reads as "nothing to do".
11
+ */
12
+ import { existsSync } from "node:fs";
13
+ import { errorText } from "../c4/likec4.js";
14
+ import { repoPath } from "../envelope/json.js";
15
+ import { scenarioBodyHash } from "../gherkin/digest.js";
16
+ import { requirementsMissingScenarios } from "../document/scenarios.js";
17
+ import { apiChanges } from "../projection/api.js";
18
+ import { eventChanges } from "../projection/events.js";
19
+ import { featurePaths, featureSpecPaths, SPEC_AXES } from "../repo/paths.js";
20
+ import { claimId, DIGEST_LENGTH, ID_LENGTH } from "../verify/claims/identity.js";
21
+ import { describedUnitId, doneCheck } from "./unit.js";
22
+ /**
23
+ * Both spec axes of every service the feature carries a delta for, in the order
24
+ * `core/verify/checklist.ts` walks them — service-major, `spec.md` before
25
+ * `arch.spec.md`. The order is load-bearing rather than tidy: it is what makes
26
+ * the recomputed claim ids below land on the same occurrence counters the
27
+ * checklist minted.
28
+ */
29
+ export async function readDeltaAxes(docsDir, featureDir, services, context) {
30
+ const out = [];
31
+ for (const service of services) {
32
+ const paths = featureSpecPaths(featureDir, service);
33
+ for (const axis of SPEC_AXES) {
34
+ const file = paths[axis.key];
35
+ if (!existsSync(file))
36
+ continue;
37
+ out.push({ service, axis, path: repoPath(docsDir, file), requirements: await context.readRequirements(file) });
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * Every scenario claim's source, keyed by the claim id it will carry.
44
+ *
45
+ * THE ID IS THE JOIN, and that is stronger than matching on the digest alone.
46
+ * A scenario claim's id is `claimId(featureId, "scenario.tested", tuple, seen)`
47
+ * over a tuple that already contains the requirement's identity, the scenario
48
+ * name and a prefix of the body hash — so recomputing it from the same walk, in
49
+ * the same order, with one shared occurrence map, reproduces the checklist's own
50
+ * answer rather than approximating it. Two identically-worded scenarios under
51
+ * one requirement share a digest and are still two questions: `claimId`'s `seen`
52
+ * counter numbers them in DOCUMENT ORDER, and this walk visits them in document
53
+ * order too, so the nth scenario pairs with the nth claim exactly.
54
+ *
55
+ * The digest is carried alongside and checked by the caller before a source is
56
+ * attached. That is the belt: if this walk and the checklist's ever disagreed
57
+ * about what a scenario says, the packet must show the unit with NO source
58
+ * rather than with somebody else's Given/When/Then.
59
+ *
60
+ * Only ADDED and MODIFIED requirements, exactly as the checklist does: a BASE
61
+ * requirement is the living state quoted inside the delta, and a REMOVED one is
62
+ * being retired. Neither is work anybody has to have done.
63
+ */
64
+ export function scenarioSources(featureId, axes) {
65
+ const out = new Map();
66
+ const seen = new Map();
67
+ for (const { service, axis, path, requirements } of axes) {
68
+ const arch = axis.key === "archSpec";
69
+ for (const requirement of requirements) {
70
+ if (requirement.kind === "BASE" || requirement.kind === "REMOVED")
71
+ continue;
72
+ const identity = requirement.id ?? requirement.name;
73
+ for (const scenario of requirement.scenarios) {
74
+ const body = scenarioBodyHash(service, scenario.lines, arch ? "arch" : "business");
75
+ // The tuple is `core/verify/checklist.ts`'s, part for part — the arch
76
+ // axis carries `arch.spec.md` as its second element, which is what keeps
77
+ // two identically-worded scenarios on the two axes two questions.
78
+ const parts = arch
79
+ ? [service, "arch.spec.md", identity, scenario.name, body.slice(0, ID_LENGTH)]
80
+ : [service, identity, scenario.name, body.slice(0, ID_LENGTH)];
81
+ const id = claimId(featureId, "scenario.tested", parts, seen);
82
+ out.set(id, {
83
+ digest: body.slice(0, DIGEST_LENGTH),
84
+ source: {
85
+ path,
86
+ ...(scenario.line === undefined ? {} : { line: scenario.line }),
87
+ requirement: requirement.name,
88
+ ...(requirement.id === undefined ? {} : { requirementId: requirement.id }),
89
+ scenario: scenario.name,
90
+ lines: scenario.lines,
91
+ },
92
+ });
93
+ }
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+ /**
99
+ * The first BDD act, as units: a requirement nobody has written a scenario for.
100
+ *
101
+ * `requirementsMissingScenarios` is the coverage rule every other surface
102
+ * applies — REMOVED requirements are exempt there and stay exempt here, content
103
+ * on its way out owes no acceptance criteria — so a packet can never disagree
104
+ * with `loam validate` about which requirements are bare.
105
+ */
106
+ export function describeUnits(featureId, axes) {
107
+ const out = [];
108
+ for (const { service, axis, path, requirements } of axes) {
109
+ for (const requirement of requirementsMissingScenarios(requirements)) {
110
+ const id = describedUnitId(featureId, service, axis.file, requirement.id ?? requirement.name);
111
+ out.push({
112
+ id,
113
+ kind: "requirement.described",
114
+ subject: service,
115
+ at: "describe",
116
+ claim: `requirement '${requirement.name}' (${service}, ${axis.file}) has no scenarios` +
117
+ " — write the Given/When/Then first",
118
+ dependsOn: [],
119
+ source: {
120
+ path,
121
+ ...(requirement.line === undefined ? {} : { line: requirement.line }),
122
+ requirement: requirement.name,
123
+ ...(requirement.id === undefined ? {} : { requirementId: requirement.id }),
124
+ },
125
+ done: { check: doneCheck({ kind: "requirement.described" }) },
126
+ });
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+ /**
132
+ * The two contract axes and the architecture delta, read for their COUNTS and
133
+ * for whether they could be read.
134
+ *
135
+ * The same readers `loam delta` projects through (`apiChanges`/`eventChanges`),
136
+ * so a packet can never report an operation the brief does not. The
137
+ * architecture half is read through the invocation's context, which means the
138
+ * parse is shared with the checklist derivation that has already loaded the same
139
+ * document — one LikeC4 workspace per invocation rather than two.
140
+ */
141
+ export async function readContracts(docsDir, featureDir, services, context) {
142
+ let operations = 0;
143
+ let messages = 0;
144
+ const broken = [];
145
+ for (const service of services) {
146
+ const paths = featureSpecPaths(featureDir, service);
147
+ const api = await apiChanges(paths.openapi);
148
+ const events = await eventChanges(paths.asyncapi);
149
+ if (api.unreadable || events.unreadable)
150
+ broken.push(service);
151
+ operations += api.changes.filter((change) => !change.remove).length;
152
+ messages += events.changes.filter((change) => !change.remove).length;
153
+ }
154
+ const delta = featurePaths(featureDir).delta;
155
+ let architecture;
156
+ if (existsSync(delta)) {
157
+ const loaded = await context.loadLikeC4(delta);
158
+ // The first error only: the packet says WHICH document could not be read and
159
+ // why, and a reader who needs the rest runs `loam validate --feature`, whose
160
+ // job the whole list is.
161
+ const first = loaded.errors[0];
162
+ if (first !== undefined)
163
+ architecture = errorText(first);
164
+ }
165
+ return {
166
+ operations,
167
+ messages,
168
+ unreadable: {
169
+ ...(architecture === undefined ? {} : { architecture }),
170
+ ...(broken.length === 0 ? {} : { contracts: broken }),
171
+ },
172
+ };
173
+ }
174
+ /** How many requirements and scenarios the delta axes hold, whatever their kind. */
175
+ export function axisSize(axes) {
176
+ return {
177
+ requirements: axes.reduce((n, a) => n + a.requirements.length, 0),
178
+ scenarios: axes.reduce((n, a) => n + a.requirements.reduce((m, r) => m + r.scenarios.length, 0), 0),
179
+ };
180
+ }
181
+ //# sourceMappingURL=sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sources.js","sourceRoot":"","sources":["../../../src/core/tasks/sources.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The feature's own delta documents, read once: the requirement axes, the join\n * from a scenario claim back to the words it was minted from, and whether the\n * architecture and contract documents could be read at all.\n *\n * Everything here reads; nothing grades. The readability answers travel beside\n * the counts for `core/projection/api.ts`'s reason, one axis over: a delta that\n * does not parse yields an EMPTY change list, which is the same answer as a\n * delta that genuinely changes nothing — and a packet is consumed as \"what is\n * left to do\", where an empty answer over a YAML error reads as \"nothing to do\".\n */\nimport { existsSync } from \"node:fs\";\nimport { errorText } from \"../c4/likec4.js\";\nimport { repoPath } from \"../envelope/json.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { scenarioBodyHash } from \"../gherkin/digest.js\";\nimport type { DocsDir, FeatureDir } from \"../kernel/ids/dirs.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport { requirementsMissingScenarios } from \"../document/scenarios.js\";\nimport type { Requirement } from \"../document/spec.js\";\nimport { apiChanges } from \"../projection/api.js\";\nimport { eventChanges } from \"../projection/events.js\";\nimport { featurePaths, featureSpecPaths, SPEC_AXES, type SpecAxis } from \"../repo/paths.js\";\nimport { claimId, DIGEST_LENGTH, ID_LENGTH } from \"../verify/claims/identity.js\";\nimport { describedUnitId, doneCheck, type ScenarioSource, type TaskUnit } from \"./unit.js\";\n\n/** One spec axis of one service's delta, parsed. */\nexport interface DeltaAxis {\n service: string;\n axis: SpecAxis;\n /** Repo-relative, for the payload and for a human opening the file. */\n path: string;\n requirements: Requirement[];\n}\n\n/**\n * Both spec axes of every service the feature carries a delta for, in the order\n * `core/verify/checklist.ts` walks them — service-major, `spec.md` before\n * `arch.spec.md`. The order is load-bearing rather than tidy: it is what makes\n * the recomputed claim ids below land on the same occurrence counters the\n * checklist minted.\n */\nexport async function readDeltaAxes(\n docsDir: DocsDir,\n featureDir: FeatureDir,\n services: readonly PathableService[],\n context: FleetContext,\n): Promise<DeltaAxis[]> {\n const out: DeltaAxis[] = [];\n for (const service of services) {\n const paths = featureSpecPaths(featureDir, service);\n for (const axis of SPEC_AXES) {\n const file = paths[axis.key];\n if (!existsSync(file)) continue;\n out.push({ service, axis, path: repoPath(docsDir, file), requirements: await context.readRequirements(file) });\n }\n }\n return out;\n}\n\n/**\n * A scenario's words, with the digest the claim that owns them must carry. The\n * digest travels beside the source rather than inside it because it is not part\n * of the source — it is the proof that this walk and the checklist's are talking\n * about the same scenario.\n */\nexport interface MintedSource {\n digest: string;\n source: ScenarioSource;\n}\n\n/**\n * Every scenario claim's source, keyed by the claim id it will carry.\n *\n * THE ID IS THE JOIN, and that is stronger than matching on the digest alone.\n * A scenario claim's id is `claimId(featureId, \"scenario.tested\", tuple, seen)`\n * over a tuple that already contains the requirement's identity, the scenario\n * name and a prefix of the body hash — so recomputing it from the same walk, in\n * the same order, with one shared occurrence map, reproduces the checklist's own\n * answer rather than approximating it. Two identically-worded scenarios under\n * one requirement share a digest and are still two questions: `claimId`'s `seen`\n * counter numbers them in DOCUMENT ORDER, and this walk visits them in document\n * order too, so the nth scenario pairs with the nth claim exactly.\n *\n * The digest is carried alongside and checked by the caller before a source is\n * attached. That is the belt: if this walk and the checklist's ever disagreed\n * about what a scenario says, the packet must show the unit with NO source\n * rather than with somebody else's Given/When/Then.\n *\n * Only ADDED and MODIFIED requirements, exactly as the checklist does: a BASE\n * requirement is the living state quoted inside the delta, and a REMOVED one is\n * being retired. Neither is work anybody has to have done.\n */\nexport function scenarioSources(featureId: string, axes: readonly DeltaAxis[]): Map<string, MintedSource> {\n const out = new Map<string, MintedSource>();\n const seen = new Map<string, number>();\n for (const { service, axis, path, requirements } of axes) {\n const arch = axis.key === \"archSpec\";\n for (const requirement of requirements) {\n if (requirement.kind === \"BASE\" || requirement.kind === \"REMOVED\") continue;\n const identity = requirement.id ?? requirement.name;\n for (const scenario of requirement.scenarios) {\n const body = scenarioBodyHash(service, scenario.lines, arch ? \"arch\" : \"business\");\n // The tuple is `core/verify/checklist.ts`'s, part for part — the arch\n // axis carries `arch.spec.md` as its second element, which is what keeps\n // two identically-worded scenarios on the two axes two questions.\n const parts = arch\n ? [service, \"arch.spec.md\", identity, scenario.name, body.slice(0, ID_LENGTH)]\n : [service, identity, scenario.name, body.slice(0, ID_LENGTH)];\n const id = claimId(featureId, \"scenario.tested\", parts, seen);\n out.set(id, {\n digest: body.slice(0, DIGEST_LENGTH),\n source: {\n path,\n ...(scenario.line === undefined ? {} : { line: scenario.line }),\n requirement: requirement.name,\n ...(requirement.id === undefined ? {} : { requirementId: requirement.id }),\n scenario: scenario.name,\n lines: scenario.lines,\n },\n });\n }\n }\n }\n return out;\n}\n\n/**\n * The first BDD act, as units: a requirement nobody has written a scenario for.\n *\n * `requirementsMissingScenarios` is the coverage rule every other surface\n * applies — REMOVED requirements are exempt there and stay exempt here, content\n * on its way out owes no acceptance criteria — so a packet can never disagree\n * with `loam validate` about which requirements are bare.\n */\nexport function describeUnits(featureId: string, axes: readonly DeltaAxis[]): TaskUnit[] {\n const out: TaskUnit[] = [];\n for (const { service, axis, path, requirements } of axes) {\n for (const requirement of requirementsMissingScenarios(requirements)) {\n const id = describedUnitId(featureId, service, axis.file, requirement.id ?? requirement.name);\n out.push({\n id,\n kind: \"requirement.described\",\n subject: service,\n at: \"describe\",\n claim:\n `requirement '${requirement.name}' (${service}, ${axis.file}) has no scenarios` +\n \" — write the Given/When/Then first\",\n dependsOn: [],\n source: {\n path,\n ...(requirement.line === undefined ? {} : { line: requirement.line }),\n requirement: requirement.name,\n ...(requirement.id === undefined ? {} : { requirementId: requirement.id }),\n },\n done: { check: doneCheck({ kind: \"requirement.described\" }) },\n });\n }\n }\n return out;\n}\n\n/** What could not be read. Keys are omitted when clean, so `{}` never means \"checked and fine\". */\nexport interface Unreadable {\n architecture?: string;\n contracts?: string[];\n}\n\n/** The delta's size, as documents rather than as open work — see `WorkPacket.size`. */\nexport interface DeltaSize {\n requirements: number;\n scenarios: number;\n operations: number;\n messages: number;\n calls: number;\n}\n\n/**\n * The two contract axes and the architecture delta, read for their COUNTS and\n * for whether they could be read.\n *\n * The same readers `loam delta` projects through (`apiChanges`/`eventChanges`),\n * so a packet can never report an operation the brief does not. The\n * architecture half is read through the invocation's context, which means the\n * parse is shared with the checklist derivation that has already loaded the same\n * document — one LikeC4 workspace per invocation rather than two.\n */\nexport async function readContracts(\n docsDir: DocsDir,\n featureDir: FeatureDir,\n services: readonly PathableService[],\n context: FleetContext,\n): Promise<{ operations: number; messages: number; unreadable: Unreadable }> {\n let operations = 0;\n let messages = 0;\n const broken: string[] = [];\n for (const service of services) {\n const paths = featureSpecPaths(featureDir, service);\n const api = await apiChanges(paths.openapi);\n const events = await eventChanges(paths.asyncapi);\n if (api.unreadable || events.unreadable) broken.push(service);\n operations += api.changes.filter((change) => !change.remove).length;\n messages += events.changes.filter((change) => !change.remove).length;\n }\n const delta = featurePaths(featureDir).delta;\n let architecture: string | undefined;\n if (existsSync(delta)) {\n const loaded = await context.loadLikeC4(delta);\n // The first error only: the packet says WHICH document could not be read and\n // why, and a reader who needs the rest runs `loam validate --feature`, whose\n // job the whole list is.\n const first = loaded.errors[0];\n if (first !== undefined) architecture = errorText(first);\n }\n return {\n operations,\n messages,\n unreadable: {\n ...(architecture === undefined ? {} : { architecture }),\n ...(broken.length === 0 ? {} : { contracts: broken }),\n },\n };\n}\n\n/** How many requirements and scenarios the delta axes hold, whatever their kind. */\nexport function axisSize(axes: readonly DeltaAxis[]): { requirements: number; scenarios: number } {\n return {\n requirements: axes.reduce((n, a) => n + a.requirements.length, 0),\n scenarios: axes.reduce((n, a) => n + a.requirements.reduce((m, r) => m + r.scenarios.length, 0), 0),\n };\n}\n"]}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * What the generated suite in THIS repository already carries — read-only, and
3
+ * never written.
4
+ *
5
+ * `loam gherkin` stamps every scenario it emits with `@loam-digest-<16hex>`,
6
+ * the same hex a `scenario.tested` claim carries (`core/gherkin/digest.ts` owns
7
+ * the one recipe). So the presence of a stamp answers exactly one question: has
8
+ * a test skeleton for THESE words been generated yet? That is the difference
9
+ * between a unit waiting at `generate` and one waiting at `implement`, and it is
10
+ * the only reason a packet looks at the service repository at all.
11
+ *
12
+ * The resolution is `loam gherkin`'s own — `gherkinRoot` for the directory,
13
+ * `parseStampedFeature` for the bytes — because a second spelling of either
14
+ * would let the packet grade a suite the emitter does not own, or miss one it
15
+ * does.
16
+ *
17
+ * TWO ABSENCES, KEPT APART. A root directory that is not there means the suite
18
+ * has not been generated: the stamps ARE readable and the set is empty, so every
19
+ * scenario unit honestly reads `generate`. No binding, an unreadable repository,
20
+ * or an output directory that escapes it means loam COULD NOT LOOK, which is
21
+ * reported as "unknown" and never as an empty set — "nothing is wrong" and "I
22
+ * could not look" are opposite facts, and only one of them is a task list.
23
+ */
24
+ import { existsSync } from "node:fs";
25
+ import { readdir, readFile } from "node:fs/promises";
26
+ import { join } from "node:path";
27
+ import { parseStampedFeature } from "../gherkin/read.js";
28
+ import { gherkinRoot } from "../gherkin/stamp.js";
29
+ export async function readStamps(binding) {
30
+ const unknown = { state: "unknown", digests: new Set(), service: null };
31
+ if (binding.service === undefined)
32
+ return unknown;
33
+ let root;
34
+ try {
35
+ root = gherkinRoot(binding.repoDir, binding.gherkinDir);
36
+ }
37
+ catch {
38
+ // An output directory that escapes the repo is a configuration loam refuses
39
+ // to write through (`core/kernel/path-safety.ts`), so it is also one this
40
+ // must not read through — and it is a could-not-look, not an empty suite.
41
+ return unknown;
42
+ }
43
+ if (!existsSync(root))
44
+ return { state: "read", digests: new Set(), service: binding.service };
45
+ const digests = new Set();
46
+ let entries;
47
+ try {
48
+ entries = await readdir(root, { withFileTypes: true });
49
+ }
50
+ catch {
51
+ // The directory is there and will not open. Unreadable is not empty.
52
+ return unknown;
53
+ }
54
+ for (const entry of entries) {
55
+ if (entry.isDirectory() || !entry.name.endsWith(".feature"))
56
+ continue;
57
+ let text;
58
+ try {
59
+ text = await readFile(join(root, entry.name), "utf8");
60
+ }
61
+ catch {
62
+ // One unreadable file under a readable root: the rest of the suite still
63
+ // answers, and a file loam cannot read carries no stamps it may credit.
64
+ continue;
65
+ }
66
+ const stamped = parseStampedFeature(text);
67
+ // Null means line 1 is not loam's stamp — the file is not loam's writing,
68
+ // and no staleness judgment may be built on it.
69
+ if (stamped === null)
70
+ continue;
71
+ for (const scenario of stamped.scenarios)
72
+ digests.add(scenario.digest);
73
+ }
74
+ return { state: "read", digests, service: binding.service };
75
+ }
76
+ //# sourceMappingURL=stamps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stamps.js","sourceRoot":"","sources":["../../../src/core/tasks/stamps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAoBlD,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAuB;IACtD,MAAM,OAAO,GAAiB,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACtF,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IAClD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;QAC5E,0EAA0E;QAC1E,0EAA0E;QAC1E,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;IAC9F,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,qEAAqE;QACrE,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;YAAE,SAAS;QACtE,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;YACzE,wEAAwE;YACxE,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;QAC1C,0EAA0E;QAC1E,gDAAgD;QAChD,IAAI,OAAO,KAAK,IAAI;YAAE,SAAS;QAC/B,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,SAAS;YAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;AAC9D,CAAC","sourcesContent":["/**\n * What the generated suite in THIS repository already carries — read-only, and\n * never written.\n *\n * `loam gherkin` stamps every scenario it emits with `@loam-digest-<16hex>`,\n * the same hex a `scenario.tested` claim carries (`core/gherkin/digest.ts` owns\n * the one recipe). So the presence of a stamp answers exactly one question: has\n * a test skeleton for THESE words been generated yet? That is the difference\n * between a unit waiting at `generate` and one waiting at `implement`, and it is\n * the only reason a packet looks at the service repository at all.\n *\n * The resolution is `loam gherkin`'s own — `gherkinRoot` for the directory,\n * `parseStampedFeature` for the bytes — because a second spelling of either\n * would let the packet grade a suite the emitter does not own, or miss one it\n * does.\n *\n * TWO ABSENCES, KEPT APART. A root directory that is not there means the suite\n * has not been generated: the stamps ARE readable and the set is empty, so every\n * scenario unit honestly reads `generate`. No binding, an unreadable repository,\n * or an output directory that escapes it means loam COULD NOT LOOK, which is\n * reported as \"unknown\" and never as an empty set — \"nothing is wrong\" and \"I\n * could not look\" are opposite facts, and only one of them is a task list.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { parseStampedFeature } from \"../gherkin/read.js\";\nimport { gherkinRoot } from \"../gherkin/stamp.js\";\n\n/** Which repository's suite to read, as loam.json describes it. */\nexport interface GherkinBinding {\n /** The repo root loam.json was found in — never the cwd, which may be any subdirectory. */\n repoDir: string;\n /** The service this repository says it is; absent in a docs repo or an unbound checkout. */\n service?: string;\n /** `gherkinDir` from loam.json, when it names one. */\n gherkinDir?: string;\n}\n\nexport interface StampReading {\n state: \"read\" | \"unknown\";\n /** The `@loam-digest-…` values found. Meaningful only when `state` is \"read\". */\n digests: ReadonlySet<string>;\n /** The service the stamps belong to — nothing may be said about any other repository's suite. */\n service: string | null;\n}\n\nexport async function readStamps(binding: GherkinBinding): Promise<StampReading> {\n const unknown: StampReading = { state: \"unknown\", digests: new Set(), service: null };\n if (binding.service === undefined) return unknown;\n let root: string;\n try {\n root = gherkinRoot(binding.repoDir, binding.gherkinDir);\n } catch {\n // An output directory that escapes the repo is a configuration loam refuses\n // to write through (`core/kernel/path-safety.ts`), so it is also one this\n // must not read through — and it is a could-not-look, not an empty suite.\n return unknown;\n }\n if (!existsSync(root)) return { state: \"read\", digests: new Set(), service: binding.service };\n const digests = new Set<string>();\n let entries;\n try {\n entries = await readdir(root, { withFileTypes: true });\n } catch {\n // The directory is there and will not open. Unreadable is not empty.\n return unknown;\n }\n for (const entry of entries) {\n if (entry.isDirectory() || !entry.name.endsWith(\".feature\")) continue;\n let text: string;\n try {\n text = await readFile(join(root, entry.name), \"utf8\");\n } catch {\n // One unreadable file under a readable root: the rest of the suite still\n // answers, and a file loam cannot read carries no stamps it may credit.\n continue;\n }\n const stamped = parseStampedFeature(text);\n // Null means line 1 is not loam's stamp — the file is not loam's writing,\n // and no staleness judgment may be built on it.\n if (stamped === null) continue;\n for (const scenario of stamped.scenarios) digests.add(scenario.digest);\n }\n return { state: \"read\", digests, service: binding.service };\n}\n"]}
Binary file
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unit.js","sourceRoot":"","sources":["../../../src/core/tasks/unit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * What a unit of work IS, and the total order they are read in.\r\n *\r\n * A unit is the third rendering of one ledger. `loam verify` answers \"answered\r\n * by whom\"; this answers \"what is still open, at which step, and what closes\r\n * it\". Nothing here is a second definition of a claim: for a claim-backed unit\r\n * the id, kind, subject, claim text, digest, operation and token are the\r\n * `Claim`'s own fields verbatim (`core/verify/checklist.ts`) — the task id IS\r\n * the claim id, which is what lets an agent hand a unit id straight to\r\n * `loam verify` and lets two runs be diffed.\r\n *\r\n * `at` is the step the unit is waiting at, and it is deliberately NOT spelled\r\n * `stage` or `verdict`: `stage` is the published `ArtifactStatus` key\r\n * (`core/status/report.ts`) and `verdict` is `VerificationVerdict`\r\n * (`core/verify/record.ts`). A third meaning for either word on a payload an\r\n * agent reads beside the other two is how a consumer comes to branch on the\r\n * wrong field.\r\n */\r\nimport { createHash } from \"node:crypto\";\r\nimport { CLAIM_KINDS, ID_LENGTH } from \"../verify/claims/identity.js\";\r\nimport { compareIds } from \"../repo/entries.js\";\r\n\r\n/**\r\n * Where a unit is waiting.\r\n *\r\n * `describe` is the first BDD act — a requirement with no scenario at all, so\r\n * there is nothing to generate a test from and nothing to claim. `generate`\r\n * means the scenario exists and this repository's generated suite does not\r\n * carry its stamp yet. `implement` is everything else that is open.\r\n * `attested` is the one step that is not work: the claim is confirmed on an\r\n * agent's word rather than on a run, so the unit never vanishes — it stays\r\n * visible, and closing it means a cucumber report, not an edit.\r\n */\r\nexport type TaskStep = \"describe\" | \"generate\" | \"implement\" | \"attested\";\r\n\r\n/** Where the words a unit is about are written down, so a reader can open the file. */\r\nexport interface RequirementSource {\r\n /** Repo-relative, like every path in the envelope. */\r\n path: string;\r\n /** 1-based heading line, when the parser recorded one. */\r\n line?: number;\r\n requirement: string;\r\n requirementId?: string;\r\n}\r\n\r\n/**\r\n * A scenario's source — the requirement's, plus the scenario's own words.\r\n *\r\n * A variant rather than four more optional fields on the shape above: `scenario`\r\n * and `lines` are only ever set together, and only for `scenario.tested`. The\r\n * body rides VERBATIM because it is the acceptance criterion the implementer is\r\n * meant to satisfy and the exact text the digest was taken over — paraphrasing\r\n * it here would be the one thing the whole digest chain exists to prevent.\r\n */\r\nexport interface ScenarioSource extends RequirementSource {\r\n scenario: string;\r\n lines: string[];\r\n}\r\n\r\nexport type TaskSource = RequirementSource | ScenarioSource;\r\n\r\nexport interface TaskUnit {\r\n /** The claim id verbatim, or a `requirement.described-…` id from `describedUnitId`. */\r\n id: string;\r\n kind: string;\r\n subject: string;\r\n at: TaskStep;\r\n claim: string;\r\n digest?: string;\r\n operation?: string;\r\n token?: string;\r\n dependsOn: string[];\r\n source?: TaskSource;\r\n done: { check: string };\r\n}\r\n\r\n/**\r\n * The one kind that is NOT a claim kind.\r\n *\r\n * `CLAIM_KINDS` is frozen: a member added to it re-hashes nothing, but it\r\n * changes the checklist's question set, and every `verification.yaml` in the\r\n * fleet would read as stale on upgrade. A requirement with no scenarios is also\r\n * not a promise anybody can answer with evidence — there is no text to test —\r\n * so it lives in a namespace of its own, with an id recipe of its own.\r\n */\r\nexport const DESCRIBED_KIND = \"requirement.described\";\r\n\r\n/**\r\n * The id of a `requirement.described` unit: content-derived, like a claim id\r\n * and for a claim id's reason — rename the requirement and the unit retires\r\n * itself rather than following a heading around.\r\n *\r\n * `\"loam.task\"` leads the tuple so this namespace can never collide with\r\n * `claimId`'s, whose tuples start with the feature id. NUL-joined for\r\n * `claimId`'s reason: no part's own text can spell the separator, so\r\n * [\"a b\",\"c\"] and [\"a\",\"b c\"] stay two units. `axisFile` is `spec.md` or\r\n * `arch.spec.md` — the two axes are two namespaces everywhere else in loam\r\n * (`core/gherkin/digest.ts` salts the scenario hash with it), and one\r\n * requirement name may legitimately appear on both.\r\n */\r\nexport function describedUnitId(\r\n featureId: string,\r\n service: string,\r\n axisFile: string,\r\n requirement: string,\r\n): string {\r\n const tuple = [\"loam.task\", featureId, DESCRIBED_KIND, service, axisFile, requirement].join(\"\u0000\");\r\n return `${DESCRIBED_KIND}-${createHash(\"sha256\").update(tuple).digest(\"hex\").slice(0, ID_LENGTH)}`;\r\n}\r\n\r\n/**\r\n * What closes this unit, in one sentence a reader can act on without opening\r\n * another document. Every arm names the mechanical channel that may answer it,\r\n * because the channels are not interchangeable: a scenario claim confirmed\r\n * without a run is `attested` and not `verified` (core/verify/record.ts), and an\r\n * api.exposes claim is answered by a contract report rather than by a cucumber\r\n * one.\r\n */\r\nexport function doneCheck(unit: { kind: string; digest?: string; operation?: string }): string {\r\n if (unit.kind === DESCRIBED_KIND) return \"the requirement carries at least one #### Scenario:\";\r\n if (unit.kind === \"scenario.tested\") {\r\n // Both report shapes by name: a fleet whose runner has no cucumber\r\n // formatter otherwise reads this as \"cucumber or nothing\", and the\r\n // runner-neutral shape exists precisely so it is not.\r\n return (\r\n `scenario @loam-digest-${unit.digest ?? \"\"} passes in a test run — a cucumber JSON report, ` +\r\n \"or a loamScenarioReport entry carrying that digest — and is recorded confirmed (loam verify --results)\"\r\n );\r\n }\r\n if (unit.kind === \"api.exposes\") {\r\n return `operationId '${unit.operation ?? \"\"}' reported passed via loam verify --contract-results and recorded`;\r\n }\r\n return \"recorded confirmed with file:line evidence (loam verify --record)\";\r\n}\r\n\r\n/**\r\n * The one edge v1 draws: a `c4.calls` unit depends on the `api.exposes` units\r\n * that promise the operation it calls.\r\n *\r\n * The join is EXACT and cross-kind on purpose — a call edge's `token` against a\r\n * provider claim's `operation`, never token against token. An AsyncAPI message\r\n * name and an operationId are different namespaces that routinely spell the\r\n * same word (`payment.Authorized` is not an operationId, but `createSplit` is a\r\n * perfectly plausible message name), and `event.declares` carries a `token`\r\n * too: joining on token equality would wire a consumer's call to an unrelated\r\n * message declaration and then order the whole packet by that fiction.\r\n * `operation` is set on `api.exposes` and nothing else — `core/verify/checklist.ts`\r\n * says so in the field's own docblock, and `contestedOperations` already\r\n * depends on it.\r\n *\r\n * Ordering is ADVICE, never a lock. The frontier does not exclude a unit for\r\n * having open dependencies; the sort merely puts providers first. An\r\n * implementer who starts at the consumer is not doing anything wrong, and a\r\n * packet that refused to show them the work would be a scheduler.\r\n */\r\nexport function linkDependencies(units: TaskUnit[]): void {\r\n const providers = new Map<string, string[]>();\r\n for (const unit of units) {\r\n if (unit.kind !== \"api.exposes\" || unit.operation === undefined) continue;\r\n const ids = providers.get(unit.operation) ?? [];\r\n ids.push(unit.id);\r\n providers.set(unit.operation, ids);\r\n }\r\n for (const unit of units) {\r\n if (unit.kind !== \"c4.calls\" || unit.token === undefined) continue;\r\n unit.dependsOn = [...(providers.get(unit.token) ?? [])].sort();\r\n }\r\n}\r\n\r\n/**\r\n * The reading order, total and deterministic — `--json` consumers diff it.\r\n *\r\n * Describe first, because a requirement with no scenario blocks the whole BDD\r\n * chain under it: there is nothing to generate and nothing to test. Then the\r\n * claim kinds in `CLAIM_KINDS` order, which is read off the vocabulary rather\r\n * than respelled here — that order already reads as the story of a feature (the\r\n * service exists, it exposes its operations, it declares its messages, the calls\r\n * into it are wired, the behaviour is tested), and a second copy of it would be\r\n * free to disagree.\r\n *\r\n * Within a kind a unit somebody depends on sorts before one nobody does, so the\r\n * provider of a shared operation leads its own kind. Then the subject by\r\n * `compareIds` (the fleet's one id ordering), then the source line so a service's\r\n * units read in document order, then the id as the final tiebreak — a unit with\r\n * no source must still land somewhere stable.\r\n */\r\nexport function sortUnits(units: TaskUnit[]): TaskUnit[] {\r\n const dependedOn = new Set(units.flatMap((unit) => unit.dependsOn));\r\n const kindRank = (unit: TaskUnit): number => {\r\n const index = (CLAIM_KINDS as readonly string[]).indexOf(unit.kind);\r\n // A kind outside the vocabulary sorts last rather than first: -1 would put\r\n // an unknown kind ahead of `service.exists`, which is the one position that\r\n // silently reorders every packet the day a kind is added.\r\n return index === -1 ? CLAIM_KINDS.length : index;\r\n };\r\n const lineOf = (unit: TaskUnit): number => unit.source?.line ?? Number.MAX_SAFE_INTEGER;\r\n return [...units].sort(\r\n (a, b) =>\r\n Number(a.at !== \"describe\") - Number(b.at !== \"describe\") ||\r\n kindRank(a) - kindRank(b) ||\r\n Number(dependedOn.has(b.id)) - Number(dependedOn.has(a.id)) ||\r\n compareIds(a.subject, b.subject) ||\r\n lineOf(a) - lineOf(b) ||\r\n compareIds(a.id, b.id),\r\n );\r\n}\r\n\r\n/** How many unit ids the frontier carries before it starts eliding. */\r\nexport const FRONTIER_LIMIT = 10;\r\n\r\n/** An open unit is one that is still work: `attested` is complete work resting on an assertion. */\r\nexport function isOpen(unit: TaskUnit): boolean {\r\n return unit.at !== \"attested\";\r\n}\r\n"]}
@@ -25,6 +25,7 @@ import { capabilityRequirementIndex } from "../capabilities/findings.js";
25
25
  import { compareIds } from "../repo/entries.js";
26
26
  import { readUseCases } from "./fleet.js";
27
27
  import { viewFile } from "./place.js";
28
+ import { participantsOf } from "./suites/participants.js";
28
29
  /** The tag spelling a declared capability id would carry — `identity/tokens` → `cap-identity-tokens`. */
29
30
  function tagFor(capability) {
30
31
  return `${CAP_TAG_PREFIX}${tagSlug(capability)}`;
@@ -65,6 +66,14 @@ export function flowsClaiming(scan, capability) {
65
66
  * exactly why it is written down that the reason it exists is the return type,
66
67
  * not the call site.
67
68
  *
69
+ * The walk itself is `./suites/participants.ts`' `participantsOf`, which
70
+ * this function used to spell for itself: a suite asks the same question of a
71
+ * different set of views and needs the participants that resolve to NO service
72
+ * as well. The restriction now lives here as a choice — this function reads
73
+ * `services` and drops `unresolved` — rather than as a filter inside the walk,
74
+ * so the one union a test stand is provisioned from and the one `explore` seeds
75
+ * from cannot resolve an element two ways.
76
+ *
68
77
  * The cost of the restriction, stated so the next reader can weigh it: a flow
69
78
  * drawn through a service the fleet has not adopted yet contributes no seed.
70
79
  * That service has no documentation to explore, so what is lost is a name in a
@@ -75,20 +84,8 @@ export function servicesInFlowsClaiming(scan, capabilities) {
75
84
  if (scan.kind !== "read")
76
85
  return [];
77
86
  const tags = new Set(capabilities.map(tagFor));
78
- const known = scan.model.known;
79
- const out = new Set();
80
- for (const view of scan.views) {
81
- if (![...tags].some((tag) => claims(view, tag)))
82
- continue;
83
- for (const step of view.steps) {
84
- for (const id of [step.source, step.target]) {
85
- const service = scan.resolve(id);
86
- if (known.has(service))
87
- out.add(service);
88
- }
89
- }
90
- }
91
- return [...out].sort(compareIds);
87
+ const claiming = scan.views.filter((view) => [...tags].some((tag) => claims(view, tag)));
88
+ return participantsOf(scan, claiming).services;
92
89
  }
93
90
  /**
94
91
  * The `<capability>#<Requirement-ID>` pairs a fleet's flows RESOLVE to — the
@@ -1 +1 @@
1
- {"version":3,"file":"capability.js","sourceRoot":"","sources":["../../../src/core/usecases/capability.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The capability half of the use-case axis: which flows CLAIM a declared\n * capability, and which services those flows run through.\n *\n * `core/capabilities/usecase-join.ts` owns the tag grammar and the resolution\n * ladder — it answers \"what does this tag name?\", including the `many` arm the\n * slug's non-injectivity forces. This module asks the question the other way\n * round: given a capability id somebody already has, which views carry its tag?\n * That direction needs no ladder, because the id is not in doubt; it needs only\n * the slug, so it takes the slug and stops.\n *\n * ## What this deliberately does NOT do\n *\n * It does not grade the tag. A view tagged `#cap-payments-refunds` matches BOTH\n * `payments/refunds` and `payments-refunds` when a fleet declares both, and this\n * module reports it under both — because that is what the tag says, and the tag\n * is what the author wrote. `usecase.capability-unresolved` is already an ERROR\n * over exactly that collision (`validate`'s use-case package), so a second\n * verdict here would be a second answer to a question the gate has already\n * settled. The same stance `PackPermission` takes on an undeclared permission:\n * carry the claim, let validate convict it.\n */\nimport {\n tagSlug,\n CAP_TAG_PREFIX,\n resolveCapabilityTags,\n resolveRequirementTags,\n} from \"../capabilities/usecase-join.js\";\nimport { capabilityRequirementIndex } from \"../capabilities/findings.js\";\nimport type { CapabilityVocabulary } from \"../capabilities/capabilities.js\";\nimport type { Requirement } from \"../document/spec.js\";\nimport { compareIds } from \"../repo/entries.js\";\nimport type { ParsedView } from \"../c4/parsed/dynamic-views.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { readUseCases, type UseCaseScan } from \"./fleet.js\";\nimport { viewFile } from \"./place.js\";\n\n/** One flow claiming a capability: enough to name it and to open the file it lives in. */\nexport interface ClaimingFlow {\n id: string;\n title?: string;\n /** Repo-relative document the view is written in. */\n file: string;\n}\n\n/** The tag spelling a declared capability id would carry — `identity/tokens` → `cap-identity-tokens`. */\nfunction tagFor(capability: string): string {\n return `${CAP_TAG_PREFIX}${tagSlug(capability)}`;\n}\n\n/** Does this view claim the capability? Exact tag match, case included, as `resolveCapabilityTags` matches. */\nfunction claims(view: ParsedView, tag: string): boolean {\n return view.tags.includes(tag);\n}\n\n/**\n * The flows claiming one declared capability, sorted by (file, id).\n *\n * Sorted rather than reported in LikeC4's record order for the reason every\n * other list in this axis is: nothing has measured that the parse preserves\n * declaration order, and a payload ordered by an unmeasured upstream detail\n * reorders under a dependency bump.\n */\nexport function flowsClaiming(scan: UseCaseScan, capability: string): ClaimingFlow[] {\n if (scan.kind !== \"read\") return [];\n const tag = tagFor(capability);\n return scan.views\n .filter((view) => claims(view, tag))\n .map((view) => ({ id: view.id, ...(view.title === undefined ? {} : { title: view.title }), file: viewFile(view) }))\n .sort((a, b) => compareIds(a.file, b.file) || compareIds(a.id, b.id));\n}\n\n/**\n * Every ENUMERATED service the flows claiming these capabilities run through.\n *\n * The restriction to the enumerated fleet is this function's own contract, not\n * a guard somebody downstream is relying on: a step's endpoints resolve to\n * whatever the element stands for, and a sequence diagram is full of endpoints\n * that own no directory — the actor who starts the flow, the topic in the\n * middle, the database at the end. A function called `servicesInFlows…` that\n * answered `Customer` would be lying in its own name. `loam explore`, today's\n * only caller, filters the result against its enumeration again for a different\n * reason (it needs `ServiceEntry`s, in enumeration order), so removing the\n * filter here would not currently change what that command prints — which is\n * exactly why it is written down that the reason it exists is the return type,\n * not the call site.\n *\n * The cost of the restriction, stated so the next reader can weigh it: a flow\n * drawn through a service the fleet has not adopted yet contributes no seed.\n * That service has no documentation to explore, so what is lost is a name in a\n * list, and `usecase.step-unlinked` / `landscape.service-undocumented` are where\n * the fleet is told about it.\n */\nexport function servicesInFlowsClaiming(scan: UseCaseScan, capabilities: readonly string[]): string[] {\n if (scan.kind !== \"read\") return [];\n const tags = new Set(capabilities.map(tagFor));\n const known = scan.model.known;\n const out = new Set<string>();\n for (const view of scan.views) {\n if (![...tags].some((tag) => claims(view, tag))) continue;\n for (const step of view.steps) {\n for (const id of [step.source, step.target]) {\n const service = scan.resolve(id);\n if (known.has(service)) out.add(service);\n }\n }\n }\n return [...out].sort(compareIds);\n}\n\n/**\n * The `<capability>#<Requirement-ID>` pairs a fleet's flows RESOLVE to — the\n * business promises a use case claims to keep.\n *\n * Lives in core rather than beside the grade that reports a broken tag, because\n * two very different callers need the same answer and they must not compute it\n * twice: `validate --all` subtracts these from\n * `capability.requirement-unrealized` (a promise a flow keeps is not unkept),\n * and `loam list capabilities` reports them beside the service requirements\n * that realize the same promise. Two implementations of \"which promises does\n * this fleet keep\" is two answers to the question the axis exists to settle.\n *\n * A MAP RATHER THAN A SET, because the two callers need different depths of the\n * same answer and one of them needs names. `validate` only asks whether a\n * promise is kept (`.has`, which a Map answers identically); a listing that said\n * \"kept\" without saying by WHAT would leave a reader unable to open the flow, go\n * look at its hops and judge whether the promise really is kept — which is the\n * whole reason the listing carries the realizing service requirements by name\n * rather than as a count. Widening the return type was cheaper than a second\n * function, and a second function is how the two corpora start disagreeing.\n *\n * ONLY RESOLVED CLAIMS COUNT, and both halves must resolve: the `#cap-` tag to\n * exactly one declared capability, and the `#req-` tag to exactly one of its\n * requirements. A broken tag of either kind is already an ERROR\n * (`usecase.capability-unresolved`, `usecase.requirement-unresolved`) and must\n * never also mark a promise kept — a typo that silenced the unrealized warning\n * would turn a mistake into a green fleet.\n *\n * `declared` carries the vocabulary ladder: `null` means there is nothing to\n * grade against, and the honest answer is then an empty map — no flow can be\n * said to keep a promise loam cannot see declared.\n *\n * Diff-stable by construction: the view ids under each promise are DEDUPLICATED\n * and sorted with compareIds, so nothing here depends on LikeC4's view order,\n * on how many tags one view carried, or on the readdir order the project was\n * staged in. The map's own key order reaches no output — both callers look\n * promises up by key — but the values are printed, so they are sorted.\n */\nexport function useCaseRequirementClaims(\n views: readonly ParsedView[],\n declared: readonly string[] | null,\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined,\n): ReadonlyMap<string, string[]> {\n const claimed = new Map<string, Set<string>>();\n if (declared !== null) {\n for (const view of views) {\n const scope = resolveCapabilityTags(view.tags, declared).flatMap((claim) =>\n claim.kind === \"resolved\" ? [claim.id] : [],\n );\n for (const claim of resolveRequirementTags(view.tags, scope, requirementsOf)) {\n if (claim.kind !== \"resolved\") continue;\n const key = `${claim.capability}#${claim.id}`;\n const flows = claimed.get(key) ?? new Set<string>();\n flows.add(view.id);\n claimed.set(key, flows);\n }\n }\n }\n return new Map([...claimed].map(([key, flows]) => [key, [...flows].sort(compareIds)]));\n}\n\n/**\n * The same answer as `useCaseRequirementClaims`, read from disk — or the honest\n * refusal to give one.\n *\n * `validate --all` already holds the parsed project and has already applied the\n * vocabulary ladder by the time it grades, so it calls the pure function above\n * with what it has. `loam list capabilities` holds neither, and the three steps\n * between a docsDir and that answer — read the flows, take the ladder, index the\n * documents' requirement ids — are three chances to get the suppression rules\n * wrong. They live here rather than in the command for that reason and not for\n * brevity: a command that spelled `[...vocab.byId.keys()]` in place of the\n * ladder would report a flow's promise as kept against a vocabulary nobody could\n * read.\n *\n * THE `unreadable` ARM MUST TRAVEL. An `architecture/` that did not parse yields\n * no views, and an empty map is indistinguishable from \"no flow keeps\n * anything\" — which is the single wrong answer this axis exists to prevent, and\n * the reason `readUseCases` is a tagged union rather than a list.\n */\nexport type PromisesKept =\n | { kind: \"read\"; kept: ReadonlyMap<string, readonly string[]> }\n | { kind: \"unreadable\"; errors: string[] };\n\nexport interface PromisesKeptRequest {\n docsDir: DocsDir;\n vocab: CapabilityVocabulary;\n /** The enumerated fleet — `readUseCases`' own requirement, for its own reason. */\n known: ReadonlySet<string>;\n /** The requirement reader — pass a FleetContext's readRequirements, bound. */\n read: (path: string) => Promise<Requirement[]>;\n}\n\nexport async function promisesKeptByFlows(req: PromisesKeptRequest): Promise<PromisesKept> {\n const scan = await readUseCases({ docsDir: req.docsDir, known: req.known });\n if (scan.kind !== \"read\") return { kind: \"unreadable\", errors: scan.errors };\n // No flow, no claim — and no capability document is opened to prove it.\n // `readUseCases`' byte gate answers a fleet with no use cases without starting\n // LikeC4 at all, and reading every `capabilities/<id>/spec.md` here to resolve\n // a tag set that is empty would put back the cost the gate exists to remove.\n // The `read` arm is still what travels: the fleet WAS looked at.\n if (scan.views.length === 0) return { kind: \"read\", kept: new Map() };\n const index = await capabilityRequirementIndex(req.vocab, req.read);\n // `index.declared` rather than a second call to `gradableCapabilityIds`: the\n // index applied the ladder when it was built, and two applications of one rule\n // is where the two halves of this join start disagreeing about whether the\n // vocabulary is gradable at all.\n return {\n kind: \"read\",\n kept: useCaseRequirementClaims(scan.views, index.declared, (c) => index.byCapability.get(c)),\n };\n}\n"]}
1
+ {"version":3,"file":"capability.js","sourceRoot":"","sources":["../../../src/core/usecases/capability.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The capability half of the use-case axis: which flows CLAIM a declared\n * capability, and which services those flows run through.\n *\n * `core/capabilities/usecase-join.ts` owns the tag grammar and the resolution\n * ladder — it answers \"what does this tag name?\", including the `many` arm the\n * slug's non-injectivity forces. This module asks the question the other way\n * round: given a capability id somebody already has, which views carry its tag?\n * That direction needs no ladder, because the id is not in doubt; it needs only\n * the slug, so it takes the slug and stops.\n *\n * ## What this deliberately does NOT do\n *\n * It does not grade the tag. A view tagged `#cap-payments-refunds` matches BOTH\n * `payments/refunds` and `payments-refunds` when a fleet declares both, and this\n * module reports it under both — because that is what the tag says, and the tag\n * is what the author wrote. `usecase.capability-unresolved` is already an ERROR\n * over exactly that collision (`validate`'s use-case package), so a second\n * verdict here would be a second answer to a question the gate has already\n * settled. The same stance `PackPermission` takes on an undeclared permission:\n * carry the claim, let validate convict it.\n */\nimport {\n tagSlug,\n CAP_TAG_PREFIX,\n resolveCapabilityTags,\n resolveRequirementTags,\n} from \"../capabilities/usecase-join.js\";\nimport { capabilityRequirementIndex } from \"../capabilities/findings.js\";\nimport type { CapabilityVocabulary } from \"../capabilities/capabilities.js\";\nimport type { Requirement } from \"../document/spec.js\";\nimport { compareIds } from \"../repo/entries.js\";\nimport type { ParsedView } from \"../c4/parsed/dynamic-views.js\";\nimport type { DocsDir } from \"../kernel/ids/dirs.js\";\nimport { readUseCases, type UseCaseScan } from \"./fleet.js\";\nimport { viewFile } from \"./place.js\";\nimport { participantsOf } from \"./suites/participants.js\";\n\n/** One flow claiming a capability: enough to name it and to open the file it lives in. */\nexport interface ClaimingFlow {\n id: string;\n title?: string;\n /** Repo-relative document the view is written in. */\n file: string;\n}\n\n/** The tag spelling a declared capability id would carry — `identity/tokens` → `cap-identity-tokens`. */\nfunction tagFor(capability: string): string {\n return `${CAP_TAG_PREFIX}${tagSlug(capability)}`;\n}\n\n/** Does this view claim the capability? Exact tag match, case included, as `resolveCapabilityTags` matches. */\nfunction claims(view: ParsedView, tag: string): boolean {\n return view.tags.includes(tag);\n}\n\n/**\n * The flows claiming one declared capability, sorted by (file, id).\n *\n * Sorted rather than reported in LikeC4's record order for the reason every\n * other list in this axis is: nothing has measured that the parse preserves\n * declaration order, and a payload ordered by an unmeasured upstream detail\n * reorders under a dependency bump.\n */\nexport function flowsClaiming(scan: UseCaseScan, capability: string): ClaimingFlow[] {\n if (scan.kind !== \"read\") return [];\n const tag = tagFor(capability);\n return scan.views\n .filter((view) => claims(view, tag))\n .map((view) => ({ id: view.id, ...(view.title === undefined ? {} : { title: view.title }), file: viewFile(view) }))\n .sort((a, b) => compareIds(a.file, b.file) || compareIds(a.id, b.id));\n}\n\n/**\n * Every ENUMERATED service the flows claiming these capabilities run through.\n *\n * The restriction to the enumerated fleet is this function's own contract, not\n * a guard somebody downstream is relying on: a step's endpoints resolve to\n * whatever the element stands for, and a sequence diagram is full of endpoints\n * that own no directory — the actor who starts the flow, the topic in the\n * middle, the database at the end. A function called `servicesInFlows…` that\n * answered `Customer` would be lying in its own name. `loam explore`, today's\n * only caller, filters the result against its enumeration again for a different\n * reason (it needs `ServiceEntry`s, in enumeration order), so removing the\n * filter here would not currently change what that command prints — which is\n * exactly why it is written down that the reason it exists is the return type,\n * not the call site.\n *\n * The walk itself is `./suites/participants.ts`' `participantsOf`, which\n * this function used to spell for itself: a suite asks the same question of a\n * different set of views and needs the participants that resolve to NO service\n * as well. The restriction now lives here as a choice — this function reads\n * `services` and drops `unresolved` — rather than as a filter inside the walk,\n * so the one union a test stand is provisioned from and the one `explore` seeds\n * from cannot resolve an element two ways.\n *\n * The cost of the restriction, stated so the next reader can weigh it: a flow\n * drawn through a service the fleet has not adopted yet contributes no seed.\n * That service has no documentation to explore, so what is lost is a name in a\n * list, and `usecase.step-unlinked` / `landscape.service-undocumented` are where\n * the fleet is told about it.\n */\nexport function servicesInFlowsClaiming(scan: UseCaseScan, capabilities: readonly string[]): string[] {\n if (scan.kind !== \"read\") return [];\n const tags = new Set(capabilities.map(tagFor));\n const claiming = scan.views.filter((view) => [...tags].some((tag) => claims(view, tag)));\n return participantsOf(scan, claiming).services;\n}\n\n/**\n * The `<capability>#<Requirement-ID>` pairs a fleet's flows RESOLVE to — the\n * business promises a use case claims to keep.\n *\n * Lives in core rather than beside the grade that reports a broken tag, because\n * two very different callers need the same answer and they must not compute it\n * twice: `validate --all` subtracts these from\n * `capability.requirement-unrealized` (a promise a flow keeps is not unkept),\n * and `loam list capabilities` reports them beside the service requirements\n * that realize the same promise. Two implementations of \"which promises does\n * this fleet keep\" is two answers to the question the axis exists to settle.\n *\n * A MAP RATHER THAN A SET, because the two callers need different depths of the\n * same answer and one of them needs names. `validate` only asks whether a\n * promise is kept (`.has`, which a Map answers identically); a listing that said\n * \"kept\" without saying by WHAT would leave a reader unable to open the flow, go\n * look at its hops and judge whether the promise really is kept — which is the\n * whole reason the listing carries the realizing service requirements by name\n * rather than as a count. Widening the return type was cheaper than a second\n * function, and a second function is how the two corpora start disagreeing.\n *\n * ONLY RESOLVED CLAIMS COUNT, and both halves must resolve: the `#cap-` tag to\n * exactly one declared capability, and the `#req-` tag to exactly one of its\n * requirements. A broken tag of either kind is already an ERROR\n * (`usecase.capability-unresolved`, `usecase.requirement-unresolved`) and must\n * never also mark a promise kept — a typo that silenced the unrealized warning\n * would turn a mistake into a green fleet.\n *\n * `declared` carries the vocabulary ladder: `null` means there is nothing to\n * grade against, and the honest answer is then an empty map — no flow can be\n * said to keep a promise loam cannot see declared.\n *\n * Diff-stable by construction: the view ids under each promise are DEDUPLICATED\n * and sorted with compareIds, so nothing here depends on LikeC4's view order,\n * on how many tags one view carried, or on the readdir order the project was\n * staged in. The map's own key order reaches no output — both callers look\n * promises up by key — but the values are printed, so they are sorted.\n */\nexport function useCaseRequirementClaims(\n views: readonly ParsedView[],\n declared: readonly string[] | null,\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined,\n): ReadonlyMap<string, string[]> {\n const claimed = new Map<string, Set<string>>();\n if (declared !== null) {\n for (const view of views) {\n const scope = resolveCapabilityTags(view.tags, declared).flatMap((claim) =>\n claim.kind === \"resolved\" ? [claim.id] : [],\n );\n for (const claim of resolveRequirementTags(view.tags, scope, requirementsOf)) {\n if (claim.kind !== \"resolved\") continue;\n const key = `${claim.capability}#${claim.id}`;\n const flows = claimed.get(key) ?? new Set<string>();\n flows.add(view.id);\n claimed.set(key, flows);\n }\n }\n }\n return new Map([...claimed].map(([key, flows]) => [key, [...flows].sort(compareIds)]));\n}\n\n/**\n * The same answer as `useCaseRequirementClaims`, read from disk — or the honest\n * refusal to give one.\n *\n * `validate --all` already holds the parsed project and has already applied the\n * vocabulary ladder by the time it grades, so it calls the pure function above\n * with what it has. `loam list capabilities` holds neither, and the three steps\n * between a docsDir and that answer — read the flows, take the ladder, index the\n * documents' requirement ids — are three chances to get the suppression rules\n * wrong. They live here rather than in the command for that reason and not for\n * brevity: a command that spelled `[...vocab.byId.keys()]` in place of the\n * ladder would report a flow's promise as kept against a vocabulary nobody could\n * read.\n *\n * THE `unreadable` ARM MUST TRAVEL. An `architecture/` that did not parse yields\n * no views, and an empty map is indistinguishable from \"no flow keeps\n * anything\" — which is the single wrong answer this axis exists to prevent, and\n * the reason `readUseCases` is a tagged union rather than a list.\n */\nexport type PromisesKept =\n | { kind: \"read\"; kept: ReadonlyMap<string, readonly string[]> }\n | { kind: \"unreadable\"; errors: string[] };\n\nexport interface PromisesKeptRequest {\n docsDir: DocsDir;\n vocab: CapabilityVocabulary;\n /** The enumerated fleet — `readUseCases`' own requirement, for its own reason. */\n known: ReadonlySet<string>;\n /** The requirement reader — pass a FleetContext's readRequirements, bound. */\n read: (path: string) => Promise<Requirement[]>;\n}\n\nexport async function promisesKeptByFlows(req: PromisesKeptRequest): Promise<PromisesKept> {\n const scan = await readUseCases({ docsDir: req.docsDir, known: req.known });\n if (scan.kind !== \"read\") return { kind: \"unreadable\", errors: scan.errors };\n // No flow, no claim — and no capability document is opened to prove it.\n // `readUseCases`' byte gate answers a fleet with no use cases without starting\n // LikeC4 at all, and reading every `capabilities/<id>/spec.md` here to resolve\n // a tag set that is empty would put back the cost the gate exists to remove.\n // The `read` arm is still what travels: the fleet WAS looked at.\n if (scan.views.length === 0) return { kind: \"read\", kept: new Map() };\n const index = await capabilityRequirementIndex(req.vocab, req.read);\n // `index.declared` rather than a second call to `gradableCapabilityIds`: the\n // index applied the ladder when it was built, and two applications of one rule\n // is where the two halves of this join start disagreeing about whether the\n // vocabulary is gradable at all.\n return {\n kind: \"read\",\n kept: useCaseRequirementClaims(scan.views, index.declared, (c) => index.byCapability.get(c)),\n };\n}\n"]}
@@ -33,15 +33,16 @@
33
33
  * `core/c4/splice/delta-blocks.ts` goes on refusing a `dynamic view` inside
34
34
  * `delta.likec4` and now names this slot as the place it belongs.
35
35
  *
36
- * ONE CALL for the walk. `architectureDocuments` already implements "every
36
+ * ONE CALL for the walk (`relativeDocuments`, which the topology axis in
37
+ * `core/deployment/delta.ts` shares). `architectureDocuments` already implements "every
37
38
  * `.likec4` under a directory, recursively, in a stable order, and an absent
38
39
  * directory is an empty list" — which is the whole of what a feature's flow
39
40
  * directory is. A second walk would be two implementations free to disagree
40
41
  * about what a flow file is, and they would disagree first about nesting.
41
42
  */
42
43
  import { existsSync } from "node:fs";
43
- import { join, relative, resolve } from "node:path";
44
- import { architectureDocuments } from "../../c4/project/documents.js";
44
+ import { join } from "node:path";
45
+ import { relativeDocuments } from "../../c4/project/documents.js";
45
46
  import { architectureDir } from "../../c4/project/architecture.js";
46
47
  import { featureUseCasesDir } from "../../repo/authored/paths.js";
47
48
  import {} from "../../vocabulary/issue.js";
@@ -55,10 +56,7 @@ export const USECASE_SUBDIR = "usecases";
55
56
  * answers an absent directory with an empty list rather than a throw.
56
57
  */
57
58
  export async function featureFlows(featureDir) {
58
- const dir = featureUseCasesDir(featureDir);
59
- const paths = await architectureDocuments(dir);
60
- const base = resolve(dir);
61
- return paths.map((path) => ({ rel: relative(base, path).split(/[\\/]/).join("/"), path }));
59
+ return relativeDocuments(featureUseCasesDir(featureDir));
62
60
  }
63
61
  /**
64
62
  * The LIVING path a feature-local flow addresses. Spelled here because three
@@ -1 +1 @@
1
- {"version":3,"file":"flows.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/flows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,OAAO,EAAc,MAAM,2BAA2B,CAAC;AAEvD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;AAezC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAsB;IACvD,MAAM,GAAG,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAC7F,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB,EAAE,GAAW;IAC1D,OAAO,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAgB,EAAE,UAAsB;IAC5E,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,UAAU,CAAC,CAAC;IAC7C,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;SAC/D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACd,QAAQ,EAAE,OAAgB;QAC1B,IAAI,EAAE,qBAA8B;QACpC,OAAO,EAAE,IAAI,CAAC,GAAG;QACjB,OAAO,EACL,gBAAgB,cAAc,IAAI,IAAI,CAAC,GAAG,oFAAoF;YAC9H,uEAAuE;YACvE,0BAA0B,cAAc,IAAI,IAAI,CAAC,GAAG,0BAA0B,cAAc,IAAI,IAAI,CAAC,GAAG,iCAAiC;YACzI,6EAA6E;KAChF,CAAC,CAAC,CAAC;AACR,CAAC","sourcesContent":["/**\n * A feature's own flows: `features/<FEAT>/usecases/<name>.likec4`.\n *\n * A `dynamic view` is the only realizer a cross-service promise has, and until\n * this slot existed it was the one realizer no feature could carry. Every other\n * axis had a delta — a C4 delta, a requirement delta on both spec axes, both\n * contract deltas, a capability delta, a glossary term — so the business axis's\n * own headline case had to be written backwards: an analyst adds a capability\n * requirement, the architect answers it with a flow, and the flow could not ship\n * in that change. It merged only after the promise was already living, which\n * meant the promise landed first with nothing keeping it.\n *\n * CREATE-ONLY, for `core/glossary/delta.ts`'s reasons rather than a weaker\n * version of them. A capability document is a DELTA because it merges INTO a\n * living document and two features editing one requirement would otherwise\n * overwrite each other silently. A flow has none of that: the file is one\n * ordered hop sequence, the merge is a whole-file copy, and there is nothing to\n * merge partially. Rewriting a living flow through a feature would be a silent\n * whole-file replacement with no pin to collide on, where editing\n * `architecture/usecases/<name>.likec4` directly in the same pull request\n * produces an ordinary git conflict a human resolves. `usecase.flow-exists`\n * refuses the first and names the second — the same shape, and the same\n * severity, as `glossary.term-exists`.\n *\n * WHY A FILE OF ITS OWN AND NOT `delta.likec4`. The C4 delta is a MODEL delta:\n * it re-declares the landscape's own identifiers to anchor its new edges, and it\n * carries its own `specification` block because LikeC4 parses it standalone.\n * Both make it unstageable beside the landscape in one project — a duplicate\n * specification is an error blamed on both files, and a re-declared element is a\n * duplicate id. A views-only document declares neither, which is exactly why\n * living use cases already live in files of their own; the feature-local slot\n * mirrors that layout so the merge is a copy and the overlay is a stage.\n * `core/c4/splice/delta-blocks.ts` goes on refusing a `dynamic view` inside\n * `delta.likec4` and now names this slot as the place it belongs.\n *\n * ONE CALL for the walk. `architectureDocuments` already implements \"every\n * `.likec4` under a directory, recursively, in a stable order, and an absent\n * directory is an empty list\" — which is the whole of what a feature's flow\n * directory is. A second walk would be two implementations free to disagree\n * about what a flow file is, and they would disagree first about nesting.\n */\nimport { existsSync } from \"node:fs\";\nimport { join, relative, resolve } from \"node:path\";\nimport { architectureDocuments } from \"../../c4/project/documents.js\";\nimport { architectureDir } from \"../../c4/project/architecture.js\";\nimport { featureUseCasesDir } from \"../../repo/authored/paths.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { type Issue } from \"../../vocabulary/issue.js\";\n\n/** The directory living flows are filed under, inside `architecture/`. */\nexport const USECASE_SUBDIR = \"usecases\";\n\n/** One flow document a feature carries. */\nexport interface FeatureFlow {\n /**\n * The path INSIDE the feature's `usecases/` directory, `/`-separated — the id\n * this axis has, and the same path the merge writes under\n * `architecture/usecases/`. Nesting is spelled by the tree, as it is for every\n * other authored tree in the repo.\n */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\n/**\n * The flows one feature carries, ordered by path.\n *\n * Zero readdirs when the feature has no `usecases/` directory, which is every\n * feature in a fleet that has not adopted the axis: `architectureDocuments`\n * answers an absent directory with an empty list rather than a throw.\n */\nexport async function featureFlows(featureDir: FeatureDir): Promise<FeatureFlow[]> {\n const dir = featureUseCasesDir(featureDir);\n const paths = await architectureDocuments(dir);\n const base = resolve(dir);\n return paths.map((path) => ({ rel: relative(base, path).split(/[\\\\/]/).join(\"/\"), path }));\n}\n\n/**\n * The LIVING path a feature-local flow addresses. Spelled here because three\n * readers need it — the refusal below, the archive merge, and the overlay that\n * grades the flow before either — and a reader resolving it differently would\n * refuse a flow the merge then wrote anyway.\n *\n * `rel` is joined un-split, which `node:path` normalizes. What holds the join is\n * PROVENANCE, exactly as it holds `livingTermPath` one axis over: every `rel`\n * here came out of an `architectureDocuments` walk, so each component is a\n * directory entry that exists. No caller takes one from argv.\n */\nexport function livingFlowPath(docsDir: DocsDir, rel: string): string {\n return join(architectureDir(docsDir), USECASE_SUBDIR, rel);\n}\n\n/**\n * `usecase.flow-exists` — a flow this feature introduces that the living\n * `architecture/` already holds.\n *\n * An ERROR and never a warning-that-gates, for `glossary.term-exists`'s reason:\n * there is no legal reading of it. The merge would replace an authored flow\n * wholesale, and no `--approve` makes that a different act than it is. Two\n * features introducing the same flow is the case that makes it load-bearing\n * rather than pedantic — whichever archives first creates the living document,\n * and the second is told before it merges that its own file is the one to\n * delete.\n */\nexport async function flowDeltaIssues(docsDir: DocsDir, featureDir: FeatureDir): Promise<Issue[]> {\n const flows = await featureFlows(featureDir);\n return flows\n .filter((flow) => existsSync(livingFlowPath(docsDir, flow.rel)))\n .map((flow) => ({\n severity: \"error\" as const,\n code: \"usecase.flow-exists\" as const,\n subject: flow.rel,\n message:\n `architecture/${USECASE_SUBDIR}/${flow.rel} already exists, so this feature's flow would replace an authored one wholesale — ` +\n \"a feature-local flow INTRODUCES a use case, it does not rewrite one. \" +\n `Delete features/<FEAT>/${USECASE_SUBDIR}/${flow.rel} and edit architecture/${USECASE_SUBDIR}/${flow.rel} directly in this same change, ` +\n \"where git produces an ordinary conflict if somebody else is editing it too.\",\n }));\n}\n"]}
1
+ {"version":3,"file":"flows.js","sourceRoot":"","sources":["../../../../src/core/usecases/delta/flows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,OAAO,EAAc,MAAM,2BAA2B,CAAC;AAEvD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;AAezC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAsB;IACvD,OAAO,iBAAiB,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB,EAAE,GAAW;IAC1D,OAAO,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAgB,EAAE,UAAsB;IAC5E,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,UAAU,CAAC,CAAC;IAC7C,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;SAC/D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACd,QAAQ,EAAE,OAAgB;QAC1B,IAAI,EAAE,qBAA8B;QACpC,OAAO,EAAE,IAAI,CAAC,GAAG;QACjB,OAAO,EACL,gBAAgB,cAAc,IAAI,IAAI,CAAC,GAAG,oFAAoF;YAC9H,uEAAuE;YACvE,0BAA0B,cAAc,IAAI,IAAI,CAAC,GAAG,0BAA0B,cAAc,IAAI,IAAI,CAAC,GAAG,iCAAiC;YACzI,6EAA6E;KAChF,CAAC,CAAC,CAAC;AACR,CAAC","sourcesContent":["/**\n * A feature's own flows: `features/<FEAT>/usecases/<name>.likec4`.\n *\n * A `dynamic view` is the only realizer a cross-service promise has, and until\n * this slot existed it was the one realizer no feature could carry. Every other\n * axis had a delta — a C4 delta, a requirement delta on both spec axes, both\n * contract deltas, a capability delta, a glossary term — so the business axis's\n * own headline case had to be written backwards: an analyst adds a capability\n * requirement, the architect answers it with a flow, and the flow could not ship\n * in that change. It merged only after the promise was already living, which\n * meant the promise landed first with nothing keeping it.\n *\n * CREATE-ONLY, for `core/glossary/delta.ts`'s reasons rather than a weaker\n * version of them. A capability document is a DELTA because it merges INTO a\n * living document and two features editing one requirement would otherwise\n * overwrite each other silently. A flow has none of that: the file is one\n * ordered hop sequence, the merge is a whole-file copy, and there is nothing to\n * merge partially. Rewriting a living flow through a feature would be a silent\n * whole-file replacement with no pin to collide on, where editing\n * `architecture/usecases/<name>.likec4` directly in the same pull request\n * produces an ordinary git conflict a human resolves. `usecase.flow-exists`\n * refuses the first and names the second — the same shape, and the same\n * severity, as `glossary.term-exists`.\n *\n * WHY A FILE OF ITS OWN AND NOT `delta.likec4`. The C4 delta is a MODEL delta:\n * it re-declares the landscape's own identifiers to anchor its new edges, and it\n * carries its own `specification` block because LikeC4 parses it standalone.\n * Both make it unstageable beside the landscape in one project — a duplicate\n * specification is an error blamed on both files, and a re-declared element is a\n * duplicate id. A views-only document declares neither, which is exactly why\n * living use cases already live in files of their own; the feature-local slot\n * mirrors that layout so the merge is a copy and the overlay is a stage.\n * `core/c4/splice/delta-blocks.ts` goes on refusing a `dynamic view` inside\n * `delta.likec4` and now names this slot as the place it belongs.\n *\n * ONE CALL for the walk (`relativeDocuments`, which the topology axis in\n * `core/deployment/delta.ts` shares). `architectureDocuments` already implements \"every\n * `.likec4` under a directory, recursively, in a stable order, and an absent\n * directory is an empty list\" — which is the whole of what a feature's flow\n * directory is. A second walk would be two implementations free to disagree\n * about what a flow file is, and they would disagree first about nesting.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { relativeDocuments } from \"../../c4/project/documents.js\";\nimport { architectureDir } from \"../../c4/project/architecture.js\";\nimport { featureUseCasesDir } from \"../../repo/authored/paths.js\";\nimport type { DocsDir, FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport { type Issue } from \"../../vocabulary/issue.js\";\n\n/** The directory living flows are filed under, inside `architecture/`. */\nexport const USECASE_SUBDIR = \"usecases\";\n\n/** One flow document a feature carries. */\nexport interface FeatureFlow {\n /**\n * The path INSIDE the feature's `usecases/` directory, `/`-separated — the id\n * this axis has, and the same path the merge writes under\n * `architecture/usecases/`. Nesting is spelled by the tree, as it is for every\n * other authored tree in the repo.\n */\n rel: string;\n /** The absolute path of the authored file. */\n path: string;\n}\n\n/**\n * The flows one feature carries, ordered by path.\n *\n * Zero readdirs when the feature has no `usecases/` directory, which is every\n * feature in a fleet that has not adopted the axis: `architectureDocuments`\n * answers an absent directory with an empty list rather than a throw.\n */\nexport async function featureFlows(featureDir: FeatureDir): Promise<FeatureFlow[]> {\n return relativeDocuments(featureUseCasesDir(featureDir));\n}\n\n/**\n * The LIVING path a feature-local flow addresses. Spelled here because three\n * readers need it — the refusal below, the archive merge, and the overlay that\n * grades the flow before either — and a reader resolving it differently would\n * refuse a flow the merge then wrote anyway.\n *\n * `rel` is joined un-split, which `node:path` normalizes. What holds the join is\n * PROVENANCE, exactly as it holds `livingTermPath` one axis over: every `rel`\n * here came out of an `architectureDocuments` walk, so each component is a\n * directory entry that exists. No caller takes one from argv.\n */\nexport function livingFlowPath(docsDir: DocsDir, rel: string): string {\n return join(architectureDir(docsDir), USECASE_SUBDIR, rel);\n}\n\n/**\n * `usecase.flow-exists` — a flow this feature introduces that the living\n * `architecture/` already holds.\n *\n * An ERROR and never a warning-that-gates, for `glossary.term-exists`'s reason:\n * there is no legal reading of it. The merge would replace an authored flow\n * wholesale, and no `--approve` makes that a different act than it is. Two\n * features introducing the same flow is the case that makes it load-bearing\n * rather than pedantic — whichever archives first creates the living document,\n * and the second is told before it merges that its own file is the one to\n * delete.\n */\nexport async function flowDeltaIssues(docsDir: DocsDir, featureDir: FeatureDir): Promise<Issue[]> {\n const flows = await featureFlows(featureDir);\n return flows\n .filter((flow) => existsSync(livingFlowPath(docsDir, flow.rel)))\n .map((flow) => ({\n severity: \"error\" as const,\n code: \"usecase.flow-exists\" as const,\n subject: flow.rel,\n message:\n `architecture/${USECASE_SUBDIR}/${flow.rel} already exists, so this feature's flow would replace an authored one wholesale — ` +\n \"a feature-local flow INTRODUCES a use case, it does not rewrite one. \" +\n `Delete features/<FEAT>/${USECASE_SUBDIR}/${flow.rel} and edit architecture/${USECASE_SUBDIR}/${flow.rel} directly in this same change, ` +\n \"where git produces an ordinary conflict if somebody else is editing it too.\",\n }));\n}\n"]}