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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (691) hide show
  1. package/CHANGELOG.md +1477 -1
  2. package/COMPARISON.md +91 -3
  3. package/CONTRIBUTING.md +37 -0
  4. package/MIGRATING-from-OpenSpec.md +40 -22
  5. package/QUICKSTART.md +181 -9
  6. package/README.md +256 -57
  7. package/ROADMAP.md +108 -11
  8. package/SCHEMA.md +721 -130
  9. package/WORKFLOW.md +272 -62
  10. package/dist/cli.js +26 -8
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/adopt/adopt.js +5 -10
  13. package/dist/commands/adopt/adopt.js.map +1 -1
  14. package/dist/commands/archive/archive.js +15 -18
  15. package/dist/commands/archive/archive.js.map +1 -1
  16. package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
  17. package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
  18. package/dist/commands/archive/plan/contracts/consumers.js +56 -0
  19. package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
  20. package/dist/commands/archive/plan/contracts/openapi.js +25 -42
  21. package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
  22. package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
  23. package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
  24. package/dist/commands/archive/plan/gate.js +3 -7
  25. package/dist/commands/archive/plan/gate.js.map +1 -1
  26. package/dist/commands/archive/plan/landscape.js +4 -4
  27. package/dist/commands/archive/plan/landscape.js.map +1 -1
  28. package/dist/commands/archive/plan/refusal.js +8 -18
  29. package/dist/commands/archive/plan/refusal.js.map +1 -1
  30. package/dist/commands/archive/plan/specs.js +134 -25
  31. package/dist/commands/archive/plan/specs.js.map +1 -1
  32. package/dist/commands/archive/plan/state.js +1 -0
  33. package/dist/commands/archive/plan/state.js.map +1 -1
  34. package/dist/commands/archive/run.js +82 -33
  35. package/dist/commands/archive/run.js.map +1 -1
  36. package/dist/commands/archive/shipped.js +49 -0
  37. package/dist/commands/archive/shipped.js.map +1 -0
  38. package/dist/commands/context/context.js +4 -7
  39. package/dist/commands/context/context.js.map +1 -1
  40. package/dist/commands/context/print.js +2 -2
  41. package/dist/commands/context/print.js.map +1 -1
  42. package/dist/commands/delta/delta.js +44 -12
  43. package/dist/commands/delta/delta.js.map +1 -1
  44. package/dist/commands/delta/print.js +1 -1
  45. package/dist/commands/delta/print.js.map +1 -1
  46. package/dist/commands/dependencies.js +3 -7
  47. package/dist/commands/dependencies.js.map +1 -1
  48. package/dist/commands/diff/diff.js +20 -27
  49. package/dist/commands/diff/diff.js.map +1 -1
  50. package/dist/commands/explore.js +4 -8
  51. package/dist/commands/explore.js.map +1 -1
  52. package/dist/commands/gate/gate.js +45 -8
  53. package/dist/commands/gate/gate.js.map +1 -1
  54. package/dist/commands/gate/print.js +15 -3
  55. package/dist/commands/gate/print.js.map +1 -1
  56. package/dist/commands/gherkin/commit.js +13 -54
  57. package/dist/commands/gherkin/commit.js.map +1 -1
  58. package/dist/commands/gherkin/gherkin.js +36 -20
  59. package/dist/commands/gherkin/gherkin.js.map +1 -1
  60. package/dist/commands/gherkin/reconcile.js +10 -13
  61. package/dist/commands/gherkin/reconcile.js.map +1 -1
  62. package/dist/commands/init/first-hour.js +44 -0
  63. package/dist/commands/init/first-hour.js.map +1 -1
  64. package/dist/commands/init/init.js +18 -22
  65. package/dist/commands/init/init.js.map +1 -1
  66. package/dist/commands/list/campaign/campaign.js +4 -6
  67. package/dist/commands/list/campaign/campaign.js.map +1 -1
  68. package/dist/commands/list/json.js +0 -4
  69. package/dist/commands/list/json.js.map +1 -1
  70. package/dist/commands/list/list.js +38 -23
  71. package/dist/commands/list/list.js.map +1 -1
  72. package/dist/commands/list/print.js +38 -6
  73. package/dist/commands/list/print.js.map +1 -1
  74. package/dist/commands/list/suites/suites.js +140 -0
  75. package/dist/commands/list/suites/suites.js.map +1 -0
  76. package/dist/commands/list/views.js +28 -33
  77. package/dist/commands/list/views.js.map +1 -1
  78. package/dist/commands/mcp/dispatch.js +91 -36
  79. package/dist/commands/mcp/dispatch.js.map +1 -1
  80. package/dist/commands/mcp/mcp.js +56 -15
  81. package/dist/commands/mcp/mcp.js.map +1 -1
  82. package/dist/commands/mcp/serve.js.map +1 -1
  83. package/dist/commands/migrate-openspec/materialize/active.js +27 -22
  84. package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
  85. package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
  86. package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
  87. package/dist/commands/migrate-openspec/materialize/target.js +2 -2
  88. package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
  89. package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
  90. package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
  91. package/dist/commands/migrate-openspec/openspec/error.js +7 -8
  92. package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
  93. package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
  94. package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
  95. package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
  96. package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
  97. package/dist/commands/new/capability-template.js +118 -0
  98. package/dist/commands/new/capability-template.js.map +1 -0
  99. package/dist/commands/new/commit.js +8 -22
  100. package/dist/commands/new/commit.js.map +1 -1
  101. package/dist/commands/new/modifies.js +206 -0
  102. package/dist/commands/new/modifies.js.map +1 -0
  103. package/dist/commands/new/new.js +59 -14
  104. package/dist/commands/new/new.js.map +1 -1
  105. package/dist/commands/new/templates.js +48 -124
  106. package/dist/commands/new/templates.js.map +1 -1
  107. package/dist/commands/open.js +3 -7
  108. package/dist/commands/open.js.map +1 -1
  109. package/dist/commands/policy/gate.js +27 -0
  110. package/dist/commands/policy/gate.js.map +1 -1
  111. package/dist/commands/rebase/plan.js +2 -2
  112. package/dist/commands/rebase/plan.js.map +1 -1
  113. package/dist/commands/rebase/rebase.js +60 -35
  114. package/dist/commands/rebase/rebase.js.map +1 -1
  115. package/dist/commands/seed/commit.js +55 -26
  116. package/dist/commands/seed/commit.js.map +1 -1
  117. package/dist/commands/seed/plan.js +64 -13
  118. package/dist/commands/seed/plan.js.map +1 -1
  119. package/dist/commands/seed/seed.js +6 -9
  120. package/dist/commands/seed/seed.js.map +1 -1
  121. package/dist/commands/show/show.js +3 -7
  122. package/dist/commands/show/show.js.map +1 -1
  123. package/dist/commands/status/print.js +13 -4
  124. package/dist/commands/status/print.js.map +1 -1
  125. package/dist/commands/status/status.js +3 -7
  126. package/dist/commands/status/status.js.map +1 -1
  127. package/dist/commands/steps/steps.js +3 -7
  128. package/dist/commands/steps/steps.js.map +1 -1
  129. package/dist/commands/subsystem/edit.js +7 -7
  130. package/dist/commands/subsystem/edit.js.map +1 -1
  131. package/dist/commands/subsystem/move.js +60 -25
  132. package/dist/commands/subsystem/move.js.map +1 -1
  133. package/dist/commands/subsystem/query.js +2 -4
  134. package/dist/commands/subsystem/query.js.map +1 -1
  135. package/dist/commands/subsystem/subsystem.js +4 -7
  136. package/dist/commands/subsystem/subsystem.js.map +1 -1
  137. package/dist/commands/subsystem/sync.js +141 -71
  138. package/dist/commands/subsystem/sync.js.map +1 -1
  139. package/dist/commands/subsystem/txn/report.js +55 -5
  140. package/dist/commands/subsystem/txn/report.js.map +1 -1
  141. package/dist/commands/subsystem/txn/txn.js +121 -45
  142. package/dist/commands/subsystem/txn/txn.js.map +1 -1
  143. package/dist/commands/subsystem/txn/views.js +82 -12
  144. package/dist/commands/subsystem/txn/views.js.map +1 -1
  145. package/dist/commands/tasks/render.js +116 -0
  146. package/dist/commands/tasks/render.js.map +1 -0
  147. package/dist/commands/tasks/tasks.js +113 -0
  148. package/dist/commands/tasks/tasks.js.map +1 -0
  149. package/dist/commands/unarchive/manifest.js +6 -9
  150. package/dist/commands/unarchive/manifest.js.map +1 -1
  151. package/dist/commands/unarchive/restore.js +17 -28
  152. package/dist/commands/unarchive/restore.js.map +1 -1
  153. package/dist/commands/unarchive/unarchive.js +22 -30
  154. package/dist/commands/unarchive/unarchive.js.map +1 -1
  155. package/dist/commands/validate/arch-coverage.js +6 -15
  156. package/dist/commands/validate/arch-coverage.js.map +1 -1
  157. package/dist/commands/validate/checks/fleet-shape.js +4 -49
  158. package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
  159. package/dist/commands/validate/checks/health-deps.js +9 -6
  160. package/dist/commands/validate/checks/health-deps.js.map +1 -1
  161. package/dist/commands/validate/checks/requirements.js +18 -8
  162. package/dist/commands/validate/checks/requirements.js.map +1 -1
  163. package/dist/commands/validate/checks/sources.js +6 -2
  164. package/dist/commands/validate/checks/sources.js.map +1 -1
  165. package/dist/commands/validate/feature.js +23 -19
  166. package/dist/commands/validate/feature.js.map +1 -1
  167. package/dist/commands/validate/fleet/census.js +26 -3
  168. package/dist/commands/validate/fleet/census.js.map +1 -1
  169. package/dist/commands/validate/fleet/landscape.js +35 -32
  170. package/dist/commands/validate/fleet/landscape.js.map +1 -1
  171. package/dist/commands/validate/fleet/map/attest.js +7 -12
  172. package/dist/commands/validate/fleet/map/attest.js.map +1 -1
  173. package/dist/commands/validate/fleet/map/bindings.js +16 -5
  174. package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
  175. package/dist/commands/validate/fleet/map/consumers.js +43 -122
  176. package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
  177. package/dist/commands/validate/fleet/map/usecases.js +5 -11
  178. package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
  179. package/dist/commands/validate/fleet/obligations.js +1 -5
  180. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  181. package/dist/commands/validate/fleet/scorecard/print.js +5 -1
  182. package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
  183. package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
  184. package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
  185. package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
  186. package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
  187. package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
  188. package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
  189. package/dist/commands/validate/fleet/views/ids.js +33 -7
  190. package/dist/commands/validate/fleet/views/ids.js.map +1 -1
  191. package/dist/commands/validate/fleet/views/root-documents.js +17 -35
  192. package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
  193. package/dist/commands/validate/fleet/views/stale.js +31 -14
  194. package/dist/commands/validate/fleet/views/stale.js.map +1 -1
  195. package/dist/commands/validate/links/corpus.js +1 -4
  196. package/dist/commands/validate/links/corpus.js.map +1 -1
  197. package/dist/commands/validate/links/glossary.js.map +1 -1
  198. package/dist/commands/validate/report.js +5 -4
  199. package/dist/commands/validate/report.js.map +1 -1
  200. package/dist/commands/validate/scope.js +113 -0
  201. package/dist/commands/validate/scope.js.map +1 -0
  202. package/dist/commands/validate/service/api.js +16 -104
  203. package/dist/commands/validate/service/api.js.map +1 -1
  204. package/dist/commands/validate/service/contracts/generated.js +134 -0
  205. package/dist/commands/validate/service/contracts/generated.js.map +1 -0
  206. package/dist/commands/validate/service/events/events.js +1 -1
  207. package/dist/commands/validate/service/events/events.js.map +1 -1
  208. package/dist/commands/validate/service/events/message-producers.js +1 -1
  209. package/dist/commands/validate/service/events/message-producers.js.map +1 -1
  210. package/dist/commands/validate/service/evidence-pins.js +56 -10
  211. package/dist/commands/validate/service/evidence-pins.js.map +1 -1
  212. package/dist/commands/validate/service/service.js +16 -26
  213. package/dist/commands/validate/service/service.js.map +1 -1
  214. package/dist/commands/validate/service/specs.js +31 -38
  215. package/dist/commands/validate/service/specs.js.map +1 -1
  216. package/dist/commands/validate/service/spine.js +8 -11
  217. package/dist/commands/validate/service/spine.js.map +1 -1
  218. package/dist/commands/validate/service/usecases/usecases.js +3 -3
  219. package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
  220. package/dist/commands/validate/validate.js +44 -26
  221. package/dist/commands/validate/validate.js.map +1 -1
  222. package/dist/commands/verify/cross/diff.js +2 -6
  223. package/dist/commands/verify/cross/diff.js.map +1 -1
  224. package/dist/commands/verify/evidence/contract.js +1 -1
  225. package/dist/commands/verify/evidence/contract.js.map +1 -1
  226. package/dist/commands/verify/evidence/read.js +12 -3
  227. package/dist/commands/verify/evidence/read.js.map +1 -1
  228. package/dist/commands/verify/frozen.js +45 -23
  229. package/dist/commands/verify/frozen.js.map +1 -1
  230. package/dist/commands/verify/record.js +69 -17
  231. package/dist/commands/verify/record.js.map +1 -1
  232. package/dist/commands/verify/report.js +27 -44
  233. package/dist/commands/verify/report.js.map +1 -1
  234. package/dist/commands/verify/results.js +112 -30
  235. package/dist/commands/verify/results.js.map +1 -1
  236. package/dist/commands/verify/verify.js +54 -51
  237. package/dist/commands/verify/verify.js.map +1 -1
  238. package/dist/commands/vouch/pack/pack.js +2 -2
  239. package/dist/commands/vouch/pack/pack.js.map +1 -1
  240. package/dist/commands/vouch/pack/print.js +1 -6
  241. package/dist/commands/vouch/pack/print.js.map +1 -1
  242. package/dist/commands/vouch/pack/sections.js +1 -2
  243. package/dist/commands/vouch/pack/sections.js.map +1 -1
  244. package/dist/commands/vouch/run.js +26 -81
  245. package/dist/commands/vouch/run.js.map +1 -1
  246. package/dist/commands/vouch/sample/plan.js +20 -34
  247. package/dist/commands/vouch/sample/plan.js.map +1 -1
  248. package/dist/commands/vouch/vet/verify.js +39 -7
  249. package/dist/commands/vouch/vet/verify.js.map +1 -1
  250. package/dist/commands/vouch/vouch.js +10 -17
  251. package/dist/commands/vouch/vouch.js.map +1 -1
  252. package/dist/core/agent/agents-md/command-map.js +57 -102
  253. package/dist/core/agent/agents-md/command-map.js.map +1 -1
  254. package/dist/core/agent/agents-md/map/explain.js +4 -3
  255. package/dist/core/agent/agents-md/map/explain.js.map +1 -1
  256. package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
  257. package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
  258. package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
  259. package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
  260. package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
  261. package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
  262. package/dist/core/agent/agents-md/map/mcp.js +10 -7
  263. package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
  264. package/dist/core/agent/agents-md/map/status.js +9 -3
  265. package/dist/core/agent/agents-md/map/status.js.map +1 -1
  266. package/dist/core/agent/agents-md/map/subsystem.js +31 -17
  267. package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
  268. package/dist/core/agent/agents-md/map/usecases.js +24 -6
  269. package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
  270. package/dist/core/agent/agents-md/refusals.js +37 -21
  271. package/dist/core/agent/agents-md/refusals.js.map +1 -1
  272. package/dist/core/agent/agents-md.js +25 -10
  273. package/dist/core/agent/agents-md.js.map +1 -1
  274. package/dist/core/agent/agents-stamp.js +10 -0
  275. package/dist/core/agent/agents-stamp.js.map +1 -1
  276. package/dist/core/agent/scaffold.js +10 -15
  277. package/dist/core/agent/scaffold.js.map +1 -1
  278. package/dist/core/agent/tools/dialects.js +1 -0
  279. package/dist/core/agent/tools/dialects.js.map +1 -1
  280. package/dist/core/agent/tools/registry.js +41 -76
  281. package/dist/core/agent/tools/registry.js.map +1 -1
  282. package/dist/core/agent/workflows/adopt.js +5 -3
  283. package/dist/core/agent/workflows/adopt.js.map +1 -1
  284. package/dist/core/agent/workflows/check.js +25 -22
  285. package/dist/core/agent/workflows/check.js.map +1 -1
  286. package/dist/core/agent/workflows/closing.js +54 -21
  287. package/dist/core/agent/workflows/closing.js.map +1 -1
  288. package/dist/core/agent/workflows/feature.js +79 -11
  289. package/dist/core/agent/workflows/feature.js.map +1 -1
  290. package/dist/core/agent/workflows/implement.js +91 -13
  291. package/dist/core/agent/workflows/implement.js.map +1 -1
  292. package/dist/core/agent/workflows/reference/authoring.js +3 -1
  293. package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
  294. package/dist/core/agent/workflows/reference/codes.js +8 -6
  295. package/dist/core/agent/workflows/reference/codes.js.map +1 -1
  296. package/dist/core/agent/workflows/reference/done-check.js +37 -15
  297. package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
  298. package/dist/core/agent/workflows/reference/reference.js.map +1 -1
  299. package/dist/core/agent/workflows/reference/spine.js +62 -24
  300. package/dist/core/agent/workflows/reference/spine.js.map +1 -1
  301. package/dist/core/asyncapi/baseline/plan.js +14 -81
  302. package/dist/core/asyncapi/baseline/plan.js.map +1 -1
  303. package/dist/core/asyncapi/digest.js +30 -25
  304. package/dist/core/asyncapi/digest.js.map +1 -1
  305. package/dist/core/asyncapi/merge/components.js +5 -4
  306. package/dist/core/asyncapi/merge/components.js.map +1 -1
  307. package/dist/core/asyncapi/merge/error.js +7 -2
  308. package/dist/core/asyncapi/merge/error.js.map +1 -1
  309. package/dist/core/asyncapi/merge/markers.js +30 -50
  310. package/dist/core/asyncapi/merge/markers.js.map +1 -1
  311. package/dist/core/asyncapi/merge/merge.js +31 -53
  312. package/dist/core/asyncapi/merge/merge.js.map +1 -1
  313. package/dist/core/asyncapi/merge/pin.js +10 -36
  314. package/dist/core/asyncapi/merge/pin.js.map +1 -1
  315. package/dist/core/asyncapi/model.js.map +1 -1
  316. package/dist/core/asyncapi/read.js +36 -12
  317. package/dist/core/asyncapi/read.js.map +1 -1
  318. package/dist/core/brief/brief.js +30 -25
  319. package/dist/core/brief/brief.js.map +1 -1
  320. package/dist/core/brief/landscape.js +8 -12
  321. package/dist/core/brief/landscape.js.map +1 -1
  322. package/dist/core/brief/map/extend.js +8 -5
  323. package/dist/core/brief/map/extend.js.map +1 -1
  324. package/dist/core/brief/map/owed.js +8 -26
  325. package/dist/core/brief/map/owed.js.map +1 -1
  326. package/dist/core/brief/targets.js +79 -5
  327. package/dist/core/brief/targets.js.map +1 -1
  328. package/dist/core/brief/unchecked.js +1 -1
  329. package/dist/core/brief/unchecked.js.map +1 -1
  330. package/dist/core/c4/likec4.js +147 -16
  331. package/dist/core/c4/likec4.js.map +1 -1
  332. package/dist/core/c4/locate/unlocated.js +150 -0
  333. package/dist/core/c4/locate/unlocated.js.map +1 -0
  334. package/dist/core/c4/parsed/values.js +54 -1
  335. package/dist/core/c4/parsed/values.js.map +1 -1
  336. package/dist/core/c4/prefetch/fleet.js +1 -9
  337. package/dist/core/c4/prefetch/fleet.js.map +1 -1
  338. package/dist/core/c4/project/architecture.js +31 -9
  339. package/dist/core/c4/project/architecture.js.map +1 -1
  340. package/dist/core/c4/project/documents.js +36 -10
  341. package/dist/core/c4/project/documents.js.map +1 -1
  342. package/dist/core/c4/project/load.js +14 -21
  343. package/dist/core/c4/project/load.js.map +1 -1
  344. package/dist/core/c4/project/staged.js +7 -3
  345. package/dist/core/c4/project/staged.js.map +1 -1
  346. package/dist/core/c4/resolve/external.js +51 -0
  347. package/dist/core/c4/resolve/external.js.map +1 -0
  348. package/dist/core/c4/resolve/service.js +25 -1
  349. package/dist/core/c4/resolve/service.js.map +1 -1
  350. package/dist/core/c4/seed/template.js +15 -1
  351. package/dist/core/c4/seed/template.js.map +1 -1
  352. package/dist/core/c4/service-model/shape.js +2 -22
  353. package/dist/core/c4/service-model/shape.js.map +1 -1
  354. package/dist/core/c4/source-mask.js +288 -4
  355. package/dist/core/c4/source-mask.js.map +1 -1
  356. package/dist/core/c4/source-scan.js +203 -147
  357. package/dist/core/c4/source-scan.js.map +1 -1
  358. package/dist/core/c4/splice/authored-source.js +19 -0
  359. package/dist/core/c4/splice/authored-source.js.map +1 -1
  360. package/dist/core/c4/splice/delta-blocks.js +5 -18
  361. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  362. package/dist/core/c4/splice/landscape-merge.js +13 -8
  363. package/dist/core/c4/splice/landscape-merge.js.map +1 -1
  364. package/dist/core/c4/splice/model/merge.js +10 -6
  365. package/dist/core/c4/splice/model/merge.js.map +1 -1
  366. package/dist/core/c4/splice/placement.js +108 -18
  367. package/dist/core/c4/splice/placement.js.map +1 -1
  368. package/dist/core/c4/workspace.js +13 -22
  369. package/dist/core/c4/workspace.js.map +1 -1
  370. package/dist/core/capabilities/capabilities.js +6 -6
  371. package/dist/core/capabilities/capabilities.js.map +1 -1
  372. package/dist/core/capabilities/delta/doc.js +7 -10
  373. package/dist/core/capabilities/delta/doc.js.map +1 -1
  374. package/dist/core/capabilities/findings.js +15 -20
  375. package/dist/core/capabilities/findings.js.map +1 -1
  376. package/dist/core/capabilities/realizes/findings.js +12 -19
  377. package/dist/core/capabilities/realizes/findings.js.map +1 -1
  378. package/dist/core/capabilities/realizes/join.js +0 -20
  379. package/dist/core/capabilities/realizes/join.js.map +1 -1
  380. package/dist/core/capabilities/rollup.js +6 -0
  381. package/dist/core/capabilities/rollup.js.map +1 -1
  382. package/dist/core/capabilities/tree.js +6 -28
  383. package/dist/core/capabilities/tree.js.map +1 -1
  384. package/dist/core/capabilities/usecase-join.js +3 -13
  385. package/dist/core/capabilities/usecase-join.js.map +1 -1
  386. package/dist/core/coherence/authoring/scaffold.js +11 -3
  387. package/dist/core/coherence/authoring/scaffold.js.map +1 -1
  388. package/dist/core/coherence/authoring/sentinels.js +2 -1
  389. package/dist/core/coherence/authoring/sentinels.js.map +1 -1
  390. package/dist/core/coherence/coherence.js +30 -16
  391. package/dist/core/coherence/coherence.js.map +1 -1
  392. package/dist/core/coherence/consumed/operations.js +70 -0
  393. package/dist/core/coherence/consumed/operations.js.map +1 -0
  394. package/dist/core/coherence/consumed/refusal.js +38 -0
  395. package/dist/core/coherence/consumed/refusal.js.map +1 -0
  396. package/dist/core/coherence/declared.js +60 -34
  397. package/dist/core/coherence/declared.js.map +1 -1
  398. package/dist/core/coherence/events/declared.js +44 -59
  399. package/dist/core/coherence/events/declared.js.map +1 -1
  400. package/dist/core/coherence/events/events.js +119 -8
  401. package/dist/core/coherence/events/events.js.map +1 -1
  402. package/dist/core/coherence/events/grades.js +31 -8
  403. package/dist/core/coherence/events/grades.js.map +1 -1
  404. package/dist/core/coherence/events/lookups.js +14 -82
  405. package/dist/core/coherence/events/lookups.js.map +1 -1
  406. package/dist/core/coherence/lookups.js +18 -78
  407. package/dist/core/coherence/lookups.js.map +1 -1
  408. package/dist/core/coherence/retired/archived.js +285 -0
  409. package/dist/core/coherence/retired/archived.js.map +1 -0
  410. package/dist/core/coherence/retired/event-pair.js +62 -0
  411. package/dist/core/coherence/retired/event-pair.js.map +1 -0
  412. package/dist/core/coherence/retired/merged.js +79 -0
  413. package/dist/core/coherence/retired/merged.js.map +1 -0
  414. package/dist/core/coherence/retired/retirement.js +49 -0
  415. package/dist/core/coherence/retired/retirement.js.map +1 -0
  416. package/dist/core/coherence/retired/revival/arrivals.js +131 -0
  417. package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
  418. package/dist/core/coherence/retired/revival/revived.js +297 -0
  419. package/dist/core/coherence/retired/revival/revived.js.map +1 -0
  420. package/dist/core/consumers/after.js +113 -0
  421. package/dist/core/consumers/after.js.map +1 -0
  422. package/dist/core/consumers/consumers.js +296 -0
  423. package/dist/core/consumers/consumers.js.map +1 -0
  424. package/dist/core/consumers/current.js +211 -0
  425. package/dist/core/consumers/current.js.map +1 -0
  426. package/dist/core/consumers/scan.js +46 -0
  427. package/dist/core/consumers/scan.js.map +1 -0
  428. package/dist/core/consumers/state.js +27 -0
  429. package/dist/core/consumers/state.js.map +1 -0
  430. package/dist/core/delta/delta.js +1 -1
  431. package/dist/core/delta/delta.js.map +1 -1
  432. package/dist/core/deployment/delta.js +15 -16
  433. package/dist/core/deployment/delta.js.map +1 -1
  434. package/dist/core/diff/base-git.js +94 -12
  435. package/dist/core/diff/base-git.js.map +1 -1
  436. package/dist/core/diff/base-state.js +28 -16
  437. package/dist/core/diff/base-state.js.map +1 -1
  438. package/dist/core/diff/joins.js +136 -0
  439. package/dist/core/diff/joins.js.map +1 -0
  440. package/dist/core/diff/scope/changed-paths.js +22 -6
  441. package/dist/core/diff/scope/changed-paths.js.map +1 -1
  442. package/dist/core/diff/scope/edges.js +82 -0
  443. package/dist/core/diff/scope/edges.js.map +1 -0
  444. package/dist/core/diff/semantic.js +100 -31
  445. package/dist/core/diff/semantic.js.map +1 -1
  446. package/dist/core/doctor/agents.js +34 -18
  447. package/dist/core/doctor/agents.js.map +1 -1
  448. package/dist/core/doctor/report.js.map +1 -1
  449. package/dist/core/document/apply.js +10 -2
  450. package/dist/core/document/apply.js.map +1 -1
  451. package/dist/core/document/frontmatter.js +55 -0
  452. package/dist/core/document/frontmatter.js.map +1 -1
  453. package/dist/core/document/parse.js +29 -6
  454. package/dist/core/document/parse.js.map +1 -1
  455. package/dist/core/document/scenarios.js +17 -0
  456. package/dist/core/document/scenarios.js.map +1 -1
  457. package/dist/core/document/spec.js +4 -11
  458. package/dist/core/document/spec.js.map +1 -1
  459. package/dist/core/envelope/config.js +10 -0
  460. package/dist/core/envelope/config.js.map +1 -1
  461. package/dist/core/envelope/json.js +34 -0
  462. package/dist/core/envelope/json.js.map +1 -1
  463. package/dist/core/explain/families.js +17 -12
  464. package/dist/core/explain/families.js.map +1 -1
  465. package/dist/core/explain/refusals.js +3 -3
  466. package/dist/core/explain/refusals.js.map +1 -1
  467. package/dist/core/explain/terms.js +18 -9
  468. package/dist/core/explain/terms.js.map +1 -1
  469. package/dist/core/explore/explore.js +2 -7
  470. package/dist/core/explore/explore.js.map +1 -1
  471. package/dist/core/fleet-context.js +34 -95
  472. package/dist/core/fleet-context.js.map +1 -1
  473. package/dist/core/gate/checks.js +10 -3
  474. package/dist/core/gate/checks.js.map +1 -1
  475. package/dist/core/gate/partners.js +64 -19
  476. package/dist/core/gate/partners.js.map +1 -1
  477. package/dist/core/gate/report.js.map +1 -1
  478. package/dist/core/gate/verification.js +75 -12
  479. package/dist/core/gate/verification.js.map +1 -1
  480. package/dist/core/gherkin/stale.js +13 -37
  481. package/dist/core/gherkin/stale.js.map +1 -1
  482. package/dist/core/glossary/tree.js +6 -19
  483. package/dist/core/glossary/tree.js.map +1 -1
  484. package/dist/core/kernel/concurrency.js +19 -0
  485. package/dist/core/kernel/concurrency.js.map +1 -1
  486. package/dist/core/kernel/records.js +11 -0
  487. package/dist/core/kernel/records.js.map +1 -1
  488. package/dist/core/links/backlinks.js +1 -3
  489. package/dist/core/links/backlinks.js.map +1 -1
  490. package/dist/core/links/corpus.js +5 -36
  491. package/dist/core/links/corpus.js.map +1 -1
  492. package/dist/core/mcp/argv.js +101 -0
  493. package/dist/core/mcp/argv.js.map +1 -1
  494. package/dist/core/mcp/author-tools.js +2 -1
  495. package/dist/core/mcp/author-tools.js.map +1 -1
  496. package/dist/core/mcp/protocol.js +2 -2
  497. package/dist/core/mcp/protocol.js.map +1 -1
  498. package/dist/core/mcp/tools.js +43 -33
  499. package/dist/core/mcp/tools.js.map +1 -1
  500. package/dist/core/openapi/baseline/gate.js +53 -12
  501. package/dist/core/openapi/baseline/gate.js.map +1 -1
  502. package/dist/core/openapi/baseline/plan.js +22 -8
  503. package/dist/core/openapi/baseline/plan.js.map +1 -1
  504. package/dist/core/openapi/baseline/record.js +0 -1
  505. package/dist/core/openapi/baseline/record.js.map +1 -1
  506. package/dist/core/openapi/contract-text.js +33 -0
  507. package/dist/core/openapi/contract-text.js.map +1 -0
  508. package/dist/core/openapi/contract-tree.js +56 -0
  509. package/dist/core/openapi/contract-tree.js.map +1 -0
  510. package/dist/core/openapi/depth.js +38 -24
  511. package/dist/core/openapi/depth.js.map +1 -1
  512. package/dist/core/openapi/digest.js +52 -2
  513. package/dist/core/openapi/digest.js.map +1 -1
  514. package/dist/core/openapi/doc.js +12 -4
  515. package/dist/core/openapi/doc.js.map +1 -1
  516. package/dist/core/openapi/merge/components.js +25 -60
  517. package/dist/core/openapi/merge/components.js.map +1 -1
  518. package/dist/core/openapi/merge/error.js +3 -13
  519. package/dist/core/openapi/merge/error.js.map +1 -1
  520. package/dist/core/openapi/merge/markers.js +26 -30
  521. package/dist/core/openapi/merge/markers.js.map +1 -1
  522. package/dist/core/openapi/merge/merge.js +26 -52
  523. package/dist/core/openapi/merge/merge.js.map +1 -1
  524. package/dist/core/openapi/merge/pin.js +15 -35
  525. package/dist/core/openapi/merge/pin.js.map +1 -1
  526. package/dist/core/openspec/decide/verdict.js.map +1 -1
  527. package/dist/core/openspec/inventory.js +4 -5
  528. package/dist/core/openspec/inventory.js.map +1 -1
  529. package/dist/core/openspec/model/mapping.js +30 -28
  530. package/dist/core/openspec/model/mapping.js.map +1 -1
  531. package/dist/core/openspec/model/model.js +12 -7
  532. package/dist/core/openspec/model/model.js.map +1 -1
  533. package/dist/core/openspec/scan/changes.js +10 -4
  534. package/dist/core/openspec/scan/changes.js.map +1 -1
  535. package/dist/core/openspec/scan/config.js +7 -16
  536. package/dist/core/openspec/scan/config.js.map +1 -1
  537. package/dist/core/openspec/scan/shape.js +63 -19
  538. package/dist/core/openspec/scan/shape.js.map +1 -1
  539. package/dist/core/openspec/scan/walk.js +21 -38
  540. package/dist/core/openspec/scan/walk.js.map +1 -1
  541. package/dist/core/pack/living.js +2 -16
  542. package/dist/core/pack/living.js.map +1 -1
  543. package/dist/core/provenance/findings.js +48 -12
  544. package/dist/core/provenance/findings.js.map +1 -1
  545. package/dist/core/provenance/git.js +33 -57
  546. package/dist/core/provenance/git.js.map +1 -1
  547. package/dist/core/provenance/gitq/moves.js +71 -114
  548. package/dist/core/provenance/gitq/moves.js.map +1 -1
  549. package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
  550. package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
  551. package/dist/core/provenance/sources.js +23 -6
  552. package/dist/core/provenance/sources.js.map +1 -1
  553. package/dist/core/provenance/stamp.js +5 -17
  554. package/dist/core/provenance/stamp.js.map +1 -1
  555. package/dist/core/repo/entries.js.map +1 -1
  556. package/dist/core/repo/paths.js +34 -29
  557. package/dist/core/repo/paths.js.map +1 -1
  558. package/dist/core/repo/repo.js +24 -14
  559. package/dist/core/repo/repo.js.map +1 -1
  560. package/dist/core/repo/tree/find.js +12 -3
  561. package/dist/core/repo/tree/find.js.map +1 -1
  562. package/dist/core/repo/tree/fs.js +27 -1
  563. package/dist/core/repo/tree/fs.js.map +1 -1
  564. package/dist/core/repo/tree/render/body.js +45 -6
  565. package/dist/core/repo/tree/render/body.js.map +1 -1
  566. package/dist/core/repo/tree/render/service/header.js +125 -0
  567. package/dist/core/repo/tree/render/service/header.js.map +1 -0
  568. package/dist/core/repo/tree/render/service/peers.js +28 -0
  569. package/dist/core/repo/tree/render/service/peers.js.map +1 -0
  570. package/dist/core/repo/tree/render/service/view-id.js +67 -0
  571. package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
  572. package/dist/core/repo/tree/render/service/views.js +78 -0
  573. package/dist/core/repo/tree/render/service/views.js.map +1 -0
  574. package/dist/core/repo/tree/render/stale.js +19 -2
  575. package/dist/core/repo/tree/render/stale.js.map +1 -1
  576. package/dist/core/repo/tree/render/views.js +102 -33
  577. package/dist/core/repo/tree/render/views.js.map +1 -1
  578. package/dist/core/repo/tree/walk.js +8 -30
  579. package/dist/core/repo/tree/walk.js.map +1 -1
  580. package/dist/core/results.js +47 -12
  581. package/dist/core/results.js.map +1 -1
  582. package/dist/core/staging/commit.js +10 -12
  583. package/dist/core/staging/commit.js.map +1 -1
  584. package/dist/core/staging/lock.js +74 -12
  585. package/dist/core/staging/lock.js.map +1 -1
  586. package/dist/core/staging/recovery/intent.js +2 -5
  587. package/dist/core/staging/recovery/intent.js.map +1 -1
  588. package/dist/core/staging/recovery/recover.js +60 -1
  589. package/dist/core/staging/recovery/recover.js.map +1 -1
  590. package/dist/core/staging/snapshot/manifest.js +152 -0
  591. package/dist/core/staging/snapshot/manifest.js.map +1 -0
  592. package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
  593. package/dist/core/staging/snapshot/snapshot.js.map +1 -0
  594. package/dist/core/status/actions/execution.js +11 -0
  595. package/dist/core/status/actions/execution.js.map +1 -1
  596. package/dist/core/status/feature/artifacts.js +2 -2
  597. package/dist/core/status/feature/artifacts.js.map +1 -1
  598. package/dist/core/status/feature/feature.js +5 -11
  599. package/dist/core/status/feature/feature.js.map +1 -1
  600. package/dist/core/status/fleet/fleet.js +6 -2
  601. package/dist/core/status/fleet/fleet.js.map +1 -1
  602. package/dist/core/status/fleet/next.js +26 -3
  603. package/dist/core/status/fleet/next.js.map +1 -1
  604. package/dist/core/status/report.js.map +1 -1
  605. package/dist/core/status/verification.js +30 -15
  606. package/dist/core/status/verification.js.map +1 -1
  607. package/dist/core/tasks/packet.js +174 -0
  608. package/dist/core/tasks/packet.js.map +1 -0
  609. package/dist/core/tasks/record.js +43 -0
  610. package/dist/core/tasks/record.js.map +1 -0
  611. package/dist/core/tasks/sources.js +181 -0
  612. package/dist/core/tasks/sources.js.map +1 -0
  613. package/dist/core/tasks/stamps.js +76 -0
  614. package/dist/core/tasks/stamps.js.map +1 -0
  615. package/dist/core/tasks/unit.js +0 -0
  616. package/dist/core/tasks/unit.js.map +1 -0
  617. package/dist/core/usecases/capability.js +11 -14
  618. package/dist/core/usecases/capability.js.map +1 -1
  619. package/dist/core/usecases/delta/flows.js +5 -7
  620. package/dist/core/usecases/delta/flows.js.map +1 -1
  621. package/dist/core/usecases/fleet.js +178 -31
  622. package/dist/core/usecases/fleet.js.map +1 -1
  623. package/dist/core/usecases/operations.js +5 -4
  624. package/dist/core/usecases/operations.js.map +1 -1
  625. package/dist/core/usecases/service/flows.js +9 -6
  626. package/dist/core/usecases/service/flows.js.map +1 -1
  627. package/dist/core/usecases/suites/participants.js +83 -0
  628. package/dist/core/usecases/suites/participants.js.map +1 -0
  629. package/dist/core/usecases/suites/suites.js +45 -0
  630. package/dist/core/usecases/suites/suites.js.map +1 -0
  631. package/dist/core/verify/answers.js +12 -5
  632. package/dist/core/verify/answers.js.map +1 -1
  633. package/dist/core/verify/build.js +102 -82
  634. package/dist/core/verify/build.js.map +1 -1
  635. package/dist/core/verify/checklist.js +45 -10
  636. package/dist/core/verify/checklist.js.map +1 -1
  637. package/dist/core/verify/location.js +67 -0
  638. package/dist/core/verify/location.js.map +1 -0
  639. package/dist/core/verify/pins/lint.js +3 -2
  640. package/dist/core/verify/pins/lint.js.map +1 -1
  641. package/dist/core/verify/pins/pin.js +1 -1
  642. package/dist/core/verify/pins/pin.js.map +1 -1
  643. package/dist/core/verify/read/file.js +224 -0
  644. package/dist/core/verify/read/file.js.map +1 -0
  645. package/dist/core/verify/{file.js → read/shape.js} +16 -104
  646. package/dist/core/verify/read/shape.js.map +1 -0
  647. package/dist/core/verify/read/union.js +105 -0
  648. package/dist/core/verify/read/union.js.map +1 -0
  649. package/dist/core/verify/record.js +35 -12
  650. package/dist/core/verify/record.js.map +1 -1
  651. package/dist/core/verify/store/commit.js +40 -30
  652. package/dist/core/verify/store/commit.js.map +1 -1
  653. package/dist/core/verify/store/render.js +8 -4
  654. package/dist/core/verify/store/render.js.map +1 -1
  655. package/dist/core/vocabulary/issue.js +1 -0
  656. package/dist/core/vocabulary/issue.js.map +1 -1
  657. package/dist/core/vocabulary/maturity.js +53 -3
  658. package/dist/core/vocabulary/maturity.js.map +1 -1
  659. package/examples/README.md +28 -9
  660. package/examples/docs/architecture/landscape.likec4 +21 -7
  661. package/examples/docs/architecture/services.likec4 +70 -0
  662. package/examples/docs/architecture/subsystems.likec4 +3 -0
  663. package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
  664. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
  665. package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
  666. package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
  667. package/examples/docs/services/order-service/asyncapi.yaml +3 -2
  668. package/examples/docs/services/order-service/model.likec4 +10 -8
  669. package/examples/docs/services/payment-service/model.likec4 +6 -4
  670. package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
  671. package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
  672. package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
  673. package/package.json +1 -1
  674. package/dist/commands/validate/checks/vocabulary.js +0 -16
  675. package/dist/commands/validate/checks/vocabulary.js.map +0 -1
  676. package/dist/core/agent/agents-md/artifacts.js +0 -221
  677. package/dist/core/agent/agents-md/artifacts.js.map +0 -1
  678. package/dist/core/agent/agents-md/cycle.js +0 -180
  679. package/dist/core/agent/agents-md/cycle.js.map +0 -1
  680. package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
  681. package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
  682. package/dist/core/agent/agents-md/spine.js +0 -40
  683. package/dist/core/agent/agents-md/spine.js.map +0 -1
  684. package/dist/core/coherence/events/merged.js +0 -40
  685. package/dist/core/coherence/events/merged.js.map +0 -1
  686. package/dist/core/diff/current-state.js +0 -96
  687. package/dist/core/diff/current-state.js.map +0 -1
  688. package/dist/core/diff/victims.js +0 -256
  689. package/dist/core/diff/victims.js.map +0 -1
  690. package/dist/core/staging/snapshot.js.map +0 -1
  691. package/dist/core/verify/file.js.map +0 -1
@@ -12,17 +12,12 @@ import {} from "../../core/staging/interrupted.js";
12
12
  import {} from "../../core/verify/answers.js";
13
13
  import {} from "../../core/verify/checklist.js";
14
14
  import {} from "../../core/verify/pins/pin.js";
15
- import { openClaimsNotice, tallyAnswers, verificationPath, verificationVerdict, } from "../../core/verify/record.js";
15
+ import { countedAnswers, openClaimsNotice, tallyAnswers, verificationVerdict, } from "../../core/verify/record.js";
16
+ import { verificationDir } from "../../core/verify/location.js";
16
17
  import { EXPLAIN_FOOTER, plural } from "../policy/format.js";
17
- import { answeredMark, contestedNotices, contractReportLine, forkedChecklistNotices, MARK, noticesFor, reportLine } from "./frozen.js";
18
+ import { answeredMark, contestedNotices, contractReportLine, forkedChecklistNotices, MARK, noticesFor, recordedPayload, reportLine, } from "./frozen.js";
18
19
  export function statuses(checklist, claims, recorded) {
19
- const subjects = new Map(checklist.claims.map((claim) => [claim.id, claim.subject]));
20
- const attested = recorded?.schema !== 2
21
- ? null
22
- : new Set((recorded.attestations ?? []).flatMap((attestation) => attestation.claims.filter((id) => subjects.get(id) === attestation.service)));
23
- const byId = new Map((recorded?.claims ?? [])
24
- .filter((claim) => attested === null || attested.has(claim.id))
25
- .map((claim) => [claim.id, claim]));
20
+ const byId = recorded === null ? new Map() : countedAnswers(checklist.claims, recorded);
26
21
  return claims.map((c) => {
27
22
  const answer = byId.get(c.id);
28
23
  return {
@@ -38,23 +33,33 @@ export function statuses(checklist, claims, recorded) {
38
33
  };
39
34
  });
40
35
  }
36
+ /**
37
+ * The read view.
38
+ *
39
+ * `lens` is `--service`: the same checklist, narrowed to the claims that
40
+ * service's code answers. It exists because in a ten-repo fleet the whole
41
+ * checklist is never one repository's business, and an agent that cannot ask
42
+ * "what do I still owe on FEAT-1?" answers the question by guessing. Narrowing
43
+ * narrows the verdict too — `verified` then means "every claim owned by this
44
+ * service is confirmed", which is why the payload names the service it applied.
45
+ *
46
+ * `bound` is loam.json's `service` — what this repository says it is. It only
47
+ * shapes the footer, and it is the whole of how federation is discoverable:
48
+ * the recording form printed here is the one that will actually work from here.
49
+ */
41
50
  export function report(target, checklist, recorded, scope = {}) {
42
51
  const { docsDir, featureDir, json } = target;
43
52
  const lens = scope.lens;
44
53
  const owners = [...new Set(checklist.claims.map((c) => c.subject))].sort();
45
54
  const scoped = lens === undefined ? checklist.claims : checklist.claims.filter((c) => c.subject === lens);
46
55
  const claims = statuses(checklist, scoped, recorded);
47
- const count = (v) => claims.filter((c) => c.verdict === v).length;
48
- const summary = {
49
- claims: claims.length,
50
- confirmed: count("confirmed"),
51
- unconfirmed: count("unconfirmed"),
52
- unanswered: count("unanswered"),
53
- };
56
+ const tally = tallyAnswers(claims);
57
+ // The payload's `summary` is the tally without its `attested` count, which
58
+ // travels as a key of its own beside it.
59
+ const summary = { claims: tally.claims, confirmed: tally.confirmed, unconfirmed: tally.unconfirmed, unanswered: tally.unanswered };
54
60
  // A record that answers a different question set is not an answer to this one,
55
61
  // however complete it looks — so staleness disqualifies it as a whole.
56
62
  const stale = recorded !== null && recorded.checklist !== checklist.digest;
57
- const tally = tallyAnswers(claims);
58
63
  const verdict = verificationVerdict(tally, stale);
59
64
  // `verified` keeps its name and its type, and loses its old meaning: a
60
65
  // scenario claim on an agent's word is answered, not run, so it no longer
@@ -89,17 +94,7 @@ export function report(target, checklist, recorded, scope = {}) {
89
94
  attested: tally.attested,
90
95
  summary,
91
96
  services: owners,
92
- recorded: recorded === null
93
- ? null
94
- : {
95
- path: repoPath(docsDir, verificationPath(featureDir)),
96
- recorded: recorded.recorded,
97
- checklist: recorded.checklist,
98
- stale,
99
- ...(recorded.attestations === undefined ? {} : { attestations: recorded.attestations }),
100
- ...(recorded.report === undefined ? {} : { report: recorded.report }),
101
- ...(recorded.contractReport === undefined ? {} : { contractReport: recorded.contractReport }),
102
- },
97
+ recorded: recorded === null ? null : recordedPayload(target, recorded, { stale }),
103
98
  claims,
104
99
  ...(notices.length === 0 ? {} : { notices }),
105
100
  });
@@ -122,7 +117,10 @@ export function report(target, checklist, recorded, scope = {}) {
122
117
  }
123
118
  console.log("");
124
119
  if (recorded === null) {
125
- console.log(` Not verified — no ${repoPath(docsDir, verificationPath(featureDir))}.`);
120
+ // Both places a record can be, because which one the next record lands in
121
+ // depends on the form that writes it — a service's attestation goes to its
122
+ // own file under verification/, the all-at-once form to verification.yaml.
123
+ console.log(` Not verified — no ${repoPath(docsDir, verificationDir(featureDir))}/ or verification.yaml yet.`);
126
124
  }
127
125
  else {
128
126
  console.log(` Recorded ${recorded.recorded} — ${summary.confirmed} confirmed, ${summary.unconfirmed} unconfirmed, ${summary.unanswered} unanswered.`);
@@ -205,19 +203,4 @@ export function federationNote(scope, owners) {
205
203
  }
206
204
  return [];
207
205
  }
208
- /**
209
- * The frozen view: an archived feature's record, verbatim. No checklist is
210
- * derived and no staleness is judged — there is nothing current to judge
211
- * against, and pretending otherwise is how a true record reads as a lie.
212
- * `frozen` is the marker a consumer branches on.
213
- *
214
- * The verdict is recounted from `claims[]`, never taken from `summary`: this is
215
- * the post-ship verdict on a feature nobody can re-derive a checklist for, so a
216
- * record whose counts had drifted from its own answers reported a shipped
217
- * feature as fully confirmed. (A record that contradicts itself never reaches
218
- * here — `readVerificationState` refuses it — so the recount and the summary
219
- * agree; the recount is what says WHY.) No record at all is reported as exactly
220
- * that, with `summary: null`, not zero claims, which would falsely say the
221
- * feature promised nothing.
222
- */
223
206
  //# sourceMappingURL=report.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/commands/verify/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The done-check as an answer: every claim, its verdict, and who said so.\n *\n * The per-claim rows are computed once and both surfaces read them — `--json`\n * and the terminal cannot disagree about a count, which is the failure this\n * shape prevents. The federation note is here rather than at the call sites\n * because a federated record answers per SERVICE and a reader looking at one\n * lens has to be told which of the others have not answered yet.\n */\nimport { emitJson, repoPath } from \"../../core/envelope/json.js\";\nimport { type CommitRecovery } from \"../../core/staging/interrupted.js\";\nimport { type AnsweredBy } from \"../../core/verify/answers.js\";\nimport { type Checklist } from \"../../core/verify/checklist.js\";\nimport { type EvidencePin } from \"../../core/verify/pins/pin.js\";\nimport {\n openClaimsNotice,\n tallyAnswers,\n type Verification,\n verificationPath,\n verificationVerdict,\n} from \"../../core/verify/record.js\";\nimport { EXPLAIN_FOOTER, plural } from \"../policy/format.js\";\nimport { answeredMark, contestedNotices, contractReportLine, forkedChecklistNotices, MARK, noticesFor, reportLine } from \"./frozen.js\";\n\nexport interface ClaimStatus {\n id: string;\n kind: string;\n subject: string;\n claim: string;\n verdict: \"confirmed\" | \"unconfirmed\" | \"unanswered\";\n answered_by?: AnsweredBy;\n evidence: string[];\n note?: string;\n /** The recorded pins, passed through where the record carries them — additive payload key. */\n evidence_pins?: EvidencePin[];\n}\n\nexport function statuses(checklist: Checklist, claims: Checklist[\"claims\"], recorded: Verification | null): ClaimStatus[] {\n const subjects = new Map(checklist.claims.map((claim) => [claim.id, claim.subject]));\n const attested =\n recorded?.schema !== 2\n ? null\n : new Set(\n (recorded.attestations ?? []).flatMap((attestation) =>\n attestation.claims.filter((id) => subjects.get(id) === attestation.service),\n ),\n );\n const byId = new Map(\n (recorded?.claims ?? [])\n .filter((claim) => attested === null || attested.has(claim.id))\n .map((claim) => [claim.id, claim]),\n );\n return claims.map((c) => {\n const answer = byId.get(c.id);\n return {\n id: c.id,\n kind: c.kind,\n subject: c.subject,\n claim: c.claim,\n verdict: answer?.verdict ?? \"unanswered\",\n ...(answer?.answered_by === undefined ? {} : { answered_by: answer.answered_by }),\n evidence: answer?.evidence ?? [],\n ...(answer?.note === undefined ? {} : { note: answer.note }),\n ...(answer?.evidence_pins === undefined ? {} : { evidence_pins: answer.evidence_pins }),\n };\n });\n}\n\n/**\n * The read view.\n *\n * `lens` is `--service`: the same checklist, narrowed to the claims that\n * service's code answers. It exists because in a ten-repo fleet the whole\n * checklist is never one repository's business, and an agent that cannot ask\n * \"what do I still owe on FEAT-1?\" answers the question by guessing. Narrowing\n * narrows the verdict too — `verified` then means \"every claim owned by this\n * service is confirmed\", which is why the payload names the service it applied.\n *\n * `bound` is loam.json's `service` — what this repository says it is. It only\n * shapes the footer, and it is the whole of how federation is discoverable:\n * the recording form printed here is the one that will actually work from here.\n */\n/**\n * Which feature is being reported on, and in what voice.\n *\n * Three of these travelled as loose strings beside two booleans through every\n * reporting path. They are one thing — the target of this run — and a record\n * makes the `--json` flag impossible to transpose with a lens name, which is\n * what the positional form allowed.\n */\nexport interface VerifyTarget {\n docsDir: string;\n featureDir: string;\n json: boolean;\n /**\n * Non-null when the record path first rolled a predecessor's interrupted\n * commit forward under its lock. Only `record` reports it; the read path\n * never recovers, so it never carries one.\n */\n recovered?: CommitRecovery | null;\n}\n\nexport function report(\n target: VerifyTarget,\n checklist: Checklist,\n recorded: Verification | null,\n scope: { lens?: string; bound?: string } = {},\n): void {\n const { docsDir, featureDir, json } = target;\n const lens = scope.lens;\n const owners = [...new Set(checklist.claims.map((c) => c.subject))].sort();\n const scoped = lens === undefined ? checklist.claims : checklist.claims.filter((c) => c.subject === lens);\n const claims = statuses(checklist, scoped, recorded);\n const count = (v: string): number => claims.filter((c) => c.verdict === v).length;\n const summary = {\n claims: claims.length,\n confirmed: count(\"confirmed\"),\n unconfirmed: count(\"unconfirmed\"),\n unanswered: count(\"unanswered\"),\n };\n // A record that answers a different question set is not an answer to this one,\n // however complete it looks — so staleness disqualifies it as a whole.\n const stale = recorded !== null && recorded.checklist !== checklist.digest;\n const tally = tallyAnswers(claims);\n const verdict = verificationVerdict(tally, stale);\n // `verified` keeps its name and its type, and loses its old meaning: a\n // scenario claim on an agent's word is answered, not run, so it no longer\n // reads the same as a green suite. `verdict` carries the distinction.\n const verified = verdict === \"verified\";\n // Contest is derivable from the checklist alone, so the read view says it\n // too: an agent looking at what it owes should learn that a second service\n // answers the same claim BEFORE it runs a suite whose report cannot be\n // attributed. Always from the whole checklist, never the lens — a shared\n // digest is a fact about the feature, and a narrowed view is the one place it\n // would otherwise be invisible.\n // The open-claims honesty line only where a record EXISTS: with none, the\n // surface already says \"Not verified\", and not-started is not partial.\n const open = recorded === null ? null : openClaimsNotice(tally, checklist.feature);\n const notices = [\n ...noticesFor(claims, checklist.feature),\n ...(open === null ? [] : [open]),\n ...contestedNotices(checklist.claims),\n ...forkedChecklistNotices(recorded, checklist.digest),\n ];\n\n if (json) {\n emitJson({\n command: \"verify\",\n feature: checklist.feature,\n path: repoPath(docsDir, featureDir),\n digest: checklist.digest,\n // Named only when narrowed, so a consumer can never mistake a service's\n // verdict for the feature's.\n ...(lens === undefined ? {} : { service: lens, checklistClaims: checklist.claims.length }),\n verified,\n verdict,\n attested: tally.attested,\n summary,\n services: owners,\n recorded:\n recorded === null\n ? null\n : {\n path: repoPath(docsDir, verificationPath(featureDir)),\n recorded: recorded.recorded,\n checklist: recorded.checklist,\n stale,\n ...(recorded.attestations === undefined ? {} : { attestations: recorded.attestations }),\n ...(recorded.report === undefined ? {} : { report: recorded.report }),\n ...(recorded.contractReport === undefined ? {} : { contractReport: recorded.contractReport }),\n },\n claims,\n ...(notices.length === 0 ? {} : { notices }),\n });\n return;\n }\n\n const scopeNote =\n lens === undefined ? \"\" : ` owned by ${lens} (of ${plural(checklist.claims.length, \"claim\")} on the checklist)`;\n console.log(\n `${checklist.feature} — ${plural(claims.length, \"claim\")}${scopeNote} derived from ${repoPath(docsDir, featureDir)}\\n`,\n );\n if (claims.length === 0) {\n console.log(\n lens === undefined\n ? \" Nothing to check: this feature's delta, specs and openapi promise nothing yet.\"\n : ` No claim on this checklist is owned by '${lens}'.${owners.length > 0 ? ` The services that own claims here are ${owners.join(\", \")}.` : \"\"}`,\n );\n return;\n }\n for (const c of claims) {\n console.log(` ${MARK[c.verdict]} ${c.id} [${c.subject}] ${c.claim}${answeredMark(c)}`);\n for (const e of c.evidence) console.log(` ${e}`);\n if (c.note !== undefined) console.log(` note: ${c.note}`);\n }\n\n console.log(\"\");\n if (recorded === null) {\n console.log(` Not verified — no ${repoPath(docsDir, verificationPath(featureDir))}.`);\n } else {\n console.log(\n ` Recorded ${recorded.recorded} — ${summary.confirmed} confirmed, ${summary.unconfirmed} unconfirmed, ${summary.unanswered} unanswered.`,\n );\n if (stale) {\n console.log(\" STALE: the feature changed after this was recorded. Answer the claims above again.\");\n }\n for (const attestation of recorded.attestations ?? []) {\n console.log(\n ` Attested by ${attestation.service} at ${attestation.commit.slice(0, 12)} (${attestation.recorded}, ${plural(attestation.claims.length, \"claim\")}).`,\n );\n if (attestation.report !== undefined) console.log(` from ${reportLine(attestation.report)}`);\n if (attestation.contractReport !== undefined) {\n console.log(` from ${contractReportLine(attestation.contractReport)}`);\n }\n }\n if (recorded.report !== undefined) console.log(` Answered from ${reportLine(recorded.report)}.`);\n if (recorded.contractReport !== undefined) {\n console.log(` Contract report read: ${contractReportLine(recorded.contractReport)}.`);\n }\n }\n // The notice's code LEADS the line here, where validate's findings now carry\n // theirs in a trailing `(code)` — and the two shapes are deliberate, not\n // drift. A notice is one line and its code is its subject, so it reads as a\n // label; a finding is one of hundreds in a report whose subject is the\n // message, so its code is an aside. Appending a second copy to satisfy the\n // one-shape instinct would print `⚠ verify.claims-open: … (verify.claims-open)`,\n // and the leading form is also the one the frozen post-archive view prints\n // (frozen.ts) — a live record and a shipped one must not describe the same\n // notice two ways. Either way the code is on screen, which is the whole of\n // what EXPLAIN_FOOTER below needs to be true.\n for (const notice of notices) console.log(` ⚠ ${notice.code}: ${notice.message}`);\n // An attested record has answered every question — telling it to \"answer each\n // claim\" would send an agent round a loop it has already run. The notice\n // above names the one thing left, which is a test run.\n if (verdict === \"unverified\") {\n console.log(\"\\n Answer each claim, then record the answers:\\n\");\n console.log(' [{ \"id\": \"<claim id>\", \"verdict\": \"confirmed\", \"evidence\": [\"src/x.ts:42\"] }]');\n console.log(`\\n loam verify ${checklist.feature} --record answers.json${recordSuffix(scope, owners)}`);\n for (const line of federationNote(scope, owners)) console.log(line);\n console.log(\"\\n A claim you cannot show evidence for is `unconfirmed` — say why in `note`.\");\n }\n // APPENDED, as format.ts promises — the report's last line, never a wedge\n // between the notices and the recording instructions that follow them. It\n // follows printed notice codes only: claim marks carry no code to look up,\n // and the frozen post-archive view (frozen.ts) stays without it on purpose,\n // because shipped history is not a fix-it surface.\n if (notices.length > 0) console.log(`\\n${EXPLAIN_FOOTER}`);\n}\n\n/**\n * The `--service` the printed form should carry. It comes from loam.json, never\n * from the checklist: the only attestation this repository can make is its own,\n * and a hint naming a service that lives somewhere else is a hint that fails.\n */\nexport function recordSuffix(scope: { lens?: string; bound?: string }, owners: string[]): string {\n if (scope.bound !== undefined) return ` --service ${scope.bound}`;\n return owners.length > 1 || scope.lens !== undefined ? \" --service <svc>\" : \"\";\n}\n\n/** Why the form above has a `--service` on it — the one place federation is taught. */\nexport function federationNote(scope: { lens?: string; bound?: string }, owners: string[]): string[] {\n if (scope.bound !== undefined) {\n const others = owners.filter((s) => s !== scope.bound);\n return [\n \"\",\n ` This repository is ${scope.bound}, so it attests only ${scope.bound}'s claims, bound to its git HEAD.`,\n ...(others.length === 0\n ? []\n : [` The claims owned by ${others.join(\", \")} are recorded the same way from those repositories.`]),\n ];\n }\n if (owners.length > 1 || scope.lens !== undefined) {\n return [\n \"\",\n ` This checklist spans ${plural(owners.length, \"service\")} (${owners.join(\", \")}) — each attests its own claims`,\n \" from its own repository, where loam.json names that service.\",\n ];\n }\n return [];\n}\n\n/**\n * The frozen view: an archived feature's record, verbatim. No checklist is\n * derived and no staleness is judged — there is nothing current to judge\n * against, and pretending otherwise is how a true record reads as a lie.\n * `frozen` is the marker a consumer branches on.\n *\n * The verdict is recounted from `claims[]`, never taken from `summary`: this is\n * the post-ship verdict on a feature nobody can re-derive a checklist for, so a\n * record whose counts had drifted from its own answers reported a shipped\n * feature as fully confirmed. (A record that contradicts itself never reaches\n * here — `readVerificationState` refuses it — so the recount and the summary\n * agree; the recount is what says WHY.) No record at all is reported as exactly\n * that, with `summary: null`, not zero claims, which would falsely say the\n * feature promised nothing.\n */\n"]}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/commands/verify/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The done-check as an answer: every claim, its verdict, and who said so.\n *\n * The per-claim rows are computed once and both surfaces read them — `--json`\n * and the terminal cannot disagree about a count, which is the failure this\n * shape prevents. The federation note is here rather than at the call sites\n * because a federated record answers per SERVICE and a reader looking at one\n * lens has to be told which of the others have not answered yet.\n */\nimport { emitJson, repoPath } from \"../../core/envelope/json.js\";\nimport { type CommitRecovery } from \"../../core/staging/interrupted.js\";\nimport { type AnsweredBy } from \"../../core/verify/answers.js\";\nimport { type Checklist } from \"../../core/verify/checklist.js\";\nimport { type EvidencePin } from \"../../core/verify/pins/pin.js\";\nimport {\n countedAnswers,\n openClaimsNotice,\n tallyAnswers,\n type RecordedClaim,\n type Verification,\n verificationVerdict,\n} from \"../../core/verify/record.js\";\nimport { verificationDir } from \"../../core/verify/location.js\";\nimport { EXPLAIN_FOOTER, plural } from \"../policy/format.js\";\nimport {\n answeredMark,\n contestedNotices,\n contractReportLine,\n forkedChecklistNotices,\n MARK,\n noticesFor,\n recordedPayload,\n reportLine,\n} from \"./frozen.js\";\n\nexport interface ClaimStatus {\n id: string;\n kind: string;\n subject: string;\n claim: string;\n verdict: \"confirmed\" | \"unconfirmed\" | \"unanswered\";\n answered_by?: AnsweredBy;\n evidence: string[];\n note?: string;\n /** The recorded pins, passed through where the record carries them — additive payload key. */\n evidence_pins?: EvidencePin[];\n}\n\nexport function statuses(checklist: Checklist, claims: Checklist[\"claims\"], recorded: Verification | null): ClaimStatus[] {\n const byId = recorded === null ? new Map<string, RecordedClaim>() : countedAnswers(checklist.claims, recorded);\n return claims.map((c) => {\n const answer = byId.get(c.id);\n return {\n id: c.id,\n kind: c.kind,\n subject: c.subject,\n claim: c.claim,\n verdict: answer?.verdict ?? \"unanswered\",\n ...(answer?.answered_by === undefined ? {} : { answered_by: answer.answered_by }),\n evidence: answer?.evidence ?? [],\n ...(answer?.note === undefined ? {} : { note: answer.note }),\n ...(answer?.evidence_pins === undefined ? {} : { evidence_pins: answer.evidence_pins }),\n };\n });\n}\n\n/**\n * Which feature is being reported on, and in what voice.\n *\n * Three of these travelled as loose strings beside two booleans through every\n * reporting path. They are one thing — the target of this run — and a record\n * makes the `--json` flag impossible to transpose with a lens name, which is\n * what the positional form allowed.\n */\nexport interface VerifyTarget {\n docsDir: string;\n featureDir: string;\n json: boolean;\n /**\n * Non-null when the record path first rolled a predecessor's interrupted\n * commit forward under its lock. Only `record` reports it; the read path\n * never recovers, so it never carries one.\n */\n recovered?: CommitRecovery | null;\n}\n\n/**\n * The read view.\n *\n * `lens` is `--service`: the same checklist, narrowed to the claims that\n * service's code answers. It exists because in a ten-repo fleet the whole\n * checklist is never one repository's business, and an agent that cannot ask\n * \"what do I still owe on FEAT-1?\" answers the question by guessing. Narrowing\n * narrows the verdict too — `verified` then means \"every claim owned by this\n * service is confirmed\", which is why the payload names the service it applied.\n *\n * `bound` is loam.json's `service` — what this repository says it is. It only\n * shapes the footer, and it is the whole of how federation is discoverable:\n * the recording form printed here is the one that will actually work from here.\n */\nexport function report(\n target: VerifyTarget,\n checklist: Checklist,\n recorded: Verification | null,\n scope: { lens?: string; bound?: string } = {},\n): void {\n const { docsDir, featureDir, json } = target;\n const lens = scope.lens;\n const owners = [...new Set(checklist.claims.map((c) => c.subject))].sort();\n const scoped = lens === undefined ? checklist.claims : checklist.claims.filter((c) => c.subject === lens);\n const claims = statuses(checklist, scoped, recorded);\n const tally = tallyAnswers(claims);\n // The payload's `summary` is the tally without its `attested` count, which\n // travels as a key of its own beside it.\n const summary = { claims: tally.claims, confirmed: tally.confirmed, unconfirmed: tally.unconfirmed, unanswered: tally.unanswered };\n // A record that answers a different question set is not an answer to this one,\n // however complete it looks — so staleness disqualifies it as a whole.\n const stale = recorded !== null && recorded.checklist !== checklist.digest;\n const verdict = verificationVerdict(tally, stale);\n // `verified` keeps its name and its type, and loses its old meaning: a\n // scenario claim on an agent's word is answered, not run, so it no longer\n // reads the same as a green suite. `verdict` carries the distinction.\n const verified = verdict === \"verified\";\n // Contest is derivable from the checklist alone, so the read view says it\n // too: an agent looking at what it owes should learn that a second service\n // answers the same claim BEFORE it runs a suite whose report cannot be\n // attributed. Always from the whole checklist, never the lens — a shared\n // digest is a fact about the feature, and a narrowed view is the one place it\n // would otherwise be invisible.\n // The open-claims honesty line only where a record EXISTS: with none, the\n // surface already says \"Not verified\", and not-started is not partial.\n const open = recorded === null ? null : openClaimsNotice(tally, checklist.feature);\n const notices = [\n ...noticesFor(claims, checklist.feature),\n ...(open === null ? [] : [open]),\n ...contestedNotices(checklist.claims),\n ...forkedChecklistNotices(recorded, checklist.digest),\n ];\n\n if (json) {\n emitJson({\n command: \"verify\",\n feature: checklist.feature,\n path: repoPath(docsDir, featureDir),\n digest: checklist.digest,\n // Named only when narrowed, so a consumer can never mistake a service's\n // verdict for the feature's.\n ...(lens === undefined ? {} : { service: lens, checklistClaims: checklist.claims.length }),\n verified,\n verdict,\n attested: tally.attested,\n summary,\n services: owners,\n recorded: recorded === null ? null : recordedPayload(target, recorded, { stale }),\n claims,\n ...(notices.length === 0 ? {} : { notices }),\n });\n return;\n }\n\n const scopeNote =\n lens === undefined ? \"\" : ` owned by ${lens} (of ${plural(checklist.claims.length, \"claim\")} on the checklist)`;\n console.log(\n `${checklist.feature} — ${plural(claims.length, \"claim\")}${scopeNote} derived from ${repoPath(docsDir, featureDir)}\\n`,\n );\n if (claims.length === 0) {\n console.log(\n lens === undefined\n ? \" Nothing to check: this feature's delta, specs and openapi promise nothing yet.\"\n : ` No claim on this checklist is owned by '${lens}'.${owners.length > 0 ? ` The services that own claims here are ${owners.join(\", \")}.` : \"\"}`,\n );\n return;\n }\n for (const c of claims) {\n console.log(` ${MARK[c.verdict]} ${c.id} [${c.subject}] ${c.claim}${answeredMark(c)}`);\n for (const e of c.evidence) console.log(` ${e}`);\n if (c.note !== undefined) console.log(` note: ${c.note}`);\n }\n\n console.log(\"\");\n if (recorded === null) {\n // Both places a record can be, because which one the next record lands in\n // depends on the form that writes it — a service's attestation goes to its\n // own file under verification/, the all-at-once form to verification.yaml.\n console.log(` Not verified — no ${repoPath(docsDir, verificationDir(featureDir))}/ or verification.yaml yet.`);\n } else {\n console.log(\n ` Recorded ${recorded.recorded} — ${summary.confirmed} confirmed, ${summary.unconfirmed} unconfirmed, ${summary.unanswered} unanswered.`,\n );\n if (stale) {\n console.log(\" STALE: the feature changed after this was recorded. Answer the claims above again.\");\n }\n for (const attestation of recorded.attestations ?? []) {\n console.log(\n ` Attested by ${attestation.service} at ${attestation.commit.slice(0, 12)} (${attestation.recorded}, ${plural(attestation.claims.length, \"claim\")}).`,\n );\n if (attestation.report !== undefined) console.log(` from ${reportLine(attestation.report)}`);\n if (attestation.contractReport !== undefined) {\n console.log(` from ${contractReportLine(attestation.contractReport)}`);\n }\n }\n if (recorded.report !== undefined) console.log(` Answered from ${reportLine(recorded.report)}.`);\n if (recorded.contractReport !== undefined) {\n console.log(` Contract report read: ${contractReportLine(recorded.contractReport)}.`);\n }\n }\n // The notice's code LEADS the line here, where validate's findings now carry\n // theirs in a trailing `(code)` — and the two shapes are deliberate, not\n // drift. A notice is one line and its code is its subject, so it reads as a\n // label; a finding is one of hundreds in a report whose subject is the\n // message, so its code is an aside. Appending a second copy to satisfy the\n // one-shape instinct would print `⚠ verify.claims-open: … (verify.claims-open)`,\n // and the leading form is also the one the frozen post-archive view prints\n // (frozen.ts) — a live record and a shipped one must not describe the same\n // notice two ways. Either way the code is on screen, which is the whole of\n // what EXPLAIN_FOOTER below needs to be true.\n for (const notice of notices) console.log(` ⚠ ${notice.code}: ${notice.message}`);\n // An attested record has answered every question — telling it to \"answer each\n // claim\" would send an agent round a loop it has already run. The notice\n // above names the one thing left, which is a test run.\n if (verdict === \"unverified\") {\n console.log(\"\\n Answer each claim, then record the answers:\\n\");\n console.log(' [{ \"id\": \"<claim id>\", \"verdict\": \"confirmed\", \"evidence\": [\"src/x.ts:42\"] }]');\n console.log(`\\n loam verify ${checklist.feature} --record answers.json${recordSuffix(scope, owners)}`);\n for (const line of federationNote(scope, owners)) console.log(line);\n console.log(\"\\n A claim you cannot show evidence for is `unconfirmed` — say why in `note`.\");\n }\n // APPENDED, as format.ts promises — the report's last line, never a wedge\n // between the notices and the recording instructions that follow them. It\n // follows printed notice codes only: claim marks carry no code to look up,\n // and the frozen post-archive view (frozen.ts) stays without it on purpose,\n // because shipped history is not a fix-it surface.\n if (notices.length > 0) console.log(`\\n${EXPLAIN_FOOTER}`);\n}\n\n/**\n * The `--service` the printed form should carry. It comes from loam.json, never\n * from the checklist: the only attestation this repository can make is its own,\n * and a hint naming a service that lives somewhere else is a hint that fails.\n */\nexport function recordSuffix(scope: { lens?: string; bound?: string }, owners: string[]): string {\n if (scope.bound !== undefined) return ` --service ${scope.bound}`;\n return owners.length > 1 || scope.lens !== undefined ? \" --service <svc>\" : \"\";\n}\n\n/** Why the form above has a `--service` on it — the one place federation is taught. */\nexport function federationNote(scope: { lens?: string; bound?: string }, owners: string[]): string[] {\n if (scope.bound !== undefined) {\n const others = owners.filter((s) => s !== scope.bound);\n return [\n \"\",\n ` This repository is ${scope.bound}, so it attests only ${scope.bound}'s claims, bound to its git HEAD.`,\n ...(others.length === 0\n ? []\n : [` The claims owned by ${others.join(\", \")} are recorded the same way from those repositories.`]),\n ];\n }\n if (owners.length > 1 || scope.lens !== undefined) {\n return [\n \"\",\n ` This checklist spans ${plural(owners.length, \"service\")} (${owners.join(\", \")}) — each attests its own claims`,\n \" from its own repository, where loam.json names that service.\",\n ];\n }\n return [];\n}\n"]}
@@ -9,12 +9,22 @@
9
9
  import { execFile } from "node:child_process";
10
10
  import { readFile, stat } from "node:fs/promises";
11
11
  import { resolvePortableFileInside } from "../../core/kernel/path-safety.js";
12
+ import { GIT_TIMEOUT_MS } from "../../core/provenance/git.js";
12
13
  import { isScenarioReport, readCucumberReport, readScenarioReport } from "../../core/results.js";
13
14
  import {} from "../../core/verify/answers.js";
14
15
  import { citedLine, pinnedDigest, sourceLines } from "../../core/verify/pins/pin.js";
15
16
  import {} from "../../core/verify/record.js";
16
17
  import {} from "./evidence/pins.js";
17
18
  import { readReportArtifact } from "./evidence/read.js";
19
+ /**
20
+ * The two shapes `--results` reads, named wherever a file is refused for being
21
+ * neither — a fleet whose runner has no cucumber formatter learns here that
22
+ * the runner-neutral shape exists, rather than from a parse error.
23
+ */
24
+ const RESULTS_SHAPES = "--results reads two JSON shapes: a cucumber JSON report (cucumber-js --format json, cucumber-jvm, behave, " +
25
+ "SpecFlow/Reqnroll, pytest-bdd --cucumberjson and godog's cucumber formatter all write one), or loam's runner-neutral " +
26
+ '{"loamScenarioReport": 1, "results": [{"digest": "<16 hex>", "status": "passed"}]} — the shape a JUnit, pytest, ' +
27
+ "Vitest or house runner's report is adapted into, keyed by each scenario's @loam-digest tag.";
18
28
  /**
19
29
  * The report as an artifact, not merely as a parse. The resolve/read/digest
20
30
  * plumbing lives in `./evidence/read.ts`, shared with `--contract-results`:
@@ -24,7 +34,7 @@ import { readReportArtifact } from "./evidence/read.js";
24
34
  * the record.
25
35
  */
26
36
  export async function readResults(spelled, repoDir) {
27
- const artifact = await readReportArtifact(spelled, repoDir, "test report");
37
+ const artifact = await readReportArtifact(spelled, repoDir, "test report", RESULTS_SHAPES);
28
38
  if (!artifact.ok)
29
39
  return artifact;
30
40
  // The reader is chosen by MARKER, before either shape has validated
@@ -53,8 +63,8 @@ export async function readResults(spelled, repoDir) {
53
63
  * in this repository: an agent's `file:line` resolves to a real line that is
54
64
  * unchanged at the attested commit, and each runner's evidence names the report
55
65
  * loam just read — the cucumber report for `runner`, the contract report for
56
- * `external-runner`. Legacy global mode deliberately keeps its original, looser
57
- * evidence contract for backward compatibility.
66
+ * `external-runner`. The legacy all-at-once form binds to no commit, so it gets
67
+ * only the first half of that — {@link validateLocalEvidence}.
58
68
  *
59
69
  * The same pass now STAMPS what it validated: one `EvidencePin` per agent
60
70
  * citation, built where `committedFile`'s blob is already in hand (zero new
@@ -108,29 +118,11 @@ export async function validateServiceEvidence(answers, binding, reports, tokens)
108
118
  continue;
109
119
  }
110
120
  for (const evidence of answer.evidence) {
111
- const match = /^(.+):([1-9]\d*)$/.exec(evidence);
112
- if (match === null) {
113
- return err(`Claim ${answer.id} has evidence '${evidence}' — service evidence must be a canonical relative file:line.`);
114
- }
115
- const relativePath = match[1];
116
- const line = Number(match[2]);
117
- let absolutePath;
118
- try {
119
- absolutePath = resolvePortableFileInside(repoDir, relativePath, `evidence for ${answer.id}`);
120
- }
121
- catch (cause) {
122
- return err(`Claim ${answer.id} has unsafe evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);
123
- }
121
+ const cited = await resolveCitation(repoDir, answer.id, evidence);
122
+ if (!cited.ok)
123
+ return err(cited.message);
124
+ const { relativePath, line } = cited;
124
125
  try {
125
- const info = await stat(absolutePath);
126
- if (!info.isFile()) {
127
- return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' is not a regular file.`);
128
- }
129
- const source = await readFile(absolutePath, "utf8");
130
- const lines = sourceLines(source).length;
131
- if (line > lines) {
132
- return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' has only ${lines} line(s).`);
133
- }
134
126
  const committed = await committedFile(repoDir, commit, relativePath);
135
127
  if (!committed.ok) {
136
128
  return err(`Claim ${answer.id} has evidence '${evidence}' that is not bound to ${commit.slice(0, 12)}: ${committed.message}`);
@@ -172,6 +164,92 @@ export async function validateServiceEvidence(answers, binding, reports, tokens)
172
164
  }
173
165
  return { ok: true, pins, tokenMisses };
174
166
  }
167
+ /**
168
+ * One `file:line` citation, resolved in the working tree: canonical form, a
169
+ * path that stays inside the repository, a regular file, and a line that
170
+ * exists. What both recording forms owe every agent citation before anything
171
+ * else — the federated form then binds it to the attested commit.
172
+ */
173
+ async function resolveCitation(repoDir, id, evidence) {
174
+ const err = (message) => ({ ok: false, message });
175
+ const match = /^(.+):([1-9]\d*)$/.exec(evidence);
176
+ if (match === null) {
177
+ return err(`Claim ${id} has evidence '${evidence}' — evidence must be a canonical relative file:line.`);
178
+ }
179
+ const relativePath = match[1];
180
+ const line = Number(match[2]);
181
+ let absolutePath;
182
+ try {
183
+ absolutePath = resolvePortableFileInside(repoDir, relativePath, `evidence for ${id}`);
184
+ }
185
+ catch (cause) {
186
+ return err(`Claim ${id} has unsafe evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);
187
+ }
188
+ try {
189
+ // Absence named in the repository's own spelling: the stat's ENOENT
190
+ // carries the absolute path of whatever machine ran this, which tells the
191
+ // reader nothing the citation did not.
192
+ const info = await stat(absolutePath).catch((cause) => {
193
+ if (cause.code === "ENOENT")
194
+ return null;
195
+ throw cause;
196
+ });
197
+ if (info === null)
198
+ return err(`Claim ${id} has evidence '${evidence}', but '${relativePath}' does not exist in this repository.`);
199
+ if (!info.isFile())
200
+ return err(`Claim ${id} has evidence '${evidence}', but '${relativePath}' is not a regular file.`);
201
+ const lines = sourceLines(await readFile(absolutePath, "utf8")).length;
202
+ if (line > lines)
203
+ return err(`Claim ${id} has evidence '${evidence}', but '${relativePath}' has only ${lines} line(s).`);
204
+ }
205
+ catch (cause) {
206
+ return err(`Claim ${id} has unreadable evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);
207
+ }
208
+ return { ok: true, relativePath, line };
209
+ }
210
+ /**
211
+ * The all-at-once form's evidence check: every agent citation must resolve to a
212
+ * real line of a real file in THIS repository. The form binds to no commit, so
213
+ * there is nothing to pin and no blob to compare — but it used to check
214
+ * nothing at all, and accepted `src/foo.py:1` for a file that did not exist
215
+ * while printing `verified`, in the same release whose `--service` form refused
216
+ * exactly that citation. The commit binding is the federated form's addition,
217
+ * not the difference between checked and unchecked.
218
+ *
219
+ * A CITATION is what is checked, and only a citation: an entry shaped like a
220
+ * `path:line` (see {@link citesALine}). The all-at-once form has always taken
221
+ * other evidence — "manual QA by Alice", a CI run's URL, `README.md` with no
222
+ * line — and still records it as it stands; holding every entry to the
223
+ * `file:line` grammar refused all of that with `answers-unevidenced`, which
224
+ * was never the defect. What a `path:line` claims is checkable, so it is
225
+ * checked; prose claims nothing a file can contradict. The federated form
226
+ * keeps its stricter contract — every agent entry a `file:line` bound to the
227
+ * attested commit — because its whole promise is that binding.
228
+ */
229
+ export async function validateLocalEvidence(answers, repoDir) {
230
+ for (const answer of answers) {
231
+ if (answer.verdict !== "confirmed" || answer.answered_by === "runner" || answer.answered_by === "external-runner")
232
+ continue;
233
+ for (const evidence of answer.evidence.filter(citesALine)) {
234
+ const cited = await resolveCitation(repoDir, answer.id, evidence);
235
+ if (!cited.ok)
236
+ return cited;
237
+ }
238
+ }
239
+ return { ok: true };
240
+ }
241
+ /**
242
+ * Is this evidence entry shaped like a `path:line` — something that names a
243
+ * line of a file and can therefore be wrong about it? One token (no
244
+ * whitespace: prose such as "checked at 10:30" is not a citation), not a URL
245
+ * (`https://ci.example:8443` ends in a number too), ending in `:<digits>`.
246
+ * Deliberately wider than `resolveCitation`'s canonical form: `src/x.py:0` and
247
+ * `../x.py:3` are citations, badly made, and must be refused rather than
248
+ * waved through as prose.
249
+ */
250
+ function citesALine(evidence) {
251
+ return /^\S+:\d+$/.test(evidence) && !/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(evidence);
252
+ }
175
253
  /** Require the evidence blob to exist at HEAD and have no uncommitted edits. */
176
254
  export async function committedFile(repoDir, commit, path) {
177
255
  const clean = await git(repoDir, ["diff", "--quiet", commit, "--", path]);
@@ -193,12 +271,16 @@ export async function committedFile(repoDir, commit, path) {
193
271
  * HOLDS the docs lock: a `git rev-parse` blocked on a credential-helper prompt
194
272
  * used to hang this process forever with the lock in hand, wedging every
195
273
  * archive, rebase and record in the fleet behind a live pid that
196
- * `breakStaleLock` rightly refuses to break. The timeout mirrors
197
- * `core/provenance/git.ts`; the cap is sized for `git show` of a source file
198
- * an attestation cites (the default 1 MiB refused sound evidence the moment a
199
- * generated client crossed it), not for arbitrary blobs.
274
+ * `breakStaleLock` rightly refuses to break. The timeout IS
275
+ * `core/provenance/git.ts`'s (imported, so the two cannot drift); the cap is
276
+ * sized for `git show` of a source file an attestation cites (the default
277
+ * 1 MiB refused sound evidence the moment a generated client crossed it), not
278
+ * for arbitrary blobs.
279
+ *
280
+ * Not `gitText` itself: its answer carries no signal, so a child killed by
281
+ * the deadline and one killed from outside would read alike, and the
282
+ * deadline's own message below is the one that names the fix.
200
283
  */
201
- const GIT_TIMEOUT_MS = 10_000;
202
284
  const GIT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
203
285
  function git(repoDir, args) {
204
286
  return new Promise((done) => {
@@ -1 +1 @@
1
- {"version":3,"file":"results.js","sourceRoot":"","sources":["../../../src/commands/verify/results.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The cucumber report, and the repository it has to be evidence FROM.\n *\n * A results file is only evidence if it describes the commit the attestation\n * names, so reading it and checking the repository are one subject: the git\n * helpers below are here because they answer the same question — is this\n * working tree the one the report was produced from.\n */\nimport { execFile } from \"node:child_process\";\nimport { readFile, stat } from \"node:fs/promises\";\nimport { resolvePortableFileInside } from \"../../core/kernel/path-safety.js\";\nimport { isScenarioReport, readCucumberReport, readScenarioReport, type ReportScenario } from \"../../core/results.js\";\nimport { type Answer } from \"../../core/verify/answers.js\";\nimport { citedLine, pinnedDigest, sourceLines, type EvidencePin } from \"../../core/verify/pins/pin.js\";\nimport { type ConsumedReport, type ConsumedReports } from \"../../core/verify/record.js\";\nimport { type EvidencePins, type TokenMiss } from \"./evidence/pins.js\";\nimport { readReportArtifact } from \"./evidence/read.js\";\n\nexport type ResultsRead =\n | { ok: true; report: ConsumedReport; scenarios: ReportScenario[] }\n | { ok: false; code: \"answers-unreadable\"; message: string };\n\n/**\n * The report as an artifact, not merely as a parse. The resolve/read/digest\n * plumbing lives in `./evidence/read.ts`, shared with `--contract-results`:\n * the bytes are digested and the file's mtime read, because loam cannot prove\n * a JSON file came from executing this commit and should stop implying\n * otherwise. It can say precisely which file it consumed, and that goes on\n * the record.\n */\nexport async function readResults(spelled: string, repoDir: string | undefined): Promise<ResultsRead> {\n const artifact = await readReportArtifact(spelled, repoDir, \"test report\");\n if (!artifact.ok) return artifact;\n // The reader is chosen by MARKER, before either shape has validated\n // anything. A file declaring `loamScenarioReport` must be graded as that\n // shape even when it is malformed — falling through to the cucumber parser\n // would tell an author their report is not a cucumber array, which is true,\n // useless, and hides the actual mistake.\n const parsed = isScenarioReport(artifact.doc)\n ? readScenarioReport(artifact.doc, spelled)\n : readCucumberReport(artifact.doc, spelled);\n if (!parsed.ok) return { ok: false, code: \"answers-unreadable\", message: parsed.message };\n return {\n ok: true,\n scenarios: parsed.scenarios,\n report: {\n path: artifact.spelled,\n digest: artifact.digest,\n mtime: artifact.mtime,\n scenarios: parsed.scenarios.length,\n },\n };\n}\n\n/**\n * The repository an attestation binds to and the commit it binds at — two\n * values only ever asked about together, so they travel as one and the\n * inconsistent pair (this repo, that commit) stops being representable.\n */\nexport interface AttestationBinding {\n repoDir: string;\n commit: string;\n}\n\nexport type ServiceEvidenceCheck =\n /** Every citation held up; `pins`/`tokenMisses` are the stamp and the honesty it computed en route. */\n | ({ ok: true } & EvidencePins)\n | { ok: false; message: string };\n\n/**\n * In federated mode, a confirmation is accepted only when its evidence holds up\n * in this repository: an agent's `file:line` resolves to a real line that is\n * unchanged at the attested commit, and each runner's evidence names the report\n * loam just read — the cucumber report for `runner`, the contract report for\n * `external-runner`. Legacy global mode deliberately keeps its original, looser\n * evidence contract for backward compatibility.\n *\n * The same pass now STAMPS what it validated: one `EvidencePin` per agent\n * citation, built where `committedFile`'s blob is already in hand (zero new\n * git calls), and one `TokenMiss` per citation whose blob does not contain the\n * claim's token (`tokens`, claim id → the literal the claim asserts). Runner\n * and contract answers get no pins — their evidence names a report entry, not\n * a file — and the legacy all-at-once form never calls this validator, so it\n * stamps nothing.\n */\nexport async function validateServiceEvidence(\n answers: Answer[],\n binding: AttestationBinding,\n reports: ConsumedReports,\n tokens: ReadonlyMap<string, string>,\n): Promise<ServiceEvidenceCheck> {\n const { repoDir, commit } = binding;\n const err = (message: string): ServiceEvidenceCheck => ({ ok: false, message });\n const pins = new Map<string, EvidencePin[]>();\n const tokenMisses: TokenMiss[] = [];\n for (const report of [reports.results, reports.contract]) {\n if (report === undefined) continue;\n // Most reports are build output and untracked, and `git diff` is quiet on\n // those — nothing here invents a rule for them. One the repository DOES\n // carry has to match the commit being attested, like any other file the\n // evidence rests on. Both reports independently: each is its own file\n // making its own claim about this commit.\n const clean = await git(repoDir, [\"diff\", \"--quiet\", commit, \"--\", report.path]);\n if (clean.code !== 0) {\n // Only `1` is git ANSWERING — \"that file differs\". Every other non-zero\n // code is git failing to answer at all: no git on PATH, a fork that never\n // happened, a child killed by a CI timeout or the OOM killer. Testing for\n // `1` alone made those silently pass the check, so the one run that most\n // needs the binding — the one where the machine is falling over — is the\n // one that would mint an attestation whose report was never bound to the\n // commit. An unanswered check is not a passed one, so the unknown refuses\n // on the same terms as the known, and `clean.stderr` carries whatever\n // account of itself the child managed to leave.\n return err(\n clean.code === 1\n ? `The test report '${report.path}' is committed to this repository and differs from ${commit.slice(0, 12)} — commit it or attest the commit it belongs to.`\n : `Cannot tell whether the test report '${report.path}' is bound to ${commit.slice(0, 12)} — git could not be run to completion: ${clean.stderr || \"git diff failed\"}`,\n );\n }\n }\n for (const answer of answers) {\n if (answer.verdict !== \"confirmed\") continue;\n if (answer.answered_by === \"runner\" || answer.answered_by === \"external-runner\") {\n // A runner's evidence is an entry inside its report, not a file:line in\n // the source — there is nothing to resolve. What must hold is that it\n // names THAT report: an answer pointing anywhere else is not this run's.\n const report = answer.answered_by === \"runner\" ? reports.results : reports.contract;\n const stray = answer.evidence.filter((e) => report === undefined || !e.startsWith(`${report.path}: `));\n if (stray.length > 0) {\n return err(`Claim ${answer.id} has runner evidence ${stray.map((e) => `'${e}'`).join(\", \")} that does not name the report loam read.`);\n }\n continue;\n }\n for (const evidence of answer.evidence) {\n const match = /^(.+):([1-9]\\d*)$/.exec(evidence);\n if (match === null) {\n return err(`Claim ${answer.id} has evidence '${evidence}' — service evidence must be a canonical relative file:line.`);\n }\n const relativePath = match[1]!;\n const line = Number(match[2]);\n let absolutePath: string;\n try {\n absolutePath = resolvePortableFileInside(repoDir, relativePath, `evidence for ${answer.id}`);\n } catch (cause) {\n return err(`Claim ${answer.id} has unsafe evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n try {\n const info = await stat(absolutePath);\n if (!info.isFile()) {\n return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' is not a regular file.`);\n }\n const source = await readFile(absolutePath, \"utf8\");\n const lines = sourceLines(source).length;\n if (line > lines) {\n return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' has only ${lines} line(s).`);\n }\n const committed = await committedFile(repoDir, commit, relativePath);\n if (!committed.ok) {\n return err(`Claim ${answer.id} has evidence '${evidence}' that is not bound to ${commit.slice(0, 12)}: ${committed.message}`);\n }\n // The pin, built from the blob just fetched. `citedLine` is undefined\n // exactly when the line is past the committed file's end — the refusal\n // this validator already owed — so the bounds check and the pinned\n // text can never disagree about where lines fall.\n const text = citedLine(committed.source, line);\n if (text === undefined) {\n const committedLines = sourceLines(committed.source).length;\n return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' has only ${committedLines} line(s) at ${commit.slice(0, 12)}.`);\n }\n // The token scan happens here, per citation, because the blob is in\n // hand and record time is when the answerer can still re-read what\n // they cited. Substring, never a parse: the claim asserts a literal.\n // A blob that does NOT contain the token is warned once (the notice\n // built from `tokenMisses`) and the pin then omits the token — see\n // EvidencePin.token for why re-litigating a never-there token at\n // every later validate would be a false sentence with no repair.\n const token = tokens.get(answer.id);\n const held = token !== undefined && committed.source.includes(token);\n if (token !== undefined && !held) tokenMisses.push({ id: answer.id, evidence, token });\n const list = pins.get(answer.id) ?? [];\n list.push({\n path: relativePath,\n line,\n file_sha256: pinnedDigest(committed.source),\n text,\n ...(held && token !== undefined ? { token } : {}),\n });\n pins.set(answer.id, list);\n } catch (cause) {\n return err(`Claim ${answer.id} has unreadable evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n }\n }\n return { ok: true, pins, tokenMisses };\n}\n\ntype CommitResult = { ok: true; commit: string } | { ok: false; message: string };\ntype CommittedFileResult = { ok: true; source: string } | { ok: false; message: string };\n\n/** Require the evidence blob to exist at HEAD and have no uncommitted edits. */\nexport async function committedFile(repoDir: string, commit: string, path: string): Promise<CommittedFileResult> {\n const clean = await git(repoDir, [\"diff\", \"--quiet\", commit, \"--\", path]);\n if (clean.code !== 0) {\n return {\n ok: false,\n message: clean.code === 1 ? `'${path}' has uncommitted changes` : clean.stderr || \"git diff failed\",\n };\n }\n const blob = await git(repoDir, [\"show\", `${commit}:${path}`]);\n if (blob.code !== 0) {\n return { ok: false, message: blob.stderr || `'${path}' is not tracked by that commit` };\n }\n return { ok: true, source: blob.stdout };\n}\n\ninterface GitResult {\n /**\n * Git's own exit status, or `-1` for \"git could not be run to completion\" —\n * no git on PATH, a spawn failure, a child killed by a signal (the deadline\n * below included), or output past the declared cap. Callers read specific\n * meaning into small numbers (`1` from `git diff --quiet` is \"that file\n * differs\"), so a run that never reached an exit status must not borrow one\n * of them.\n */\n code: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * How long one git question may take, and how much it may say. Both bounds\n * exist because every call in this module now runs while `loam verify --record`\n * HOLDS the docs lock: a `git rev-parse` blocked on a credential-helper prompt\n * used to hang this process forever with the lock in hand, wedging every\n * archive, rebase and record in the fleet behind a live pid that\n * `breakStaleLock` rightly refuses to break. The timeout mirrors\n * `core/provenance/git.ts`; the cap is sized for `git show` of a source file\n * an attestation cites (the default 1 MiB refused sound evidence the moment a\n * generated client crossed it), not for arbitrary blobs.\n */\nconst GIT_TIMEOUT_MS = 10_000;\nconst GIT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;\n\nfunction git(repoDir: string, args: string[]): Promise<GitResult> {\n return new Promise((done) => {\n execFile(\n \"git\",\n [\"-C\", repoDir, ...args],\n { encoding: \"utf8\", timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_OUTPUT_BYTES },\n (error, stdout, stderr) => {\n done({\n // `error.code` is an exit status only when it is a number: a spawn\n // failure reports an errno string (\"ENOENT\") and a signal-killed child\n // reports none at all. Folding those onto 1 would state that `git diff`\n // found a difference — a Ctrl-C or an OOM kill during\n // `loam verify --record` would read as an uncommitted edit and refuse\n // a sound attestation. -1 is not a status git assigns, so both diff\n // sites fall to their stderr-carrying arm and say what happened.\n code: error === null ? 0 : typeof error.code === \"number\" ? error.code : -1,\n stdout,\n // A child that never ran writes nothing to stderr, so its only account\n // of itself is on the error; without this the refusal would name no\n // cause at all. A child WE killed gets its account written for it: the\n // deadline exists for a git blocked on a credential-helper prompt, and\n // the raw \"Command failed: git …\" says nothing about a deadline — the\n // operator re-runs, waits the same 10 s, and reads the same message,\n // with the actual fix (GIT_TERMINAL_PROMPT=0, or the helper) never\n // reachable from it.\n stderr:\n error !== null && error.killed && error.signal !== null\n ? `git did not answer within ${GIT_TIMEOUT_MS / 1000}s and was stopped — a prompt or a hung remote; GIT_TERMINAL_PROMPT=0 disables credential prompts`\n : stderr.trim() || (error === null ? \"\" : error.message),\n });\n });\n });\n}\n\n/** Resolve HEAD without a shell, so repository paths remain data, never code. */\nexport async function repositoryCommit(repoDir: string): Promise<CommitResult> {\n const result = await git(repoDir, [\"rev-parse\", \"--verify\", \"HEAD^{commit}\"]);\n if (result.code !== 0) {\n return {\n ok: false,\n message: `Federated verification requires a git repository with a committed HEAD: ${result.stderr || \"git rev-parse failed\"}`,\n };\n }\n const commit = result.stdout.trim();\n if (!/^[0-9a-f]{40,64}$/i.test(commit)) {\n return { ok: false, message: `Git returned an invalid HEAD commit '${commit}'.` };\n }\n return { ok: true, commit };\n}\n"]}
1
+ {"version":3,"file":"results.js","sourceRoot":"","sources":["../../../src/commands/verify/results.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The cucumber report, and the repository it has to be evidence FROM.\n *\n * A results file is only evidence if it describes the commit the attestation\n * names, so reading it and checking the repository are one subject: the git\n * helpers below are here because they answer the same question — is this\n * working tree the one the report was produced from.\n */\nimport { execFile } from \"node:child_process\";\nimport { readFile, stat } from \"node:fs/promises\";\nimport { resolvePortableFileInside } from \"../../core/kernel/path-safety.js\";\nimport { GIT_TIMEOUT_MS } from \"../../core/provenance/git.js\";\nimport { isScenarioReport, readCucumberReport, readScenarioReport, type ReportScenario } from \"../../core/results.js\";\nimport { type Answer } from \"../../core/verify/answers.js\";\nimport { citedLine, pinnedDigest, sourceLines, type EvidencePin } from \"../../core/verify/pins/pin.js\";\nimport { type ConsumedReport, type ConsumedReports } from \"../../core/verify/record.js\";\nimport { type EvidencePins, type TokenMiss } from \"./evidence/pins.js\";\nimport { readReportArtifact } from \"./evidence/read.js\";\n\nexport type ResultsRead =\n | { ok: true; report: ConsumedReport; scenarios: ReportScenario[] }\n | { ok: false; code: \"answers-unreadable\"; message: string };\n\n/**\n * The two shapes `--results` reads, named wherever a file is refused for being\n * neither — a fleet whose runner has no cucumber formatter learns here that\n * the runner-neutral shape exists, rather than from a parse error.\n */\nconst RESULTS_SHAPES =\n \"--results reads two JSON shapes: a cucumber JSON report (cucumber-js --format json, cucumber-jvm, behave, \" +\n \"SpecFlow/Reqnroll, pytest-bdd --cucumberjson and godog's cucumber formatter all write one), or loam's runner-neutral \" +\n '{\"loamScenarioReport\": 1, \"results\": [{\"digest\": \"<16 hex>\", \"status\": \"passed\"}]} — the shape a JUnit, pytest, ' +\n \"Vitest or house runner's report is adapted into, keyed by each scenario's @loam-digest tag.\";\n\n/**\n * The report as an artifact, not merely as a parse. The resolve/read/digest\n * plumbing lives in `./evidence/read.ts`, shared with `--contract-results`:\n * the bytes are digested and the file's mtime read, because loam cannot prove\n * a JSON file came from executing this commit and should stop implying\n * otherwise. It can say precisely which file it consumed, and that goes on\n * the record.\n */\nexport async function readResults(spelled: string, repoDir: string | undefined): Promise<ResultsRead> {\n const artifact = await readReportArtifact(spelled, repoDir, \"test report\", RESULTS_SHAPES);\n if (!artifact.ok) return artifact;\n // The reader is chosen by MARKER, before either shape has validated\n // anything. A file declaring `loamScenarioReport` must be graded as that\n // shape even when it is malformed — falling through to the cucumber parser\n // would tell an author their report is not a cucumber array, which is true,\n // useless, and hides the actual mistake.\n const parsed = isScenarioReport(artifact.doc)\n ? readScenarioReport(artifact.doc, spelled)\n : readCucumberReport(artifact.doc, spelled);\n if (!parsed.ok) return { ok: false, code: \"answers-unreadable\", message: parsed.message };\n return {\n ok: true,\n scenarios: parsed.scenarios,\n report: {\n path: artifact.spelled,\n digest: artifact.digest,\n mtime: artifact.mtime,\n scenarios: parsed.scenarios.length,\n },\n };\n}\n\n/**\n * The repository an attestation binds to and the commit it binds at — two\n * values only ever asked about together, so they travel as one and the\n * inconsistent pair (this repo, that commit) stops being representable.\n */\nexport interface AttestationBinding {\n repoDir: string;\n commit: string;\n}\n\nexport type ServiceEvidenceCheck =\n /** Every citation held up; `pins`/`tokenMisses` are the stamp and the honesty it computed en route. */\n | ({ ok: true } & EvidencePins)\n | { ok: false; message: string };\n\n/**\n * In federated mode, a confirmation is accepted only when its evidence holds up\n * in this repository: an agent's `file:line` resolves to a real line that is\n * unchanged at the attested commit, and each runner's evidence names the report\n * loam just read — the cucumber report for `runner`, the contract report for\n * `external-runner`. The legacy all-at-once form binds to no commit, so it gets\n * only the first half of that — {@link validateLocalEvidence}.\n *\n * The same pass now STAMPS what it validated: one `EvidencePin` per agent\n * citation, built where `committedFile`'s blob is already in hand (zero new\n * git calls), and one `TokenMiss` per citation whose blob does not contain the\n * claim's token (`tokens`, claim id → the literal the claim asserts). Runner\n * and contract answers get no pins — their evidence names a report entry, not\n * a file — and the legacy all-at-once form never calls this validator, so it\n * stamps nothing.\n */\nexport async function validateServiceEvidence(\n answers: Answer[],\n binding: AttestationBinding,\n reports: ConsumedReports,\n tokens: ReadonlyMap<string, string>,\n): Promise<ServiceEvidenceCheck> {\n const { repoDir, commit } = binding;\n const err = (message: string): ServiceEvidenceCheck => ({ ok: false, message });\n const pins = new Map<string, EvidencePin[]>();\n const tokenMisses: TokenMiss[] = [];\n for (const report of [reports.results, reports.contract]) {\n if (report === undefined) continue;\n // Most reports are build output and untracked, and `git diff` is quiet on\n // those — nothing here invents a rule for them. One the repository DOES\n // carry has to match the commit being attested, like any other file the\n // evidence rests on. Both reports independently: each is its own file\n // making its own claim about this commit.\n const clean = await git(repoDir, [\"diff\", \"--quiet\", commit, \"--\", report.path]);\n if (clean.code !== 0) {\n // Only `1` is git ANSWERING — \"that file differs\". Every other non-zero\n // code is git failing to answer at all: no git on PATH, a fork that never\n // happened, a child killed by a CI timeout or the OOM killer. Testing for\n // `1` alone made those silently pass the check, so the one run that most\n // needs the binding — the one where the machine is falling over — is the\n // one that would mint an attestation whose report was never bound to the\n // commit. An unanswered check is not a passed one, so the unknown refuses\n // on the same terms as the known, and `clean.stderr` carries whatever\n // account of itself the child managed to leave.\n return err(\n clean.code === 1\n ? `The test report '${report.path}' is committed to this repository and differs from ${commit.slice(0, 12)} — commit it or attest the commit it belongs to.`\n : `Cannot tell whether the test report '${report.path}' is bound to ${commit.slice(0, 12)} — git could not be run to completion: ${clean.stderr || \"git diff failed\"}`,\n );\n }\n }\n for (const answer of answers) {\n if (answer.verdict !== \"confirmed\") continue;\n if (answer.answered_by === \"runner\" || answer.answered_by === \"external-runner\") {\n // A runner's evidence is an entry inside its report, not a file:line in\n // the source — there is nothing to resolve. What must hold is that it\n // names THAT report: an answer pointing anywhere else is not this run's.\n const report = answer.answered_by === \"runner\" ? reports.results : reports.contract;\n const stray = answer.evidence.filter((e) => report === undefined || !e.startsWith(`${report.path}: `));\n if (stray.length > 0) {\n return err(`Claim ${answer.id} has runner evidence ${stray.map((e) => `'${e}'`).join(\", \")} that does not name the report loam read.`);\n }\n continue;\n }\n for (const evidence of answer.evidence) {\n const cited = await resolveCitation(repoDir, answer.id, evidence);\n if (!cited.ok) return err(cited.message);\n const { relativePath, line } = cited;\n try {\n const committed = await committedFile(repoDir, commit, relativePath);\n if (!committed.ok) {\n return err(`Claim ${answer.id} has evidence '${evidence}' that is not bound to ${commit.slice(0, 12)}: ${committed.message}`);\n }\n // The pin, built from the blob just fetched. `citedLine` is undefined\n // exactly when the line is past the committed file's end — the refusal\n // this validator already owed — so the bounds check and the pinned\n // text can never disagree about where lines fall.\n const text = citedLine(committed.source, line);\n if (text === undefined) {\n const committedLines = sourceLines(committed.source).length;\n return err(`Claim ${answer.id} has evidence '${evidence}', but '${relativePath}' has only ${committedLines} line(s) at ${commit.slice(0, 12)}.`);\n }\n // The token scan happens here, per citation, because the blob is in\n // hand and record time is when the answerer can still re-read what\n // they cited. Substring, never a parse: the claim asserts a literal.\n // A blob that does NOT contain the token is warned once (the notice\n // built from `tokenMisses`) and the pin then omits the token — see\n // EvidencePin.token for why re-litigating a never-there token at\n // every later validate would be a false sentence with no repair.\n const token = tokens.get(answer.id);\n const held = token !== undefined && committed.source.includes(token);\n if (token !== undefined && !held) tokenMisses.push({ id: answer.id, evidence, token });\n const list = pins.get(answer.id) ?? [];\n list.push({\n path: relativePath,\n line,\n file_sha256: pinnedDigest(committed.source),\n text,\n ...(held && token !== undefined ? { token } : {}),\n });\n pins.set(answer.id, list);\n } catch (cause) {\n return err(`Claim ${answer.id} has unreadable evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n }\n }\n return { ok: true, pins, tokenMisses };\n}\n\ntype Citation = { ok: true; relativePath: string; line: number } | { ok: false; message: string };\n\n/**\n * One `file:line` citation, resolved in the working tree: canonical form, a\n * path that stays inside the repository, a regular file, and a line that\n * exists. What both recording forms owe every agent citation before anything\n * else — the federated form then binds it to the attested commit.\n */\nasync function resolveCitation(repoDir: string, id: string, evidence: string): Promise<Citation> {\n const err = (message: string): Citation => ({ ok: false, message });\n const match = /^(.+):([1-9]\\d*)$/.exec(evidence);\n if (match === null) {\n return err(`Claim ${id} has evidence '${evidence}' — evidence must be a canonical relative file:line.`);\n }\n const relativePath = match[1]!;\n const line = Number(match[2]);\n let absolutePath: string;\n try {\n absolutePath = resolvePortableFileInside(repoDir, relativePath, `evidence for ${id}`);\n } catch (cause) {\n return err(`Claim ${id} has unsafe evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n try {\n // Absence named in the repository's own spelling: the stat's ENOENT\n // carries the absolute path of whatever machine ran this, which tells the\n // reader nothing the citation did not.\n const info = await stat(absolutePath).catch((cause: NodeJS.ErrnoException) => {\n if (cause.code === \"ENOENT\") return null;\n throw cause;\n });\n if (info === null) return err(`Claim ${id} has evidence '${evidence}', but '${relativePath}' does not exist in this repository.`);\n if (!info.isFile()) return err(`Claim ${id} has evidence '${evidence}', but '${relativePath}' is not a regular file.`);\n const lines = sourceLines(await readFile(absolutePath, \"utf8\")).length;\n if (line > lines) return err(`Claim ${id} has evidence '${evidence}', but '${relativePath}' has only ${lines} line(s).`);\n } catch (cause) {\n return err(`Claim ${id} has unreadable evidence '${evidence}': ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n return { ok: true, relativePath, line };\n}\n\n/**\n * The all-at-once form's evidence check: every agent citation must resolve to a\n * real line of a real file in THIS repository. The form binds to no commit, so\n * there is nothing to pin and no blob to compare — but it used to check\n * nothing at all, and accepted `src/foo.py:1` for a file that did not exist\n * while printing `verified`, in the same release whose `--service` form refused\n * exactly that citation. The commit binding is the federated form's addition,\n * not the difference between checked and unchecked.\n *\n * A CITATION is what is checked, and only a citation: an entry shaped like a\n * `path:line` (see {@link citesALine}). The all-at-once form has always taken\n * other evidence — \"manual QA by Alice\", a CI run's URL, `README.md` with no\n * line — and still records it as it stands; holding every entry to the\n * `file:line` grammar refused all of that with `answers-unevidenced`, which\n * was never the defect. What a `path:line` claims is checkable, so it is\n * checked; prose claims nothing a file can contradict. The federated form\n * keeps its stricter contract — every agent entry a `file:line` bound to the\n * attested commit — because its whole promise is that binding.\n */\nexport async function validateLocalEvidence(answers: Answer[], repoDir: string): Promise<{ ok: true } | { ok: false; message: string }> {\n for (const answer of answers) {\n if (answer.verdict !== \"confirmed\" || answer.answered_by === \"runner\" || answer.answered_by === \"external-runner\") continue;\n for (const evidence of answer.evidence.filter(citesALine)) {\n const cited = await resolveCitation(repoDir, answer.id, evidence);\n if (!cited.ok) return cited;\n }\n }\n return { ok: true };\n}\n\n/**\n * Is this evidence entry shaped like a `path:line` — something that names a\n * line of a file and can therefore be wrong about it? One token (no\n * whitespace: prose such as \"checked at 10:30\" is not a citation), not a URL\n * (`https://ci.example:8443` ends in a number too), ending in `:<digits>`.\n * Deliberately wider than `resolveCitation`'s canonical form: `src/x.py:0` and\n * `../x.py:3` are citations, badly made, and must be refused rather than\n * waved through as prose.\n */\nfunction citesALine(evidence: string): boolean {\n return /^\\S+:\\d+$/.test(evidence) && !/^[A-Za-z][A-Za-z0-9+.-]*:\\/\\//.test(evidence);\n}\n\ntype CommitResult = { ok: true; commit: string } | { ok: false; message: string };\ntype CommittedFileResult = { ok: true; source: string } | { ok: false; message: string };\n\n/** Require the evidence blob to exist at HEAD and have no uncommitted edits. */\nexport async function committedFile(repoDir: string, commit: string, path: string): Promise<CommittedFileResult> {\n const clean = await git(repoDir, [\"diff\", \"--quiet\", commit, \"--\", path]);\n if (clean.code !== 0) {\n return {\n ok: false,\n message: clean.code === 1 ? `'${path}' has uncommitted changes` : clean.stderr || \"git diff failed\",\n };\n }\n const blob = await git(repoDir, [\"show\", `${commit}:${path}`]);\n if (blob.code !== 0) {\n return { ok: false, message: blob.stderr || `'${path}' is not tracked by that commit` };\n }\n return { ok: true, source: blob.stdout };\n}\n\ninterface GitResult {\n /**\n * Git's own exit status, or `-1` for \"git could not be run to completion\" —\n * no git on PATH, a spawn failure, a child killed by a signal (the deadline\n * below included), or output past the declared cap. Callers read specific\n * meaning into small numbers (`1` from `git diff --quiet` is \"that file\n * differs\"), so a run that never reached an exit status must not borrow one\n * of them.\n */\n code: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * How long one git question may take, and how much it may say. Both bounds\n * exist because every call in this module now runs while `loam verify --record`\n * HOLDS the docs lock: a `git rev-parse` blocked on a credential-helper prompt\n * used to hang this process forever with the lock in hand, wedging every\n * archive, rebase and record in the fleet behind a live pid that\n * `breakStaleLock` rightly refuses to break. The timeout IS\n * `core/provenance/git.ts`'s (imported, so the two cannot drift); the cap is\n * sized for `git show` of a source file an attestation cites (the default\n * 1 MiB refused sound evidence the moment a generated client crossed it), not\n * for arbitrary blobs.\n *\n * Not `gitText` itself: its answer carries no signal, so a child killed by\n * the deadline and one killed from outside would read alike, and the\n * deadline's own message below is the one that names the fix.\n */\nconst GIT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;\n\nfunction git(repoDir: string, args: string[]): Promise<GitResult> {\n return new Promise((done) => {\n execFile(\n \"git\",\n [\"-C\", repoDir, ...args],\n { encoding: \"utf8\", timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_OUTPUT_BYTES },\n (error, stdout, stderr) => {\n done({\n // `error.code` is an exit status only when it is a number: a spawn\n // failure reports an errno string (\"ENOENT\") and a signal-killed child\n // reports none at all. Folding those onto 1 would state that `git diff`\n // found a difference — a Ctrl-C or an OOM kill during\n // `loam verify --record` would read as an uncommitted edit and refuse\n // a sound attestation. -1 is not a status git assigns, so both diff\n // sites fall to their stderr-carrying arm and say what happened.\n code: error === null ? 0 : typeof error.code === \"number\" ? error.code : -1,\n stdout,\n // A child that never ran writes nothing to stderr, so its only account\n // of itself is on the error; without this the refusal would name no\n // cause at all. A child WE killed gets its account written for it: the\n // deadline exists for a git blocked on a credential-helper prompt, and\n // the raw \"Command failed: git …\" says nothing about a deadline — the\n // operator re-runs, waits the same 10 s, and reads the same message,\n // with the actual fix (GIT_TERMINAL_PROMPT=0, or the helper) never\n // reachable from it.\n stderr:\n error !== null && error.killed && error.signal !== null\n ? `git did not answer within ${GIT_TIMEOUT_MS / 1000}s and was stopped — a prompt or a hung remote; GIT_TERMINAL_PROMPT=0 disables credential prompts`\n : stderr.trim() || (error === null ? \"\" : error.message),\n });\n });\n });\n}\n\n/** Resolve HEAD without a shell, so repository paths remain data, never code. */\nexport async function repositoryCommit(repoDir: string): Promise<CommitResult> {\n const result = await git(repoDir, [\"rev-parse\", \"--verify\", \"HEAD^{commit}\"]);\n if (result.code !== 0) {\n return {\n ok: false,\n message: `Federated verification requires a git repository with a committed HEAD: ${result.stderr || \"git rev-parse failed\"}`,\n };\n }\n const commit = result.stdout.trim();\n if (!/^[0-9a-f]{40,64}$/i.test(commit)) {\n return { ok: false, message: `Git returned an invalid HEAD commit '${commit}'.` };\n }\n return { ok: true, commit };\n}\n"]}