@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.13

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 (901) hide show
  1. package/CHANGELOG.md +3448 -0
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +47 -23
  5. package/QUICKSTART.md +189 -12
  6. package/README.md +274 -65
  7. package/ROADMAP.md +572 -31
  8. package/SCHEMA.md +1709 -155
  9. package/WORKFLOW.md +298 -72
  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/adopt/render.js +50 -9
  15. package/dist/commands/adopt/render.js.map +1 -1
  16. package/dist/commands/archive/archive.js +16 -19
  17. package/dist/commands/archive/archive.js.map +1 -1
  18. package/dist/commands/archive/closing.js +42 -0
  19. package/dist/commands/archive/closing.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/asyncapi.js +36 -42
  21. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  23. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  24. package/dist/commands/archive/plan/contracts/openapi.js +77 -43
  25. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  26. package/dist/commands/archive/plan/contracts/verdict-lines.js +103 -0
  27. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  28. package/dist/commands/archive/plan/document/head.js +151 -0
  29. package/dist/commands/archive/plan/document/head.js.map +1 -0
  30. package/dist/commands/archive/plan/document/links.js +284 -0
  31. package/dist/commands/archive/plan/document/links.js.map +1 -0
  32. package/dist/commands/archive/plan/document/run.js +136 -0
  33. package/dist/commands/archive/plan/document/run.js.map +1 -0
  34. package/dist/commands/archive/plan/gate.js +21 -19
  35. package/dist/commands/archive/plan/gate.js.map +1 -1
  36. package/dist/commands/archive/plan/landscape.js +167 -30
  37. package/dist/commands/archive/plan/landscape.js.map +1 -1
  38. package/dist/commands/archive/plan/model/extending.js +85 -0
  39. package/dist/commands/archive/plan/model/extending.js.map +1 -0
  40. package/dist/commands/archive/plan/refusal.js +14 -19
  41. package/dist/commands/archive/plan/refusal.js.map +1 -1
  42. package/dist/commands/archive/plan/specs.js +137 -56
  43. package/dist/commands/archive/plan/specs.js.map +1 -1
  44. package/dist/commands/archive/plan/state.js +3 -0
  45. package/dist/commands/archive/plan/state.js.map +1 -1
  46. package/dist/commands/archive/run.js +134 -30
  47. package/dist/commands/archive/run.js.map +1 -1
  48. package/dist/commands/archive/shipped.js +49 -0
  49. package/dist/commands/archive/shipped.js.map +1 -0
  50. package/dist/commands/context/context.js +85 -18
  51. package/dist/commands/context/context.js.map +1 -1
  52. package/dist/commands/context/print.js +29 -7
  53. package/dist/commands/context/print.js.map +1 -1
  54. package/dist/commands/delta/delta.js +102 -14
  55. package/dist/commands/delta/delta.js.map +1 -1
  56. package/dist/commands/delta/print.js +17 -1
  57. package/dist/commands/delta/print.js.map +1 -1
  58. package/dist/commands/dependencies.js +3 -7
  59. package/dist/commands/dependencies.js.map +1 -1
  60. package/dist/commands/diff/diff.js +20 -27
  61. package/dist/commands/diff/diff.js.map +1 -1
  62. package/dist/commands/doctor.js +60 -1
  63. package/dist/commands/doctor.js.map +1 -1
  64. package/dist/commands/explain/explain.js +89 -13
  65. package/dist/commands/explain/explain.js.map +1 -1
  66. package/dist/commands/explore.js +16 -10
  67. package/dist/commands/explore.js.map +1 -1
  68. package/dist/commands/gate/gate.js +45 -8
  69. package/dist/commands/gate/gate.js.map +1 -1
  70. package/dist/commands/gate/print.js +15 -3
  71. package/dist/commands/gate/print.js.map +1 -1
  72. package/dist/commands/gherkin/commit.js +13 -54
  73. package/dist/commands/gherkin/commit.js.map +1 -1
  74. package/dist/commands/gherkin/gherkin.js +36 -20
  75. package/dist/commands/gherkin/gherkin.js.map +1 -1
  76. package/dist/commands/gherkin/reconcile.js +10 -13
  77. package/dist/commands/gherkin/reconcile.js.map +1 -1
  78. package/dist/commands/init/first-hour.js +44 -0
  79. package/dist/commands/init/first-hour.js.map +1 -1
  80. package/dist/commands/init/init.js +18 -22
  81. package/dist/commands/init/init.js.map +1 -1
  82. package/dist/commands/list/campaign/campaign.js +4 -6
  83. package/dist/commands/list/campaign/campaign.js.map +1 -1
  84. package/dist/commands/list/json.js +5 -7
  85. package/dist/commands/list/json.js.map +1 -1
  86. package/dist/commands/list/list.js +44 -29
  87. package/dist/commands/list/list.js.map +1 -1
  88. package/dist/commands/list/print.js +52 -9
  89. package/dist/commands/list/print.js.map +1 -1
  90. package/dist/commands/list/suites/suites.js +140 -0
  91. package/dist/commands/list/suites/suites.js.map +1 -0
  92. package/dist/commands/list/views.js +37 -34
  93. package/dist/commands/list/views.js.map +1 -1
  94. package/dist/commands/mcp/dispatch.js +91 -36
  95. package/dist/commands/mcp/dispatch.js.map +1 -1
  96. package/dist/commands/mcp/mcp.js +56 -15
  97. package/dist/commands/mcp/mcp.js.map +1 -1
  98. package/dist/commands/mcp/serve.js.map +1 -1
  99. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  100. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  101. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  102. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  103. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  104. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  105. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  106. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  107. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  108. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  109. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  110. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  111. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  112. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  113. package/dist/commands/new/capability-template.js +118 -0
  114. package/dist/commands/new/capability-template.js.map +1 -0
  115. package/dist/commands/new/commit.js +8 -22
  116. package/dist/commands/new/commit.js.map +1 -1
  117. package/dist/commands/new/modifies.js +206 -0
  118. package/dist/commands/new/modifies.js.map +1 -0
  119. package/dist/commands/new/new.js +59 -14
  120. package/dist/commands/new/new.js.map +1 -1
  121. package/dist/commands/new/templates.js +48 -124
  122. package/dist/commands/new/templates.js.map +1 -1
  123. package/dist/commands/open.js +3 -7
  124. package/dist/commands/open.js.map +1 -1
  125. package/dist/commands/policy/gate.js +27 -0
  126. package/dist/commands/policy/gate.js.map +1 -1
  127. package/dist/commands/rebase/plan.js +2 -2
  128. package/dist/commands/rebase/plan.js.map +1 -1
  129. package/dist/commands/rebase/rebase.js +60 -35
  130. package/dist/commands/rebase/rebase.js.map +1 -1
  131. package/dist/commands/seed/commit.js +55 -26
  132. package/dist/commands/seed/commit.js.map +1 -1
  133. package/dist/commands/seed/plan.js +158 -13
  134. package/dist/commands/seed/plan.js.map +1 -1
  135. package/dist/commands/seed/seed.js +13 -11
  136. package/dist/commands/seed/seed.js.map +1 -1
  137. package/dist/commands/show/feature.js +9 -5
  138. package/dist/commands/show/feature.js.map +1 -1
  139. package/dist/commands/show/introduced.js +157 -0
  140. package/dist/commands/show/introduced.js.map +1 -0
  141. package/dist/commands/show/marks.js +35 -6
  142. package/dist/commands/show/marks.js.map +1 -1
  143. package/dist/commands/show/service.js +41 -33
  144. package/dist/commands/show/service.js.map +1 -1
  145. package/dist/commands/show/show.js +17 -7
  146. package/dist/commands/show/show.js.map +1 -1
  147. package/dist/commands/status/fleet-next.js +90 -0
  148. package/dist/commands/status/fleet-next.js.map +1 -0
  149. package/dist/commands/status/json.js +2 -0
  150. package/dist/commands/status/json.js.map +1 -1
  151. package/dist/commands/status/print.js +89 -37
  152. package/dist/commands/status/print.js.map +1 -1
  153. package/dist/commands/status/status.js +3 -7
  154. package/dist/commands/status/status.js.map +1 -1
  155. package/dist/commands/steps/steps.js +103 -23
  156. package/dist/commands/steps/steps.js.map +1 -1
  157. package/dist/commands/subsystem/edit.js +7 -7
  158. package/dist/commands/subsystem/edit.js.map +1 -1
  159. package/dist/commands/subsystem/move.js +106 -46
  160. package/dist/commands/subsystem/move.js.map +1 -1
  161. package/dist/commands/subsystem/query.js +2 -4
  162. package/dist/commands/subsystem/query.js.map +1 -1
  163. package/dist/commands/subsystem/subsystem.js +4 -7
  164. package/dist/commands/subsystem/subsystem.js.map +1 -1
  165. package/dist/commands/subsystem/sync.js +289 -74
  166. package/dist/commands/subsystem/sync.js.map +1 -1
  167. package/dist/commands/subsystem/txn/exclude.js +132 -0
  168. package/dist/commands/subsystem/txn/exclude.js.map +1 -0
  169. package/dist/commands/subsystem/txn/links.js +128 -0
  170. package/dist/commands/subsystem/txn/links.js.map +1 -0
  171. package/dist/commands/subsystem/txn/report.js +174 -0
  172. package/dist/commands/subsystem/txn/report.js.map +1 -0
  173. package/dist/commands/subsystem/txn/txn.js +147 -45
  174. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  175. package/dist/commands/subsystem/txn/views.js +200 -21
  176. package/dist/commands/subsystem/txn/views.js.map +1 -1
  177. package/dist/commands/tasks/actions.js +107 -0
  178. package/dist/commands/tasks/actions.js.map +1 -0
  179. package/dist/commands/tasks/render.js +253 -0
  180. package/dist/commands/tasks/render.js.map +1 -0
  181. package/dist/commands/tasks/tasks.js +119 -0
  182. package/dist/commands/tasks/tasks.js.map +1 -0
  183. package/dist/commands/unarchive/manifest.js +42 -26
  184. package/dist/commands/unarchive/manifest.js.map +1 -1
  185. package/dist/commands/unarchive/restore.js +83 -33
  186. package/dist/commands/unarchive/restore.js.map +1 -1
  187. package/dist/commands/unarchive/unarchive.js +22 -30
  188. package/dist/commands/unarchive/unarchive.js.map +1 -1
  189. package/dist/commands/validate/arch-coverage.js +78 -27
  190. package/dist/commands/validate/arch-coverage.js.map +1 -1
  191. package/dist/commands/validate/checks/fleet-shape.js +34 -126
  192. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  193. package/dist/commands/validate/checks/health-deps.js +9 -6
  194. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  195. package/dist/commands/validate/checks/introduced.js +102 -0
  196. package/dist/commands/validate/checks/introduced.js.map +1 -0
  197. package/dist/commands/validate/checks/requirements.js +18 -8
  198. package/dist/commands/validate/checks/requirements.js.map +1 -1
  199. package/dist/commands/validate/checks/sources.js +11 -2
  200. package/dist/commands/validate/checks/sources.js.map +1 -1
  201. package/dist/commands/validate/coverage/covers-after.js +105 -0
  202. package/dist/commands/validate/coverage/covers-after.js.map +1 -0
  203. package/dist/commands/validate/feature.js +26 -20
  204. package/dist/commands/validate/feature.js.map +1 -1
  205. package/dist/commands/validate/fleet/census.js +37 -13
  206. package/dist/commands/validate/fleet/census.js.map +1 -1
  207. package/dist/commands/validate/fleet/landscape.js +144 -126
  208. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  209. package/dist/commands/validate/fleet/load.js +66 -0
  210. package/dist/commands/validate/fleet/load.js.map +1 -1
  211. package/dist/commands/validate/fleet/map/attest.js +98 -0
  212. package/dist/commands/validate/fleet/map/attest.js.map +1 -0
  213. package/dist/commands/validate/fleet/map/bindings.js +107 -0
  214. package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
  215. package/dist/commands/validate/fleet/map/consumers.js +244 -0
  216. package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
  217. package/dist/commands/validate/fleet/map/isolation.js +116 -0
  218. package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
  219. package/dist/commands/validate/fleet/map/usecases.js +54 -0
  220. package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
  221. package/dist/commands/validate/fleet/obligations.js +44 -14
  222. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  223. package/dist/commands/validate/fleet/scorecard/print.js +10 -2
  224. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  225. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  226. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  227. package/dist/commands/validate/fleet/usecases/place.js.map +1 -1
  228. package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
  229. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
  230. package/dist/commands/validate/fleet/views/ids.js +50 -15
  231. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  232. package/dist/commands/validate/fleet/views/projects.js +278 -0
  233. package/dist/commands/validate/fleet/views/projects.js.map +1 -0
  234. package/dist/commands/validate/fleet/views/root-documents.js +83 -0
  235. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
  236. package/dist/commands/validate/fleet/views/stale.js +40 -47
  237. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  238. package/dist/commands/validate/links/corpus.js +1 -4
  239. package/dist/commands/validate/links/corpus.js.map +1 -1
  240. package/dist/commands/validate/links/glossary.js.map +1 -1
  241. package/dist/commands/validate/render/repeats.js +63 -0
  242. package/dist/commands/validate/render/repeats.js.map +1 -0
  243. package/dist/commands/validate/render/rollups.js +115 -0
  244. package/dist/commands/validate/render/rollups.js.map +1 -0
  245. package/dist/commands/validate/render/text.js +199 -0
  246. package/dist/commands/validate/render/text.js.map +1 -0
  247. package/dist/commands/validate/report.js +36 -160
  248. package/dist/commands/validate/report.js.map +1 -1
  249. package/dist/commands/validate/scope.js +113 -0
  250. package/dist/commands/validate/scope.js.map +1 -0
  251. package/dist/commands/validate/service/api.js +21 -107
  252. package/dist/commands/validate/service/api.js.map +1 -1
  253. package/dist/commands/validate/service/contracts/generated.js +134 -0
  254. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  255. package/dist/commands/validate/service/events/events.js +36 -14
  256. package/dist/commands/validate/service/events/events.js.map +1 -1
  257. package/dist/commands/validate/service/events/message-producers.js +1 -1
  258. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  259. package/dist/commands/validate/service/evidence-pins.js +56 -10
  260. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  261. package/dist/commands/validate/service/model/diverged.js +226 -0
  262. package/dist/commands/validate/service/model/diverged.js.map +1 -0
  263. package/dist/commands/validate/service/model/grade.js +163 -0
  264. package/dist/commands/validate/service/model/grade.js.map +1 -0
  265. package/dist/commands/validate/service/service.js +121 -61
  266. package/dist/commands/validate/service/service.js.map +1 -1
  267. package/dist/commands/validate/service/specs.js +88 -28
  268. package/dist/commands/validate/service/specs.js.map +1 -1
  269. package/dist/commands/validate/service/spine.js +38 -3
  270. package/dist/commands/validate/service/spine.js.map +1 -1
  271. package/dist/commands/validate/service/usecases/tags.js +113 -0
  272. package/dist/commands/validate/service/usecases/tags.js.map +1 -0
  273. package/dist/commands/validate/service/usecases/usecases.js +209 -0
  274. package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
  275. package/dist/commands/validate/validate.js +85 -60
  276. package/dist/commands/validate/validate.js.map +1 -1
  277. package/dist/commands/verify/cross/diff.js +2 -6
  278. package/dist/commands/verify/cross/diff.js.map +1 -1
  279. package/dist/commands/verify/evidence/contract.js +1 -1
  280. package/dist/commands/verify/evidence/contract.js.map +1 -1
  281. package/dist/commands/verify/evidence/read.js +12 -3
  282. package/dist/commands/verify/evidence/read.js.map +1 -1
  283. package/dist/commands/verify/frozen.js +45 -23
  284. package/dist/commands/verify/frozen.js.map +1 -1
  285. package/dist/commands/verify/record.js +69 -17
  286. package/dist/commands/verify/record.js.map +1 -1
  287. package/dist/commands/verify/report.js +27 -44
  288. package/dist/commands/verify/report.js.map +1 -1
  289. package/dist/commands/verify/results.js +112 -30
  290. package/dist/commands/verify/results.js.map +1 -1
  291. package/dist/commands/verify/verify.js +55 -52
  292. package/dist/commands/verify/verify.js.map +1 -1
  293. package/dist/commands/vouch/pack/pack.js +2 -2
  294. package/dist/commands/vouch/pack/pack.js.map +1 -1
  295. package/dist/commands/vouch/pack/print.js +1 -6
  296. package/dist/commands/vouch/pack/print.js.map +1 -1
  297. package/dist/commands/vouch/pack/sections.js +1 -2
  298. package/dist/commands/vouch/pack/sections.js.map +1 -1
  299. package/dist/commands/vouch/run.js +26 -81
  300. package/dist/commands/vouch/run.js.map +1 -1
  301. package/dist/commands/vouch/sample/plan.js +20 -34
  302. package/dist/commands/vouch/sample/plan.js.map +1 -1
  303. package/dist/commands/vouch/vet/verify.js +39 -7
  304. package/dist/commands/vouch/vet/verify.js.map +1 -1
  305. package/dist/commands/vouch/vouch.js +10 -17
  306. package/dist/commands/vouch/vouch.js.map +1 -1
  307. package/dist/core/agent/agents-md/command-map.js +81 -106
  308. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  309. package/dist/core/agent/agents-md/map/explain.js +21 -9
  310. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  311. package/dist/core/agent/agents-md/map/lenses/context.js +17 -8
  312. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  313. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  314. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  315. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  316. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  317. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  318. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  319. package/dist/core/agent/agents-md/map/status.js +24 -8
  320. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  321. package/dist/core/agent/agents-md/map/subsystem.js +80 -8
  322. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  323. package/dist/core/agent/agents-md/map/usecases.js +88 -10
  324. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  325. package/dist/core/agent/agents-md/refusals.js +49 -22
  326. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  327. package/dist/core/agent/agents-md.js +39 -6
  328. package/dist/core/agent/agents-md.js.map +1 -1
  329. package/dist/core/agent/agents-stamp.js +10 -0
  330. package/dist/core/agent/agents-stamp.js.map +1 -1
  331. package/dist/core/agent/protocol.js +1 -1
  332. package/dist/core/agent/protocol.js.map +1 -1
  333. package/dist/core/agent/scaffold.js +10 -15
  334. package/dist/core/agent/scaffold.js.map +1 -1
  335. package/dist/core/agent/support/report.js +46 -5
  336. package/dist/core/agent/support/report.js.map +1 -1
  337. package/dist/core/agent/tools/dialects.js +1 -0
  338. package/dist/core/agent/tools/dialects.js.map +1 -1
  339. package/dist/core/agent/tools/registry.js +41 -76
  340. package/dist/core/agent/tools/registry.js.map +1 -1
  341. package/dist/core/agent/workflows/adopt.js +158 -11
  342. package/dist/core/agent/workflows/adopt.js.map +1 -1
  343. package/dist/core/agent/workflows/check.js +78 -39
  344. package/dist/core/agent/workflows/check.js.map +1 -1
  345. package/dist/core/agent/workflows/closing.js +88 -27
  346. package/dist/core/agent/workflows/closing.js.map +1 -1
  347. package/dist/core/agent/workflows/feature.js +128 -13
  348. package/dist/core/agent/workflows/feature.js.map +1 -1
  349. package/dist/core/agent/workflows/implement.js +96 -13
  350. package/dist/core/agent/workflows/implement.js.map +1 -1
  351. package/dist/core/agent/workflows/reference/authoring.js +106 -4
  352. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  353. package/dist/core/agent/workflows/reference/codes.js +8 -6
  354. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  355. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  356. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  357. package/dist/core/agent/workflows/reference/drawing/drawing.js +253 -0
  358. package/dist/core/agent/workflows/reference/drawing/drawing.js.map +1 -0
  359. package/dist/core/agent/workflows/reference/reference.js +6 -3
  360. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  361. package/dist/core/agent/workflows/reference/spine.js +68 -27
  362. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  363. package/dist/core/asyncapi/baseline/plan.js +14 -81
  364. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  365. package/dist/core/asyncapi/digest.js +30 -25
  366. package/dist/core/asyncapi/digest.js.map +1 -1
  367. package/dist/core/asyncapi/merge/components.js +5 -4
  368. package/dist/core/asyncapi/merge/components.js.map +1 -1
  369. package/dist/core/asyncapi/merge/error.js +7 -2
  370. package/dist/core/asyncapi/merge/error.js.map +1 -1
  371. package/dist/core/asyncapi/merge/markers.js +32 -50
  372. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  373. package/dist/core/asyncapi/merge/merge.js +33 -53
  374. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  375. package/dist/core/asyncapi/merge/pin.js +12 -36
  376. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  377. package/dist/core/asyncapi/model.js.map +1 -1
  378. package/dist/core/asyncapi/read.js +36 -12
  379. package/dist/core/asyncapi/read.js.map +1 -1
  380. package/dist/core/brief/brief.js +96 -36
  381. package/dist/core/brief/brief.js.map +1 -1
  382. package/dist/core/brief/checks.js +138 -3
  383. package/dist/core/brief/checks.js.map +1 -1
  384. package/dist/core/brief/landscape.js +209 -130
  385. package/dist/core/brief/landscape.js.map +1 -1
  386. package/dist/core/brief/map/extend.js +84 -0
  387. package/dist/core/brief/map/extend.js.map +1 -0
  388. package/dist/core/brief/map/owed.js +266 -0
  389. package/dist/core/brief/map/owed.js.map +1 -0
  390. package/dist/core/brief/targets.js +109 -31
  391. package/dist/core/brief/targets.js.map +1 -1
  392. package/dist/core/brief/unchecked.js +14 -3
  393. package/dist/core/brief/unchecked.js.map +1 -1
  394. package/dist/core/c4/arch.js +90 -182
  395. package/dist/core/c4/arch.js.map +1 -1
  396. package/dist/core/c4/direction/direction.js +114 -0
  397. package/dist/core/c4/direction/direction.js.map +1 -0
  398. package/dist/core/c4/likec4.js +169 -14
  399. package/dist/core/c4/likec4.js.map +1 -1
  400. package/dist/core/c4/locate/unlocated.js +150 -0
  401. package/dist/core/c4/locate/unlocated.js.map +1 -0
  402. package/dist/core/c4/parsed/deployment.js +160 -0
  403. package/dist/core/c4/parsed/deployment.js.map +1 -0
  404. package/dist/core/c4/parsed/dynamic-views.js.map +1 -1
  405. package/dist/core/c4/parsed/values.js +54 -1
  406. package/dist/core/c4/parsed/values.js.map +1 -1
  407. package/dist/core/c4/parsed/view-ids.js +39 -1
  408. package/dist/core/c4/parsed/view-ids.js.map +1 -1
  409. package/dist/core/c4/prefetch/fleet.js +134 -0
  410. package/dist/core/c4/prefetch/fleet.js.map +1 -0
  411. package/dist/core/c4/project/architecture.js +123 -7
  412. package/dist/core/c4/project/architecture.js.map +1 -1
  413. package/dist/core/c4/project/batch.js +140 -0
  414. package/dist/core/c4/project/batch.js.map +1 -0
  415. package/dist/core/c4/project/documents.js +43 -12
  416. package/dist/core/c4/project/documents.js.map +1 -1
  417. package/dist/core/c4/project/load.js +79 -43
  418. package/dist/core/c4/project/load.js.map +1 -1
  419. package/dist/core/c4/project/staged.js +277 -0
  420. package/dist/core/c4/project/staged.js.map +1 -0
  421. package/dist/core/c4/resolve/attested.js +0 -0
  422. package/dist/core/c4/resolve/attested.js.map +1 -0
  423. package/dist/core/c4/resolve/external.js +51 -0
  424. package/dist/core/c4/resolve/external.js.map +1 -0
  425. package/dist/core/c4/resolve/service.js +78 -17
  426. package/dist/core/c4/resolve/service.js.map +1 -1
  427. package/dist/core/c4/resolve/steps.js +290 -0
  428. package/dist/core/c4/resolve/steps.js.map +1 -0
  429. package/dist/core/c4/resolve/systems.js +154 -0
  430. package/dist/core/c4/resolve/systems.js.map +1 -0
  431. package/dist/core/c4/root-project/exclude.js +352 -0
  432. package/dist/core/c4/root-project/exclude.js.map +1 -0
  433. package/dist/core/c4/seed/template.js +65 -1
  434. package/dist/core/c4/seed/template.js.map +1 -1
  435. package/dist/core/c4/service-model/created/models.js +96 -0
  436. package/dist/core/c4/service-model/created/models.js.map +1 -0
  437. package/dist/core/c4/service-model/documents.js +47 -0
  438. package/dist/core/c4/service-model/documents.js.map +1 -0
  439. package/dist/core/c4/service-model/fleet/extending.js +82 -0
  440. package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
  441. package/dist/core/c4/service-model/load.js +165 -0
  442. package/dist/core/c4/service-model/load.js.map +1 -0
  443. package/dist/core/c4/service-model/renderer.js +128 -0
  444. package/dist/core/c4/service-model/renderer.js.map +1 -0
  445. package/dist/core/c4/service-model/shape.js +106 -0
  446. package/dist/core/c4/service-model/shape.js.map +1 -0
  447. package/dist/core/c4/service-model/slice.js +188 -0
  448. package/dist/core/c4/service-model/slice.js.map +1 -0
  449. package/dist/core/c4/source-mask.js +297 -10
  450. package/dist/core/c4/source-mask.js.map +1 -1
  451. package/dist/core/c4/source-scan.js +222 -141
  452. package/dist/core/c4/source-scan.js.map +1 -1
  453. package/dist/core/c4/splice/authored-source.js +79 -13
  454. package/dist/core/c4/splice/authored-source.js.map +1 -1
  455. package/dist/core/c4/splice/contract.js +20 -0
  456. package/dist/core/c4/splice/contract.js.map +1 -1
  457. package/dist/core/c4/splice/delta-blocks.js +20 -20
  458. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  459. package/dist/core/c4/splice/identity/declared.js +87 -0
  460. package/dist/core/c4/splice/identity/declared.js.map +1 -0
  461. package/dist/core/c4/splice/identity/edges.js +37 -0
  462. package/dist/core/c4/splice/identity/edges.js.map +1 -1
  463. package/dist/core/c4/splice/identity/existing.js +80 -0
  464. package/dist/core/c4/splice/identity/existing.js.map +1 -0
  465. package/dist/core/c4/splice/identity/interior.js +80 -0
  466. package/dist/core/c4/splice/identity/interior.js.map +1 -0
  467. package/dist/core/c4/splice/identity/owner.js +50 -0
  468. package/dist/core/c4/splice/identity/owner.js.map +1 -0
  469. package/dist/core/c4/splice/landscape-merge.js +182 -114
  470. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  471. package/dist/core/c4/splice/model/merge.js +151 -0
  472. package/dist/core/c4/splice/model/merge.js.map +1 -0
  473. package/dist/core/c4/splice/placement.js +160 -56
  474. package/dist/core/c4/splice/placement.js.map +1 -1
  475. package/dist/core/c4/splice/text/spot.js +131 -0
  476. package/dist/core/c4/splice/text/spot.js.map +1 -0
  477. package/dist/core/c4/workspace.js +13 -19
  478. package/dist/core/c4/workspace.js.map +1 -1
  479. package/dist/core/capabilities/capabilities.js +6 -6
  480. package/dist/core/capabilities/capabilities.js.map +1 -1
  481. package/dist/core/capabilities/delta/doc.js +7 -10
  482. package/dist/core/capabilities/delta/doc.js.map +1 -1
  483. package/dist/core/capabilities/delta/uncovered.js +2 -2
  484. package/dist/core/capabilities/delta/uncovered.js.map +1 -1
  485. package/dist/core/capabilities/findings.js +18 -22
  486. package/dist/core/capabilities/findings.js.map +1 -1
  487. package/dist/core/capabilities/realizes/findings.js +12 -19
  488. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  489. package/dist/core/capabilities/realizes/join.js +0 -20
  490. package/dist/core/capabilities/realizes/join.js.map +1 -1
  491. package/dist/core/capabilities/rollup.js +6 -0
  492. package/dist/core/capabilities/rollup.js.map +1 -1
  493. package/dist/core/capabilities/tree.js +6 -28
  494. package/dist/core/capabilities/tree.js.map +1 -1
  495. package/dist/core/capabilities/usecase-join.js +3 -13
  496. package/dist/core/capabilities/usecase-join.js.map +1 -1
  497. package/dist/core/coherence/authoring/scaffold.js +11 -3
  498. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  499. package/dist/core/coherence/authoring/sentinels.js +2 -1
  500. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  501. package/dist/core/coherence/calls/called.js +52 -0
  502. package/dist/core/coherence/calls/called.js.map +1 -0
  503. package/dist/core/coherence/coherence.js +103 -22
  504. package/dist/core/coherence/coherence.js.map +1 -1
  505. package/dist/core/coherence/consumed/operations.js +70 -0
  506. package/dist/core/coherence/consumed/operations.js.map +1 -0
  507. package/dist/core/coherence/consumed/refusal.js +38 -0
  508. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  509. package/dist/core/coherence/covers/health.js +85 -0
  510. package/dist/core/coherence/covers/health.js.map +1 -0
  511. package/dist/core/coherence/declared.js +60 -34
  512. package/dist/core/coherence/declared.js.map +1 -1
  513. package/dist/core/coherence/events/declared.js +44 -59
  514. package/dist/core/coherence/events/declared.js.map +1 -1
  515. package/dist/core/coherence/events/events.js +147 -16
  516. package/dist/core/coherence/events/events.js.map +1 -1
  517. package/dist/core/coherence/events/grades.js +31 -8
  518. package/dist/core/coherence/events/grades.js.map +1 -1
  519. package/dist/core/coherence/events/lookups.js +28 -87
  520. package/dist/core/coherence/events/lookups.js.map +1 -1
  521. package/dist/core/coherence/events/produced.js +100 -0
  522. package/dist/core/coherence/events/produced.js.map +1 -0
  523. package/dist/core/coherence/lookups.js +18 -78
  524. package/dist/core/coherence/lookups.js.map +1 -1
  525. package/dist/core/coherence/retired/archived.js +285 -0
  526. package/dist/core/coherence/retired/archived.js.map +1 -0
  527. package/dist/core/coherence/retired/event-pair.js +62 -0
  528. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  529. package/dist/core/coherence/retired/merged.js +79 -0
  530. package/dist/core/coherence/retired/merged.js.map +1 -0
  531. package/dist/core/coherence/retired/retirement.js +49 -0
  532. package/dist/core/coherence/retired/retirement.js.map +1 -0
  533. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  534. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  535. package/dist/core/coherence/retired/revival/revived.js +297 -0
  536. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  537. package/dist/core/consumers/after.js +113 -0
  538. package/dist/core/consumers/after.js.map +1 -0
  539. package/dist/core/consumers/consumers.js +296 -0
  540. package/dist/core/consumers/consumers.js.map +1 -0
  541. package/dist/core/consumers/current.js +211 -0
  542. package/dist/core/consumers/current.js.map +1 -0
  543. package/dist/core/consumers/scan.js +46 -0
  544. package/dist/core/consumers/scan.js.map +1 -0
  545. package/dist/core/consumers/state.js +27 -0
  546. package/dist/core/consumers/state.js.map +1 -0
  547. package/dist/core/delta/delta.js +51 -6
  548. package/dist/core/delta/delta.js.map +1 -1
  549. package/dist/core/dependencies/fanin.js +2 -2
  550. package/dist/core/dependencies/fanin.js.map +1 -1
  551. package/dist/core/deployment/delta.js +191 -0
  552. package/dist/core/deployment/delta.js.map +1 -0
  553. package/dist/core/deployment/objects.js +145 -0
  554. package/dist/core/deployment/objects.js.map +1 -0
  555. package/dist/core/diff/base-git.js +94 -12
  556. package/dist/core/diff/base-git.js.map +1 -1
  557. package/dist/core/diff/base-state.js +49 -21
  558. package/dist/core/diff/base-state.js.map +1 -1
  559. package/dist/core/diff/joins.js +136 -0
  560. package/dist/core/diff/joins.js.map +1 -0
  561. package/dist/core/diff/scope/changed-paths.js +22 -6
  562. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  563. package/dist/core/diff/scope/edges.js +82 -0
  564. package/dist/core/diff/scope/edges.js.map +1 -0
  565. package/dist/core/diff/semantic.js +100 -31
  566. package/dist/core/diff/semantic.js.map +1 -1
  567. package/dist/core/docs.js +31 -22
  568. package/dist/core/docs.js.map +1 -1
  569. package/dist/core/doctor/agents.js +34 -18
  570. package/dist/core/doctor/agents.js.map +1 -1
  571. package/dist/core/doctor/config.js +7 -3
  572. package/dist/core/doctor/config.js.map +1 -1
  573. package/dist/core/doctor/doctor.js +91 -11
  574. package/dist/core/doctor/doctor.js.map +1 -1
  575. package/dist/core/doctor/report.js.map +1 -1
  576. package/dist/core/doctor/reports/scan.js +242 -0
  577. package/dist/core/doctor/reports/scan.js.map +1 -0
  578. package/dist/core/document/apply.js +158 -11
  579. package/dist/core/document/apply.js.map +1 -1
  580. package/dist/core/document/frontmatter.js +55 -0
  581. package/dist/core/document/frontmatter.js.map +1 -1
  582. package/dist/core/document/parse.js +29 -6
  583. package/dist/core/document/parse.js.map +1 -1
  584. package/dist/core/document/scenarios.js +17 -0
  585. package/dist/core/document/scenarios.js.map +1 -1
  586. package/dist/core/document/spec.js +4 -11
  587. package/dist/core/document/spec.js.map +1 -1
  588. package/dist/core/envelope/config.js +10 -0
  589. package/dist/core/envelope/config.js.map +1 -1
  590. package/dist/core/envelope/json.js +34 -0
  591. package/dist/core/envelope/json.js.map +1 -1
  592. package/dist/core/explain/families.js +26 -21
  593. package/dist/core/explain/families.js.map +1 -1
  594. package/dist/core/explain/lookup.js +14 -1
  595. package/dist/core/explain/lookup.js.map +1 -1
  596. package/dist/core/explain/refusals.js +5 -5
  597. package/dist/core/explain/refusals.js.map +1 -1
  598. package/dist/core/explain/terms/documents.js +227 -0
  599. package/dist/core/explain/terms/documents.js.map +1 -0
  600. package/dist/core/explain/terms/done-check.js +144 -0
  601. package/dist/core/explain/terms/done-check.js.map +1 -0
  602. package/dist/core/explain/terms/entry.js +14 -0
  603. package/dist/core/explain/terms/entry.js.map +1 -0
  604. package/dist/core/explain/terms/stages.js +71 -0
  605. package/dist/core/explain/terms/stages.js.map +1 -0
  606. package/dist/core/explain/terms/tasks.js +97 -0
  607. package/dist/core/explain/terms/tasks.js.map +1 -0
  608. package/dist/core/explain/terms.js +68 -109
  609. package/dist/core/explain/terms.js.map +1 -1
  610. package/dist/core/explore/explore.js +40 -10
  611. package/dist/core/explore/explore.js.map +1 -1
  612. package/dist/core/fleet-context.js +81 -95
  613. package/dist/core/fleet-context.js.map +1 -1
  614. package/dist/core/gate/checks.js +11 -4
  615. package/dist/core/gate/checks.js.map +1 -1
  616. package/dist/core/gate/partners.js +69 -28
  617. package/dist/core/gate/partners.js.map +1 -1
  618. package/dist/core/gate/report.js.map +1 -1
  619. package/dist/core/gate/verification.js +75 -12
  620. package/dist/core/gate/verification.js.map +1 -1
  621. package/dist/core/gherkin/stale.js +13 -37
  622. package/dist/core/gherkin/stale.js.map +1 -1
  623. package/dist/core/glossary/delta.js +10 -7
  624. package/dist/core/glossary/delta.js.map +1 -1
  625. package/dist/core/glossary/tree.js +6 -19
  626. package/dist/core/glossary/tree.js.map +1 -1
  627. package/dist/core/kernel/concurrency.js +19 -0
  628. package/dist/core/kernel/concurrency.js.map +1 -1
  629. package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
  630. package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
  631. package/dist/core/kernel/path-safety.js +38 -0
  632. package/dist/core/kernel/path-safety.js.map +1 -1
  633. package/dist/core/kernel/records.js +11 -0
  634. package/dist/core/kernel/records.js.map +1 -1
  635. package/dist/core/links/backlinks.js +1 -3
  636. package/dist/core/links/backlinks.js.map +1 -1
  637. package/dist/core/links/corpus.js +51 -37
  638. package/dist/core/links/corpus.js.map +1 -1
  639. package/dist/core/links/findings.js.map +1 -1
  640. package/dist/core/links/parse.js +38 -9
  641. package/dist/core/links/parse.js.map +1 -1
  642. package/dist/core/mcp/argv.js +101 -0
  643. package/dist/core/mcp/argv.js.map +1 -1
  644. package/dist/core/mcp/author-tools.js +2 -1
  645. package/dist/core/mcp/author-tools.js.map +1 -1
  646. package/dist/core/mcp/protocol.js +2 -2
  647. package/dist/core/mcp/protocol.js.map +1 -1
  648. package/dist/core/mcp/tools.js +43 -33
  649. package/dist/core/mcp/tools.js.map +1 -1
  650. package/dist/core/openapi/baseline/gate.js +53 -12
  651. package/dist/core/openapi/baseline/gate.js.map +1 -1
  652. package/dist/core/openapi/baseline/plan.js +40 -10
  653. package/dist/core/openapi/baseline/plan.js.map +1 -1
  654. package/dist/core/openapi/baseline/record.js +0 -1
  655. package/dist/core/openapi/baseline/record.js.map +1 -1
  656. package/dist/core/openapi/contract-text.js +33 -0
  657. package/dist/core/openapi/contract-text.js.map +1 -0
  658. package/dist/core/openapi/contract-tree.js +301 -0
  659. package/dist/core/openapi/contract-tree.js.map +1 -0
  660. package/dist/core/openapi/depth.js +38 -24
  661. package/dist/core/openapi/depth.js.map +1 -1
  662. package/dist/core/openapi/digest.js +52 -2
  663. package/dist/core/openapi/digest.js.map +1 -1
  664. package/dist/core/openapi/doc.js +12 -4
  665. package/dist/core/openapi/doc.js.map +1 -1
  666. package/dist/core/openapi/merge/components.js +25 -60
  667. package/dist/core/openapi/merge/components.js.map +1 -1
  668. package/dist/core/openapi/merge/error.js +3 -13
  669. package/dist/core/openapi/merge/error.js.map +1 -1
  670. package/dist/core/openapi/merge/markers.js +26 -30
  671. package/dist/core/openapi/merge/markers.js.map +1 -1
  672. package/dist/core/openapi/merge/merge.js +28 -52
  673. package/dist/core/openapi/merge/merge.js.map +1 -1
  674. package/dist/core/openapi/merge/pin.js +17 -35
  675. package/dist/core/openapi/merge/pin.js.map +1 -1
  676. package/dist/core/openspec/decide/verdict.js.map +1 -1
  677. package/dist/core/openspec/inventory.js +4 -5
  678. package/dist/core/openspec/inventory.js.map +1 -1
  679. package/dist/core/openspec/model/mapping.js +30 -28
  680. package/dist/core/openspec/model/mapping.js.map +1 -1
  681. package/dist/core/openspec/model/model.js +12 -7
  682. package/dist/core/openspec/model/model.js.map +1 -1
  683. package/dist/core/openspec/scan/changes.js +10 -4
  684. package/dist/core/openspec/scan/changes.js.map +1 -1
  685. package/dist/core/openspec/scan/config.js +7 -16
  686. package/dist/core/openspec/scan/config.js.map +1 -1
  687. package/dist/core/openspec/scan/shape.js +63 -19
  688. package/dist/core/openspec/scan/shape.js.map +1 -1
  689. package/dist/core/openspec/scan/walk.js +21 -38
  690. package/dist/core/openspec/scan/walk.js.map +1 -1
  691. package/dist/core/pack/living.js +43 -4
  692. package/dist/core/pack/living.js.map +1 -1
  693. package/dist/core/pack/pack.js +50 -6
  694. package/dist/core/pack/pack.js.map +1 -1
  695. package/dist/core/projection/arch-slice.js +2 -2
  696. package/dist/core/projection/arch-slice.js.map +1 -1
  697. package/dist/core/projection/introduced.js +14 -0
  698. package/dist/core/projection/introduced.js.map +1 -0
  699. package/dist/core/provenance/findings.js +48 -12
  700. package/dist/core/provenance/findings.js.map +1 -1
  701. package/dist/core/provenance/git.js +33 -57
  702. package/dist/core/provenance/git.js.map +1 -1
  703. package/dist/core/provenance/gitq/moves.js +71 -114
  704. package/dist/core/provenance/gitq/moves.js.map +1 -1
  705. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  706. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  707. package/dist/core/provenance/sources.js +23 -6
  708. package/dist/core/provenance/sources.js.map +1 -1
  709. package/dist/core/provenance/stamp.js +5 -17
  710. package/dist/core/provenance/stamp.js.map +1 -1
  711. package/dist/core/repo/authored/paths.js +18 -0
  712. package/dist/core/repo/authored/paths.js.map +1 -1
  713. package/dist/core/repo/entries.js.map +1 -1
  714. package/dist/core/repo/paths.js +92 -6
  715. package/dist/core/repo/paths.js.map +1 -1
  716. package/dist/core/repo/repo.js +74 -14
  717. package/dist/core/repo/repo.js.map +1 -1
  718. package/dist/core/repo/tree/find.js +12 -3
  719. package/dist/core/repo/tree/find.js.map +1 -1
  720. package/dist/core/repo/tree/fs.js +27 -1
  721. package/dist/core/repo/tree/fs.js.map +1 -1
  722. package/dist/core/repo/tree/render/body.js +258 -0
  723. package/dist/core/repo/tree/render/body.js.map +1 -0
  724. package/dist/core/repo/tree/render/projects.js +182 -0
  725. package/dist/core/repo/tree/render/projects.js.map +1 -0
  726. package/dist/core/repo/tree/render/service/header.js +125 -0
  727. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  728. package/dist/core/repo/tree/render/service/peers.js +28 -0
  729. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  730. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  731. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  732. package/dist/core/repo/tree/render/service/views.js +78 -0
  733. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  734. package/dist/core/repo/tree/render/stale.js +42 -0
  735. package/dist/core/repo/tree/render/stale.js.map +1 -0
  736. package/dist/core/repo/tree/render/text.js +84 -0
  737. package/dist/core/repo/tree/render/text.js.map +1 -0
  738. package/dist/core/repo/tree/render/views.js +293 -0
  739. package/dist/core/repo/tree/render/views.js.map +1 -0
  740. package/dist/core/repo/tree/walk.js +53 -32
  741. package/dist/core/repo/tree/walk.js.map +1 -1
  742. package/dist/core/results.js +47 -12
  743. package/dist/core/results.js.map +1 -1
  744. package/dist/core/scaffold/landscape.js +121 -13
  745. package/dist/core/scaffold/landscape.js.map +1 -1
  746. package/dist/core/scaffold/legacy.js +271 -0
  747. package/dist/core/scaffold/legacy.js.map +1 -0
  748. package/dist/core/staging/commit.js +26 -15
  749. package/dist/core/staging/commit.js.map +1 -1
  750. package/dist/core/staging/lock.js +74 -12
  751. package/dist/core/staging/lock.js.map +1 -1
  752. package/dist/core/staging/recovery/intent.js +2 -5
  753. package/dist/core/staging/recovery/intent.js.map +1 -1
  754. package/dist/core/staging/recovery/recover.js +84 -6
  755. package/dist/core/staging/recovery/recover.js.map +1 -1
  756. package/dist/core/staging/snapshot/manifest.js +188 -0
  757. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  758. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +39 -45
  759. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  760. package/dist/core/staging/txn/forward.js +24 -14
  761. package/dist/core/staging/txn/forward.js.map +1 -1
  762. package/dist/core/staging/writes.js.map +1 -1
  763. package/dist/core/status/actions/execution.js +11 -0
  764. package/dist/core/status/actions/execution.js.map +1 -1
  765. package/dist/core/status/contracts.js +39 -15
  766. package/dist/core/status/contracts.js.map +1 -1
  767. package/dist/core/status/feature/artifacts.js +33 -8
  768. package/dist/core/status/feature/artifacts.js.map +1 -1
  769. package/dist/core/status/feature/feature.js +24 -17
  770. package/dist/core/status/feature/feature.js.map +1 -1
  771. package/dist/core/status/feature/next.js +20 -7
  772. package/dist/core/status/feature/next.js.map +1 -1
  773. package/dist/core/status/feature/words.js +96 -0
  774. package/dist/core/status/feature/words.js.map +1 -0
  775. package/dist/core/status/fleet/fleet.js +43 -14
  776. package/dist/core/status/fleet/fleet.js.map +1 -1
  777. package/dist/core/status/fleet/next.js +153 -58
  778. package/dist/core/status/fleet/next.js.map +1 -1
  779. package/dist/core/status/report.js.map +1 -1
  780. package/dist/core/status/scan.js.map +1 -1
  781. package/dist/core/status/verification.js +30 -15
  782. package/dist/core/status/verification.js.map +1 -1
  783. package/dist/core/tasks/packet.js +199 -0
  784. package/dist/core/tasks/packet.js.map +1 -0
  785. package/dist/core/tasks/record.js +43 -0
  786. package/dist/core/tasks/record.js.map +1 -0
  787. package/dist/core/tasks/sources.js +181 -0
  788. package/dist/core/tasks/sources.js.map +1 -0
  789. package/dist/core/tasks/stamps.js +76 -0
  790. package/dist/core/tasks/stamps.js.map +1 -0
  791. package/dist/core/tasks/unit.js +0 -0
  792. package/dist/core/tasks/unit.js.map +1 -0
  793. package/dist/core/usecases/capability.js +11 -14
  794. package/dist/core/usecases/capability.js.map +1 -1
  795. package/dist/core/usecases/delta/claims.js +31 -7
  796. package/dist/core/usecases/delta/claims.js.map +1 -1
  797. package/dist/core/usecases/delta/flows.js +5 -7
  798. package/dist/core/usecases/delta/flows.js.map +1 -1
  799. package/dist/core/usecases/delta/modified.js +315 -0
  800. package/dist/core/usecases/delta/modified.js.map +1 -0
  801. package/dist/core/usecases/delta/overlay.js +32 -191
  802. package/dist/core/usecases/delta/overlay.js.map +1 -1
  803. package/dist/core/usecases/fleet.js +195 -36
  804. package/dist/core/usecases/fleet.js.map +1 -1
  805. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/capability-tag.js +8 -9
  806. package/dist/core/usecases/grade/capability-tag.js.map +1 -0
  807. package/dist/core/usecases/grade/feature.js +88 -0
  808. package/dist/core/usecases/grade/feature.js.map +1 -0
  809. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/requirement-tag.js +3 -4
  810. package/dist/core/usecases/grade/requirement-tag.js.map +1 -0
  811. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/steps.js +102 -33
  812. package/dist/core/usecases/grade/steps.js.map +1 -0
  813. package/dist/{commands/validate/fleet/usecases → core/usecases/grade}/usecases.js +23 -10
  814. package/dist/core/usecases/grade/usecases.js.map +1 -0
  815. package/dist/core/usecases/operations.js +26 -14
  816. package/dist/core/usecases/operations.js.map +1 -1
  817. package/dist/core/usecases/place.js.map +1 -1
  818. package/dist/core/usecases/service/exclude.js +119 -0
  819. package/dist/core/usecases/service/exclude.js.map +1 -0
  820. package/dist/core/usecases/service/flows.js +164 -0
  821. package/dist/core/usecases/service/flows.js.map +1 -0
  822. package/dist/core/usecases/suites/participants.js +83 -0
  823. package/dist/core/usecases/suites/participants.js.map +1 -0
  824. package/dist/core/usecases/suites/suites.js +45 -0
  825. package/dist/core/usecases/suites/suites.js.map +1 -0
  826. package/dist/core/verify/answers.js +12 -5
  827. package/dist/core/verify/answers.js.map +1 -1
  828. package/dist/core/verify/build.js +102 -82
  829. package/dist/core/verify/build.js.map +1 -1
  830. package/dist/core/verify/checklist.js +70 -12
  831. package/dist/core/verify/checklist.js.map +1 -1
  832. package/dist/core/verify/location.js +67 -0
  833. package/dist/core/verify/location.js.map +1 -0
  834. package/dist/core/verify/pins/lint.js +3 -2
  835. package/dist/core/verify/pins/lint.js.map +1 -1
  836. package/dist/core/verify/pins/pin.js +1 -1
  837. package/dist/core/verify/pins/pin.js.map +1 -1
  838. package/dist/core/verify/read/file.js +224 -0
  839. package/dist/core/verify/read/file.js.map +1 -0
  840. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  841. package/dist/core/verify/read/shape.js.map +1 -0
  842. package/dist/core/verify/read/union.js +105 -0
  843. package/dist/core/verify/read/union.js.map +1 -0
  844. package/dist/core/verify/record.js +35 -12
  845. package/dist/core/verify/record.js.map +1 -1
  846. package/dist/core/verify/store/commit.js +40 -30
  847. package/dist/core/verify/store/commit.js.map +1 -1
  848. package/dist/core/verify/store/render.js +8 -4
  849. package/dist/core/verify/store/render.js.map +1 -1
  850. package/dist/core/vocabulary/issue.js +4 -0
  851. package/dist/core/vocabulary/issue.js.map +1 -1
  852. package/dist/core/vocabulary/maturity.js +75 -4
  853. package/dist/core/vocabulary/maturity.js.map +1 -1
  854. package/dist/core/vocabulary/report.js +16 -1
  855. package/dist/core/vocabulary/report.js.map +1 -1
  856. package/examples/README.md +54 -11
  857. package/examples/docs/architecture/landscape.likec4 +62 -6
  858. package/examples/docs/architecture/services.likec4 +70 -0
  859. package/examples/docs/architecture/subsystems.likec4 +7 -0
  860. package/examples/docs/architecture/usecases/order-notification.likec4 +16 -7
  861. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  862. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  863. package/examples/docs/features/FEAT-112-retire-order-v1/specs/order-service/openapi.yaml +4 -1
  864. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  865. package/examples/docs/likec4.config.json +0 -1
  866. package/examples/docs/services/checkout-web/model.likec4 +39 -56
  867. package/examples/docs/services/order-service/arch.spec.md +13 -6
  868. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  869. package/examples/docs/services/order-service/model.likec4 +58 -70
  870. package/examples/docs/services/payment-service/arch.spec.md +7 -4
  871. package/examples/docs/services/payment-service/model.likec4 +38 -37
  872. package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
  873. package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
  874. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  875. package/examples/docs/services/platform/notification-service/health.yaml +8 -3
  876. package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
  877. package/package.json +1 -1
  878. package/dist/commands/validate/checks/vocabulary.js +0 -31
  879. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  880. package/dist/commands/validate/fleet/usecases/capability-tag.js.map +0 -1
  881. package/dist/commands/validate/fleet/usecases/requirement-tag.js.map +0 -1
  882. package/dist/commands/validate/fleet/usecases/steps.js.map +0 -1
  883. package/dist/commands/validate/fleet/usecases/usecases.js.map +0 -1
  884. package/dist/core/agent/agents-md/artifacts.js +0 -220
  885. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  886. package/dist/core/agent/agents-md/cycle.js +0 -165
  887. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  888. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  889. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  890. package/dist/core/agent/agents-md/spine.js +0 -40
  891. package/dist/core/agent/agents-md/spine.js.map +0 -1
  892. package/dist/core/coherence/events/merged.js +0 -40
  893. package/dist/core/coherence/events/merged.js.map +0 -1
  894. package/dist/core/diff/current-state.js +0 -96
  895. package/dist/core/diff/current-state.js.map +0 -1
  896. package/dist/core/diff/victims.js +0 -247
  897. package/dist/core/diff/victims.js.map +0 -1
  898. package/dist/core/repo/tree/views.js +0 -172
  899. package/dist/core/repo/tree/views.js.map +0 -1
  900. package/dist/core/staging/snapshot.js.map +0 -1
  901. package/dist/core/verify/file.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"rollup.js","sourceRoot":"","sources":["../../../src/core/capabilities/rollup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet total over the capability join, built once and reused by the fleet\n * grade, `loam list capabilities` and `loam explore --capability` — one walk,\n * so the unrealized warning and the rollup a reader diffs can never disagree\n * about what \"realized\" means.\n *\n * Deterministic and diff-stable by construction: rows sorted with compareIds,\n * realizedBy sorted by (service, file, requirement), statuses' keys sorted —\n * no timestamps, no absolute paths, no readdir-order dependence. The reader is\n * INJECTED (`read`) rather than a FleetContext, because fleet-context.ts\n * imports this package for its memo and an import back would be a package\n * cycle (see the memo's comment); callers pass `fleet.readRequirements` bound,\n * so under `validate --all` every parse is already cached from the service\n * targets and the rollup costs no additional per-service read.\n *\n * TWO CORPORA MEET HERE, and neither is derived from the other. A promise is\n * kept by a service requirement's `Realizes:` line (walked below) or by a\n * `#cap-`/`#req-` tagged flow (injected as `keptByFlows`, because the module\n * that computes it imports this one). The rollup carries both on one row for the\n * reason it carries both `Capability:` joins on one row: `loam list capabilities`\n * and `capability.requirement-unrealized` must not be able to give different\n * answers to \"is this business promise kept\", and they can only give one answer\n * if they read one row.\n */\nimport { existsSync } from \"node:fs\";\nimport type { Requirement } from \"../document/spec.js\";\nimport { compareIds, type ServiceEntry } from \"../repo/entries.js\";\nimport { SPEC_AXES, servicePathsAt } from \"../repo/paths.js\";\nimport type { CapabilityVocabulary } from \"./capabilities.js\";\nimport { splitRealizesEntry } from \"./realizes/join.js\";\n\n/** One requirement realizing one capability, located exactly. */\nexport interface CapabilityRealization {\n service: string;\n file: \"spec.md\" | \"arch.spec.md\";\n /** The requirement's `### Requirement:` name. */\n requirement: string;\n}\n\n/** One requirement OF a capability document, and what realizes it. */\nexport interface CapabilityRequirementRow {\n /** Its `Requirement-ID:`. A requirement without one is absent here — see `capabilityRequirementRows`. */\n id: string;\n /** Its `### Requirement:` heading. */\n name: string;\n /** Service requirements whose `Realizes:` line names `<capability>#<id>`. */\n realizedBy: CapabilityRealization[];\n /**\n * The ids of the declared flows that keep this promise — a `#cap-`/`#req-`\n * tagged `dynamic view` whose BOTH tags resolve. THE SECOND CORPUS, and the\n * only carrier a cross-service promise has: \"I enter a login and a password\n * and I am in\" belongs to no single service's spec, so a fleet can keep it\n * perfectly through a flow and realize it by no `Realizes:` line at all.\n *\n * Three states, all of them different answers, exactly as `requirements`\n * itself has three:\n * present and non-empty — these flows keep it;\n * present and empty — loam read the flows and none of them keeps it;\n * ABSENT — nobody looked. Either the caller did not ask\n * (`CapabilityRollupInput.keptByFlows` omitted) or\n * it asked and `architecture/` could not be read.\n * The third must never collapse into the second. \"Kept by no flow\" over a\n * project that did not parse is the vacuously-green claim the whole use-case\n * axis exists to refuse, and it is the claim a `[]` default would make.\n */\n keptBy?: string[];\n}\n\n/** One declared capability with everything the fleet says about it. */\nexport interface CapabilityRow {\n id: string;\n description?: string;\n owner?: string;\n /**\n * Service requirements realizing this capability BY EITHER JOIN, deduplicated\n * on (service, file, requirement).\n *\n * Both joins land here on purpose. `Realizes: checkout#CHK-1` realizes part of\n * `checkout` as surely as `Capability: checkout` does, and a rollup that\n * counted only the second would report a capability as realized by nobody\n * while the fleet pointed at its individual promises — which would then be\n * one spurious `capability.unrealized` per capability an author had documented\n * carefully enough to join at requirement level. The deduplication is what\n * lets a requirement carry both lines, which is the normal shape: the theme\n * and the promise are different claims.\n */\n realizedBy: CapabilityRealization[];\n /** Distinct realizing services, sorted. */\n services: string[];\n /**\n * Realizing requirements counted by their service's frontmatter status\n * (`unset` for a service nobody has marked) — the draft/verified split the\n * roadmap asks the total to answer. Counted over the DEDUPLICATED set above,\n * so a requirement writing both joins is one requirement.\n */\n statuses: Record<string, number>;\n /**\n * The capability document's own requirements, each with what realizes it.\n * Present only for a capability that HAS a document — absent and empty are\n * different answers, and only the second means \"the document declares none\".\n */\n requirements?: CapabilityRequirementRow[];\n}\n\nexport interface CapabilityRollupInput {\n services: ServiceEntry[];\n vocab: CapabilityVocabulary;\n /** The requirement reader — pass a FleetContext's readRequirements, bound. */\n read: (path: string) => Promise<Requirement[]>;\n /**\n * `<capability>#<Requirement-ID>` → the ids of the flows that keep it, from\n * `core/usecases`' `useCaseRequirementClaims`. Injected as PLAIN DATA, exactly\n * as `read` is, and for a harder reason than the memo: `core/usecases` imports\n * this package, so an import back would be a package cycle and\n * `npm run arch:check` refuses it. What travels is a Map of strings, which\n * carries no dependency in either direction.\n *\n * OMIT IT to answer \"nobody looked\": every `keptBy` above is then absent\n * rather than empty, which is what keeps a caller that never asked about\n * flows — `explore`, the context pack, `validate`'s own rollup — from\n * publishing a `[]` that reads as \"no flow keeps this\". A caller that DID look\n * and found nothing passes an empty Map; a caller that looked and could not\n * see must omit it and say so in its own words (`loam list` prints the note\n * and carries `useCases.unreadable`).\n */\n keptByFlows?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * Walk every service's spec.md and arch.spec.md (existsSync-guarded, exactly\n * like permissionFindings: an absent OPTIONAL artifact must never take a whole\n * `--all` run down) and join non-REMOVED requirements' `Capability:` entries to\n * the declared vocabulary. Rows exist for every DECLARED id — an unrealized\n * declaration is a row with empty realizedBy, which is what the unrealized\n * grade and the list's `0 — unrealized` marker both read.\n */\nexport async function capabilityRollup(input: CapabilityRollupInput): Promise<CapabilityRow[]> {\n const { services, vocab, read, keptByFlows } = input;\n // Rows exist only for DECLARED ids, so an empty vocabulary (absent file, or\n // present with nothing declared) can build nothing — walking every service's\n // two spec files to produce zero rows is what a vocabless 120-service fleet\n // would otherwise pay on every `list`/`explore` that asks.\n if (vocab.byId.size === 0) return [];\n const rows = new Map<string, CapabilityRow>();\n for (const id of [...vocab.byId.keys()].sort(compareIds)) {\n const decl = vocab.byId.get(id)!;\n rows.set(id, {\n id,\n ...(decl.description !== undefined ? { description: decl.description } : {}),\n ...(decl.owner !== undefined ? { owner: decl.owner } : {}),\n realizedBy: [],\n services: [],\n statuses: {},\n });\n }\n // The documents, before the services, because a `Realizes:` entry can only\n // land on a requirement row that already exists.\n for (const doc of vocab.tree.docs) {\n const row = rows.get(doc.id);\n if (row === undefined) continue; // unreachable: the tree is one half of byId\n row.requirements = capabilityRequirementRows(await read(doc.spec));\n }\n\n // Realizations are collected as a SET per row before they become a list, so\n // the two joins cannot count one requirement twice — see `CapabilityRow.realizedBy`.\n const claimed = new Map<CapabilityRow, Map<string, CapabilityRealization>>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n for (const r of await read(path)) {\n if (r.kind === \"REMOVED\") continue;\n const where: CapabilityRealization = { service: entry.id, file: axis.file, requirement: r.name };\n for (const capability of r.capabilities) claim(rows, claimed, capability, where);\n for (const target of r.realizes.map(splitRealizesEntry)) {\n if (target === null) continue; // malformed — capability.realizes-unknown's business\n claim(rows, claimed, target.capability, where);\n // The requirement-level half. An entry naming an id the document does\n // not declare lands nowhere, which is exactly right: it is already\n // `capability.realizes-unknown`, and inventing a row for it here would\n // make a typo look like a promise somebody wrote.\n rows\n .get(target.capability)\n ?.requirements?.find((req) => req.id === target.requirement)\n ?.realizedBy.push(where);\n }\n }\n }\n }\n\n for (const row of rows.values()) {\n row.realizedBy = [...(claimed.get(row)?.values() ?? [])].sort(compareRealizations);\n row.services = [...new Set(row.realizedBy.map((r) => r.service))].sort(compareIds);\n const byStatus: Record<string, number> = {};\n for (const r of row.realizedBy) {\n const status = statusOf(services, r.service);\n byStatus[status] = (byStatus[status] ?? 0) + 1;\n }\n row.statuses = Object.fromEntries(Object.entries(byStatus).sort(([a], [b]) => compareIds(a, b)));\n for (const req of row.requirements ?? []) {\n req.realizedBy.sort(compareRealizations);\n // Copied out of the injected map rather than aliased into the row: the row\n // is a mutable value a caller may sort or splice, and handing it the\n // caller's own array would let one consumer's edit rewrite the answer the\n // next one reads. Already sorted upstream — copied, never re-sorted, so\n // there is exactly one statement of the order.\n if (keptByFlows !== undefined) req.keptBy = [...(keptByFlows.get(`${row.id}#${req.id}`) ?? [])];\n }\n }\n return [...rows.values()];\n}\n\n/** Deterministic realization order — the diff-stable rule this module states for its rows. */\nfunction compareRealizations(a: CapabilityRealization, b: CapabilityRealization): number {\n return (\n compareIds(a.service, b.service) ||\n a.file.localeCompare(b.file) ||\n compareIds(a.requirement, b.requirement)\n );\n}\n\n/** The service's declared frontmatter status, or `unset` for one nobody has marked. */\nfunction statusOf(services: ServiceEntry[], id: string): string {\n return services.find((s) => s.id === id)?.status ?? \"unset\";\n}\n\n/** Record one realization of one capability, deduplicated on where it came from. */\nfunction claim(\n rows: Map<string, CapabilityRow>,\n claimed: Map<CapabilityRow, Map<string, CapabilityRealization>>,\n capability: string,\n where: CapabilityRealization,\n): void {\n const row = rows.get(capability);\n if (row === undefined) return; // undeclared — capability.unknown's business, not the rollup's\n const seen = claimed.get(row) ?? new Map<string, CapabilityRealization>();\n seen.set(`${where.service}\\0${where.file}\\0${where.requirement}`, where);\n claimed.set(row, seen);\n}\n\n/**\n * A capability document's requirements as rows, keyed by their stable id.\n *\n * A requirement WITHOUT a `Requirement-ID:` is dropped rather than keyed by its\n * heading, and the drop is the honest answer rather than a shortcut: nothing\n * can address it — `Realizes:` names an id — so a row for it would be a promise\n * reported as unrealized that no author could ever mark realized.\n * `capability.requirement-unidentified` is the finding that names it, and it is\n * an ERROR, so the state does not persist quietly.\n *\n * A duplicate id keeps the FIRST, matching `requirementIdProblems`' own\n * `duplicate` grade: the document already earns an error for it, and picking a\n * winner here is only about not producing two identical rows.\n */\nexport function capabilityRequirementRows(reqs: Requirement[]): CapabilityRequirementRow[] {\n const rows = new Map<string, CapabilityRequirementRow>();\n for (const r of reqs) {\n if (r.kind === \"REMOVED\" || r.id === undefined || rows.has(r.id)) continue;\n rows.set(r.id, { id: r.id, name: r.name, realizedBy: [] });\n }\n return [...rows.values()].sort((a, b) => compareIds(a.id, b.id));\n}\n\n/** The declared ids something realizes — the set the unrealized grade is taken against. */\nexport function usedCapabilities(rows: CapabilityRow[]): Set<string> {\n return new Set(rows.filter(isRealized).map((row) => row.id));\n}\n\n/**\n * Does anything in the fleet realize this capability — by EITHER corpus?\n *\n * A flow keeping one of a capability's promises realizes part of that\n * capability as surely as a `Capability:` line does, and the case is not\n * hypothetical: a capability whose promises are all cross-service can be kept\n * perfectly by `#cap-`/`#req-` tagged flows and named by no service requirement\n * at all. Counting only the service corpus told such a fleet its capability was\n * \"a promise nobody implemented or a word nobody adopted\" while the fleet's own\n * use cases said otherwise — one warning, contradicted by loam's own answer two\n * lines further down the same report.\n *\n * ASYMMETRIC WITH `capability.requirement-unrealized`, on purpose. That grade\n * suspends entirely when the flows cannot be read, because a promise crossing\n * services often has NO possible realizer but a flow — unreadable flows remove\n * the only evidence there was. A capability has independent primary evidence in\n * the `Capability:`/`Realizes:` lines, so unreadable flows leave it answering\n * from less rather than from nothing, and going silent would lose the warning\n * for a fleet that has a vocabulary and no fleet map at all — a real and legal\n * shape, and one where the whole point of the code is to be heard.\n */\nfunction isRealized(row: CapabilityRow): boolean {\n if (row.realizedBy.length > 0) return true;\n return (row.requirements ?? []).some((req) => (req.keptBy?.length ?? 0) > 0);\n}\n\n/**\n * The requested ids the rollup cannot seed from: undeclared, or declared but\n * realized by nothing. One list on purpose — explore's contract is 'every miss\n * is a field', and splitting the two miss kinds waits for a consumer to ask.\n */\nexport function unresolvedCapabilities(rows: CapabilityRow[], requested: string[]): string[] {\n const realized = usedCapabilities(rows);\n return requested.filter((id) => !realized.has(id));\n}\n"]}
1
+ {"version":3,"file":"rollup.js","sourceRoot":"","sources":["../../../src/core/capabilities/rollup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The fleet total over the capability join, built once and reused by the fleet\n * grade, `loam list capabilities` and `loam explore --capability` — one walk,\n * so the unrealized warning and the rollup a reader diffs can never disagree\n * about what \"realized\" means.\n *\n * Deterministic and diff-stable by construction: rows sorted with compareIds,\n * realizedBy sorted by (service, file, requirement), statuses' keys sorted —\n * no timestamps, no absolute paths, no readdir-order dependence. The reader is\n * INJECTED (`read`) rather than a FleetContext, because fleet-context.ts\n * imports this package for its memo and an import back would be a package\n * cycle (see the memo's comment); callers pass `fleet.readRequirements` bound,\n * so under `validate --all` every parse is already cached from the service\n * targets and the rollup costs no additional per-service read.\n *\n * TWO CORPORA MEET HERE, and neither is derived from the other. A promise is\n * kept by a service requirement's `Realizes:` line (walked below) or by a\n * `#cap-`/`#req-` tagged flow (injected as `keptByFlows`, because the module\n * that computes it imports this one). The rollup carries both on one row for the\n * reason it carries both `Capability:` joins on one row: `loam list capabilities`\n * and `capability.requirement-unrealized` must not be able to give different\n * answers to \"is this business promise kept\", and they can only give one answer\n * if they read one row.\n */\nimport { existsSync } from \"node:fs\";\nimport type { Requirement } from \"../document/spec.js\";\nimport { compareIds, type ServiceEntry } from \"../repo/entries.js\";\nimport { SPEC_AXES, servicePathsAt } from \"../repo/paths.js\";\nimport type { CapabilityVocabulary } from \"./capabilities.js\";\nimport { splitRealizesEntry } from \"./realizes/join.js\";\n\n/** One requirement realizing one capability, located exactly. */\nexport interface CapabilityRealization {\n service: string;\n file: \"spec.md\" | \"arch.spec.md\";\n /** The requirement's `### Requirement:` name. */\n requirement: string;\n}\n\n/** One requirement OF a capability document, and what realizes it. */\nexport interface CapabilityRequirementRow {\n /** Its `Requirement-ID:`. A requirement without one is absent here — see `capabilityRequirementRows`. */\n id: string;\n /** Its `### Requirement:` heading. */\n name: string;\n /** Service requirements whose `Realizes:` line names `<capability>#<id>`. */\n realizedBy: CapabilityRealization[];\n /**\n * The ids of the declared flows that keep this promise — a `#cap-`/`#req-`\n * tagged `dynamic view` whose BOTH tags resolve. THE SECOND CORPUS, and the\n * only carrier a cross-service promise has: \"I enter a login and a password\n * and I am in\" belongs to no single service's spec, so a fleet can keep it\n * perfectly through a flow and realize it by no `Realizes:` line at all.\n *\n * Three states, all of them different answers, exactly as `requirements`\n * itself has three:\n * present and non-empty — these flows keep it;\n * present and empty — loam read the flows and none of them keeps it;\n * ABSENT — nobody looked. Either the caller did not ask\n * (`CapabilityRollupInput.keptByFlows` omitted) or\n * it asked and `architecture/` could not be read.\n * The third must never collapse into the second. \"Kept by no flow\" over a\n * project that did not parse is the vacuously-green claim the whole use-case\n * axis exists to refuse, and it is the claim a `[]` default would make.\n */\n keptBy?: string[];\n}\n\n/** One declared capability with everything the fleet says about it. */\nexport interface CapabilityRow {\n id: string;\n description?: string;\n owner?: string;\n /**\n * Service requirements realizing this capability BY EITHER JOIN, deduplicated\n * on (service, file, requirement).\n *\n * Both joins land here on purpose. `Realizes: checkout#CHK-1` realizes part of\n * `checkout` as surely as `Capability: checkout` does, and a rollup that\n * counted only the second would report a capability as realized by nobody\n * while the fleet pointed at its individual promises — which would then be\n * one spurious `capability.unrealized` per capability an author had documented\n * carefully enough to join at requirement level. The deduplication is what\n * lets a requirement carry both lines, which is the normal shape: the theme\n * and the promise are different claims.\n */\n realizedBy: CapabilityRealization[];\n /** Distinct realizing services, sorted. */\n services: string[];\n /**\n * Realizing requirements counted by their service's frontmatter status\n * (`unset` for a service nobody has marked) — the draft/verified split the\n * roadmap asks the total to answer. Counted over the DEDUPLICATED set above,\n * so a requirement writing both joins is one requirement.\n */\n statuses: Record<string, number>;\n /**\n * The capability document's own requirements, each with what realizes it.\n * Present only for a capability that HAS a document — absent and empty are\n * different answers, and only the second means \"the document declares none\".\n */\n requirements?: CapabilityRequirementRow[];\n}\n\nexport interface CapabilityRollupInput {\n services: ServiceEntry[];\n vocab: CapabilityVocabulary;\n /** The requirement reader — pass a FleetContext's readRequirements, bound. */\n read: (path: string) => Promise<Requirement[]>;\n /**\n * `<capability>#<Requirement-ID>` → the ids of the flows that keep it, from\n * `core/usecases`' `useCaseRequirementClaims`. Injected as PLAIN DATA, exactly\n * as `read` is, and for a harder reason than the memo: `core/usecases` imports\n * this package, so an import back would be a package cycle and\n * `npm run arch:check` refuses it. What travels is a Map of strings, which\n * carries no dependency in either direction.\n *\n * OMIT IT to answer \"nobody looked\": every `keptBy` above is then absent\n * rather than empty, which is what keeps a caller that never asked about\n * flows — `explore`, the context pack, `validate`'s own rollup — from\n * publishing a `[]` that reads as \"no flow keeps this\". A caller that DID look\n * and found nothing passes an empty Map; a caller that looked and could not\n * see must omit it and say so in its own words (`loam list` prints the note\n * and carries `useCases.unreadable`).\n */\n keptByFlows?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * Walk every service's spec.md and arch.spec.md (existsSync-guarded, exactly\n * like permissionFindings: an absent OPTIONAL artifact must never take a whole\n * `--all` run down) and join non-REMOVED requirements' `Capability:` entries to\n * the declared vocabulary. Rows exist for every DECLARED id — an unrealized\n * declaration is a row with empty realizedBy, which is what the unrealized\n * grade and the list's `0 — unrealized` marker both read.\n */\nexport async function capabilityRollup(input: CapabilityRollupInput): Promise<CapabilityRow[]> {\n const { services, vocab, read, keptByFlows } = input;\n // Rows exist only for DECLARED ids, so an empty vocabulary (absent file, or\n // present with nothing declared) can build nothing — walking every service's\n // two spec files to produce zero rows is what a vocabless 120-service fleet\n // would otherwise pay on every `list`/`explore` that asks.\n if (vocab.byId.size === 0) return [];\n const rows = new Map<string, CapabilityRow>();\n for (const id of [...vocab.byId.keys()].sort(compareIds)) {\n const decl = vocab.byId.get(id)!;\n rows.set(id, {\n id,\n ...(decl.description !== undefined ? { description: decl.description } : {}),\n ...(decl.owner !== undefined ? { owner: decl.owner } : {}),\n realizedBy: [],\n services: [],\n statuses: {},\n });\n }\n // The documents, before the services, because a `Realizes:` entry can only\n // land on a requirement row that already exists.\n for (const doc of vocab.tree.docs) {\n const row = rows.get(doc.id);\n if (row === undefined) continue; // unreachable: the tree is one half of byId\n row.requirements = capabilityRequirementRows(await read(doc.spec));\n }\n\n // Realizations are collected as a SET per row before they become a list, so\n // the two joins cannot count one requirement twice — see `CapabilityRow.realizedBy`.\n const claimed = new Map<CapabilityRow, Map<string, CapabilityRealization>>();\n for (const entry of services) {\n const paths = servicePathsAt(entry.dir);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n for (const r of await read(path)) {\n if (r.kind === \"REMOVED\") continue;\n const where: CapabilityRealization = { service: entry.id, file: axis.file, requirement: r.name };\n for (const capability of r.capabilities) claim(rows, claimed, capability, where);\n for (const target of r.realizes.map(splitRealizesEntry)) {\n if (target === null) continue; // malformed — capability.realizes-unknown's business\n // The capability is claimed BEFORE asking whether the requirement id\n // resolves, and that is deliberate: an entry naming an undeclared id\n // already earns `capability.realizes-unknown`, and letting it also\n // leave its capability `capability.unrealized` would be two findings\n // for one mistake — the second pointing at the analyst's document\n // rather than at the typo.\n claim(rows, claimed, target.capability, where);\n // The requirement-level half. An entry naming an id the document does\n // not declare lands nowhere, which is exactly right: it is already\n // `capability.realizes-unknown`, and inventing a row for it here would\n // make a typo look like a promise somebody wrote.\n rows\n .get(target.capability)\n ?.requirements?.find((req) => req.id === target.requirement)\n ?.realizedBy.push(where);\n }\n }\n }\n }\n\n for (const row of rows.values()) {\n row.realizedBy = [...(claimed.get(row)?.values() ?? [])].sort(compareRealizations);\n row.services = [...new Set(row.realizedBy.map((r) => r.service))].sort(compareIds);\n const byStatus: Record<string, number> = {};\n for (const r of row.realizedBy) {\n const status = statusOf(services, r.service);\n byStatus[status] = (byStatus[status] ?? 0) + 1;\n }\n row.statuses = Object.fromEntries(Object.entries(byStatus).sort(([a], [b]) => compareIds(a, b)));\n for (const req of row.requirements ?? []) {\n req.realizedBy.sort(compareRealizations);\n // Copied out of the injected map rather than aliased into the row: the row\n // is a mutable value a caller may sort or splice, and handing it the\n // caller's own array would let one consumer's edit rewrite the answer the\n // next one reads. Already sorted upstream — copied, never re-sorted, so\n // there is exactly one statement of the order.\n if (keptByFlows !== undefined) req.keptBy = [...(keptByFlows.get(`${row.id}#${req.id}`) ?? [])];\n }\n }\n return [...rows.values()];\n}\n\n/** Deterministic realization order — the diff-stable rule this module states for its rows. */\nfunction compareRealizations(a: CapabilityRealization, b: CapabilityRealization): number {\n return (\n compareIds(a.service, b.service) ||\n a.file.localeCompare(b.file) ||\n compareIds(a.requirement, b.requirement)\n );\n}\n\n/** The service's declared frontmatter status, or `unset` for one nobody has marked. */\nfunction statusOf(services: ServiceEntry[], id: string): string {\n return services.find((s) => s.id === id)?.status ?? \"unset\";\n}\n\n/** Record one realization of one capability, deduplicated on where it came from. */\nfunction claim(\n rows: Map<string, CapabilityRow>,\n claimed: Map<CapabilityRow, Map<string, CapabilityRealization>>,\n capability: string,\n where: CapabilityRealization,\n): void {\n const row = rows.get(capability);\n if (row === undefined) return; // undeclared — capability.unknown's business, not the rollup's\n const seen = claimed.get(row) ?? new Map<string, CapabilityRealization>();\n seen.set(`${where.service}\\0${where.file}\\0${where.requirement}`, where);\n claimed.set(row, seen);\n}\n\n/**\n * A capability document's requirements as rows, keyed by their stable id.\n *\n * A requirement WITHOUT a `Requirement-ID:` is dropped rather than keyed by its\n * heading, and the drop is the honest answer rather than a shortcut: nothing\n * can address it — `Realizes:` names an id — so a row for it would be a promise\n * reported as unrealized that no author could ever mark realized.\n * `capability.requirement-unidentified` is the finding that names it, and it is\n * an ERROR, so the state does not persist quietly.\n *\n * A duplicate id keeps the FIRST, matching `requirementIdProblems`' own\n * `duplicate` grade: the document already earns an error for it, and picking a\n * winner here is only about not producing two identical rows.\n */\nexport function capabilityRequirementRows(reqs: Requirement[]): CapabilityRequirementRow[] {\n const rows = new Map<string, CapabilityRequirementRow>();\n for (const r of reqs) {\n if (r.kind === \"REMOVED\" || r.id === undefined || rows.has(r.id)) continue;\n rows.set(r.id, { id: r.id, name: r.name, realizedBy: [] });\n }\n return [...rows.values()].sort((a, b) => compareIds(a.id, b.id));\n}\n\n/** The declared ids something realizes — the set the unrealized grade is taken against. */\nexport function usedCapabilities(rows: CapabilityRow[]): Set<string> {\n return new Set(rows.filter(isRealized).map((row) => row.id));\n}\n\n/**\n * Does anything in the fleet realize this capability — by EITHER corpus?\n *\n * A flow keeping one of a capability's promises realizes part of that\n * capability as surely as a `Capability:` line does, and the case is not\n * hypothetical: a capability whose promises are all cross-service can be kept\n * perfectly by `#cap-`/`#req-` tagged flows and named by no service requirement\n * at all. Counting only the service corpus told such a fleet its capability was\n * \"a promise nobody implemented or a word nobody adopted\" while the fleet's own\n * use cases said otherwise — one warning, contradicted by loam's own answer two\n * lines further down the same report.\n *\n * ASYMMETRIC WITH `capability.requirement-unrealized`, on purpose. That grade\n * suspends entirely when the flows cannot be read, because a promise crossing\n * services often has NO possible realizer but a flow — unreadable flows remove\n * the only evidence there was. A capability has independent primary evidence in\n * the `Capability:`/`Realizes:` lines, so unreadable flows leave it answering\n * from less rather than from nothing, and going silent would lose the warning\n * for a fleet that has a vocabulary and no fleet map at all — a real and legal\n * shape, and one where the whole point of the code is to be heard.\n */\nfunction isRealized(row: CapabilityRow): boolean {\n if (row.realizedBy.length > 0) return true;\n return (row.requirements ?? []).some((req) => (req.keptBy?.length ?? 0) > 0);\n}\n\n/**\n * The requested ids the rollup cannot seed from: undeclared, or declared but\n * realized by nothing. One list on purpose — explore's contract is 'every miss\n * is a field', and splitting the two miss kinds waits for a consumer to ask.\n */\nexport function unresolvedCapabilities(rows: CapabilityRow[], requested: string[]): string[] {\n const realized = usedCapabilities(rows);\n return requested.filter((id) => !realized.has(id));\n}\n"]}
@@ -33,12 +33,12 @@
33
33
  * the reason `./rollup.ts` states: this package is imported by
34
34
  * `core/fleet-context.ts` and may never import it back.
35
35
  */
36
- import { existsSync, realpathSync } from "node:fs";
36
+ import { existsSync } from "node:fs";
37
37
  import { readdir } from "node:fs/promises";
38
38
  import { join } from "node:path";
39
39
  import { compareIds } from "../repo/entries.js";
40
40
  import { capabilityDocPathsAt } from "../repo/authored/paths.js";
41
- import { entryIs } from "../repo/tree/fs.js";
41
+ import { entryIs, markDescended, subdirNames } from "../repo/tree/fs.js";
42
42
  /**
43
43
  * Read the tree under `root` (`capabilityDocsDir(docsDir)`).
44
44
  *
@@ -54,7 +54,9 @@ export async function readCapabilityTree(root) {
54
54
  if (!tree.present)
55
55
  return tree;
56
56
  const state = { tree, visited: new Set() };
57
- if (descended(root, state)) {
57
+ // The same guard `core/repo/tree/walk.ts` carries (`markDescended`), for
58
+ // the same reason: `entryIs` follows symlinks on purpose.
59
+ if (markDescended(root, state.visited)) {
58
60
  for (const name of await subdirNames(root)) {
59
61
  await visit({ dir: join(root, name), chain: [name] }, state);
60
62
  }
@@ -63,30 +65,6 @@ export async function readCapabilityTree(root) {
63
65
  tree.undocumented.sort(compareIds);
64
66
  return tree;
65
67
  }
66
- /**
67
- * Mark a directory as descended-into; false when it (or another name for it)
68
- * already was. The same guard `core/repo/tree/walk.ts` carries and for the same
69
- * reason: `entryIs` follows symlinks on purpose, and a followed cycle without a
70
- * visited set recurses until the stack ends.
71
- */
72
- function descended(dir, state) {
73
- let real;
74
- try {
75
- real = realpathSync(dir);
76
- }
77
- catch {
78
- return false;
79
- }
80
- if (state.visited.has(real))
81
- return false;
82
- state.visited.add(real);
83
- return true;
84
- }
85
- /** Sorted non-dot subdirectory names — the walk's deterministic child order. */
86
- async function subdirNames(dir) {
87
- const entries = await readdir(dir, { withFileTypes: true });
88
- return entries.filter((e) => !e.name.startsWith(".") && entryIs(dir, e, "dir")).map((e) => e.name).sort();
89
- }
90
68
  /**
91
69
  * Classify one directory and walk what is inside it. Returns whether a
92
70
  * capability was found at or beneath it — the only thing a caller needs, since
@@ -105,7 +83,7 @@ async function visit(spot, state) {
105
83
  if (documented)
106
84
  state.tree.docs.push({ id: spot.chain.join("/"), dir: spot.dir, spec: paths.spec });
107
85
  let beneath = false;
108
- if (descended(spot.dir, state)) {
86
+ if (markDescended(spot.dir, state.visited)) {
109
87
  const dirs = entries.filter((e) => entryIs(spot.dir, e, "dir")).map((e) => e.name).sort();
110
88
  for (const child of dirs) {
111
89
  // Sequential, not Promise.all: the visited set is shared mutable state and
@@ -1 +1 @@
1
- {"version":3,"file":"tree.js","sourceRoot":"","sources":["../../../src/core/capabilities/tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The capability tree: `capabilities/<id>/spec.md`, and the rule that a\n * directory holding one IS a declared capability.\n *\n * WHY A TREE AND NOT A SECOND LIST IN YAML. `architecture/capabilities.yaml`\n * declares names; it cannot carry prose, and a capability a business analyst\n * actually wrote is mostly prose. The alternative — a single vocabulary file\n * grown to hold paragraphs — is the exact drift `loam init`'s removed\n * `loam.docs.json` was removed for: a second list nothing keeps current. So the\n * DIRECTORY is the list, and the general rule it settles is that an entry with\n * prose gets a file while an entry without prose stays a line in YAML\n * (`architecture/permissions.yaml` is the principled exception — a\n * `user/profile:read` pair has no prose, and a document per pair is ceremony).\n *\n * THE VOCABULARY IS THE UNION of this tree and the YAML, and the YAML is not\n * deprecated by it. A fleet that declared twenty names and has written four\n * documents is the normal state of an adoption, and a tree that refused to\n * grade the other sixteen would make adopting the axis a cliff.\n * `./capabilities.ts` merges the two and records which side declared each id.\n *\n * PRESENCE CLASSIFIES, one tree over from `isServiceArtifactName`: a directory\n * holding the document is a capability, and one holding only other directories\n * is a group the walk descends through. Both at once is legal and normal —\n * `payments` may be a capability with `payments/refunds` nested inside it —\n * because a capability id spells its own nesting wherever it is written and the\n * tree only mirrors that. What is NOT legal is a directory that is neither:\n * `capability.doc-missing` names it, rather than letting a half-created\n * capability sit in the tree looking declared.\n *\n * The walk reads directories only. It never parses a document — the requirement\n * reader is injected by whoever grades (`./findings.ts` takes parsed\n * requirements, `commands/validate/` supplies the fleet's cached reader), for\n * the reason `./rollup.ts` states: this package is imported by\n * `core/fleet-context.ts` and may never import it back.\n */\nimport { existsSync, realpathSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { compareIds } from \"../repo/entries.js\";\nimport { capabilityDocPathsAt } from \"../repo/authored/paths.js\";\nimport { entryIs } from \"../repo/tree/fs.js\";\n\n/** One authored capability: where its directory is, and where its document is. */\nexport interface CapabilityDoc {\n /** The id — the directory names between `capabilities/` and this one, joined with `/`. */\n id: string;\n dir: string;\n /** `<dir>/spec.md`. Its existence is what made this a capability. */\n spec: string;\n}\n\nexport interface CapabilityTree {\n /**\n * False when `capabilities/` does not exist. The directory's existence is\n * this half of the axis's opt-in, so a fleet without one produces nothing\n * here — not an empty tree that then reads as \"the capabilities went\n * missing\" to anything counting.\n */\n present: boolean;\n /** Every capability found, at any depth, ordered by id. */\n docs: CapabilityDoc[];\n /**\n * Repo-relative paths of directories under `capabilities/` that hold no\n * document and have no capability beneath them — the half-created capability\n * `capability.doc-missing` names. Ordered, for a diff-stable finding list.\n */\n undocumented: string[];\n}\n\n/** One directory the walk is looking at: where it is, and how it was reached. */\ninterface Spot {\n dir: string;\n /** Directory names between `capabilities/` and this one, outermost first, including it. */\n chain: string[];\n}\n\ninterface WalkState {\n tree: CapabilityTree;\n /** Realpaths already DESCENDED into — the symlink-cycle guard. */\n visited: Set<string>;\n}\n\n/**\n * Read the tree under `root` (`capabilityDocsDir(docsDir)`).\n *\n * Cost is one readdir per directory, capability directories included: their own\n * listing is what classifies them, and it is the same listing that finds the\n * capabilities nested inside. A fleet is expected to hold tens of these\n * documents rather than hundreds — the corpus is sized like the landscape, not\n * like the service tree — which is what keeps a second requirement corpus\n * reviewable at all, and what makes the walk's cost a non-question.\n */\nexport async function readCapabilityTree(root: string): Promise<CapabilityTree> {\n const tree: CapabilityTree = { present: existsSync(root), docs: [], undocumented: [] };\n if (!tree.present) return tree;\n const state: WalkState = { tree, visited: new Set() };\n if (descended(root, state)) {\n for (const name of await subdirNames(root)) {\n await visit({ dir: join(root, name), chain: [name] }, state);\n }\n }\n tree.docs.sort((a, b) => compareIds(a.id, b.id));\n tree.undocumented.sort(compareIds);\n return tree;\n}\n\n/**\n * Mark a directory as descended-into; false when it (or another name for it)\n * already was. The same guard `core/repo/tree/walk.ts` carries and for the same\n * reason: `entryIs` follows symlinks on purpose, and a followed cycle without a\n * visited set recurses until the stack ends.\n */\nfunction descended(dir: string, state: WalkState): boolean {\n let real: string;\n try {\n real = realpathSync(dir);\n } catch {\n return false;\n }\n if (state.visited.has(real)) return false;\n state.visited.add(real);\n return true;\n}\n\n/** Sorted non-dot subdirectory names — the walk's deterministic child order. */\nasync function subdirNames(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => !e.name.startsWith(\".\") && entryIs(dir, e, \"dir\")).map((e) => e.name).sort();\n}\n\n/**\n * Classify one directory and walk what is inside it. Returns whether a\n * capability was found at or beneath it — the only thing a caller needs, since\n * a group directory earns no finding for being a group.\n *\n * A directory reached a SECOND time through a symlink answers `true` rather\n * than `false`, and the distinction is a quiet enumeration against a spurious\n * finding: it was already walked under its first name, so everything in it is\n * already recorded, and reporting it undocumented would name a directory whose\n * document loam has in fact already read.\n */\nasync function visit(spot: Spot, state: WalkState): Promise<boolean> {\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n const paths = capabilityDocPathsAt(spot.dir);\n const documented = entries.some((e) => join(spot.dir, e.name) === paths.spec && entryIs(spot.dir, e, \"file\"));\n if (documented) state.tree.docs.push({ id: spot.chain.join(\"/\"), dir: spot.dir, spec: paths.spec });\n\n let beneath = false;\n if (descended(spot.dir, state)) {\n const dirs = entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort();\n for (const child of dirs) {\n // Sequential, not Promise.all: the visited set is shared mutable state and\n // the walk's correctness under symlinks rests on a child being marked\n // before its sibling asks. These trees are tens of directories at most, so\n // there is nothing to win by racing them.\n if (await visit({ dir: join(spot.dir, child), chain: [...spot.chain, child] }, state)) beneath = true;\n }\n } else {\n beneath = true;\n }\n\n if (!documented && !beneath) state.tree.undocumented.push([\"capabilities\", ...spot.chain].join(\"/\"));\n return documented || beneath;\n}\n"]}
1
+ {"version":3,"file":"tree.js","sourceRoot":"","sources":["../../../src/core/capabilities/tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The capability tree: `capabilities/<id>/spec.md`, and the rule that a\n * directory holding one IS a declared capability.\n *\n * WHY A TREE AND NOT A SECOND LIST IN YAML. `architecture/capabilities.yaml`\n * declares names; it cannot carry prose, and a capability a business analyst\n * actually wrote is mostly prose. The alternative — a single vocabulary file\n * grown to hold paragraphs — is the exact drift `loam init`'s removed\n * `loam.docs.json` was removed for: a second list nothing keeps current. So the\n * DIRECTORY is the list, and the general rule it settles is that an entry with\n * prose gets a file while an entry without prose stays a line in YAML\n * (`architecture/permissions.yaml` is the principled exception — a\n * `user/profile:read` pair has no prose, and a document per pair is ceremony).\n *\n * THE VOCABULARY IS THE UNION of this tree and the YAML, and the YAML is not\n * deprecated by it. A fleet that declared twenty names and has written four\n * documents is the normal state of an adoption, and a tree that refused to\n * grade the other sixteen would make adopting the axis a cliff.\n * `./capabilities.ts` merges the two and records which side declared each id.\n *\n * PRESENCE CLASSIFIES, one tree over from `isServiceArtifactName`: a directory\n * holding the document is a capability, and one holding only other directories\n * is a group the walk descends through. Both at once is legal and normal —\n * `payments` may be a capability with `payments/refunds` nested inside it —\n * because a capability id spells its own nesting wherever it is written and the\n * tree only mirrors that. What is NOT legal is a directory that is neither:\n * `capability.doc-missing` names it, rather than letting a half-created\n * capability sit in the tree looking declared.\n *\n * The walk reads directories only. It never parses a document — the requirement\n * reader is injected by whoever grades (`./findings.ts` takes parsed\n * requirements, `commands/validate/` supplies the fleet's cached reader), for\n * the reason `./rollup.ts` states: this package is imported by\n * `core/fleet-context.ts` and may never import it back.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { compareIds } from \"../repo/entries.js\";\nimport { capabilityDocPathsAt } from \"../repo/authored/paths.js\";\nimport { entryIs, markDescended, subdirNames } from \"../repo/tree/fs.js\";\n\n/** One authored capability: where its directory is, and where its document is. */\nexport interface CapabilityDoc {\n /** The id — the directory names between `capabilities/` and this one, joined with `/`. */\n id: string;\n dir: string;\n /** `<dir>/spec.md`. Its existence is what made this a capability. */\n spec: string;\n}\n\nexport interface CapabilityTree {\n /**\n * False when `capabilities/` does not exist. The directory's existence is\n * this half of the axis's opt-in, so a fleet without one produces nothing\n * here — not an empty tree that then reads as \"the capabilities went\n * missing\" to anything counting.\n */\n present: boolean;\n /** Every capability found, at any depth, ordered by id. */\n docs: CapabilityDoc[];\n /**\n * Repo-relative paths of directories under `capabilities/` that hold no\n * document and have no capability beneath them — the half-created capability\n * `capability.doc-missing` names. Ordered, for a diff-stable finding list.\n */\n undocumented: string[];\n}\n\n/** One directory the walk is looking at: where it is, and how it was reached. */\ninterface Spot {\n dir: string;\n /** Directory names between `capabilities/` and this one, outermost first, including it. */\n chain: string[];\n}\n\ninterface WalkState {\n tree: CapabilityTree;\n /** Realpaths already DESCENDED into — the symlink-cycle guard. */\n visited: Set<string>;\n}\n\n/**\n * Read the tree under `root` (`capabilityDocsDir(docsDir)`).\n *\n * Cost is one readdir per directory, capability directories included: their own\n * listing is what classifies them, and it is the same listing that finds the\n * capabilities nested inside. A fleet is expected to hold tens of these\n * documents rather than hundreds — the corpus is sized like the landscape, not\n * like the service tree — which is what keeps a second requirement corpus\n * reviewable at all, and what makes the walk's cost a non-question.\n */\nexport async function readCapabilityTree(root: string): Promise<CapabilityTree> {\n const tree: CapabilityTree = { present: existsSync(root), docs: [], undocumented: [] };\n if (!tree.present) return tree;\n const state: WalkState = { tree, visited: new Set() };\n // The same guard `core/repo/tree/walk.ts` carries (`markDescended`), for\n // the same reason: `entryIs` follows symlinks on purpose.\n if (markDescended(root, state.visited)) {\n for (const name of await subdirNames(root)) {\n await visit({ dir: join(root, name), chain: [name] }, state);\n }\n }\n tree.docs.sort((a, b) => compareIds(a.id, b.id));\n tree.undocumented.sort(compareIds);\n return tree;\n}\n\n/**\n * Classify one directory and walk what is inside it. Returns whether a\n * capability was found at or beneath it — the only thing a caller needs, since\n * a group directory earns no finding for being a group.\n *\n * A directory reached a SECOND time through a symlink answers `true` rather\n * than `false`, and the distinction is a quiet enumeration against a spurious\n * finding: it was already walked under its first name, so everything in it is\n * already recorded, and reporting it undocumented would name a directory whose\n * document loam has in fact already read.\n */\nasync function visit(spot: Spot, state: WalkState): Promise<boolean> {\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n const paths = capabilityDocPathsAt(spot.dir);\n const documented = entries.some((e) => join(spot.dir, e.name) === paths.spec && entryIs(spot.dir, e, \"file\"));\n if (documented) state.tree.docs.push({ id: spot.chain.join(\"/\"), dir: spot.dir, spec: paths.spec });\n\n let beneath = false;\n if (markDescended(spot.dir, state.visited)) {\n const dirs = entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort();\n for (const child of dirs) {\n // Sequential, not Promise.all: the visited set is shared mutable state and\n // the walk's correctness under symlinks rests on a child being marked\n // before its sibling asks. These trees are tens of directories at most, so\n // there is nothing to win by racing them.\n if (await visit({ dir: join(spot.dir, child), chain: [...spot.chain, child] }, state)) beneath = true;\n }\n } else {\n beneath = true;\n }\n\n if (!documented && !beneath) state.tree.undocumented.push([\"capabilities\", ...spot.chain].join(\"/\"));\n return documented || beneath;\n}\n"]}
@@ -256,22 +256,12 @@ export function resolveRequirementTags(tags, scope, requirementsOf) {
256
256
  }
257
257
  /**
258
258
  * Close ids for a slug nothing in the document matches — suggested against the
259
- * SLUGS, then mapped back to the real ids, exactly as `closeCapabilityIds`
260
- * does and for its two reasons: comparing on slugs is what lets a typo reach an
259
+ * SLUGS, then mapped back to the real ids: `closeCapabilityIds` over this
260
+ * document's ids, for its two reasons: comparing on slugs is what lets a typo reach an
261
261
  * id whose awkward character sits where the typo does not, and mapping back is
262
262
  * what keeps the suggestion writable.
263
263
  */
264
264
  function closeRequirementIds(slug, ids) {
265
- const bySlug = new Map();
266
- for (const id of ids) {
267
- const key = tagSlug(id);
268
- const found = bySlug.get(key);
269
- if (found === undefined)
270
- bySlug.set(key, [id]);
271
- else
272
- found.push(id);
273
- }
274
- const near = closeIds(slug, [...bySlug.keys()]);
275
- return [...new Set(near.flatMap((s) => bySlug.get(s) ?? []))].sort(compareIds);
265
+ return closeCapabilityIds(slug, idsBySlug([...ids]));
276
266
  }
277
267
  //# sourceMappingURL=usecase-join.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"usecase-join.js","sourceRoot":"","sources":["../../../src/core/capabilities/usecase-join.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The joins from a use case's tags: back to a declared capability, and to one\n * of that capability's requirements.\n *\n * TWO TAGS, ONE CLAIM. `#cap-<slug>` says which capability a flow is about;\n * `#req-<slug>` says which of its named promises the flow satisfies. The second\n * is the architect's ANSWER to the analyst's requirement, and it is the only\n * carrier a cross-service criterion has: \"I enter a login and a password and I\n * am in\" cannot be promised by any single service's spec, because each promises\n * only its own part — but a flow can, because it IS the hop sequence. That is\n * why `Realizes:` on a service requirement is not enough on its own, and why\n * this join exists at all.\n *\n * THE SECOND TAG IS SCOPED BY THE FIRST. A `Requirement-ID` is unique only\n * inside its own document, so `#req-CHK-ONCE` means nothing until the view has\n * said which capability it is about. A view carrying `#req-` with no resolved\n * `#cap-`, or with two, is refused rather than guessed at — the same stance the\n * `many` arm takes one join down.\n *\n * A LikeC4 `dynamic view` opts in to being graded as a business use case by\n * carrying a tag `#cap-<slug>`, and this module is the whole of what both tags\n * mean. It is deliberately the narrowest layer that can mean it: no file is\n * read, no finding is emitted, and nothing is thrown — a pure function over ids\n * somebody else supplies. That last property is not tidiness. Its caller runs\n * inside `loam validate --all` over every document a fleet has, and the one\n * behaviour a check added to that run may never have is failing the run it was\n * added to.\n *\n * WHY A SLUG AT ALL. Neither id is spelled in characters a tag name accepts. A\n * capability id spells its nesting inside the id — `identity/tokens` and\n * `payments/settlement` both ship in loam's own\n * `examples/docs/architecture/capabilities.yaml`, and `./capabilities.ts` keeps\n * the key exactly as the requirement writes it, slashes preserved — while a\n * `Requirement-ID` may legally contain a `.`. A LikeC4 tag name accepts exactly\n * `[A-Za-z0-9_-]` (measured at the 1.59.2 pin, pinned in\n * `test/likec4-view-shape.test.ts`), so both tags carry their id with the rest\n * flattened and both joins run over the flattened form.\n *\n * WHY THE JOIN CAN ANSWER \"MANY\". The slug is NOT injective. `payments/refunds`\n * and `payments-refunds` are two distinct, legal, declarable ids that flatten to\n * one slug, so `#cap-payments-refunds` names both and nothing in the tag can say\n * which. loam refuses and names ambiguity rather than guessing, and here the\n * cost of guessing is unusually high: a silently-picked capability attributes a\n * whole business flow — every hop, every service, every rollup built over it —\n * to the wrong owner, and goes on doing so invisibly because the wrong answer\n * looks exactly like the right one. The \"many\" arm is why this returns a union\n * at all instead of `string | undefined`.\n *\n * WHAT THE CALLER STILL OWES. The vocabulary ladder in `./findings.ts` — an\n * absent or unreadable `architecture/capabilities.yaml` means total silence, not\n * a fleet full of unresolved tags — is applied BEFORE calling here. An empty\n * `declared` list at this boundary therefore means \"the fleet declares no\n * capabilities\", which is a real answer this module will happily grade; it does\n * not mean \"there is no vocabulary\", and this module cannot tell the two apart.\n */\nimport { closeIds } from \"../c4/arch.js\";\nimport { compareIds } from \"../repo/entries.js\";\n\n/**\n * The reserved tag prefix, matched EXACTLY — case included.\n *\n * A view tag that does not start with it is not a capability claim and is\n * invisible here: `#FEAT-101`, `#platform` and `#deprecated` are the author's\n * own vocabulary and grading them would turn every existing hand-drawn view in\n * a brownfield fleet red on upgrade. The match is case-sensitive because what\n * survives it is a slug that must round-trip to a `capabilities.yaml` key, and\n * those keys are case-sensitive YAML; accepting `#Cap-checkout` here would hand\n * the lookup a slug whose case loam had already stopped believing in.\n */\nexport const CAP_TAG_PREFIX = \"cap-\";\n\n/**\n * Every character a LikeC4 tag name accepts, at the 1.59.2 pin. A WHITELIST,\n * and that is the load-bearing choice: the complement is infinite, so a rule\n * written as \"flatten a slash\" is a rule that is right about one character and\n * silent about the rest.\n *\n * Measured, not assumed (`test/likec4-view-shape.test.ts` pins it): `.`, `/`,\n * `:`, a space, `+`, `@` and a non-ASCII letter each make the DECLARATION a\n * parse error — and the failure is worse than a refusal, because the parser\n * TRUNCATES the name at the offending character. `#x-a.b` comes back as\n * `[\"x-a\"]`. Nothing in loam reads a model with errors, so the truncation\n * cannot reach a join today; it is recorded because the day something does read\n * one, a truncated tag resolves to a different, possibly real, capability.\n */\nconst TAG_NAME_SAFE = /[^A-Za-z0-9_-]/g;\n\n/**\n * An id as its tag spells it: every character a tag name cannot carry flattened\n * to a hyphen.\n *\n * Used by BOTH tags this module resolves, which is why it is no longer called\n * `capabilitySlug`. A capability id spells its nesting with `/`\n * (`identity/tokens`) and a `Requirement-ID` may legally contain `.`\n * (`REQUIREMENT_ID_RE` allows it) — two different characters, one rule, because\n * an author who learns that a tag flattens the awkward character in one id\n * should not have to learn it again for the other.\n *\n * Deliberately lossy, and the `many` arms below carry the whole cost of that:\n * two ids can produce one slug and nothing here notices.\n */\nexport function tagSlug(id: string): string {\n return id.replace(TAG_NAME_SAFE, \"-\");\n}\n\n/** The tag a claim is about, and the slug left after the prefix. */\ninterface ClaimTag {\n /** The tag verbatim, as the view declares it — no `#`, prefix still on. */\n tag: string;\n /** The tag with its reserved prefix removed: the name the join actually ran on. */\n slug: string;\n}\n\n/**\n * What one `#cap-` tag joins to. Branch on `kind`; every arm carries the tag it\n * is about, so a message can name the tag, the slug and the answer without\n * re-deriving any of them from the view.\n */\nexport type CapabilityClaim =\n /** Exactly one declared id flattens to this tag. `id` is the real id, slashes intact. */\n | (ClaimTag & { kind: \"resolved\"; id: string })\n /**\n * No declared id flattens to it. `close` holds real capability ids — names the\n * author can paste into `capabilities.yaml` or into the tag — never the slugs\n * the comparison was actually made against, because a slug is loam's spelling\n * of a name and offering it as a suggestion would be loam inventing a\n * capability id that nobody declared.\n */\n | (ClaimTag & { kind: \"none\"; close: string[] })\n /** Two or more declared ids flatten to it. Every colliding id, sorted, so a message can name them all. */\n | (ClaimTag & { kind: \"many\"; ids: string[] });\n\n/**\n * Every declared id that produces each slug.\n *\n * A map rather than a scan per tag because the \"many\" arm needs the WHOLE\n * colliding set: a lookup that stopped at the first hit could still report\n * \"resolved\" for a slug two ids answer to, which is precisely the silent wrong\n * answer this module exists to refuse.\n */\nfunction idsBySlug(declared: readonly string[]): Map<string, string[]> {\n const bySlug = new Map<string, string[]>();\n for (const id of declared) {\n const slug = tagSlug(id);\n const ids = bySlug.get(slug);\n if (ids === undefined) bySlug.set(slug, [id]);\n else ids.push(id);\n }\n return bySlug;\n}\n\n/**\n * Close names for a slug nothing declares — suggested against the SLUGS, then\n * mapped back to the ids that produced them.\n *\n * Both halves are load-bearing. Comparing against slugs is what lets\n * `#cap-identiy-tokns` reach `identity/tokens` at all: `closeIds` matches on\n * substring and shared prefix, and the slash sits where the author's typo does\n * not. Mapping back is what keeps the suggestion writable — a hint naming\n * `identity-tokens` would send the author to look for a capability declared\n * under a name that appears nowhere in `capabilities.yaml`.\n *\n * A winning slug may map back to more than one id, and the expansion is not\n * re-capped: `closeIds` caps the SLUGS it offers at five, and dropping a real\n * declared id afterwards to keep an arithmetic promise nobody made would hide\n * the very collision the \"many\" arm is here to name.\n */\nfunction closeCapabilityIds(slug: string, bySlug: ReadonlyMap<string, string[]>): string[] {\n const near = closeIds(slug, [...bySlug.keys()]);\n // `?? []` rather than `!`: `closeIds` promises every name it offers is one it\n // was given, so a miss here would mean that promise broke — and the honest\n // answer to a broken invariant inside a hint is one fewer suggestion, not a\n // `TypeError` thrown out of a `validate --all` run over somebody's fleet.\n return [...new Set(near.flatMap((s) => bySlug.get(s) ?? []))].sort(compareIds);\n}\n\n/**\n * The capability claims a view's tags make: one claim per `#cap-` tag, each\n * answered on its own terms.\n *\n * A VIEW MAY CARRY MORE THAN ONE `#cap-` TAG, and the tags are never collapsed\n * into a single verdict for the view. Both halves of that are deliberate.\n *\n * A use case realizing two capabilities is a real thing — a checkout flow that\n * also issues the identity token realizes both, and there is no honest way to\n * make its author pick one — so a second tag must NOT be read the way the \"many\"\n * arm reads a second colliding id. Those two look alike and are opposites: in\n * \"many\" the author wrote one name and loam cannot tell which id it means, here\n * the author wrote two names and meant both.\n *\n * And a view whose tags disagree — one resolving, one naming nothing — is not a\n * view that resolved. Collapsing to the first success would swallow the broken\n * tag and grade the view as if the typo were not there; collapsing to the first\n * failure would lose the capability the view genuinely does realize. One tag,\n * one verdict, is the only shape that keeps both facts, and it leaves the caller\n * free to emit one finding per failing tag while still collecting every id the\n * view resolved.\n *\n * Claims come back SORTED BY TAG rather than in the order LikeC4 handed the tags\n * over. Nothing in loam has measured that LikeC4 preserves the author's tag\n * order, and an output ordered by an unmeasured upstream detail is one that\n * reorders under a version bump — the diff-stability rule `./rollup.ts` states\n * for its rows, applied to a smaller list. A repeated tag collapses to one\n * claim, for the reason one breach earns one finding.\n *\n * An empty result means the view makes no capability claim at all — either it\n * carries no tags or none of them are reserved — which is the opt-in, not a\n * failure to resolve.\n */\nexport function resolveCapabilityTags(\n tags: readonly string[],\n declared: readonly string[],\n): CapabilityClaim[] {\n const bySlug = idsBySlug(declared);\n const claimed = [...new Set(tags.filter((tag) => tag.startsWith(CAP_TAG_PREFIX)))].sort(compareIds);\n\n return claimed.map((tag) => {\n // A bare `#cap-` slugs to the empty string, and it stays a claim on purpose:\n // the prefix is the author's opt-in however little follows it, so dropping\n // the tag as unreserved would leave a view that asked to be graded silently\n // ungraded. What that empty slug then resolves to is the lookup's business\n // like any other — `none` in every real fleet, `resolved` in the one that\n // declared an empty id, and neither is decided here.\n const slug = tag.slice(CAP_TAG_PREFIX.length);\n // Destructured rather than indexed so the three arms are told apart by the\n // shape of the match itself — no bounds assertion, and no `undefined` check\n // separate from the \"nothing declared this\" answer it already is.\n const [first, ...rest] = bySlug.get(slug) ?? [];\n if (first === undefined) return { tag, slug, kind: \"none\" as const, close: closeCapabilityIds(slug, bySlug) };\n if (rest.length === 0) return { tag, slug, kind: \"resolved\" as const, id: first };\n return { tag, slug, kind: \"many\" as const, ids: [first, ...rest].sort(compareIds) };\n });\n}\n\n/**\n * The reserved tag prefix for the requirement claim, matched EXACTLY as\n * `CAP_TAG_PREFIX` is and for the same two reasons: a tag that does not start\n * with it is the author's own vocabulary and is invisible here, and the slug\n * that survives the match must round-trip to a case-sensitive `Requirement-ID:`.\n */\nexport const REQ_TAG_PREFIX = \"req-\";\n\n/**\n * What one `#req-` tag joins to, given the capabilities the same view resolved.\n *\n * Five arms because there are five different fixes, and they are deliberately\n * the same five `RealizesClaim` draws in `./realizes/join.ts`: the two carriers\n * of one claim must fail in the same vocabulary, or an author who learns the\n * `Realizes:` messages learns nothing about the tag.\n */\nexport type RequirementClaim =\n /** Exactly one requirement of the scoping capability flattens to this tag. */\n | (ClaimTag & { kind: \"resolved\"; capability: string; id: string })\n /**\n * The view resolved no capability, or more than one, so a `Requirement-ID`\n * has no document to be unique inside. `capabilities` is what it DID resolve\n * — empty, or the two-or-more that make the scope ambiguous.\n */\n | (ClaimTag & { kind: \"unscoped\"; capabilities: string[] })\n /** The scoping capability is declared but has no `capabilities/<id>/spec.md`, so it carries no requirements. */\n | (ClaimTag & { kind: \"undocumented\"; capability: string })\n /** Its document exists and declares no requirements yet. */\n | (ClaimTag & { kind: \"empty\"; capability: string })\n /**\n * Its document declares requirements, none flattening to this slug. `close`\n * holds real `Requirement-ID`s — never the slugs the comparison ran against,\n * because a slug is loam's spelling and offering it would send the author\n * looking for an id that appears nowhere in the document.\n */\n | (ClaimTag & { kind: \"none\"; capability: string; close: string[] })\n /** Two or more of its requirement ids flatten to this slug. Every colliding id, sorted. */\n | (ClaimTag & { kind: \"many\"; capability: string; ids: string[] });\n\n/**\n * Resolve every `#req-` tag a view carries against the capability it is about.\n *\n * `scope` is the capability ids the view's OWN `#cap-` tags resolved to —\n * `resolveCapabilityTags`' `resolved` arms, and nothing else. An unresolved\n * `#cap-` tag is already `usecase.capability-unresolved`, and letting it also\n * scope a requirement lookup would build a second finding on top of a name loam\n * has just said it cannot place.\n *\n * Claims come back SORTED BY TAG for `resolveCapabilityTags`' reason: nothing in\n * loam has measured that LikeC4 preserves the author's tag order, and an output\n * ordered by an unmeasured upstream detail reorders under a version bump. A\n * repeated tag collapses to one claim, for the reason one breach earns one\n * finding.\n *\n * SEVERAL `#req-` TAGS ARE LEGAL AND NORMAL. One flow commonly satisfies two of\n * a capability's promises — a checkout flow that charges once AND honours the\n * price — and there is no honest way to make its author pick one. That is the\n * same shape `resolveCapabilityTags` allows for `#cap-`, and it is the opposite\n * of the `many` arm: there the author wrote one name loam cannot place, here\n * they wrote two and meant both.\n */\nexport function resolveRequirementTags(\n tags: readonly string[],\n scope: readonly string[],\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined,\n): RequirementClaim[] {\n const claimed = [...new Set(tags.filter((tag) => tag.startsWith(REQ_TAG_PREFIX)))].sort(compareIds);\n if (claimed.length === 0) return [];\n const capability = scope.length === 1 ? scope[0]! : null;\n\n return claimed.map((tag): RequirementClaim => {\n // A bare `#req-` slugs to the empty string and stays a claim, exactly as a\n // bare `#cap-` does: the prefix is the opt-in however little follows it, and\n // dropping it would leave a view that asked to be graded silently ungraded.\n const slug = tag.slice(REQ_TAG_PREFIX.length);\n if (capability === null) return { tag, slug, kind: \"unscoped\", capabilities: [...scope].sort(compareIds) };\n const ids = requirementsOf(capability);\n if (ids === undefined) return { tag, slug, kind: \"undocumented\", capability };\n if (ids.size === 0) return { tag, slug, kind: \"empty\", capability };\n const matching = [...ids].filter((id) => tagSlug(id) === slug).sort(compareIds);\n const [first, ...rest] = matching;\n if (first === undefined) {\n return { tag, slug, kind: \"none\", capability, close: closeRequirementIds(slug, ids) };\n }\n if (rest.length === 0) return { tag, slug, kind: \"resolved\", capability, id: first };\n return { tag, slug, kind: \"many\", capability, ids: matching };\n });\n}\n\n/**\n * Close ids for a slug nothing in the document matches — suggested against the\n * SLUGS, then mapped back to the real ids, exactly as `closeCapabilityIds`\n * does and for its two reasons: comparing on slugs is what lets a typo reach an\n * id whose awkward character sits where the typo does not, and mapping back is\n * what keeps the suggestion writable.\n */\nfunction closeRequirementIds(slug: string, ids: ReadonlySet<string>): string[] {\n const bySlug = new Map<string, string[]>();\n for (const id of ids) {\n const key = tagSlug(id);\n const found = bySlug.get(key);\n if (found === undefined) bySlug.set(key, [id]);\n else found.push(id);\n }\n const near = closeIds(slug, [...bySlug.keys()]);\n return [...new Set(near.flatMap((s) => bySlug.get(s) ?? []))].sort(compareIds);\n}\n"]}
1
+ {"version":3,"file":"usecase-join.js","sourceRoot":"","sources":["../../../src/core/capabilities/usecase-join.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The joins from a use case's tags: back to a declared capability, and to one\n * of that capability's requirements.\n *\n * TWO TAGS, ONE CLAIM. `#cap-<slug>` says which capability a flow is about;\n * `#req-<slug>` says which of its named promises the flow satisfies. The second\n * is the architect's ANSWER to the analyst's requirement, and it is the only\n * carrier a cross-service criterion has: \"I enter a login and a password and I\n * am in\" cannot be promised by any single service's spec, because each promises\n * only its own part — but a flow can, because it IS the hop sequence. That is\n * why `Realizes:` on a service requirement is not enough on its own, and why\n * this join exists at all.\n *\n * THE SECOND TAG IS SCOPED BY THE FIRST. A `Requirement-ID` is unique only\n * inside its own document, so `#req-CHK-ONCE` means nothing until the view has\n * said which capability it is about. A view carrying `#req-` with no resolved\n * `#cap-`, or with two, is refused rather than guessed at — the same stance the\n * `many` arm takes one join down.\n *\n * A LikeC4 `dynamic view` opts in to being graded as a business use case by\n * carrying a tag `#cap-<slug>`, and this module is the whole of what both tags\n * mean. It is deliberately the narrowest layer that can mean it: no file is\n * read, no finding is emitted, and nothing is thrown — a pure function over ids\n * somebody else supplies. That last property is not tidiness. Its caller runs\n * inside `loam validate --all` over every document a fleet has, and the one\n * behaviour a check added to that run may never have is failing the run it was\n * added to.\n *\n * WHY A SLUG AT ALL. Neither id is spelled in characters a tag name accepts. A\n * capability id spells its nesting inside the id — `identity/tokens` and\n * `payments/settlement` both ship in loam's own\n * `examples/docs/architecture/capabilities.yaml`, and `./capabilities.ts` keeps\n * the key exactly as the requirement writes it, slashes preserved — while a\n * `Requirement-ID` may legally contain a `.`. A LikeC4 tag name accepts exactly\n * `[A-Za-z0-9_-]` (measured at the 1.59.2 pin, pinned in\n * `test/likec4-view-shape.test.ts`), so both tags carry their id with the rest\n * flattened and both joins run over the flattened form.\n *\n * WHY THE JOIN CAN ANSWER \"MANY\". The slug is NOT injective. `payments/refunds`\n * and `payments-refunds` are two distinct, legal, declarable ids that flatten to\n * one slug, so `#cap-payments-refunds` names both and nothing in the tag can say\n * which. loam refuses and names ambiguity rather than guessing, and here the\n * cost of guessing is unusually high: a silently-picked capability attributes a\n * whole business flow — every hop, every service, every rollup built over it —\n * to the wrong owner, and goes on doing so invisibly because the wrong answer\n * looks exactly like the right one. The \"many\" arm is why this returns a union\n * at all instead of `string | undefined`.\n *\n * WHAT THE CALLER STILL OWES. The vocabulary ladder in `./findings.ts` — an\n * absent or unreadable `architecture/capabilities.yaml` means total silence, not\n * a fleet full of unresolved tags — is applied BEFORE calling here. An empty\n * `declared` list at this boundary therefore means \"the fleet declares no\n * capabilities\", which is a real answer this module will happily grade; it does\n * not mean \"there is no vocabulary\", and this module cannot tell the two apart.\n */\nimport { closeIds } from \"../c4/arch.js\";\nimport { compareIds } from \"../repo/entries.js\";\n\n/**\n * The reserved tag prefix, matched EXACTLY — case included.\n *\n * A view tag that does not start with it is not a capability claim and is\n * invisible here: `#FEAT-101`, `#platform` and `#deprecated` are the author's\n * own vocabulary and grading them would turn every existing hand-drawn view in\n * a brownfield fleet red on upgrade. The match is case-sensitive because what\n * survives it is a slug that must round-trip to a `capabilities.yaml` key, and\n * those keys are case-sensitive YAML; accepting `#Cap-checkout` here would hand\n * the lookup a slug whose case loam had already stopped believing in.\n */\nexport const CAP_TAG_PREFIX = \"cap-\";\n\n/**\n * Every character a LikeC4 tag name accepts, at the 1.59.2 pin. A WHITELIST,\n * and that is the load-bearing choice: the complement is infinite, so a rule\n * written as \"flatten a slash\" is a rule that is right about one character and\n * silent about the rest.\n *\n * Measured, not assumed (`test/likec4-view-shape.test.ts` pins it): `.`, `/`,\n * `:`, a space, `+`, `@` and a non-ASCII letter each make the DECLARATION a\n * parse error — and the failure is worse than a refusal, because the parser\n * TRUNCATES the name at the offending character. `#x-a.b` comes back as\n * `[\"x-a\"]`. Nothing in loam reads a model with errors, so the truncation\n * cannot reach a join today; it is recorded because the day something does read\n * one, a truncated tag resolves to a different, possibly real, capability.\n */\nconst TAG_NAME_SAFE = /[^A-Za-z0-9_-]/g;\n\n/**\n * An id as its tag spells it: every character a tag name cannot carry flattened\n * to a hyphen.\n *\n * Used by BOTH tags this module resolves, which is why it is no longer called\n * `capabilitySlug`. A capability id spells its nesting with `/`\n * (`identity/tokens`) and a `Requirement-ID` may legally contain `.`\n * (`REQUIREMENT_ID_RE` allows it) — two different characters, one rule, because\n * an author who learns that a tag flattens the awkward character in one id\n * should not have to learn it again for the other.\n *\n * Deliberately lossy, and the `many` arms below carry the whole cost of that:\n * two ids can produce one slug and nothing here notices.\n */\nexport function tagSlug(id: string): string {\n return id.replace(TAG_NAME_SAFE, \"-\");\n}\n\n/** The tag a claim is about, and the slug left after the prefix. */\ninterface ClaimTag {\n /** The tag verbatim, as the view declares it — no `#`, prefix still on. */\n tag: string;\n /** The tag with its reserved prefix removed: the name the join actually ran on. */\n slug: string;\n}\n\n/**\n * What one `#cap-` tag joins to. Branch on `kind`; every arm carries the tag it\n * is about, so a message can name the tag, the slug and the answer without\n * re-deriving any of them from the view.\n */\nexport type CapabilityClaim =\n /** Exactly one declared id flattens to this tag. `id` is the real id, slashes intact. */\n | (ClaimTag & { kind: \"resolved\"; id: string })\n /**\n * No declared id flattens to it. `close` holds real capability ids — names the\n * author can paste into `capabilities.yaml` or into the tag — never the slugs\n * the comparison was actually made against, because a slug is loam's spelling\n * of a name and offering it as a suggestion would be loam inventing a\n * capability id that nobody declared.\n */\n | (ClaimTag & { kind: \"none\"; close: string[] })\n /** Two or more declared ids flatten to it. Every colliding id, sorted, so a message can name them all. */\n | (ClaimTag & { kind: \"many\"; ids: string[] });\n\n/**\n * Every declared id that produces each slug.\n *\n * A map rather than a scan per tag because the \"many\" arm needs the WHOLE\n * colliding set: a lookup that stopped at the first hit could still report\n * \"resolved\" for a slug two ids answer to, which is precisely the silent wrong\n * answer this module exists to refuse.\n */\nfunction idsBySlug(declared: readonly string[]): Map<string, string[]> {\n const bySlug = new Map<string, string[]>();\n for (const id of declared) {\n const slug = tagSlug(id);\n const ids = bySlug.get(slug);\n if (ids === undefined) bySlug.set(slug, [id]);\n else ids.push(id);\n }\n return bySlug;\n}\n\n/**\n * Close names for a slug nothing declares — suggested against the SLUGS, then\n * mapped back to the ids that produced them.\n *\n * Both halves are load-bearing. Comparing against slugs is what lets\n * `#cap-identiy-tokns` reach `identity/tokens` at all: `closeIds` matches on\n * substring and shared prefix, and the slash sits where the author's typo does\n * not. Mapping back is what keeps the suggestion writable — a hint naming\n * `identity-tokens` would send the author to look for a capability declared\n * under a name that appears nowhere in `capabilities.yaml`.\n *\n * A winning slug may map back to more than one id, and the expansion is not\n * re-capped: `closeIds` caps the SLUGS it offers at five, and dropping a real\n * declared id afterwards to keep an arithmetic promise nobody made would hide\n * the very collision the \"many\" arm is here to name.\n */\nfunction closeCapabilityIds(slug: string, bySlug: ReadonlyMap<string, string[]>): string[] {\n const near = closeIds(slug, [...bySlug.keys()]);\n // `?? []` rather than `!`: `closeIds` promises every name it offers is one it\n // was given, so a miss here would mean that promise broke — and the honest\n // answer to a broken invariant inside a hint is one fewer suggestion, not a\n // `TypeError` thrown out of a `validate --all` run over somebody's fleet.\n return [...new Set(near.flatMap((s) => bySlug.get(s) ?? []))].sort(compareIds);\n}\n\n/**\n * The capability claims a view's tags make: one claim per `#cap-` tag, each\n * answered on its own terms.\n *\n * A VIEW MAY CARRY MORE THAN ONE `#cap-` TAG, and the tags are never collapsed\n * into a single verdict for the view. Both halves of that are deliberate.\n *\n * A use case realizing two capabilities is a real thing — a checkout flow that\n * also issues the identity token realizes both, and there is no honest way to\n * make its author pick one — so a second tag must NOT be read the way the \"many\"\n * arm reads a second colliding id. Those two look alike and are opposites: in\n * \"many\" the author wrote one name and loam cannot tell which id it means, here\n * the author wrote two names and meant both.\n *\n * And a view whose tags disagree — one resolving, one naming nothing — is not a\n * view that resolved. Collapsing to the first success would swallow the broken\n * tag and grade the view as if the typo were not there; collapsing to the first\n * failure would lose the capability the view genuinely does realize. One tag,\n * one verdict, is the only shape that keeps both facts, and it leaves the caller\n * free to emit one finding per failing tag while still collecting every id the\n * view resolved.\n *\n * Claims come back SORTED BY TAG rather than in the order LikeC4 handed the tags\n * over. Nothing in loam has measured that LikeC4 preserves the author's tag\n * order, and an output ordered by an unmeasured upstream detail is one that\n * reorders under a version bump — the diff-stability rule `./rollup.ts` states\n * for its rows, applied to a smaller list. A repeated tag collapses to one\n * claim, for the reason one breach earns one finding.\n *\n * An empty result means the view makes no capability claim at all — either it\n * carries no tags or none of them are reserved — which is the opt-in, not a\n * failure to resolve.\n */\nexport function resolveCapabilityTags(\n tags: readonly string[],\n declared: readonly string[],\n): CapabilityClaim[] {\n const bySlug = idsBySlug(declared);\n const claimed = [...new Set(tags.filter((tag) => tag.startsWith(CAP_TAG_PREFIX)))].sort(compareIds);\n\n return claimed.map((tag) => {\n // A bare `#cap-` slugs to the empty string, and it stays a claim on purpose:\n // the prefix is the author's opt-in however little follows it, so dropping\n // the tag as unreserved would leave a view that asked to be graded silently\n // ungraded. What that empty slug then resolves to is the lookup's business\n // like any other — `none` in every real fleet, `resolved` in the one that\n // declared an empty id, and neither is decided here.\n const slug = tag.slice(CAP_TAG_PREFIX.length);\n // Destructured rather than indexed so the three arms are told apart by the\n // shape of the match itself — no bounds assertion, and no `undefined` check\n // separate from the \"nothing declared this\" answer it already is.\n const [first, ...rest] = bySlug.get(slug) ?? [];\n if (first === undefined) return { tag, slug, kind: \"none\" as const, close: closeCapabilityIds(slug, bySlug) };\n if (rest.length === 0) return { tag, slug, kind: \"resolved\" as const, id: first };\n return { tag, slug, kind: \"many\" as const, ids: [first, ...rest].sort(compareIds) };\n });\n}\n\n/**\n * The reserved tag prefix for the requirement claim, matched EXACTLY as\n * `CAP_TAG_PREFIX` is and for the same two reasons: a tag that does not start\n * with it is the author's own vocabulary and is invisible here, and the slug\n * that survives the match must round-trip to a case-sensitive `Requirement-ID:`.\n */\nexport const REQ_TAG_PREFIX = \"req-\";\n\n/**\n * What one `#req-` tag joins to, given the capabilities the same view resolved.\n *\n * Five arms because there are five different fixes, and they are deliberately\n * the same five `RealizesClaim` draws in `./realizes/join.ts`: the two carriers\n * of one claim must fail in the same vocabulary, or an author who learns the\n * `Realizes:` messages learns nothing about the tag.\n */\nexport type RequirementClaim =\n /** Exactly one requirement of the scoping capability flattens to this tag. */\n | (ClaimTag & { kind: \"resolved\"; capability: string; id: string })\n /**\n * The view resolved no capability, or more than one, so a `Requirement-ID`\n * has no document to be unique inside. `capabilities` is what it DID resolve\n * — empty, or the two-or-more that make the scope ambiguous.\n */\n | (ClaimTag & { kind: \"unscoped\"; capabilities: string[] })\n /** The scoping capability is declared but has no `capabilities/<id>/spec.md`, so it carries no requirements. */\n | (ClaimTag & { kind: \"undocumented\"; capability: string })\n /** Its document exists and declares no requirements yet. */\n | (ClaimTag & { kind: \"empty\"; capability: string })\n /**\n * Its document declares requirements, none flattening to this slug. `close`\n * holds real `Requirement-ID`s — never the slugs the comparison ran against,\n * because a slug is loam's spelling and offering it would send the author\n * looking for an id that appears nowhere in the document.\n */\n | (ClaimTag & { kind: \"none\"; capability: string; close: string[] })\n /** Two or more of its requirement ids flatten to this slug. Every colliding id, sorted. */\n | (ClaimTag & { kind: \"many\"; capability: string; ids: string[] });\n\n/**\n * Resolve every `#req-` tag a view carries against the capability it is about.\n *\n * `scope` is the capability ids the view's OWN `#cap-` tags resolved to —\n * `resolveCapabilityTags`' `resolved` arms, and nothing else. An unresolved\n * `#cap-` tag is already `usecase.capability-unresolved`, and letting it also\n * scope a requirement lookup would build a second finding on top of a name loam\n * has just said it cannot place.\n *\n * Claims come back SORTED BY TAG for `resolveCapabilityTags`' reason: nothing in\n * loam has measured that LikeC4 preserves the author's tag order, and an output\n * ordered by an unmeasured upstream detail reorders under a version bump. A\n * repeated tag collapses to one claim, for the reason one breach earns one\n * finding.\n *\n * SEVERAL `#req-` TAGS ARE LEGAL AND NORMAL. One flow commonly satisfies two of\n * a capability's promises — a checkout flow that charges once AND honours the\n * price — and there is no honest way to make its author pick one. That is the\n * same shape `resolveCapabilityTags` allows for `#cap-`, and it is the opposite\n * of the `many` arm: there the author wrote one name loam cannot place, here\n * they wrote two and meant both.\n */\nexport function resolveRequirementTags(\n tags: readonly string[],\n scope: readonly string[],\n requirementsOf: (capability: string) => ReadonlySet<string> | undefined,\n): RequirementClaim[] {\n const claimed = [...new Set(tags.filter((tag) => tag.startsWith(REQ_TAG_PREFIX)))].sort(compareIds);\n if (claimed.length === 0) return [];\n const capability = scope.length === 1 ? scope[0]! : null;\n\n return claimed.map((tag): RequirementClaim => {\n // A bare `#req-` slugs to the empty string and stays a claim, exactly as a\n // bare `#cap-` does: the prefix is the opt-in however little follows it, and\n // dropping it would leave a view that asked to be graded silently ungraded.\n const slug = tag.slice(REQ_TAG_PREFIX.length);\n if (capability === null) return { tag, slug, kind: \"unscoped\", capabilities: [...scope].sort(compareIds) };\n const ids = requirementsOf(capability);\n if (ids === undefined) return { tag, slug, kind: \"undocumented\", capability };\n if (ids.size === 0) return { tag, slug, kind: \"empty\", capability };\n const matching = [...ids].filter((id) => tagSlug(id) === slug).sort(compareIds);\n const [first, ...rest] = matching;\n if (first === undefined) {\n return { tag, slug, kind: \"none\", capability, close: closeRequirementIds(slug, ids) };\n }\n if (rest.length === 0) return { tag, slug, kind: \"resolved\", capability, id: first };\n return { tag, slug, kind: \"many\", capability, ids: matching };\n });\n}\n\n/**\n * Close ids for a slug nothing in the document matches — suggested against the\n * SLUGS, then mapped back to the real ids: `closeCapabilityIds` over this\n * document's ids, for its two reasons: comparing on slugs is what lets a typo reach an\n * id whose awkward character sits where the typo does not, and mapping back is\n * what keeps the suggestion writable.\n */\nfunction closeRequirementIds(slug: string, ids: ReadonlySet<string>): string[] {\n return closeCapabilityIds(slug, idsBySlug([...ids]));\n}\n"]}
@@ -152,10 +152,18 @@ async function intentIssue(scope, issues) {
152
152
  return;
153
153
  issues.push({
154
154
  ...empty,
155
- message: `intent.md says nothing yet — its Why and Scope are still the scaffold's comments. Write why ${scope.featureId} exists before archiving it into the living docs, or archive with --approve.`,
155
+ message: `intent.md says nothing yet — its Why and Scope are still the scaffold's comments (questions under \`## Open questions\` are not a Why). Write why ${scope.featureId} exists before archiving it into the living docs, or archive with --approve.`,
156
156
  });
157
157
  }
158
- /** Is there any authored text outside frontmatter, HTML comments and headings? */
158
+ /**
159
+ * The scaffold's `## Open questions` section, up to the next heading of its
160
+ * level or above. loam-feature's analyze step WRITES there, so prose in it is
161
+ * an agent's questions about the feature, never the reason it exists — and
162
+ * counting it would let a feature whose Why nobody wrote clear the one gate
163
+ * that asks for it, as soon as its author did the step that asks questions.
164
+ */
165
+ const OPEN_QUESTIONS_SECTION = /^##[ \t]+open questions[ \t]*\n[\s\S]*?(?=^#{1,2}[ \t]|(?![\s\S]))/gim;
166
+ /** Is there any authored text outside frontmatter, HTML comments, headings and the open questions? */
159
167
  function hasProse(text) {
160
168
  // Normalized BEFORE the fence regex, the discipline document/parse.ts
161
169
  // applies to every other markdown read: `---\r` is not `---`, so a CRLF
@@ -169,7 +177,7 @@ function hasProse(text) {
169
177
  const fm = /^---\n[\s\S]*?\n---\n/.exec(body);
170
178
  if (fm !== null)
171
179
  body = body.slice(fm[0].length);
172
- body = body.replace(/<!--[\s\S]*?-->/g, "");
180
+ body = body.replace(/<!--[\s\S]*?-->/g, "").replace(OPEN_QUESTIONS_SECTION, "");
173
181
  return body
174
182
  .split("\n")
175
183
  .some((line) => line.trim().length > 0 && !line.trimStart().startsWith("#"));
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../../../src/core/coherence/authoring/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The gate between `loam new` and `loam archive`: a scaffold nobody edited must\n * not become the living truth.\n *\n * `loam new` writes documents whose whole point is to be replaced — a TODO\n * requirement heading, angle-bracket step fill-ins, a TODO service description,\n * an intent whose Why/Scope are comments. Every one of them parsed as real\n * content, validated clean, and archived at exit 0, which put literal `TODO —\n * name the behaviour` into a living spec the rest of the fleet then reads as a\n * requirement somebody meant. These checks refuse that merge.\n *\n * Both are warnings that GATE (issue.ts: severity says the DOCUMENT is legal —\n * it is, it parses — gating says the MERGE is unsafe). `--approve` overrides,\n * as with every judgment about the feature.\n *\n * BOTH DELTA CORPORA ARE READ, because `loam new` scaffolds into both. A\n * `--capability` run writes `features/<FEAT>/capabilities/<id>/spec.md` from a\n * template whose example sits inside an HTML comment exactly like the two spec\n * templates', and an author who copies that block out and archives without\n * editing it would publish `TODO — name the promise` into a LIVING capability\n * document — which is strictly worse than the service case this gate was built\n * for: a capability document outlives every service that realizes it, and a\n * `Realizes:` line pointed at the placeholder's id would then be a join to a\n * promise nobody wrote. The capability deltas are read HERE rather than handed\n * in through `AuthoringScope`, because this is a walk over the feature's own\n * documents and the scope already names the feature; the read is one\n * `existsSync` for the fleets that have not adopted the axis, and the memo when\n * a context was threaded.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Elem } from \"../../c4/likec4.js\";\nimport type { PathableService } from \"../../kernel/ids/service.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport type { FleetContext } from \"../../fleet-context.js\";\nimport type { Requirement } from \"../../document/spec.js\";\nimport { parseRequirements } from \"../../document/parse.js\";\nimport { featureCapabilityDeltas } from \"../../capabilities/delta/tree.js\";\nimport { featurePaths, featureSpecPaths, SPEC_AXES } from \"../../repo/paths.js\";\nimport type { FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport {\n BODY_SENTINELS,\n HEADING_SENTINELS,\n SERVICE_DESCRIPTION_SENTINEL,\n} from \"./sentinels.js\";\n\n/** What the placeholder gate reads: the feature's own documents, nothing else. */\nexport interface AuthoringScope {\n featureDir: FeatureDir;\n featureId: string;\n /**\n * Every element whose authored block the landscape merge would SPLICE — the\n * tagged elements and everything nested inside their blocks. The scope is\n * the splice's, not the tag's, for `c4.service-binding-invalid`'s reason\n * (coherence.ts): the merge carries a tagged element's block over byte for\n * byte, untagged children included, so a TODO description on a nested child\n * reaches the fleet map exactly as its tagged parent's would.\n */\n splicedEls: Elem[];\n svcNames: PathableService[];\n context?: FleetContext;\n}\n\nexport async function authoringIssues(scope: AuthoringScope): Promise<Issue[]> {\n const issues: Issue[] = [];\n await intentIssue(scope, issues);\n\n for (const e of scope.splicedEls) {\n if (e.description !== SERVICE_DESCRIPTION_SENTINEL) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n message: `delta.likec4: '${e.title}' (${e.id}) still carries the scaffolded description '${SERVICE_DESCRIPTION_SENTINEL}' — the merge would publish it to the fleet map. Say what the service owns, or archive with --approve.`,\n });\n }\n\n for (const svc of scope.svcNames) {\n const paths = featureSpecPaths(scope.featureDir, svc);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n const found = unauthored(await requirementsOf(scope, path));\n if (found.size === 0) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n subject: svc,\n message: `${svc}: ${axis.file} still carries scaffold placeholder(s) nobody authored — ${quote(found)}. The merge would write them into the living spec as requirements somebody meant. Replace them, or archive with --approve.`,\n });\n }\n }\n\n // The business corpus. `subject` is the capability id — a THIRD provenance\n // for this code's subject, after a service id and none at all — and the\n // artifact table resolves it by MEMBERSHIP against the capability documents\n // the feature carries (`status/feature/artifacts.ts`), the same resolution\n // every `delta.*` issue on this axis already relies on. So the capability's\n // own row turns draft, and a service row does too only where a fleet holds a\n // service and a capability of one name, which is that module's deliberately\n // pessimistic answer rather than a mis-attribution here.\n const capabilities =\n scope.context === undefined\n ? await featureCapabilityDeltas(scope.featureDir)\n : await scope.context.featureCapabilityDeltas(scope.featureDir);\n for (const doc of capabilities.docs) {\n const found = unauthored(await requirementsOf(scope, doc.spec));\n if (found.size === 0) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n subject: doc.id,\n message: `capability ${doc.id}: spec.md still carries scaffold placeholder(s) nobody authored — ${quote(found)}. The merge would write them into the living capabilities/${doc.id}/spec.md as promises somebody meant — and a capability document outlives every service that realizes it. Replace them, or archive with --approve.`,\n });\n }\n return issues;\n}\n\n/** One delta document's requirements, through the invocation's read index when there is one. */\nasync function requirementsOf(scope: AuthoringScope, path: string): Promise<Requirement[]> {\n if (scope.context === undefined) return parseRequirements(await readFile(path, \"utf8\"));\n return scope.context.readRequirements(path);\n}\n\n/**\n * The scaffold strings a delta's MERGING requirements still carry.\n *\n * ADDED/MODIFIED only: those are the texts this MERGE would write. A BASE\n * requirement is a quote of the living state — if a sentinel sits there, some\n * earlier archive published it and this feature is not the author to gate on it\n * — and a REMOVED one is leaving the document anyway.\n */\nfunction unauthored(reqs: Requirement[]): Set<string> {\n const found = new Set<string>();\n for (const r of reqs) {\n if (r.kind === \"ADDED\" || r.kind === \"MODIFIED\") sentinelsIn(r, found);\n }\n return found;\n}\n\n/** The found sentinels as a message fragment — one spelling, so the two corpora read alike. */\nfunction quote(found: ReadonlySet<string>): string {\n return [...found].map((s) => `'${s}'`).join(\", \");\n}\n\n/** The exact template strings this requirement still carries, if any. */\nfunction sentinelsIn(r: Requirement, found: Set<string>): void {\n for (const heading of HEADING_SENTINELS) {\n if (r.name === heading) found.add(heading);\n for (const s of r.scenarios) {\n if (s.name === heading) found.add(heading);\n }\n }\n const body = [r.text.join(\"\\n\"), ...r.scenarios.map((s) => s.lines.join(\"\\n\"))].join(\"\\n\");\n for (const marker of BODY_SENTINELS) {\n if (body.includes(marker)) found.add(marker);\n }\n}\n\n/**\n * Does intent.md SAY anything? The scaffold's Why/Scope bodies are HTML\n * comments, so \"strip frontmatter, comments and headings, look for prose\" is\n * exactly the question \"did a person write the sentence the reviewer reads\n * first\". A missing file gates the same way: a feature with no stated intent\n * is not less anonymous for not having the file.\n */\nasync function intentIssue(scope: AuthoringScope, issues: Issue[]): Promise<void> {\n const path = featurePaths(scope.featureDir).intent;\n const empty = { severity: \"warn\", gates: true, code: \"intent.empty\" } as const;\n if (!existsSync(path)) {\n issues.push({\n ...empty,\n message: `intent.md is missing — nothing says why ${scope.featureId} exists. Write the Why before archiving it into the living docs, or archive with --approve.`,\n });\n return;\n }\n if (hasProse(await readFile(path, \"utf8\"))) return;\n issues.push({\n ...empty,\n message: `intent.md says nothing yet — its Why and Scope are still the scaffold's comments. Write why ${scope.featureId} exists before archiving it into the living docs, or archive with --approve.`,\n });\n}\n\n/** Is there any authored text outside frontmatter, HTML comments and headings? */\nfunction hasProse(text: string): boolean {\n // Normalized BEFORE the fence regex, the discipline document/parse.ts\n // applies to every other markdown read: `---\\r` is not `---`, so a CRLF\n // rewrite (or a BOM some Windows editor prefixed) kept the frontmatter,\n // which then counted as prose — and the untouched scaffold archived at\n // exit 0 through the exact gate built to refuse it.\n // The BOM is spelled as an escape, never as the raw character — a raw one is\n // invisible in review and to grep, which is how it would survive a rewrite.\n let body = text.replace(/^\\uFEFF/, \"\").replace(/\\r\\n/g, \"\\n\");\n // Frontmatter: the leading `---` block only, exactly where the scaffold puts it.\n const fm = /^---\\n[\\s\\S]*?\\n---\\n/.exec(body);\n if (fm !== null) body = body.slice(fm[0].length);\n body = body.replace(/<!--[\\s\\S]*?-->/g, \"\");\n return body\n .split(\"\\n\")\n .some((line) => line.trim().length > 0 && !line.trimStart().startsWith(\"#\"));\n}\n"]}
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../../../src/core/coherence/authoring/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The gate between `loam new` and `loam archive`: a scaffold nobody edited must\n * not become the living truth.\n *\n * `loam new` writes documents whose whole point is to be replaced — a TODO\n * requirement heading, angle-bracket step fill-ins, a TODO service description,\n * an intent whose Why/Scope are comments. Every one of them parsed as real\n * content, validated clean, and archived at exit 0, which put literal `TODO —\n * name the behaviour` into a living spec the rest of the fleet then reads as a\n * requirement somebody meant. These checks refuse that merge.\n *\n * Both are warnings that GATE (issue.ts: severity says the DOCUMENT is legal —\n * it is, it parses — gating says the MERGE is unsafe). `--approve` overrides,\n * as with every judgment about the feature.\n *\n * BOTH DELTA CORPORA ARE READ, because `loam new` scaffolds into both. A\n * `--capability` run writes `features/<FEAT>/capabilities/<id>/spec.md` from a\n * template whose example sits inside an HTML comment exactly like the two spec\n * templates', and an author who copies that block out and archives without\n * editing it would publish `TODO — name the promise` into a LIVING capability\n * document — which is strictly worse than the service case this gate was built\n * for: a capability document outlives every service that realizes it, and a\n * `Realizes:` line pointed at the placeholder's id would then be a join to a\n * promise nobody wrote. The capability deltas are read HERE rather than handed\n * in through `AuthoringScope`, because this is a walk over the feature's own\n * documents and the scope already names the feature; the read is one\n * `existsSync` for the fleets that have not adopted the axis, and the memo when\n * a context was threaded.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Elem } from \"../../c4/likec4.js\";\nimport type { PathableService } from \"../../kernel/ids/service.js\";\nimport type { Issue } from \"../../vocabulary/issue.js\";\nimport type { FleetContext } from \"../../fleet-context.js\";\nimport type { Requirement } from \"../../document/spec.js\";\nimport { parseRequirements } from \"../../document/parse.js\";\nimport { featureCapabilityDeltas } from \"../../capabilities/delta/tree.js\";\nimport { featurePaths, featureSpecPaths, SPEC_AXES } from \"../../repo/paths.js\";\nimport type { FeatureDir } from \"../../kernel/ids/dirs.js\";\nimport {\n BODY_SENTINELS,\n HEADING_SENTINELS,\n SERVICE_DESCRIPTION_SENTINEL,\n} from \"./sentinels.js\";\n\n/** What the placeholder gate reads: the feature's own documents, nothing else. */\nexport interface AuthoringScope {\n featureDir: FeatureDir;\n featureId: string;\n /**\n * Every element whose authored block the landscape merge would SPLICE — the\n * tagged elements and everything nested inside their blocks. The scope is\n * the splice's, not the tag's, for `c4.service-binding-invalid`'s reason\n * (coherence.ts): the merge carries a tagged element's block over byte for\n * byte, untagged children included, so a TODO description on a nested child\n * reaches the fleet map exactly as its tagged parent's would.\n */\n splicedEls: Elem[];\n svcNames: PathableService[];\n context?: FleetContext;\n}\n\nexport async function authoringIssues(scope: AuthoringScope): Promise<Issue[]> {\n const issues: Issue[] = [];\n await intentIssue(scope, issues);\n\n for (const e of scope.splicedEls) {\n if (e.description !== SERVICE_DESCRIPTION_SENTINEL) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n message: `delta.likec4: '${e.title}' (${e.id}) still carries the scaffolded description '${SERVICE_DESCRIPTION_SENTINEL}' — the merge would publish it to the fleet map. Say what the service owns, or archive with --approve.`,\n });\n }\n\n for (const svc of scope.svcNames) {\n const paths = featureSpecPaths(scope.featureDir, svc);\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n const found = unauthored(await requirementsOf(scope, path));\n if (found.size === 0) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n subject: svc,\n message: `${svc}: ${axis.file} still carries scaffold placeholder(s) nobody authored — ${quote(found)}. The merge would write them into the living spec as requirements somebody meant. Replace them, or archive with --approve.`,\n });\n }\n }\n\n // The business corpus. `subject` is the capability id — a THIRD provenance\n // for this code's subject, after a service id and none at all — and the\n // artifact table resolves it by MEMBERSHIP against the capability documents\n // the feature carries (`status/feature/artifacts.ts`), the same resolution\n // every `delta.*` issue on this axis already relies on. So the capability's\n // own row turns draft, and a service row does too only where a fleet holds a\n // service and a capability of one name, which is that module's deliberately\n // pessimistic answer rather than a mis-attribution here.\n const capabilities =\n scope.context === undefined\n ? await featureCapabilityDeltas(scope.featureDir)\n : await scope.context.featureCapabilityDeltas(scope.featureDir);\n for (const doc of capabilities.docs) {\n const found = unauthored(await requirementsOf(scope, doc.spec));\n if (found.size === 0) continue;\n issues.push({\n severity: \"warn\",\n gates: true,\n code: \"scaffold.placeholder\",\n subject: doc.id,\n message: `capability ${doc.id}: spec.md still carries scaffold placeholder(s) nobody authored — ${quote(found)}. The merge would write them into the living capabilities/${doc.id}/spec.md as promises somebody meant — and a capability document outlives every service that realizes it. Replace them, or archive with --approve.`,\n });\n }\n return issues;\n}\n\n/** One delta document's requirements, through the invocation's read index when there is one. */\nasync function requirementsOf(scope: AuthoringScope, path: string): Promise<Requirement[]> {\n if (scope.context === undefined) return parseRequirements(await readFile(path, \"utf8\"));\n return scope.context.readRequirements(path);\n}\n\n/**\n * The scaffold strings a delta's MERGING requirements still carry.\n *\n * ADDED/MODIFIED only: those are the texts this MERGE would write. A BASE\n * requirement is a quote of the living state — if a sentinel sits there, some\n * earlier archive published it and this feature is not the author to gate on it\n * — and a REMOVED one is leaving the document anyway.\n */\nfunction unauthored(reqs: Requirement[]): Set<string> {\n const found = new Set<string>();\n for (const r of reqs) {\n if (r.kind === \"ADDED\" || r.kind === \"MODIFIED\") sentinelsIn(r, found);\n }\n return found;\n}\n\n/** The found sentinels as a message fragment — one spelling, so the two corpora read alike. */\nfunction quote(found: ReadonlySet<string>): string {\n return [...found].map((s) => `'${s}'`).join(\", \");\n}\n\n/** The exact template strings this requirement still carries, if any. */\nfunction sentinelsIn(r: Requirement, found: Set<string>): void {\n for (const heading of HEADING_SENTINELS) {\n if (r.name === heading) found.add(heading);\n for (const s of r.scenarios) {\n if (s.name === heading) found.add(heading);\n }\n }\n const body = [r.text.join(\"\\n\"), ...r.scenarios.map((s) => s.lines.join(\"\\n\"))].join(\"\\n\");\n for (const marker of BODY_SENTINELS) {\n if (body.includes(marker)) found.add(marker);\n }\n}\n\n/**\n * Does intent.md SAY anything? The scaffold's Why/Scope bodies are HTML\n * comments, so \"strip frontmatter, comments and headings, look for prose\" is\n * exactly the question \"did a person write the sentence the reviewer reads\n * first\". A missing file gates the same way: a feature with no stated intent\n * is not less anonymous for not having the file.\n */\nasync function intentIssue(scope: AuthoringScope, issues: Issue[]): Promise<void> {\n const path = featurePaths(scope.featureDir).intent;\n const empty = { severity: \"warn\", gates: true, code: \"intent.empty\" } as const;\n if (!existsSync(path)) {\n issues.push({\n ...empty,\n message: `intent.md is missing — nothing says why ${scope.featureId} exists. Write the Why before archiving it into the living docs, or archive with --approve.`,\n });\n return;\n }\n if (hasProse(await readFile(path, \"utf8\"))) return;\n issues.push({\n ...empty,\n message: `intent.md says nothing yet — its Why and Scope are still the scaffold's comments (questions under \\`## Open questions\\` are not a Why). Write why ${scope.featureId} exists before archiving it into the living docs, or archive with --approve.`,\n });\n}\n\n/**\n * The scaffold's `## Open questions` section, up to the next heading of its\n * level or above. loam-feature's analyze step WRITES there, so prose in it is\n * an agent's questions about the feature, never the reason it exists — and\n * counting it would let a feature whose Why nobody wrote clear the one gate\n * that asks for it, as soon as its author did the step that asks questions.\n */\nconst OPEN_QUESTIONS_SECTION = /^##[ \\t]+open questions[ \\t]*\\n[\\s\\S]*?(?=^#{1,2}[ \\t]|(?![\\s\\S]))/gim;\n\n/** Is there any authored text outside frontmatter, HTML comments, headings and the open questions? */\nfunction hasProse(text: string): boolean {\n // Normalized BEFORE the fence regex, the discipline document/parse.ts\n // applies to every other markdown read: `---\\r` is not `---`, so a CRLF\n // rewrite (or a BOM some Windows editor prefixed) kept the frontmatter,\n // which then counted as prose — and the untouched scaffold archived at\n // exit 0 through the exact gate built to refuse it.\n // The BOM is spelled as an escape, never as the raw character — a raw one is\n // invisible in review and to grep, which is how it would survive a rewrite.\n let body = text.replace(/^\\uFEFF/, \"\").replace(/\\r\\n/g, \"\\n\");\n // Frontmatter: the leading `---` block only, exactly where the scaffold puts it.\n const fm = /^---\\n[\\s\\S]*?\\n---\\n/.exec(body);\n if (fm !== null) body = body.slice(fm[0].length);\n body = body.replace(/<!--[\\s\\S]*?-->/g, \"\").replace(OPEN_QUESTIONS_SECTION, \"\");\n return body\n .split(\"\\n\")\n .some((line) => line.trim().length > 0 && !line.trimStart().startsWith(\"#\"));\n}\n"]}
@@ -2,7 +2,8 @@
2
2
  * The placeholder strings `loam new` scaffolds, spelled ONCE.
3
3
  *
4
4
  * Both sides of the placeholder gate read this module: the templates in
5
- * `commands/new/templates.ts` write these exact strings, and the coherence
5
+ * `commands/new/templates.ts` and `commands/new/capability-template.ts` write
6
+ * these exact strings, and the coherence
6
7
  * check in `./scaffold.ts` refuses to archive them. Spelling a sentinel in two
7
8
  * places is how the gate stops matching the scaffold — a one-character rewording
8
9
  * of a template would silently retire the check that guards it.
@@ -1 +1 @@
1
- {"version":3,"file":"sentinels.js","sourceRoot":"","sources":["../../../../src/core/coherence/authoring/sentinels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,oBAAoB,GAAG,2BAA2B,CAAC;AAEhE,oFAAoF;AACpF,MAAM,CAAC,MAAM,yBAAyB,GAAG,0CAA0C,CAAC;AAEpF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,yBAAyB,CAAC;AAEzE,iEAAiE;AACjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAExD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,4BAA4B,GAAG,+BAA+B,CAAC;AAE5E,yDAAyD;AACzD,MAAM,CAAC,MAAM,cAAc,GAAG,2DAA2D,CAAC;AAE1F,6DAA6D;AAC7D,MAAM,CAAC,MAAM,mBAAmB,GAAG,6DAA6D,CAAC;AAEjG,sFAAsF;AACtF,MAAM,CAAC,MAAM,yBAAyB,GAAG,yEAAyE,CAAC;AAEnH;;;uBAGuB;AACvB,MAAM,CAAC,MAAM,cAAc,GAAG,sBAAsB,CAAC;AACrD,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAC7C,MAAM,CAAC,MAAM,aAAa,GAAG,0BAA0B,CAAC;AAExD;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB;IAC/C,cAAc;IACd,mBAAmB;IACnB,yBAAyB;IACzB,cAAc;IACd,aAAa;IACb,aAAa;CACd,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,oBAAoB;IACpB,yBAAyB;IACzB,+BAA+B;IAC/B,iBAAiB;CAClB,CAAC","sourcesContent":["/**\n * The placeholder strings `loam new` scaffolds, spelled ONCE.\n *\n * Both sides of the placeholder gate read this module: the templates in\n * `commands/new/templates.ts` write these exact strings, and the coherence\n * check in `./scaffold.ts` refuses to archive them. Spelling a sentinel in two\n * places is how the gate stops matching the scaffold — a one-character rewording\n * of a template would silently retire the check that guards it.\n *\n * Detection is EXACT on purpose. A requirement whose author genuinely writes\n * \"TODO\" in prose is that author's call to make; only the strings loam itself\n * scaffolded are ones nobody wrote.\n */\n\n/** The scaffolded business-requirement heading (`specs/<svc>/spec.md`). */\nexport const REQUIREMENT_SENTINEL = \"TODO — name the behaviour\";\n\n/** The scaffolded architecture-requirement heading (`specs/<svc>/arch.spec.md`). */\nexport const ARCH_REQUIREMENT_SENTINEL = \"TODO — name the architectural obligation\";\n\n/**\n * The scaffolded capability-requirement heading\n * (`features/<FEAT>/capabilities/<id>/spec.md`).\n *\n * Its own string rather than a reuse of `REQUIREMENT_SENTINEL`, because the two\n * documents ask for different things and the heading is where the template says\n * so: a service requirement names a BEHAVIOUR the service performs, a capability\n * requirement names a PROMISE the fleet keeps. A shared \"TODO — name the\n * behaviour\" would scaffold the business document with the altitude\n * `capability.requirement-service-scoped` exists to refuse.\n */\nexport const CAPABILITY_REQUIREMENT_SENTINEL = \"TODO — name the promise\";\n\n/** The scaffolded scenario heading, shared by both spec axes. */\nexport const SCENARIO_SENTINEL = \"TODO — name the case\";\n\n/** The scaffolded description on a `--new-service` element in delta.likec4. */\nexport const SERVICE_DESCRIPTION_SENTINEL = \"TODO — what this service owns\";\n\n/** The scaffolded SHALL fill-in on the business axis. */\nexport const SHALL_SENTINEL = \"<observable behaviour, testable without reading the code>\";\n\n/** The scaffolded SHALL fill-in on the architecture axis. */\nexport const ARCH_SHALL_SENTINEL = \"<the operational/integration behaviour, observable in test>\";\n\n/** The scaffolded SHALL fill-in on the business axis — a promise, not a mechanism. */\nexport const CAPABILITY_SHALL_SENTINEL = \"<the promise, observable outside the fleet — a customer could check it>\";\n\n/** The scaffolded Given/When/Then fill-ins, shared by both spec axes. Three\n * constants rather than one keyed object because a `then` property makes an\n * object thenable — one accidental `await` away from a hang the linter\n * refuses outright. */\nexport const GIVEN_SENTINEL = \"<the starting state>\";\nexport const WHEN_SENTINEL = \"<the trigger>\";\nexport const THEN_SENTINEL = \"<the observable outcome>\";\n\n/**\n * The angle-bracket fill-ins inside scaffolded requirement bodies and steps.\n * Any one of them surviving to archive means the line around it was never\n * authored — the brackets are the template telling the author what to replace.\n */\nexport const BODY_SENTINELS: readonly string[] = [\n SHALL_SENTINEL,\n ARCH_SHALL_SENTINEL,\n CAPABILITY_SHALL_SENTINEL,\n GIVEN_SENTINEL,\n WHEN_SENTINEL,\n THEN_SENTINEL,\n];\n\n/**\n * Every scaffolded heading a requirement or scenario may still carry.\n *\n * The list is not partitioned by which document scaffolded which string, and\n * that is deliberate: the gate asks \"did anybody author this line\", and a\n * capability heading pasted into a service delta was authored exactly as little\n * as it was in the document it came from.\n */\nexport const HEADING_SENTINELS: readonly string[] = [\n REQUIREMENT_SENTINEL,\n ARCH_REQUIREMENT_SENTINEL,\n CAPABILITY_REQUIREMENT_SENTINEL,\n SCENARIO_SENTINEL,\n];\n"]}
1
+ {"version":3,"file":"sentinels.js","sourceRoot":"","sources":["../../../../src/core/coherence/authoring/sentinels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,oBAAoB,GAAG,2BAA2B,CAAC;AAEhE,oFAAoF;AACpF,MAAM,CAAC,MAAM,yBAAyB,GAAG,0CAA0C,CAAC;AAEpF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,yBAAyB,CAAC;AAEzE,iEAAiE;AACjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAExD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,4BAA4B,GAAG,+BAA+B,CAAC;AAE5E,yDAAyD;AACzD,MAAM,CAAC,MAAM,cAAc,GAAG,2DAA2D,CAAC;AAE1F,6DAA6D;AAC7D,MAAM,CAAC,MAAM,mBAAmB,GAAG,6DAA6D,CAAC;AAEjG,sFAAsF;AACtF,MAAM,CAAC,MAAM,yBAAyB,GAAG,yEAAyE,CAAC;AAEnH;;;uBAGuB;AACvB,MAAM,CAAC,MAAM,cAAc,GAAG,sBAAsB,CAAC;AACrD,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAC7C,MAAM,CAAC,MAAM,aAAa,GAAG,0BAA0B,CAAC;AAExD;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB;IAC/C,cAAc;IACd,mBAAmB;IACnB,yBAAyB;IACzB,cAAc;IACd,aAAa;IACb,aAAa;CACd,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,oBAAoB;IACpB,yBAAyB;IACzB,+BAA+B;IAC/B,iBAAiB;CAClB,CAAC","sourcesContent":["/**\n * The placeholder strings `loam new` scaffolds, spelled ONCE.\n *\n * Both sides of the placeholder gate read this module: the templates in\n * `commands/new/templates.ts` and `commands/new/capability-template.ts` write\n * these exact strings, and the coherence\n * check in `./scaffold.ts` refuses to archive them. Spelling a sentinel in two\n * places is how the gate stops matching the scaffold — a one-character rewording\n * of a template would silently retire the check that guards it.\n *\n * Detection is EXACT on purpose. A requirement whose author genuinely writes\n * \"TODO\" in prose is that author's call to make; only the strings loam itself\n * scaffolded are ones nobody wrote.\n */\n\n/** The scaffolded business-requirement heading (`specs/<svc>/spec.md`). */\nexport const REQUIREMENT_SENTINEL = \"TODO — name the behaviour\";\n\n/** The scaffolded architecture-requirement heading (`specs/<svc>/arch.spec.md`). */\nexport const ARCH_REQUIREMENT_SENTINEL = \"TODO — name the architectural obligation\";\n\n/**\n * The scaffolded capability-requirement heading\n * (`features/<FEAT>/capabilities/<id>/spec.md`).\n *\n * Its own string rather than a reuse of `REQUIREMENT_SENTINEL`, because the two\n * documents ask for different things and the heading is where the template says\n * so: a service requirement names a BEHAVIOUR the service performs, a capability\n * requirement names a PROMISE the fleet keeps. A shared \"TODO — name the\n * behaviour\" would scaffold the business document with the altitude\n * `capability.requirement-service-scoped` exists to refuse.\n */\nexport const CAPABILITY_REQUIREMENT_SENTINEL = \"TODO — name the promise\";\n\n/** The scaffolded scenario heading, shared by both spec axes. */\nexport const SCENARIO_SENTINEL = \"TODO — name the case\";\n\n/** The scaffolded description on a `--new-service` element in delta.likec4. */\nexport const SERVICE_DESCRIPTION_SENTINEL = \"TODO — what this service owns\";\n\n/** The scaffolded SHALL fill-in on the business axis. */\nexport const SHALL_SENTINEL = \"<observable behaviour, testable without reading the code>\";\n\n/** The scaffolded SHALL fill-in on the architecture axis. */\nexport const ARCH_SHALL_SENTINEL = \"<the operational/integration behaviour, observable in test>\";\n\n/** The scaffolded SHALL fill-in on the business axis — a promise, not a mechanism. */\nexport const CAPABILITY_SHALL_SENTINEL = \"<the promise, observable outside the fleet — a customer could check it>\";\n\n/** The scaffolded Given/When/Then fill-ins, shared by both spec axes. Three\n * constants rather than one keyed object because a `then` property makes an\n * object thenable — one accidental `await` away from a hang the linter\n * refuses outright. */\nexport const GIVEN_SENTINEL = \"<the starting state>\";\nexport const WHEN_SENTINEL = \"<the trigger>\";\nexport const THEN_SENTINEL = \"<the observable outcome>\";\n\n/**\n * The angle-bracket fill-ins inside scaffolded requirement bodies and steps.\n * Any one of them surviving to archive means the line around it was never\n * authored — the brackets are the template telling the author what to replace.\n */\nexport const BODY_SENTINELS: readonly string[] = [\n SHALL_SENTINEL,\n ARCH_SHALL_SENTINEL,\n CAPABILITY_SHALL_SENTINEL,\n GIVEN_SENTINEL,\n WHEN_SENTINEL,\n THEN_SENTINEL,\n];\n\n/**\n * Every scaffolded heading a requirement or scenario may still carry.\n *\n * The list is not partitioned by which document scaffolded which string, and\n * that is deliberate: the gate asks \"did anybody author this line\", and a\n * capability heading pasted into a service delta was authored exactly as little\n * as it was in the document it came from.\n */\nexport const HEADING_SENTINELS: readonly string[] = [\n REQUIREMENT_SENTINEL,\n ARCH_REQUIREMENT_SENTINEL,\n CAPABILITY_REQUIREMENT_SENTINEL,\n SCENARIO_SENTINEL,\n];\n"]}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The services a feature's delta CALLS an operation on — the target of every
3
+ * tagged edge carrying `metadata { op }`, resolved the way coherence's own
4
+ * edge checks resolve it.
5
+ *
6
+ * Read for `loam status` (`core/status/contracts.ts`), which has to know
7
+ * whether a service is owed an `openapi.yaml` and used to answer "always" for a
8
+ * service the living docs had never heard of. An op-linked edge into a service
9
+ * is one of the two things that make its contract owed: `c4-api.op-undefined`
10
+ * grades exactly these edges against that file, so a service called this way
11
+ * with no contract is a gate error, and a status that said "none owed" beside
12
+ * it would contradict its own payload.
13
+ *
14
+ * A second reading of the join `../coherence.ts`'s edge loop makes, not that
15
+ * loop itself: the loop grades each edge as it goes and keeps no list. It lives
16
+ * beside it so the two are read together — the same tagged edges, the same
17
+ * fleet set handed to the same resolver — and a change to which edges the gate
18
+ * grades has to be made in both.
19
+ *
20
+ * Through the context's memo: both status forms have already parsed the delta
21
+ * by the time they ask (the coherence pass, the dependency graph), so this is
22
+ * a lookup, never a second LikeC4 workspace.
23
+ */
24
+ import { existsSync } from "node:fs";
25
+ import { serviceResolver } from "../../c4/resolve/service.js";
26
+ import { featurePaths } from "../../repo/paths.js";
27
+ import { featureSpecServices } from "../../repo/repo.js";
28
+ import { enumeratedServiceIds } from "../../repo/service-target.js";
29
+ /**
30
+ * The called services, or `unreadable` when the delta does not parse — which
31
+ * is not "nobody is called": loam could not look, and `delta.invalid` already
32
+ * gates. A feature with no delta calls nobody.
33
+ */
34
+ export async function calledServices(scope, context) {
35
+ const { docsDir, featureDir, featureId } = scope;
36
+ const deltaPath = featurePaths(featureDir).delta;
37
+ if (!existsSync(deltaPath))
38
+ return new Set();
39
+ const doc = await context.loadLikeC4(deltaPath);
40
+ if (doc.errors.length > 0)
41
+ return "unreadable";
42
+ // The same fleet set coherence hands its resolver — `services/` plus this
43
+ // feature's own `specs/` — so an edge into a modelled container names the
44
+ // service that owns it, never a service called after the container.
45
+ const known = new Set([
46
+ ...(await enumeratedServiceIds(docsDir, context)),
47
+ ...(await featureSpecServices(featureDir, context)),
48
+ ]);
49
+ const svcOf = serviceResolver(doc.elements, known);
50
+ return new Set(doc.relationships.filter((r) => r.op !== undefined && r.tags.includes(featureId)).map((r) => svcOf(r.target)));
51
+ }
52
+ //# sourceMappingURL=called.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"called.js","sourceRoot":"","sources":["../../../../src/core/coherence/calls/called.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAAiB,EAAE,OAAqB;IAC3E,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IACjD,MAAM,SAAS,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC;IACjD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,GAAG,EAAE,CAAC;IAC7C,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAChD,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IAC/C,0EAA0E;IAC1E,0EAA0E;IAC1E,oEAAoE;IACpE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS;QAC5B,GAAG,CAAC,MAAM,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACjD,GAAG,CAAC,MAAM,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;KACpD,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACnD,OAAO,IAAI,GAAG,CACZ,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAC9G,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The services a feature's delta CALLS an operation on — the target of every\n * tagged edge carrying `metadata { op }`, resolved the way coherence's own\n * edge checks resolve it.\n *\n * Read for `loam status` (`core/status/contracts.ts`), which has to know\n * whether a service is owed an `openapi.yaml` and used to answer \"always\" for a\n * service the living docs had never heard of. An op-linked edge into a service\n * is one of the two things that make its contract owed: `c4-api.op-undefined`\n * grades exactly these edges against that file, so a service called this way\n * with no contract is a gate error, and a status that said \"none owed\" beside\n * it would contradict its own payload.\n *\n * A second reading of the join `../coherence.ts`'s edge loop makes, not that\n * loop itself: the loop grades each edge as it goes and keeps no list. It lives\n * beside it so the two are read together — the same tagged edges, the same\n * fleet set handed to the same resolver — and a change to which edges the gate\n * grades has to be made in both.\n *\n * Through the context's memo: both status forms have already parsed the delta\n * by the time they ask (the coherence pass, the dependency graph), so this is\n * a lookup, never a second LikeC4 workspace.\n */\nimport { existsSync } from \"node:fs\";\nimport { serviceResolver } from \"../../c4/resolve/service.js\";\nimport type { FleetContext } from \"../../fleet-context.js\";\nimport { featurePaths } from \"../../repo/paths.js\";\nimport { featureSpecServices } from \"../../repo/repo.js\";\nimport { enumeratedServiceIds } from \"../../repo/service-target.js\";\nimport type { DeltaScope } from \"../declared.js\";\n\n/**\n * The called services, or `unreadable` when the delta does not parse — which\n * is not \"nobody is called\": loam could not look, and `delta.invalid` already\n * gates. A feature with no delta calls nobody.\n */\nexport async function calledServices(scope: DeltaScope, context: FleetContext): Promise<ReadonlySet<string> | \"unreadable\"> {\n const { docsDir, featureDir, featureId } = scope;\n const deltaPath = featurePaths(featureDir).delta;\n if (!existsSync(deltaPath)) return new Set();\n const doc = await context.loadLikeC4(deltaPath);\n if (doc.errors.length > 0) return \"unreadable\";\n // The same fleet set coherence hands its resolver — `services/` plus this\n // feature's own `specs/` — so an edge into a modelled container names the\n // service that owns it, never a service called after the container.\n const known = new Set<string>([\n ...(await enumeratedServiceIds(docsDir, context)),\n ...(await featureSpecServices(featureDir, context)),\n ]);\n const svcOf = serviceResolver(doc.elements, known);\n return new Set(\n doc.relationships.filter((r) => r.op !== undefined && r.tags.includes(featureId)).map((r) => svcOf(r.target)),\n );\n}\n"]}