@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
@@ -23,19 +23,56 @@
23
23
  * this walk would close the exact package cycle `scripts/package-graph.mjs`
24
24
  * exists to refuse.
25
25
  *
26
+ * A SERVICE'S OWN SUBDIRECTORIES ARE NEVER CLASSIFIED (`RESERVED_INTERIOR`).
27
+ * That is a different question from `isServiceArtifact`, which asks whether a
28
+ * name makes the directory holding it a service; this asks whether a child
29
+ * directory may be READ as one at all, and the two answers diverged the moment
30
+ * SCHEMA's service layout grew a slot the artifact table does not carry. A
31
+ * `services/<svc>/` holding only the documented `usecases/` flow directory was
32
+ * classified as a group: the walk descended, filed `usecases` as a service, and
33
+ * one `loam validate --all` reported `subsystem.unmarked`,
34
+ * `landscape.service-unmodelled` for `services/<svc>/usecases/` and
35
+ * `landscape.binding-unknown` for the real service — three errors whose text
36
+ * says the fleet is losing services, earned by following the instruction `loam
37
+ * init` had just scaffolded (verification 2026-09-04). The names are SCHEMA's
38
+ * service layout, and a directory named for one is service interior wherever it
39
+ * sits, so no branch of the classification descends into one.
40
+ *
26
41
  * Cost: exactly one readdir per directory walked, service directories included
27
42
  * (their own listing is what classifies them), plus one realpath per directory
28
43
  * for the symlink-cycle guard — `entryIs` follows symlinks on purpose, and a
29
44
  * followed cycle without the visited-set is an unbounded recursion.
30
45
  */
31
- import { realpathSync } from "node:fs";
32
46
  import { readdir } from "node:fs/promises";
33
47
  import { join } from "node:path";
34
48
  import { serviceDirOf, serviceTreePath } from "../../kernel/ids/dirs.js";
35
49
  import { rawServiceId } from "../../kernel/ids/service.js";
36
50
  import { subsystemNameProblem } from "../../kernel/ids/subsystem.js";
37
- import { entryIs } from "./fs.js";
51
+ import { entryIs, markDescended, subdirNames } from "./fs.js";
38
52
  import { readSubsystemMarker, SUBSYSTEM_MARKER } from "./marker.js";
53
+ /**
54
+ * The subdirectory names SCHEMA's service layout reserves for a service's own
55
+ * interior (`SCHEMA.md`, "services/<svc>/"): `adrs/` the decisions, `usecases/`
56
+ * the flows drawn over its containers, `ui/` the page-specs. A directory with
57
+ * one of these names is never a service and never a subsystem — it is inside
58
+ * one — so the walk neither classifies it nor counts it as a subdirectory that
59
+ * makes its parent a group.
60
+ *
61
+ * Kept here rather than folded into `isServiceArtifact` because the two
62
+ * questions are not the same one: `adrs/` answers both (it is an artifact AND
63
+ * interior) and `usecases/` answers only this one today. Widening the injected
64
+ * artifact table instead would also make a lone `usecases/` classify its parent
65
+ * as a service by ARTIFACT, which hides a real service filed beside it — the
66
+ * silent shrink this module refuses.
67
+ *
68
+ * Exported for the one other reader of this rule: `core/diff/base-state.ts`
69
+ * classifies a base git ref's tree listing, which no walk can readdir, and its
70
+ * banner already commits it to restating this module's branches rather than
71
+ * inventing its own. It shares the SET for the reason it shares the artifact
72
+ * table — a name respelled there is the drift both modules are written to
73
+ * avoid.
74
+ */
75
+ export const RESERVED_INTERIOR = new Set(["adrs", "usecases", "ui"]);
39
76
  export async function walkServicesTree(request) {
40
77
  const ctx = {
41
78
  request,
@@ -44,48 +81,27 @@ export async function walkServicesTree(request) {
44
81
  names: new Map(),
45
82
  };
46
83
  const root = join(request.docsDir, "services");
47
- guardDescent(root, ctx);
84
+ markDescended(root, ctx.visited);
48
85
  for (const name of await subdirNames(root)) {
49
86
  await visitDir({ dir: join(root, name), name, chain: [] }, ctx);
50
87
  }
51
88
  namespaceFindings(ctx);
52
89
  return ctx.tree;
53
90
  }
54
- /**
55
- * Mark a directory as descended-into; false when it (or another name for it)
56
- * already was. Guards SUBTREES, not entries: a directory reachable under two
57
- * names is classified twice — both names exist and hiding the second would be
58
- * the silent shrink this module refuses — but its children are enumerated
59
- * once, so a symlink cycle terminates instead of recursing forever. A path
60
- * realpath cannot resolve is one whose children cannot be read either.
61
- */
62
- function guardDescent(dir, ctx) {
63
- let real;
64
- try {
65
- real = realpathSync(dir);
66
- }
67
- catch {
68
- return false;
69
- }
70
- if (ctx.visited.has(real))
71
- return false;
72
- ctx.visited.add(real);
73
- return true;
74
- }
75
- /** Sorted non-dot subdirectory names — the walk's deterministic child order. */
76
- async function subdirNames(dir) {
77
- const entries = await readdir(dir, { withFileTypes: true });
78
- return entries.filter((e) => !e.name.startsWith(".") && entryIs(dir, e, "dir")).map((e) => e.name).sort();
79
- }
80
91
  /** Classify one directory and, for the group kinds, walk what is inside it. */
81
92
  async function visitDir(spot, ctx) {
82
93
  const rel = ["services", ...spot.chain, spot.name].join("/");
83
94
  const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith("."));
84
95
  const files = entries.filter((e) => entryIs(spot.dir, e, "file")).map((e) => e.name);
85
- const dirs = entries.filter((e) => entryIs(spot.dir, e, "dir")).map((e) => e.name).sort();
96
+ const all = entries.filter((e) => entryIs(spot.dir, e, "dir")).map((e) => e.name).sort();
97
+ // The classifiable children — a service's own interior removed once, here, so
98
+ // every branch below agrees about it (see `RESERVED_INTERIOR`). `all` is still
99
+ // what the artifact probe reads: `adrs/` makes its parent a service, and
100
+ // `usecases/` is interior the parent may hold without becoming a group.
101
+ const dirs = all.filter((d) => !RESERVED_INTERIOR.has(d));
86
102
  const marker = files.includes(SUBSYSTEM_MARKER);
87
103
  const artifact = files.some((f) => ctx.request.isServiceArtifact(f, "file")) ||
88
- dirs.some((d) => ctx.request.isServiceArtifact(d, "dir"));
104
+ all.some((d) => ctx.request.isServiceArtifact(d, "dir"));
89
105
  if (marker && artifact) {
90
106
  // Artifacts win the classification: the documents are somebody's work and
91
107
  // the marker is one stray file, so the directory stays an addressable
@@ -176,7 +192,12 @@ async function visitSubsystem(spot, at, ctx) {
176
192
  */
177
193
  async function descendInto(spot, dirs, ctx) {
178
194
  const before = ctx.tree.services.length;
179
- if (guardDescent(spot.dir, ctx)) {
195
+ // The guard (`./fs.ts`'s `markDescended`) protects SUBTREES, not entries: a
196
+ // directory reachable under two names is classified twice — both names
197
+ // exist and hiding the second would be the silent shrink this module
198
+ // refuses — but its children are enumerated once, so a symlink cycle
199
+ // terminates instead of recursing forever.
200
+ if (markDescended(spot.dir, ctx.visited)) {
180
201
  for (const child of dirs) {
181
202
  await visitDir({ dir: join(spot.dir, child), name: child, chain: [...spot.chain, spot.name] }, ctx);
182
203
  }
@@ -1 +1 @@
1
- {"version":3,"file":"walk.js","sourceRoot":"","sources":["../../../../src/core/repo/tree/walk.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The tree read model: three-way classification of everything under\n * `services/`, in one walk.\n *\n * A directory holding `subsystem.yaml` is a SUBSYSTEM and is walked; one\n * holding any service artifact is a SERVICE and is not walked deeper — unless\n * a marker sits BESIDE the artifacts, where the walk descends anyway (the\n * marker says a group was here, and its members must not vanish); one holding\n * neither while containing subdirectories is an ERROR that still names every\n * service found beneath it. The third branch is the whole point:\n * two-way classification loses a service silently on an ordinary clean merge —\n * one branch deletes an emptied subsystem's marker while another moves a\n * service into it, and the group directory is then read as a service whose\n * real services vanish from the fleet with no finding. No directory under\n * `services/` may ever be reinterpreted as a different kind without saying so,\n * and the fleet is never reported smaller: the walk descends into the broken\n * group precisely so the stranded services stay enumerated and named.\n *\n * The package direction is `repo → repo/tree`, never back. That is why the\n * artifact-name question arrives INJECTED (`TreeWalkRequest.isServiceArtifact`\n * — `repo/paths.ts` owns the spellings and `repo/repo.ts` builds the request):\n * importing `paths.ts` from here while `repo.ts` delegates its enumeration to\n * this walk would close the exact package cycle `scripts/package-graph.mjs`\n * exists to refuse.\n *\n * Cost: exactly one readdir per directory walked, service directories included\n * (their own listing is what classifies them), plus one realpath per directory\n * for the symlink-cycle guard — `entryIs` follows symlinks on purpose, and a\n * followed cycle without the visited-set is an unbounded recursion.\n */\nimport { realpathSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { serviceDirOf, serviceTreePath, type DocsDir, type ServiceDir } from \"../../kernel/ids/dirs.js\";\nimport { rawServiceId, type RawServiceId } from \"../../kernel/ids/service.js\";\nimport { subsystemNameProblem } from \"../../kernel/ids/subsystem.js\";\nimport type { Finding } from \"../../vocabulary/report.js\";\nimport { entryIs } from \"./fs.js\";\nimport { readSubsystemMarker, SUBSYSTEM_MARKER, type SubsystemMeta } from \"./marker.js\";\n\nexport interface WalkedService {\n id: RawServiceId;\n dir: ServiceDir;\n /**\n * Names of the directories between `services/` and the service, outermost\n * first. Empty = unfiled — directly under `services/`, which is permanent,\n * normal and SILENT. For a service stranded under an unmarked group the\n * chain still records the physical placement: the path is a fact even while\n * `subsystem.unmarked` says the grouping is broken.\n */\n subsystem: string[];\n}\n\nexport interface SubsystemEntry {\n /** The directory name — the subsystem's name in the flat namespace. */\n name: string;\n /** Names from `services/` down to and including this one. */\n path: string[];\n /** Absolute path of the subsystem directory. */\n dir: string;\n meta: SubsystemMeta;\n}\n\nexport interface FleetTree {\n services: WalkedService[];\n subsystems: SubsystemEntry[];\n findings: Finding[];\n}\n\nexport interface TreeWalkRequest {\n docsDir: DocsDir;\n /** Does this entry name a service artifact? `repo/paths.ts`'s table, injected — the module doc says why. */\n isServiceArtifact: (name: string, kind: \"file\" | \"dir\") => boolean;\n}\n\ninterface WalkContext {\n request: TreeWalkRequest;\n tree: FleetTree;\n /** Realpaths already DESCENDED into — the symlink-cycle guard. */\n visited: Set<string>;\n /** Every name in the flat namespace → the directories that claim it. */\n names: Map<string, string[]>;\n}\n\n/** One directory the walk is looking at: where it is, and how it is reached. */\ninterface Spot {\n dir: string;\n name: string;\n /** Directory names between `services/` and this one, outermost first. */\n chain: string[];\n}\n\nexport async function walkServicesTree(request: TreeWalkRequest): Promise<FleetTree> {\n const ctx: WalkContext = {\n request,\n tree: { services: [], subsystems: [], findings: [] },\n visited: new Set(),\n names: new Map(),\n };\n const root = join(request.docsDir, \"services\");\n guardDescent(root, ctx);\n for (const name of await subdirNames(root)) {\n await visitDir({ dir: join(root, name), name, chain: [] }, ctx);\n }\n namespaceFindings(ctx);\n return ctx.tree;\n}\n\n/**\n * Mark a directory as descended-into; false when it (or another name for it)\n * already was. Guards SUBTREES, not entries: a directory reachable under two\n * names is classified twice — both names exist and hiding the second would be\n * the silent shrink this module refuses — but its children are enumerated\n * once, so a symlink cycle terminates instead of recursing forever. A path\n * realpath cannot resolve is one whose children cannot be read either.\n */\nfunction guardDescent(dir: string, ctx: WalkContext): boolean {\n let real: string;\n try {\n real = realpathSync(dir);\n } catch {\n return false;\n }\n if (ctx.visited.has(real)) return false;\n ctx.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/** Classify one directory and, for the group kinds, walk what is inside it. */\nasync function visitDir(spot: Spot, ctx: WalkContext): Promise<void> {\n const rel = [\"services\", ...spot.chain, spot.name].join(\"/\");\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n const files = entries.filter((e) => entryIs(spot.dir, e, \"file\")).map((e) => e.name);\n const dirs = entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort();\n const marker = files.includes(SUBSYSTEM_MARKER);\n const artifact =\n files.some((f) => ctx.request.isServiceArtifact(f, \"file\")) ||\n dirs.some((d) => ctx.request.isServiceArtifact(d, \"dir\"));\n\n if (marker && artifact) {\n // Artifacts win the classification: the documents are somebody's work and\n // the marker is one stray file, so the directory stays an addressable\n // service and the finding names the contradiction instead of the walk\n // quietly picking a reading. But the walk STILL descends (the module\n // invariant: no reinterpretation may shrink the fleet) — the marker says\n // a GROUP was here, and returning without walking made every service\n // beneath it vanish with only this finding to hint why. Child directories\n // that are themselves artifacts (`adrs/`) are service interior, not walked.\n recordService(spot, ctx);\n const stranded = await descendInto(spot, dirs.filter((d) => !ctx.request.isServiceArtifact(d, \"dir\")), ctx);\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.marker-misplaced\",\n subject: spot.name,\n message:\n `${rel}/ holds ${SUBSYSTEM_MARKER} BESIDE service artifacts — a directory is a subsystem or a service, never both. ` +\n `It stays classified as a service; delete ${rel}/${SUBSYSTEM_MARKER}, ` +\n `or move the artifacts into a service directory of their own inside the group.` +\n (stranded.length > 0 ? ` ${stranded.length} service(s) beneath it stay enumerated meanwhile: ${stranded.map((s) => s.id).join(\", \")}.` : \"\"),\n ...(stranded.length > 0 ? { details: stranded.map(serviceTreePath) } : {}),\n });\n return;\n }\n if (marker) {\n await visitSubsystem(spot, { rel, dirs }, ctx);\n return;\n }\n if (artifact) {\n recordService(spot, ctx);\n return;\n }\n if (dirs.length > 0) {\n await visitUnmarked(spot, { rel, dirs }, ctx);\n return;\n }\n // Neither marker nor artifact, and nothing beneath: today's flat behaviour —\n // an empty (or not-yet-adopted) directory under services/ is a service.\n recordService(spot, ctx);\n}\n\nfunction recordService(spot: Spot, ctx: WalkContext): void {\n ctx.tree.services.push({\n id: rawServiceId(spot.name),\n dir: serviceDirOf(spot.dir),\n subsystem: spot.chain,\n });\n claimName(spot, ctx);\n}\n\n/** A subsystem: record it, report a broken marker or an illegal name, walk in. */\nasync function visitSubsystem(spot: Spot, at: { rel: string; dirs: string[] }, ctx: WalkContext): Promise<void> {\n const meta = await readSubsystemMarker(join(spot.dir, SUBSYSTEM_MARKER));\n if (meta.invalid !== undefined) {\n // Exactly one finding per marker file: presence already classified the\n // directory, so the content failure is metadata, not identity.\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.invalid\",\n subject: at.rel,\n message:\n `${at.rel}/${SUBSYSTEM_MARKER} cannot be read as subsystem metadata — ${meta.invalid}. ` +\n `The directory still counts as a subsystem (presence classifies; content is metadata). ` +\n `Fix the file: a YAML mapping with optional title, description and owner — an empty file is also a valid marker.`,\n });\n }\n const problem = subsystemNameProblem(spot.name, \"subsystem directory name\");\n if (problem !== null) {\n // Walked anyway, mirroring how an illegal service id stays enumerated\n // (`idProblem`): the directory exists and hiding its subtree would shrink\n // the fleet over a naming mistake.\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.name-invalid\",\n subject: spot.name,\n message: `${at.rel}/ — ${problem} Rename the directory; its services stay enumerated meanwhile.`,\n });\n }\n ctx.tree.subsystems.push({\n name: spot.name,\n path: [...spot.chain, spot.name],\n dir: spot.dir,\n meta,\n });\n claimName(spot, ctx);\n await descendInto(spot, at.dirs, ctx);\n}\n\n/**\n * Walk the children of a group directory (symlink-cycle guarded), returning\n * the services newly recorded beneath it — the ONE spelling of descent, so\n * the three descending branches agree about the guard and the chain.\n */\nasync function descendInto(spot: Spot, dirs: string[], ctx: WalkContext): Promise<WalkedService[]> {\n const before = ctx.tree.services.length;\n if (guardDescent(spot.dir, ctx)) {\n for (const child of dirs) {\n await visitDir({ dir: join(spot.dir, child), name: child, chain: [...spot.chain, spot.name] }, ctx);\n }\n }\n return ctx.tree.services.slice(before);\n}\n\n/**\n * The refusal branch — a group directory nothing marks. The walk STILL\n * descends, so the services beneath stay in the enumeration and the finding\n * can name them: the fleet must never be reported smaller than it is.\n */\nasync function visitUnmarked(spot: Spot, at: { rel: string; dirs: string[] }, ctx: WalkContext): Promise<void> {\n const stranded = await descendInto(spot, at.dirs, ctx);\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.unmarked\",\n subject: at.rel,\n message:\n `${at.rel}/ is neither a subsystem (no ${SUBSYSTEM_MARKER}) nor a service (no service artifact), ` +\n `but ${stranded.length} service(s) sit beneath it` +\n (stranded.length > 0 ? `: ${stranded.map((s) => s.id).join(\", \")}. ` : `. `) +\n `Without the marker this directory would be read as a service and everything under it would vanish from the fleet — ` +\n `the shape an ordinary merge produces when one branch deletes an emptied subsystem's marker while another moves a service in. ` +\n `Restore ${at.rel}/${SUBSYSTEM_MARKER} (an empty file is a valid marker), or move the directories out.`,\n ...(stranded.length > 0 ? { details: stranded.map(serviceTreePath) } : {}),\n });\n}\n\nfunction claimName(spot: Spot, ctx: WalkContext): void {\n const rel = [\"services\", ...spot.chain, spot.name].join(\"/\");\n ctx.names.set(spot.name, [...(ctx.names.get(spot.name) ?? []), rel]);\n}\n\n/**\n * One flat namespace: every subsystem name and service id, unique across the\n * whole tree at any depth, never colliding with each other. The identity\n * decision demands it — a service id is its leaf directory name ALONE, so two\n * leaves spelling one name are one identity claimed twice, wherever they sit.\n * One finding per name, naming every claimant, so the fix is a choice rather\n * than a scavenger hunt.\n */\nfunction namespaceFindings(ctx: WalkContext): void {\n for (const [name, dirs] of [...ctx.names].sort(([a], [b]) => (a < b ? -1 : 1))) {\n if (dirs.length < 2) continue;\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.name-collision\",\n subject: name,\n message:\n `'${name}' is claimed by ${dirs.length} directories in the services/ tree: ${dirs.join(\", \")} — ` +\n `service ids and subsystem names share one flat namespace, unique at any depth, ` +\n `because a service's identity is its leaf directory name alone and placement never disambiguates it. ` +\n `Rename all but one.`,\n details: dirs,\n });\n }\n}\n"]}
1
+ {"version":3,"file":"walk.js","sourceRoot":"","sources":["../../../../src/core/repo/tree/walk.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The tree read model: three-way classification of everything under\n * `services/`, in one walk.\n *\n * A directory holding `subsystem.yaml` is a SUBSYSTEM and is walked; one\n * holding any service artifact is a SERVICE and is not walked deeper — unless\n * a marker sits BESIDE the artifacts, where the walk descends anyway (the\n * marker says a group was here, and its members must not vanish); one holding\n * neither while containing subdirectories is an ERROR that still names every\n * service found beneath it. The third branch is the whole point:\n * two-way classification loses a service silently on an ordinary clean merge —\n * one branch deletes an emptied subsystem's marker while another moves a\n * service into it, and the group directory is then read as a service whose\n * real services vanish from the fleet with no finding. No directory under\n * `services/` may ever be reinterpreted as a different kind without saying so,\n * and the fleet is never reported smaller: the walk descends into the broken\n * group precisely so the stranded services stay enumerated and named.\n *\n * The package direction is `repo → repo/tree`, never back. That is why the\n * artifact-name question arrives INJECTED (`TreeWalkRequest.isServiceArtifact`\n * — `repo/paths.ts` owns the spellings and `repo/repo.ts` builds the request):\n * importing `paths.ts` from here while `repo.ts` delegates its enumeration to\n * this walk would close the exact package cycle `scripts/package-graph.mjs`\n * exists to refuse.\n *\n * A SERVICE'S OWN SUBDIRECTORIES ARE NEVER CLASSIFIED (`RESERVED_INTERIOR`).\n * That is a different question from `isServiceArtifact`, which asks whether a\n * name makes the directory holding it a service; this asks whether a child\n * directory may be READ as one at all, and the two answers diverged the moment\n * SCHEMA's service layout grew a slot the artifact table does not carry. A\n * `services/<svc>/` holding only the documented `usecases/` flow directory was\n * classified as a group: the walk descended, filed `usecases` as a service, and\n * one `loam validate --all` reported `subsystem.unmarked`,\n * `landscape.service-unmodelled` for `services/<svc>/usecases/` and\n * `landscape.binding-unknown` for the real service — three errors whose text\n * says the fleet is losing services, earned by following the instruction `loam\n * init` had just scaffolded (verification 2026-09-04). The names are SCHEMA's\n * service layout, and a directory named for one is service interior wherever it\n * sits, so no branch of the classification descends into one.\n *\n * Cost: exactly one readdir per directory walked, service directories included\n * (their own listing is what classifies them), plus one realpath per directory\n * for the symlink-cycle guard — `entryIs` follows symlinks on purpose, and a\n * followed cycle without the visited-set is an unbounded recursion.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { serviceDirOf, serviceTreePath, type DocsDir, type ServiceDir } from \"../../kernel/ids/dirs.js\";\nimport { rawServiceId, type RawServiceId } from \"../../kernel/ids/service.js\";\nimport { subsystemNameProblem } from \"../../kernel/ids/subsystem.js\";\nimport type { Finding } from \"../../vocabulary/report.js\";\nimport { entryIs, markDescended, subdirNames } from \"./fs.js\";\nimport { readSubsystemMarker, SUBSYSTEM_MARKER, type SubsystemMeta } from \"./marker.js\";\n\n/**\n * The subdirectory names SCHEMA's service layout reserves for a service's own\n * interior (`SCHEMA.md`, \"services/<svc>/\"): `adrs/` the decisions, `usecases/`\n * the flows drawn over its containers, `ui/` the page-specs. A directory with\n * one of these names is never a service and never a subsystem — it is inside\n * one — so the walk neither classifies it nor counts it as a subdirectory that\n * makes its parent a group.\n *\n * Kept here rather than folded into `isServiceArtifact` because the two\n * questions are not the same one: `adrs/` answers both (it is an artifact AND\n * interior) and `usecases/` answers only this one today. Widening the injected\n * artifact table instead would also make a lone `usecases/` classify its parent\n * as a service by ARTIFACT, which hides a real service filed beside it — the\n * silent shrink this module refuses.\n *\n * Exported for the one other reader of this rule: `core/diff/base-state.ts`\n * classifies a base git ref's tree listing, which no walk can readdir, and its\n * banner already commits it to restating this module's branches rather than\n * inventing its own. It shares the SET for the reason it shares the artifact\n * table — a name respelled there is the drift both modules are written to\n * avoid.\n */\nexport const RESERVED_INTERIOR: ReadonlySet<string> = new Set([\"adrs\", \"usecases\", \"ui\"]);\n\nexport interface WalkedService {\n id: RawServiceId;\n dir: ServiceDir;\n /**\n * Names of the directories between `services/` and the service, outermost\n * first. Empty = unfiled — directly under `services/`, which is permanent,\n * normal and SILENT. For a service stranded under an unmarked group the\n * chain still records the physical placement: the path is a fact even while\n * `subsystem.unmarked` says the grouping is broken.\n */\n subsystem: string[];\n}\n\nexport interface SubsystemEntry {\n /** The directory name — the subsystem's name in the flat namespace. */\n name: string;\n /** Names from `services/` down to and including this one. */\n path: string[];\n /** Absolute path of the subsystem directory. */\n dir: string;\n meta: SubsystemMeta;\n}\n\nexport interface FleetTree {\n services: WalkedService[];\n subsystems: SubsystemEntry[];\n findings: Finding[];\n}\n\nexport interface TreeWalkRequest {\n docsDir: DocsDir;\n /** Does this entry name a service artifact? `repo/paths.ts`'s table, injected — the module doc says why. */\n isServiceArtifact: (name: string, kind: \"file\" | \"dir\") => boolean;\n}\n\ninterface WalkContext {\n request: TreeWalkRequest;\n tree: FleetTree;\n /** Realpaths already DESCENDED into — the symlink-cycle guard. */\n visited: Set<string>;\n /** Every name in the flat namespace → the directories that claim it. */\n names: Map<string, string[]>;\n}\n\n/** One directory the walk is looking at: where it is, and how it is reached. */\ninterface Spot {\n dir: string;\n name: string;\n /** Directory names between `services/` and this one, outermost first. */\n chain: string[];\n}\n\nexport async function walkServicesTree(request: TreeWalkRequest): Promise<FleetTree> {\n const ctx: WalkContext = {\n request,\n tree: { services: [], subsystems: [], findings: [] },\n visited: new Set(),\n names: new Map(),\n };\n const root = join(request.docsDir, \"services\");\n markDescended(root, ctx.visited);\n for (const name of await subdirNames(root)) {\n await visitDir({ dir: join(root, name), name, chain: [] }, ctx);\n }\n namespaceFindings(ctx);\n return ctx.tree;\n}\n\n/** Classify one directory and, for the group kinds, walk what is inside it. */\nasync function visitDir(spot: Spot, ctx: WalkContext): Promise<void> {\n const rel = [\"services\", ...spot.chain, spot.name].join(\"/\");\n const entries = (await readdir(spot.dir, { withFileTypes: true })).filter((e) => !e.name.startsWith(\".\"));\n const files = entries.filter((e) => entryIs(spot.dir, e, \"file\")).map((e) => e.name);\n const all = entries.filter((e) => entryIs(spot.dir, e, \"dir\")).map((e) => e.name).sort();\n // The classifiable children — a service's own interior removed once, here, so\n // every branch below agrees about it (see `RESERVED_INTERIOR`). `all` is still\n // what the artifact probe reads: `adrs/` makes its parent a service, and\n // `usecases/` is interior the parent may hold without becoming a group.\n const dirs = all.filter((d) => !RESERVED_INTERIOR.has(d));\n const marker = files.includes(SUBSYSTEM_MARKER);\n const artifact =\n files.some((f) => ctx.request.isServiceArtifact(f, \"file\")) ||\n all.some((d) => ctx.request.isServiceArtifact(d, \"dir\"));\n\n if (marker && artifact) {\n // Artifacts win the classification: the documents are somebody's work and\n // the marker is one stray file, so the directory stays an addressable\n // service and the finding names the contradiction instead of the walk\n // quietly picking a reading. But the walk STILL descends (the module\n // invariant: no reinterpretation may shrink the fleet) — the marker says\n // a GROUP was here, and returning without walking made every service\n // beneath it vanish with only this finding to hint why. Child directories\n // that are themselves artifacts (`adrs/`) are service interior, not walked.\n recordService(spot, ctx);\n const stranded = await descendInto(spot, dirs.filter((d) => !ctx.request.isServiceArtifact(d, \"dir\")), ctx);\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.marker-misplaced\",\n subject: spot.name,\n message:\n `${rel}/ holds ${SUBSYSTEM_MARKER} BESIDE service artifacts — a directory is a subsystem or a service, never both. ` +\n `It stays classified as a service; delete ${rel}/${SUBSYSTEM_MARKER}, ` +\n `or move the artifacts into a service directory of their own inside the group.` +\n (stranded.length > 0 ? ` ${stranded.length} service(s) beneath it stay enumerated meanwhile: ${stranded.map((s) => s.id).join(\", \")}.` : \"\"),\n ...(stranded.length > 0 ? { details: stranded.map(serviceTreePath) } : {}),\n });\n return;\n }\n if (marker) {\n await visitSubsystem(spot, { rel, dirs }, ctx);\n return;\n }\n if (artifact) {\n recordService(spot, ctx);\n return;\n }\n if (dirs.length > 0) {\n await visitUnmarked(spot, { rel, dirs }, ctx);\n return;\n }\n // Neither marker nor artifact, and nothing beneath: today's flat behaviour —\n // an empty (or not-yet-adopted) directory under services/ is a service.\n recordService(spot, ctx);\n}\n\nfunction recordService(spot: Spot, ctx: WalkContext): void {\n ctx.tree.services.push({\n id: rawServiceId(spot.name),\n dir: serviceDirOf(spot.dir),\n subsystem: spot.chain,\n });\n claimName(spot, ctx);\n}\n\n/** A subsystem: record it, report a broken marker or an illegal name, walk in. */\nasync function visitSubsystem(spot: Spot, at: { rel: string; dirs: string[] }, ctx: WalkContext): Promise<void> {\n const meta = await readSubsystemMarker(join(spot.dir, SUBSYSTEM_MARKER));\n if (meta.invalid !== undefined) {\n // Exactly one finding per marker file: presence already classified the\n // directory, so the content failure is metadata, not identity.\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.invalid\",\n subject: at.rel,\n message:\n `${at.rel}/${SUBSYSTEM_MARKER} cannot be read as subsystem metadata — ${meta.invalid}. ` +\n `The directory still counts as a subsystem (presence classifies; content is metadata). ` +\n `Fix the file: a YAML mapping with optional title, description and owner — an empty file is also a valid marker.`,\n });\n }\n const problem = subsystemNameProblem(spot.name, \"subsystem directory name\");\n if (problem !== null) {\n // Walked anyway, mirroring how an illegal service id stays enumerated\n // (`idProblem`): the directory exists and hiding its subtree would shrink\n // the fleet over a naming mistake.\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.name-invalid\",\n subject: spot.name,\n message: `${at.rel}/ — ${problem} Rename the directory; its services stay enumerated meanwhile.`,\n });\n }\n ctx.tree.subsystems.push({\n name: spot.name,\n path: [...spot.chain, spot.name],\n dir: spot.dir,\n meta,\n });\n claimName(spot, ctx);\n await descendInto(spot, at.dirs, ctx);\n}\n\n/**\n * Walk the children of a group directory (symlink-cycle guarded), returning\n * the services newly recorded beneath it — the ONE spelling of descent, so\n * the three descending branches agree about the guard and the chain.\n */\nasync function descendInto(spot: Spot, dirs: string[], ctx: WalkContext): Promise<WalkedService[]> {\n const before = ctx.tree.services.length;\n // The guard (`./fs.ts`'s `markDescended`) protects SUBTREES, not entries: a\n // directory reachable under two names is classified twice — both names\n // exist and hiding the second would be the silent shrink this module\n // refuses — but its children are enumerated once, so a symlink cycle\n // terminates instead of recursing forever.\n if (markDescended(spot.dir, ctx.visited)) {\n for (const child of dirs) {\n await visitDir({ dir: join(spot.dir, child), name: child, chain: [...spot.chain, spot.name] }, ctx);\n }\n }\n return ctx.tree.services.slice(before);\n}\n\n/**\n * The refusal branch — a group directory nothing marks. The walk STILL\n * descends, so the services beneath stay in the enumeration and the finding\n * can name them: the fleet must never be reported smaller than it is.\n */\nasync function visitUnmarked(spot: Spot, at: { rel: string; dirs: string[] }, ctx: WalkContext): Promise<void> {\n const stranded = await descendInto(spot, at.dirs, ctx);\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.unmarked\",\n subject: at.rel,\n message:\n `${at.rel}/ is neither a subsystem (no ${SUBSYSTEM_MARKER}) nor a service (no service artifact), ` +\n `but ${stranded.length} service(s) sit beneath it` +\n (stranded.length > 0 ? `: ${stranded.map((s) => s.id).join(\", \")}. ` : `. `) +\n `Without the marker this directory would be read as a service and everything under it would vanish from the fleet — ` +\n `the shape an ordinary merge produces when one branch deletes an emptied subsystem's marker while another moves a service in. ` +\n `Restore ${at.rel}/${SUBSYSTEM_MARKER} (an empty file is a valid marker), or move the directories out.`,\n ...(stranded.length > 0 ? { details: stranded.map(serviceTreePath) } : {}),\n });\n}\n\nfunction claimName(spot: Spot, ctx: WalkContext): void {\n const rel = [\"services\", ...spot.chain, spot.name].join(\"/\");\n ctx.names.set(spot.name, [...(ctx.names.get(spot.name) ?? []), rel]);\n}\n\n/**\n * One flat namespace: every subsystem name and service id, unique across the\n * whole tree at any depth, never colliding with each other. The identity\n * decision demands it — a service id is its leaf directory name ALONE, so two\n * leaves spelling one name are one identity claimed twice, wherever they sit.\n * One finding per name, naming every claimant, so the fix is a choice rather\n * than a scavenger hunt.\n */\nfunction namespaceFindings(ctx: WalkContext): void {\n for (const [name, dirs] of [...ctx.names].sort(([a], [b]) => (a < b ? -1 : 1))) {\n if (dirs.length < 2) continue;\n ctx.tree.findings.push({\n severity: \"error\",\n code: \"subsystem.name-collision\",\n subject: name,\n message:\n `'${name}' is claimed by ${dirs.length} directories in the services/ tree: ${dirs.join(\", \")} — ` +\n `service ids and subsystem names share one flat namespace, unique at any depth, ` +\n `because a service's identity is its leaf directory name alone and placement never disambiguates it. ` +\n `Rename all but one.`,\n details: dirs,\n });\n }\n}\n"]}
@@ -23,6 +23,10 @@
23
23
  * reports @Before/@After hook results in per-element `before[]`/`after[]`
24
24
  * arrays (a teardown assertion fails there while every step stays `passed`),
25
25
  * and behave carries an element-level `status` that a hook failure flips.
26
+ * cucumber-js puts its hooks among the `steps[]` instead, as entries marked
27
+ * `hidden` with the keyword `Before`/`After`, and they are read as hooks
28
+ * there too — the number of real steps is contract now, because a run is
29
+ * held to the step count of the scenario it claims to be (`runnerAnswers`).
26
30
  * Only those fields are contract: unknown fields, tagless elements
27
31
  * (backgrounds, hand-written scenarios) and stray entries are skipped without
28
32
  * comment. What is NOT tolerated is a file that is not that array at all: a
@@ -127,6 +131,7 @@ export function readScenarioReport(doc, reportName) {
127
131
  where: str(entry["test"]) ?? `(${SCENARIO_REPORT_MARKER} entry ${i})`,
128
132
  digests: [digest],
129
133
  steps: [status],
134
+ stepwise: false,
130
135
  hooks: [],
131
136
  });
132
137
  }
@@ -137,7 +142,8 @@ export function readCucumberReport(doc, reportName) {
137
142
  return {
138
143
  ok: false,
139
144
  message: `${reportName} is not a cucumber JSON report — expected the top-level array of features ` +
140
- "that `cucumber-js --format json` (or cucumber-jvm, behave, SpecFlow) emits.",
145
+ "that `cucumber-js --format json` (or cucumber-jvm, behave, SpecFlow) emits. " +
146
+ `A runner with no cucumber formatter answers through loam's own shape instead: {"${SCENARIO_REPORT_MARKER}": 1, "results": [{"digest": "…", "status": "passed"}]}.`,
141
147
  };
142
148
  }
143
149
  const scenarios = [];
@@ -165,17 +171,24 @@ export function readCucumberReport(doc, reportName) {
165
171
  // scenario, another tool's output. Invisible here, deliberately.
166
172
  if (digests.length === 0)
167
173
  continue;
168
- const resultStatuses = (raw) => {
169
- if (!Array.isArray(raw))
170
- return [];
171
- return raw.map((s) => {
172
- const status = isRecord(s) && isRecord(s["result"]) ? str(s["result"]["status"]) : undefined;
173
- return (status ?? "unknown").toLowerCase();
174
- });
174
+ const statusOf = (s) => {
175
+ const status = isRecord(s) && isRecord(s["result"]) ? str(s["result"]["status"]) : undefined;
176
+ return (status ?? "unknown").toLowerCase();
175
177
  };
176
- const steps = resultStatuses(el["steps"]);
178
+ const resultStatuses = (raw) => (Array.isArray(raw) ? raw.map(statusOf) : []);
179
+ // cucumber-js reports its Before/After hooks INSIDE `steps`, as entries
180
+ // marked `hidden` with the keyword `Before`/`After`; they are hooks, not
181
+ // steps of the scenario, and counting them would make every hooked run
182
+ // look like a different scenario from the one stamped.
183
+ const listed = Array.isArray(el["steps"]) ? el["steps"] : [];
184
+ const isHook = (s) => isRecord(s) && (s["hidden"] === true || /^(before|after)$/i.test(str(s["keyword"])?.trim() ?? ""));
185
+ const steps = listed.filter((s) => !isHook(s)).map(statusOf);
177
186
  // cucumber-jvm's @Before/@After hooks: separate arrays, same result shape.
178
- const hooks = [...resultStatuses(el["before"]), ...resultStatuses(el["after"])];
187
+ const hooks = [
188
+ ...resultStatuses(el["before"]),
189
+ ...listed.filter(isHook).map(statusOf),
190
+ ...resultStatuses(el["after"]),
191
+ ];
179
192
  // behave's element-level status — a hook failure flips it while the
180
193
  // steps that already ran keep "passed".
181
194
  const status = str(el["status"])?.toLowerCase();
@@ -183,6 +196,7 @@ export function readCucumberReport(doc, reportName) {
183
196
  where: `${featureName} › ${str(el["name"]) ?? "(unnamed scenario)"}`,
184
197
  digests,
185
198
  steps,
199
+ stepwise: true,
186
200
  hooks,
187
201
  ...(status === undefined ? {} : { status }),
188
202
  });
@@ -236,6 +250,14 @@ export function contestedDigests(claims) {
236
250
  * occurrence wins as failure, a skipped-only run is "skipped" not green, and
237
251
  * no match at all is "not found in report". A digest two services share is
238
252
  * refused before any of that: see {@link contestedDigests}.
253
+ *
254
+ * And a cucumber occurrence must have run as many steps as the stamped
255
+ * scenario has (`Claim.steps`). The digest is only a tag, and a tag is text:
256
+ * a one-step stub named "a step" carrying the right digest confirmed
257
+ * five-step scenarios, because nothing compared what ran with what was
258
+ * stamped. The count is the check every dialect can be held to; loam's own
259
+ * runner-neutral shape carries no steps to count, so its entries are held to
260
+ * their status alone, as that shape has always promised.
239
261
  */
240
262
  export function runnerAnswers(claims, scenarios, reportName) {
241
263
  const contested = contestedDigests(claims);
@@ -250,13 +272,16 @@ export function runnerAnswers(claims, scenarios, reportName) {
250
272
  const tag = c.digest === undefined ? "a digest tag" : scenarioDigestTag(c.digest);
251
273
  return answer(c.id, "unconfirmed", [], `not found in report — no scenario in ${reportName} carries ${tag}`);
252
274
  }
253
- const failing = runs.filter((r) => !passed(r));
275
+ const failing = runs.filter((r) => !passed(r) || !sameShape(r, c.steps));
254
276
  if (failing.length === 0) {
255
277
  return answer(c.id, "confirmed", runs.map((r) => `${reportName}: ${r.where}`));
256
278
  }
257
279
  const first = failing[0];
258
280
  const rerun = runs.length > 1 ? ` — ${failing.length} of ${runs.length} matching runs failed` : "";
259
- return answer(c.id, "unconfirmed", [], `${reason(first)} (${reportName}: ${first.where})${rerun}`);
281
+ const why = passed(first)
282
+ ? `ran ${first.steps.length} step(s) where the stamped scenario has ${c.steps} — the report's scenario is not the one \`loam gherkin\` stamped with this digest`
283
+ : reason(first);
284
+ return answer(c.id, "unconfirmed", [], `${why} (${reportName}: ${first.where})${rerun}`);
260
285
  });
261
286
  }
262
287
  function answer(id, verdict, evidence, note) {
@@ -276,6 +301,16 @@ function passed(r) {
276
301
  r.hooks.every((h) => h === "passed") &&
277
302
  (r.status === undefined || r.status === "passed"));
278
303
  }
304
+ /**
305
+ * Did this run execute the scenario the claim stamped — as far as the report
306
+ * can say? A cucumber run must have run exactly the stamped number of steps.
307
+ * An entry with no step structure (loam's own shape), and a claim carrying no
308
+ * count (only `scenario.tested` claims carry one, and only they carry a digest
309
+ * to match), are held to their status alone.
310
+ */
311
+ function sameShape(r, expected) {
312
+ return !r.stepwise || expected === undefined || r.steps.length === expected;
313
+ }
279
314
  /** Why a run did not pass, naming the step, hook or element status that says so. */
280
315
  function reason(r) {
281
316
  if (r.steps.length === 0)
@@ -1 +1 @@
1
- {"version":3,"file":"results.js","sourceRoot":"","sources":["../../src/core/results.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam verify --results` — the cucumber report as an answer sheet.\n *\n * `loam gherkin` tags every generated scenario `@loam-digest-<16hex>`, and\n * cucumber's JSON format carries tags per scenario — so a test report says,\n * mechanically, which spec scenarios ran green. This module reads that report\n * and answers `scenario.tested` claims from it. The digest is the ONLY\n * identity: a claim's `digest` and the tag are the same 16 hex of the same\n * body hash, and names match nothing — rewording a scenario breaks the match,\n * which is the point, because an agent must not be able to SAY a scenario is\n * tested; only a green run may.\n *\n * That identity is CONTENT, and content stops one step short of a fleet: two\n * services whose specs word a scenario identically share a digest, so one\n * repository's report matches the other repository's claim. Those claims are\n * refused rather than answered — see {@link contestedDigests}.\n *\n * Parsing is deliberately tolerant of everything except the shape it matches\n * on. The format is what cucumber-js `--format json`, cucumber-jvm, behave and\n * SpecFlow emit — an array of features, each `elements[]` (scenarios) with\n * `name`, `tags[] {name}` and `steps[] {result {status}}`, plus the two\n * places those dialects put a failure the steps never see: cucumber-jvm\n * reports @Before/@After hook results in per-element `before[]`/`after[]`\n * arrays (a teardown assertion fails there while every step stays `passed`),\n * and behave carries an element-level `status` that a hook failure flips.\n * Only those fields are contract: unknown fields, tagless elements\n * (backgrounds, hand-written scenarios) and stray entries are skipped without\n * comment. What is NOT tolerated is a file that is not that array at all: a\n * report loam cannot recognize must refuse rather than quietly answer every\n * claim \"not found\".\n */\nimport { DIGEST_TAG_RE, scenarioDigestTag } from \"./gherkin/stamp.js\";\nimport { isRecord } from \"./kernel/records.js\";\nimport type { Answer } from \"./verify/answers.js\";\nimport type { Claim } from \"./verify/checklist.js\";\n\n/** One report scenario, reduced to what matching needs. */\nexport interface ReportScenario {\n /** `<feature uri or name> › <scenario name>` — where the run can be seen. */\n where: string;\n /** Digests carried as `@loam-digest-…` tags. Usually one; hand-tagging can add more. */\n digests: string[];\n /** Step statuses, lowercased, in order. */\n steps: string[];\n /** Hook statuses from cucumber-jvm's `before[]`/`after[]`, lowercased, in order. */\n hooks: string[];\n /** behave's element-level status, lowercased — absent in the other dialects. */\n status?: string;\n}\n\nexport type ReportRead =\n | { ok: true; scenarios: ReportScenario[] }\n | { ok: false; message: string };\n\n/**\n * Read a parsed cucumber JSON document down to its digest-tagged scenarios.\n * `reportName` is how the caller spelled the file, for the refusal message.\n */\n/** The marker key that makes a JSON document loam's own scenario shape, and the version this loam reads. */\nexport const SCENARIO_REPORT_MARKER = \"loamScenarioReport\";\nexport const SCENARIO_REPORT_VERSION = 1;\n\n/**\n * Is this document loam's own scenario-report shape rather than a cucumber one?\n *\n * Marker-only, deliberately: the caller must be able to choose a READER before\n * either has validated anything, so that a malformed `loamScenarioReport` file\n * refuses as a malformed report of ITS kind rather than as \"not a cucumber\n * array\". A file that claims the shape and then fails it must never fall\n * through to the other parser and be told it is the wrong sort of document.\n */\nexport function isScenarioReport(doc: unknown): boolean {\n return isRecord(doc) && doc[SCENARIO_REPORT_MARKER] !== undefined;\n}\n\n/**\n * `{\"loamScenarioReport\": 1, \"results\": [{\"digest\": \"…\", \"status\": \"passed\"}]}`\n * — the runner-neutral answer sheet for `scenario.tested` claims.\n *\n * WHY THIS EXISTS. `loam gherkin` stamps every generated scenario\n * `@loam-digest-<16hex>`, and until now only cucumber's JSON carried that tag\n * back. A fleet on JUnit, pytest, Playwright, Vitest or a house runner could\n * therefore never reach `verified` — not because its evidence was weaker, but\n * because of a file format. The example fleet's own shipped record is the proof\n * of the cost: every claim in it is `answered_by: agent`, so the product's\n * showcase demonstrates only the lesser verdict.\n *\n * THE STANDARD OF PROOF IS UNCHANGED, and that is the whole argument. The\n * contract was never the JSON dialect: it is the digest — content-derived, so\n * rewording a `Given` breaks the match — plus a status that says a real run\n * reported it green. Both halves are here. An answer from this file is\n * `answered_by: runner` exactly as a cucumber one is, because it is the same\n * claim answered to the same standard by the same identity.\n *\n * It also adds no forgeability. loam cannot prove any JSON came from executing\n * a commit — SCHEMA says so, and the record stores the file's sha256 and mtime\n * precisely because that is all it can honestly claim. A hand-written cucumber\n * array was always exactly as easy to write as this is.\n *\n * The strict/tolerant split mirrors `verify/evidence/contract.ts` for its\n * reason: a cucumber report is another tool's file full of entries loam has no\n * business judging, while this shape exists for one purpose, so a malformed\n * entry is a malformed report rather than scenery to skip. `status` must be\n * exactly `passed` or `failed`; `skipped`, `pending` and vendor spellings are\n * refused rather than guessed at, because guessing is how a not-run scenario\n * becomes a confirmation.\n */\nexport function readScenarioReport(doc: unknown, reportName: string): ReportRead {\n if (!isRecord(doc)) {\n return { ok: false, message: `${reportName} is not a JSON object.` };\n }\n const version = doc[SCENARIO_REPORT_MARKER];\n if (version !== SCENARIO_REPORT_VERSION) {\n return {\n ok: false,\n message:\n `${reportName} declares \\`${SCENARIO_REPORT_MARKER}: ${JSON.stringify(version)}\\`, and this loam reads ` +\n `version ${SCENARIO_REPORT_VERSION}. A report loam cannot read is refused rather than answered in part.`,\n };\n }\n const results = doc[\"results\"];\n if (!Array.isArray(results)) {\n return {\n ok: false,\n message: `${reportName} has no \\`results\\` array — the shape is {\"${SCENARIO_REPORT_MARKER}\": 1, \"results\": [{\"digest\": \"…\", \"status\": \"passed\"}]}.`,\n };\n }\n const scenarios: ReportScenario[] = [];\n for (const [i, entry] of results.entries()) {\n const at = `${reportName} results[${i}]`;\n if (!isRecord(entry)) return { ok: false, message: `${at} is not an object.` };\n const digest = str(entry[\"digest\"]);\n if (digest === undefined || !/^[0-9a-f]{16}$/.test(digest)) {\n return {\n ok: false,\n message: `${at} has no \\`digest\\` of 16 lowercase hex characters — that is the \\`@loam-digest-…\\` tag \\`loam gherkin\\` stamped on the scenario.`,\n };\n }\n const status = str(entry[\"status\"]);\n if (status !== \"passed\" && status !== \"failed\") {\n return {\n ok: false,\n message:\n `${at} has \\`status: ${JSON.stringify(entry[\"status\"])}\\`; this shape accepts only \"passed\" or \"failed\". ` +\n \"A scenario that did not run has no place in an answer sheet — omit it, and its claim stays unanswered rather than becoming a confirmation.\",\n };\n }\n scenarios.push({\n // The runner's own name for the test, when it gave one — this is where a\n // reader goes to see the run, and it is the only free text here.\n where: str(entry[\"test\"]) ?? `(${SCENARIO_REPORT_MARKER} entry ${i})`,\n digests: [digest],\n steps: [status],\n hooks: [],\n });\n }\n return { ok: true, scenarios };\n}\n\nexport function readCucumberReport(doc: unknown, reportName: string): ReportRead {\n if (!Array.isArray(doc)) {\n return {\n ok: false,\n message:\n `${reportName} is not a cucumber JSON report — expected the top-level array of features ` +\n \"that `cucumber-js --format json` (or cucumber-jvm, behave, SpecFlow) emits.\",\n };\n }\n const scenarios: ReportScenario[] = [];\n for (const feature of doc) {\n if (!isRecord(feature)) continue;\n const featureName = str(feature[\"uri\"]) ?? str(feature[\"name\"]) ?? \"(unnamed feature)\";\n const elements = feature[\"elements\"];\n if (!Array.isArray(elements)) continue;\n for (const el of elements) {\n if (!isRecord(el)) continue;\n const digests: string[] = [];\n const tags = el[\"tags\"];\n if (Array.isArray(tags)) {\n for (const t of tags) {\n const name = typeof t === \"string\" ? t : isRecord(t) ? str(t[\"name\"]) : undefined;\n const m = name === undefined ? null : DIGEST_TAG_RE.exec(name);\n if (m) digests.push(m[1]!);\n }\n }\n // No digest tag, nothing to match: a background, a hand-written\n // scenario, another tool's output. Invisible here, deliberately.\n if (digests.length === 0) continue;\n const resultStatuses = (raw: unknown): string[] => {\n if (!Array.isArray(raw)) return [];\n return raw.map((s) => {\n const status = isRecord(s) && isRecord(s[\"result\"]) ? str(s[\"result\"][\"status\"]) : undefined;\n return (status ?? \"unknown\").toLowerCase();\n });\n };\n const steps = resultStatuses(el[\"steps\"]);\n // cucumber-jvm's @Before/@After hooks: separate arrays, same result shape.\n const hooks = [...resultStatuses(el[\"before\"]), ...resultStatuses(el[\"after\"])];\n // behave's element-level status — a hook failure flips it while the\n // steps that already ran keep \"passed\".\n const status = str(el[\"status\"])?.toLowerCase();\n scenarios.push({\n where: `${featureName} › ${str(el[\"name\"]) ?? \"(unnamed scenario)\"}`,\n digests,\n steps,\n hooks,\n ...(status === undefined ? {} : { status }),\n });\n }\n }\n return { ok: true, scenarios };\n}\n\n/**\n * The digests that more than one service claims, with the services that claim\n * them — a scenario worded identically in two services' specs.\n *\n * The digest is a hash of the scenario body and of nothing else, so `loam\n * gherkin` stamps the same tag into both repositories and a report from either\n * one matches both claims. A report says which scenario ran; it never says\n * whose suite ran it, and the two repositories never ran each other's tests.\n * loam has no way to tell them apart, so it does not choose: the answer is\n * refused, with the services named, and `--service` is the way to give a report\n * an owner (it narrows the checklist to one repository's claims before the\n * matching starts, so nothing here is contested).\n *\n * Only the SERVICE boundary counts. One service's spec repeating a scenario\n * word for word is genuinely one test — that is the documented meaning of the\n * stamp — and both claims may be answered by the one run.\n */\nexport function contestedDigests(claims: readonly Claim[]): Map<string, string[]> {\n const owners = new Map<string, Set<string>>();\n for (const c of claims) {\n if (c.digest === undefined) continue;\n const seen = owners.get(c.digest) ?? new Set<string>();\n seen.add(c.subject);\n owners.set(c.digest, seen);\n }\n const contested = new Map<string, string[]>();\n for (const [digest, services] of owners) {\n if (services.size > 1) contested.set(digest, [...services].sort());\n }\n return contested;\n}\n\n/**\n * Answer every claim from the report — the runner's half of the record. Meant\n * for `scenario.tested` claims (the only kind carrying a digest); a claim\n * without one is answered `unconfirmed` rather than skipped, because silence\n * must never read as checked.\n *\n * Confirmation is strict on purpose: every matching occurrence — a digest the\n * report holds twice is a re-run, and all of them count — ran at least one\n * step, every step `passed`, every before/after hook `passed`, and the\n * element-level status (when the dialect reports one) is `passed`. One failed\n * occurrence wins as failure, a skipped-only run is \"skipped\" not green, and\n * no match at all is \"not found in report\". A digest two services share is\n * refused before any of that: see {@link contestedDigests}.\n */\nexport function runnerAnswers(\n claims: Claim[],\n scenarios: ReportScenario[],\n reportName: string,\n): Answer[] {\n const contested = contestedDigests(claims);\n return claims.map((c) => {\n const rivals = c.digest === undefined ? undefined : contested.get(c.digest);\n if (rivals !== undefined) {\n return answer(\n c.id,\n \"unconfirmed\",\n [],\n `${scenarioDigestTag(c.digest!)} is worded identically in ${rivals.join(\", \")}, so ${reportName} cannot say whose suite ran it. ` +\n \"Record each service's claims from its own repo with --service.\",\n );\n }\n const runs = c.digest === undefined ? [] : scenarios.filter((s) => s.digests.includes(c.digest!));\n if (runs.length === 0) {\n const tag = c.digest === undefined ? \"a digest tag\" : scenarioDigestTag(c.digest);\n return answer(c.id, \"unconfirmed\", [], `not found in report — no scenario in ${reportName} carries ${tag}`);\n }\n const failing = runs.filter((r) => !passed(r));\n if (failing.length === 0) {\n return answer(c.id, \"confirmed\", runs.map((r) => `${reportName}: ${r.where}`));\n }\n const first = failing[0]!;\n const rerun = runs.length > 1 ? ` — ${failing.length} of ${runs.length} matching runs failed` : \"\";\n return answer(c.id, \"unconfirmed\", [], `${reason(first)} (${reportName}: ${first.where})${rerun}`);\n });\n}\n\nfunction answer(id: string, verdict: Answer[\"verdict\"], evidence: string[], note?: string): Answer {\n return { id, verdict, evidence, ...(note === undefined ? {} : { note }), answered_by: \"runner\" };\n}\n\n/**\n * Green means green: at least one step, every step `passed`, every before/\n * after hook `passed`, and the element-level status (when the dialect carries\n * one) `passed` too. The last two exist because a run the runner itself\n * reports as FAILED — a teardown assertion in an @After hook, exactly the\n * shape the arch axis's outbox checks take — used to read as confirmed here:\n * the steps all passed, and the hook failure lived in fields nobody read.\n */\nfunction passed(r: ReportScenario): boolean {\n return (\n r.steps.length > 0 &&\n r.steps.every((s) => s === \"passed\") &&\n r.hooks.every((h) => h === \"passed\") &&\n (r.status === undefined || r.status === \"passed\")\n );\n}\n\n/** Why a run did not pass, naming the step, hook or element status that says so. */\nfunction reason(r: ReportScenario): string {\n if (r.steps.length === 0) return \"no steps ran\";\n const i = r.steps.findIndex((s) => s !== \"passed\" && s !== \"skipped\");\n if (i >= 0) return `${r.steps[i]} at step ${i + 1}`;\n if (r.steps.some((s) => s === \"skipped\")) return \"skipped\";\n const h = r.hooks.findIndex((s) => s !== \"passed\");\n if (h >= 0) return `${r.hooks[h]} in a before/after hook`;\n return `scenario status '${r.status ?? \"unknown\"}'`;\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === \"string\" && v.trim().length > 0 ? v : undefined;\n}\n"]}
1
+ {"version":3,"file":"results.js","sourceRoot":"","sources":["../../src/core/results.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam verify --results` — the cucumber report as an answer sheet.\n *\n * `loam gherkin` tags every generated scenario `@loam-digest-<16hex>`, and\n * cucumber's JSON format carries tags per scenario — so a test report says,\n * mechanically, which spec scenarios ran green. This module reads that report\n * and answers `scenario.tested` claims from it. The digest is the ONLY\n * identity: a claim's `digest` and the tag are the same 16 hex of the same\n * body hash, and names match nothing — rewording a scenario breaks the match,\n * which is the point, because an agent must not be able to SAY a scenario is\n * tested; only a green run may.\n *\n * That identity is CONTENT, and content stops one step short of a fleet: two\n * services whose specs word a scenario identically share a digest, so one\n * repository's report matches the other repository's claim. Those claims are\n * refused rather than answered — see {@link contestedDigests}.\n *\n * Parsing is deliberately tolerant of everything except the shape it matches\n * on. The format is what cucumber-js `--format json`, cucumber-jvm, behave and\n * SpecFlow emit — an array of features, each `elements[]` (scenarios) with\n * `name`, `tags[] {name}` and `steps[] {result {status}}`, plus the two\n * places those dialects put a failure the steps never see: cucumber-jvm\n * reports @Before/@After hook results in per-element `before[]`/`after[]`\n * arrays (a teardown assertion fails there while every step stays `passed`),\n * and behave carries an element-level `status` that a hook failure flips.\n * cucumber-js puts its hooks among the `steps[]` instead, as entries marked\n * `hidden` with the keyword `Before`/`After`, and they are read as hooks\n * there too — the number of real steps is contract now, because a run is\n * held to the step count of the scenario it claims to be (`runnerAnswers`).\n * Only those fields are contract: unknown fields, tagless elements\n * (backgrounds, hand-written scenarios) and stray entries are skipped without\n * comment. What is NOT tolerated is a file that is not that array at all: a\n * report loam cannot recognize must refuse rather than quietly answer every\n * claim \"not found\".\n */\nimport { DIGEST_TAG_RE, scenarioDigestTag } from \"./gherkin/stamp.js\";\nimport { isRecord } from \"./kernel/records.js\";\nimport type { Answer } from \"./verify/answers.js\";\nimport type { Claim } from \"./verify/checklist.js\";\n\n/** One report scenario, reduced to what matching needs. */\nexport interface ReportScenario {\n /** `<feature uri or name> › <scenario name>` — where the run can be seen. */\n where: string;\n /** Digests carried as `@loam-digest-…` tags. Usually one; hand-tagging can add more. */\n digests: string[];\n /** Step statuses, lowercased, in order — hooks excluded, wherever the dialect put them. */\n steps: string[];\n /**\n * Whether `steps` is the run's own step list — true for a cucumber report —\n * or a one-status stand-in: loam's runner-neutral shape carries a status per\n * scenario and no step structure at all, so there is nothing in it to hold\n * against the stamped scenario's step count.\n */\n stepwise: boolean;\n /** Hook statuses — cucumber-jvm's `before[]`/`after[]`, cucumber-js's hidden hook steps — lowercased, in order. */\n hooks: string[];\n /** behave's element-level status, lowercased — absent in the other dialects. */\n status?: string;\n}\n\nexport type ReportRead =\n | { ok: true; scenarios: ReportScenario[] }\n | { ok: false; message: string };\n\n/**\n * Read a parsed cucumber JSON document down to its digest-tagged scenarios.\n * `reportName` is how the caller spelled the file, for the refusal message.\n */\n/** The marker key that makes a JSON document loam's own scenario shape, and the version this loam reads. */\nexport const SCENARIO_REPORT_MARKER = \"loamScenarioReport\";\nexport const SCENARIO_REPORT_VERSION = 1;\n\n/**\n * Is this document loam's own scenario-report shape rather than a cucumber one?\n *\n * Marker-only, deliberately: the caller must be able to choose a READER before\n * either has validated anything, so that a malformed `loamScenarioReport` file\n * refuses as a malformed report of ITS kind rather than as \"not a cucumber\n * array\". A file that claims the shape and then fails it must never fall\n * through to the other parser and be told it is the wrong sort of document.\n */\nexport function isScenarioReport(doc: unknown): boolean {\n return isRecord(doc) && doc[SCENARIO_REPORT_MARKER] !== undefined;\n}\n\n/**\n * `{\"loamScenarioReport\": 1, \"results\": [{\"digest\": \"…\", \"status\": \"passed\"}]}`\n * — the runner-neutral answer sheet for `scenario.tested` claims.\n *\n * WHY THIS EXISTS. `loam gherkin` stamps every generated scenario\n * `@loam-digest-<16hex>`, and until now only cucumber's JSON carried that tag\n * back. A fleet on JUnit, pytest, Playwright, Vitest or a house runner could\n * therefore never reach `verified` — not because its evidence was weaker, but\n * because of a file format. The example fleet's own shipped record is the proof\n * of the cost: every claim in it is `answered_by: agent`, so the product's\n * showcase demonstrates only the lesser verdict.\n *\n * THE STANDARD OF PROOF IS UNCHANGED, and that is the whole argument. The\n * contract was never the JSON dialect: it is the digest — content-derived, so\n * rewording a `Given` breaks the match — plus a status that says a real run\n * reported it green. Both halves are here. An answer from this file is\n * `answered_by: runner` exactly as a cucumber one is, because it is the same\n * claim answered to the same standard by the same identity.\n *\n * It also adds no forgeability. loam cannot prove any JSON came from executing\n * a commit — SCHEMA says so, and the record stores the file's sha256 and mtime\n * precisely because that is all it can honestly claim. A hand-written cucumber\n * array was always exactly as easy to write as this is.\n *\n * The strict/tolerant split mirrors `verify/evidence/contract.ts` for its\n * reason: a cucumber report is another tool's file full of entries loam has no\n * business judging, while this shape exists for one purpose, so a malformed\n * entry is a malformed report rather than scenery to skip. `status` must be\n * exactly `passed` or `failed`; `skipped`, `pending` and vendor spellings are\n * refused rather than guessed at, because guessing is how a not-run scenario\n * becomes a confirmation.\n */\nexport function readScenarioReport(doc: unknown, reportName: string): ReportRead {\n if (!isRecord(doc)) {\n return { ok: false, message: `${reportName} is not a JSON object.` };\n }\n const version = doc[SCENARIO_REPORT_MARKER];\n if (version !== SCENARIO_REPORT_VERSION) {\n return {\n ok: false,\n message:\n `${reportName} declares \\`${SCENARIO_REPORT_MARKER}: ${JSON.stringify(version)}\\`, and this loam reads ` +\n `version ${SCENARIO_REPORT_VERSION}. A report loam cannot read is refused rather than answered in part.`,\n };\n }\n const results = doc[\"results\"];\n if (!Array.isArray(results)) {\n return {\n ok: false,\n message: `${reportName} has no \\`results\\` array — the shape is {\"${SCENARIO_REPORT_MARKER}\": 1, \"results\": [{\"digest\": \"…\", \"status\": \"passed\"}]}.`,\n };\n }\n const scenarios: ReportScenario[] = [];\n for (const [i, entry] of results.entries()) {\n const at = `${reportName} results[${i}]`;\n if (!isRecord(entry)) return { ok: false, message: `${at} is not an object.` };\n const digest = str(entry[\"digest\"]);\n if (digest === undefined || !/^[0-9a-f]{16}$/.test(digest)) {\n return {\n ok: false,\n message: `${at} has no \\`digest\\` of 16 lowercase hex characters — that is the \\`@loam-digest-…\\` tag \\`loam gherkin\\` stamped on the scenario.`,\n };\n }\n const status = str(entry[\"status\"]);\n if (status !== \"passed\" && status !== \"failed\") {\n return {\n ok: false,\n message:\n `${at} has \\`status: ${JSON.stringify(entry[\"status\"])}\\`; this shape accepts only \"passed\" or \"failed\". ` +\n \"A scenario that did not run has no place in an answer sheet — omit it, and its claim stays unanswered rather than becoming a confirmation.\",\n };\n }\n scenarios.push({\n // The runner's own name for the test, when it gave one — this is where a\n // reader goes to see the run, and it is the only free text here.\n where: str(entry[\"test\"]) ?? `(${SCENARIO_REPORT_MARKER} entry ${i})`,\n digests: [digest],\n steps: [status],\n stepwise: false,\n hooks: [],\n });\n }\n return { ok: true, scenarios };\n}\n\nexport function readCucumberReport(doc: unknown, reportName: string): ReportRead {\n if (!Array.isArray(doc)) {\n return {\n ok: false,\n message:\n `${reportName} is not a cucumber JSON report — expected the top-level array of features ` +\n \"that `cucumber-js --format json` (or cucumber-jvm, behave, SpecFlow) emits. \" +\n `A runner with no cucumber formatter answers through loam's own shape instead: {\"${SCENARIO_REPORT_MARKER}\": 1, \"results\": [{\"digest\": \"…\", \"status\": \"passed\"}]}.`,\n };\n }\n const scenarios: ReportScenario[] = [];\n for (const feature of doc) {\n if (!isRecord(feature)) continue;\n const featureName = str(feature[\"uri\"]) ?? str(feature[\"name\"]) ?? \"(unnamed feature)\";\n const elements = feature[\"elements\"];\n if (!Array.isArray(elements)) continue;\n for (const el of elements) {\n if (!isRecord(el)) continue;\n const digests: string[] = [];\n const tags = el[\"tags\"];\n if (Array.isArray(tags)) {\n for (const t of tags) {\n const name = typeof t === \"string\" ? t : isRecord(t) ? str(t[\"name\"]) : undefined;\n const m = name === undefined ? null : DIGEST_TAG_RE.exec(name);\n if (m) digests.push(m[1]!);\n }\n }\n // No digest tag, nothing to match: a background, a hand-written\n // scenario, another tool's output. Invisible here, deliberately.\n if (digests.length === 0) continue;\n const statusOf = (s: unknown): string => {\n const status = isRecord(s) && isRecord(s[\"result\"]) ? str(s[\"result\"][\"status\"]) : undefined;\n return (status ?? \"unknown\").toLowerCase();\n };\n const resultStatuses = (raw: unknown): string[] => (Array.isArray(raw) ? raw.map(statusOf) : []);\n // cucumber-js reports its Before/After hooks INSIDE `steps`, as entries\n // marked `hidden` with the keyword `Before`/`After`; they are hooks, not\n // steps of the scenario, and counting them would make every hooked run\n // look like a different scenario from the one stamped.\n const listed = Array.isArray(el[\"steps\"]) ? (el[\"steps\"] as unknown[]) : [];\n const isHook = (s: unknown): boolean =>\n isRecord(s) && (s[\"hidden\"] === true || /^(before|after)$/i.test(str(s[\"keyword\"])?.trim() ?? \"\"));\n const steps = listed.filter((s) => !isHook(s)).map(statusOf);\n // cucumber-jvm's @Before/@After hooks: separate arrays, same result shape.\n const hooks = [\n ...resultStatuses(el[\"before\"]),\n ...listed.filter(isHook).map(statusOf),\n ...resultStatuses(el[\"after\"]),\n ];\n // behave's element-level status — a hook failure flips it while the\n // steps that already ran keep \"passed\".\n const status = str(el[\"status\"])?.toLowerCase();\n scenarios.push({\n where: `${featureName} › ${str(el[\"name\"]) ?? \"(unnamed scenario)\"}`,\n digests,\n steps,\n stepwise: true,\n hooks,\n ...(status === undefined ? {} : { status }),\n });\n }\n }\n return { ok: true, scenarios };\n}\n\n/**\n * The digests that more than one service claims, with the services that claim\n * them — a scenario worded identically in two services' specs.\n *\n * The digest is a hash of the scenario body and of nothing else, so `loam\n * gherkin` stamps the same tag into both repositories and a report from either\n * one matches both claims. A report says which scenario ran; it never says\n * whose suite ran it, and the two repositories never ran each other's tests.\n * loam has no way to tell them apart, so it does not choose: the answer is\n * refused, with the services named, and `--service` is the way to give a report\n * an owner (it narrows the checklist to one repository's claims before the\n * matching starts, so nothing here is contested).\n *\n * Only the SERVICE boundary counts. One service's spec repeating a scenario\n * word for word is genuinely one test — that is the documented meaning of the\n * stamp — and both claims may be answered by the one run.\n */\nexport function contestedDigests(claims: readonly Claim[]): Map<string, string[]> {\n const owners = new Map<string, Set<string>>();\n for (const c of claims) {\n if (c.digest === undefined) continue;\n const seen = owners.get(c.digest) ?? new Set<string>();\n seen.add(c.subject);\n owners.set(c.digest, seen);\n }\n const contested = new Map<string, string[]>();\n for (const [digest, services] of owners) {\n if (services.size > 1) contested.set(digest, [...services].sort());\n }\n return contested;\n}\n\n/**\n * Answer every claim from the report — the runner's half of the record. Meant\n * for `scenario.tested` claims (the only kind carrying a digest); a claim\n * without one is answered `unconfirmed` rather than skipped, because silence\n * must never read as checked.\n *\n * Confirmation is strict on purpose: every matching occurrence — a digest the\n * report holds twice is a re-run, and all of them count — ran at least one\n * step, every step `passed`, every before/after hook `passed`, and the\n * element-level status (when the dialect reports one) is `passed`. One failed\n * occurrence wins as failure, a skipped-only run is \"skipped\" not green, and\n * no match at all is \"not found in report\". A digest two services share is\n * refused before any of that: see {@link contestedDigests}.\n *\n * And a cucumber occurrence must have run as many steps as the stamped\n * scenario has (`Claim.steps`). The digest is only a tag, and a tag is text:\n * a one-step stub named \"a step\" carrying the right digest confirmed\n * five-step scenarios, because nothing compared what ran with what was\n * stamped. The count is the check every dialect can be held to; loam's own\n * runner-neutral shape carries no steps to count, so its entries are held to\n * their status alone, as that shape has always promised.\n */\nexport function runnerAnswers(\n claims: Claim[],\n scenarios: ReportScenario[],\n reportName: string,\n): Answer[] {\n const contested = contestedDigests(claims);\n return claims.map((c) => {\n const rivals = c.digest === undefined ? undefined : contested.get(c.digest);\n if (rivals !== undefined) {\n return answer(\n c.id,\n \"unconfirmed\",\n [],\n `${scenarioDigestTag(c.digest!)} is worded identically in ${rivals.join(\", \")}, so ${reportName} cannot say whose suite ran it. ` +\n \"Record each service's claims from its own repo with --service.\",\n );\n }\n const runs = c.digest === undefined ? [] : scenarios.filter((s) => s.digests.includes(c.digest!));\n if (runs.length === 0) {\n const tag = c.digest === undefined ? \"a digest tag\" : scenarioDigestTag(c.digest);\n return answer(c.id, \"unconfirmed\", [], `not found in report — no scenario in ${reportName} carries ${tag}`);\n }\n const failing = runs.filter((r) => !passed(r) || !sameShape(r, c.steps));\n if (failing.length === 0) {\n return answer(c.id, \"confirmed\", runs.map((r) => `${reportName}: ${r.where}`));\n }\n const first = failing[0]!;\n const rerun = runs.length > 1 ? ` — ${failing.length} of ${runs.length} matching runs failed` : \"\";\n const why = passed(first)\n ? `ran ${first.steps.length} step(s) where the stamped scenario has ${c.steps} — the report's scenario is not the one \\`loam gherkin\\` stamped with this digest`\n : reason(first);\n return answer(c.id, \"unconfirmed\", [], `${why} (${reportName}: ${first.where})${rerun}`);\n });\n}\n\nfunction answer(id: string, verdict: Answer[\"verdict\"], evidence: string[], note?: string): Answer {\n return { id, verdict, evidence, ...(note === undefined ? {} : { note }), answered_by: \"runner\" };\n}\n\n/**\n * Green means green: at least one step, every step `passed`, every before/\n * after hook `passed`, and the element-level status (when the dialect carries\n * one) `passed` too. The last two exist because a run the runner itself\n * reports as FAILED — a teardown assertion in an @After hook, exactly the\n * shape the arch axis's outbox checks take — used to read as confirmed here:\n * the steps all passed, and the hook failure lived in fields nobody read.\n */\nfunction passed(r: ReportScenario): boolean {\n return (\n r.steps.length > 0 &&\n r.steps.every((s) => s === \"passed\") &&\n r.hooks.every((h) => h === \"passed\") &&\n (r.status === undefined || r.status === \"passed\")\n );\n}\n\n/**\n * Did this run execute the scenario the claim stamped — as far as the report\n * can say? A cucumber run must have run exactly the stamped number of steps.\n * An entry with no step structure (loam's own shape), and a claim carrying no\n * count (only `scenario.tested` claims carry one, and only they carry a digest\n * to match), are held to their status alone.\n */\nfunction sameShape(r: ReportScenario, expected: number | undefined): boolean {\n return !r.stepwise || expected === undefined || r.steps.length === expected;\n}\n\n/** Why a run did not pass, naming the step, hook or element status that says so. */\nfunction reason(r: ReportScenario): string {\n if (r.steps.length === 0) return \"no steps ran\";\n const i = r.steps.findIndex((s) => s !== \"passed\" && s !== \"skipped\");\n if (i >= 0) return `${r.steps[i]} at step ${i + 1}`;\n if (r.steps.some((s) => s === \"skipped\")) return \"skipped\";\n const h = r.hooks.findIndex((s) => s !== \"passed\");\n if (h >= 0) return `${r.hooks[h]} in a before/after hook`;\n return `scenario status '${r.status ?? \"unknown\"}'`;\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === \"string\" && v.trim().length > 0 ? v : undefined;\n}\n"]}