approval-md 0.0.1 → 0.1.0

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 (522) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +5 -0
  3. package/README.md +909 -4
  4. package/SPEC.md +445 -32
  5. package/cli.js +29 -3
  6. package/dist/src/adapters/agentmail.js +1200 -0
  7. package/dist/src/adapters/agentmail.js.map +1 -0
  8. package/dist/src/adapters/conformance.js +461 -0
  9. package/dist/src/adapters/conformance.js.map +1 -0
  10. package/dist/src/adapters/contract.js +941 -0
  11. package/dist/src/adapters/contract.js.map +1 -0
  12. package/dist/src/adapters/email.js +749 -0
  13. package/dist/src/adapters/email.js.map +1 -0
  14. package/dist/src/adapters/env-passphrase.js +132 -0
  15. package/dist/src/adapters/env-passphrase.js.map +1 -0
  16. package/dist/src/adapters/registry.js +76 -0
  17. package/dist/src/adapters/registry.js.map +1 -0
  18. package/dist/src/adapters/smtp.js +499 -0
  19. package/dist/src/adapters/smtp.js.map +1 -0
  20. package/dist/src/adapters/vault-provider.js +161 -0
  21. package/dist/src/adapters/vault-provider.js.map +1 -0
  22. package/dist/src/channels/batch.js +121 -0
  23. package/dist/src/channels/batch.js.map +1 -0
  24. package/dist/src/channels/cli.js +468 -0
  25. package/dist/src/channels/cli.js.map +1 -0
  26. package/dist/src/channels/conformance.js +445 -0
  27. package/dist/src/channels/conformance.js.map +1 -0
  28. package/dist/src/channels/contract.js +494 -0
  29. package/dist/src/channels/contract.js.map +1 -0
  30. package/dist/src/channels/payload-view.js +43 -0
  31. package/dist/src/channels/payload-view.js.map +1 -0
  32. package/dist/src/channels/render-queue.js +564 -0
  33. package/dist/src/channels/render-queue.js.map +1 -0
  34. package/dist/src/channels/tagging.js +723 -0
  35. package/dist/src/channels/tagging.js.map +1 -0
  36. package/dist/src/channels/telegram.js +3190 -0
  37. package/dist/src/channels/telegram.js.map +1 -0
  38. package/dist/src/channels/web.js +903 -0
  39. package/dist/src/channels/web.js.map +1 -0
  40. package/dist/src/cli/adapter.js +278 -0
  41. package/dist/src/cli/adapter.js.map +1 -0
  42. package/dist/src/cli/amend.js +2171 -0
  43. package/dist/src/cli/amend.js.map +1 -0
  44. package/dist/src/cli/args.js +86 -0
  45. package/dist/src/cli/args.js.map +1 -0
  46. package/dist/src/cli/attest.js +307 -0
  47. package/dist/src/cli/attest.js.map +1 -0
  48. package/dist/src/cli/audit-card.js +201 -0
  49. package/dist/src/cli/audit-card.js.map +1 -0
  50. package/dist/src/cli/audit.js +460 -0
  51. package/dist/src/cli/audit.js.map +1 -0
  52. package/dist/src/cli/channel-telegram.js +2063 -0
  53. package/dist/src/cli/channel-telegram.js.map +1 -0
  54. package/dist/src/cli/channel-web.js +357 -0
  55. package/dist/src/cli/channel-web.js.map +1 -0
  56. package/dist/src/cli/channel.js +438 -0
  57. package/dist/src/cli/channel.js.map +1 -0
  58. package/dist/src/cli/checkpoint-tap.js +238 -0
  59. package/dist/src/cli/checkpoint-tap.js.map +1 -0
  60. package/dist/src/cli/coverage.js +343 -0
  61. package/dist/src/cli/coverage.js.map +1 -0
  62. package/dist/src/cli/daemon.js +631 -0
  63. package/dist/src/cli/daemon.js.map +1 -0
  64. package/dist/src/cli/doctor.js +2648 -0
  65. package/dist/src/cli/doctor.js.map +1 -0
  66. package/dist/src/cli/env.js +302 -0
  67. package/dist/src/cli/env.js.map +1 -0
  68. package/dist/src/cli/execute.js +1682 -0
  69. package/dist/src/cli/execute.js.map +1 -0
  70. package/dist/src/cli/exit-codes.js +82 -0
  71. package/dist/src/cli/exit-codes.js.map +1 -0
  72. package/dist/src/cli/feedback.js +205 -0
  73. package/dist/src/cli/feedback.js.map +1 -0
  74. package/dist/src/cli/gate-window.js +294 -0
  75. package/dist/src/cli/gate-window.js.map +1 -0
  76. package/dist/src/cli/gate.js +557 -0
  77. package/dist/src/cli/gate.js.map +1 -0
  78. package/dist/src/cli/git-scope.js +295 -0
  79. package/dist/src/cli/git-scope.js.map +1 -0
  80. package/dist/src/cli/gloss-attach.js +107 -0
  81. package/dist/src/cli/gloss-attach.js.map +1 -0
  82. package/dist/src/cli/gloss-codex-child.js +149 -0
  83. package/dist/src/cli/gloss-codex-child.js.map +1 -0
  84. package/dist/src/cli/gloss-codex.js +255 -0
  85. package/dist/src/cli/gloss-codex.js.map +1 -0
  86. package/dist/src/cli/gloss-options.js +79 -0
  87. package/dist/src/cli/gloss-options.js.map +1 -0
  88. package/dist/src/cli/gloss.js +362 -0
  89. package/dist/src/cli/gloss.js.map +1 -0
  90. package/dist/src/cli/help.js +2217 -0
  91. package/dist/src/cli/help.js.map +1 -0
  92. package/dist/src/cli/hook.js +2743 -0
  93. package/dist/src/cli/hook.js.map +1 -0
  94. package/dist/src/cli/import.js +175 -0
  95. package/dist/src/cli/import.js.map +1 -0
  96. package/dist/src/cli/init.js +336 -0
  97. package/dist/src/cli/init.js.map +1 -0
  98. package/dist/src/cli/instructions.js +262 -0
  99. package/dist/src/cli/instructions.js.map +1 -0
  100. package/dist/src/cli/journal.js +238 -0
  101. package/dist/src/cli/journal.js.map +1 -0
  102. package/dist/src/cli/log-advance.js +749 -0
  103. package/dist/src/cli/log-advance.js.map +1 -0
  104. package/dist/src/cli/log-anchor.js +387 -0
  105. package/dist/src/cli/log-anchor.js.map +1 -0
  106. package/dist/src/cli/log-checkpoint.js +128 -0
  107. package/dist/src/cli/log-checkpoint.js.map +1 -0
  108. package/dist/src/cli/log-sync.js +849 -0
  109. package/dist/src/cli/log-sync.js.map +1 -0
  110. package/dist/src/cli/log-verbs.js +354 -0
  111. package/dist/src/cli/log-verbs.js.map +1 -0
  112. package/dist/src/cli/long-help.js +148 -0
  113. package/dist/src/cli/long-help.js.map +1 -0
  114. package/dist/src/cli/main.js +1056 -0
  115. package/dist/src/cli/main.js.map +1 -0
  116. package/dist/src/cli/mcp.js +306 -0
  117. package/dist/src/cli/mcp.js.map +1 -0
  118. package/dist/src/cli/paths.js +79 -0
  119. package/dist/src/cli/paths.js.map +1 -0
  120. package/dist/src/cli/payload.js +253 -0
  121. package/dist/src/cli/payload.js.map +1 -0
  122. package/dist/src/cli/policy.js +229 -0
  123. package/dist/src/cli/policy.js.map +1 -0
  124. package/dist/src/cli/preflight.js +888 -0
  125. package/dist/src/cli/preflight.js.map +1 -0
  126. package/dist/src/cli/progress.js +112 -0
  127. package/dist/src/cli/progress.js.map +1 -0
  128. package/dist/src/cli/prompt.js +312 -0
  129. package/dist/src/cli/prompt.js.map +1 -0
  130. package/dist/src/cli/records.js +66 -0
  131. package/dist/src/cli/records.js.map +1 -0
  132. package/dist/src/cli/render.js +132 -0
  133. package/dist/src/cli/render.js.map +1 -0
  134. package/dist/src/cli/sandbox.js +150 -0
  135. package/dist/src/cli/sandbox.js.map +1 -0
  136. package/dist/src/cli/scaffold.js +137 -0
  137. package/dist/src/cli/scaffold.js.map +1 -0
  138. package/dist/src/cli/setup-adapter.js +475 -0
  139. package/dist/src/cli/setup-adapter.js.map +1 -0
  140. package/dist/src/cli/setup-channel.js +635 -0
  141. package/dist/src/cli/setup-channel.js.map +1 -0
  142. package/dist/src/cli/setup-checkpoint.js +196 -0
  143. package/dist/src/cli/setup-checkpoint.js.map +1 -0
  144. package/dist/src/cli/setup-common.js +376 -0
  145. package/dist/src/cli/setup-common.js.map +1 -0
  146. package/dist/src/cli/setup-flow.js +476 -0
  147. package/dist/src/cli/setup-flow.js.map +1 -0
  148. package/dist/src/cli/setup-service.js +308 -0
  149. package/dist/src/cli/setup-service.js.map +1 -0
  150. package/dist/src/cli/setup.js +473 -0
  151. package/dist/src/cli/setup.js.map +1 -0
  152. package/dist/src/cli/style.js +469 -0
  153. package/dist/src/cli/style.js.map +1 -0
  154. package/dist/src/cli/token.js +274 -0
  155. package/dist/src/cli/token.js.map +1 -0
  156. package/dist/src/cli/up.js +847 -0
  157. package/dist/src/cli/up.js.map +1 -0
  158. package/dist/src/cli/usage.js +91 -0
  159. package/dist/src/cli/usage.js.map +1 -0
  160. package/dist/src/cli/values.js +189 -0
  161. package/dist/src/cli/values.js.map +1 -0
  162. package/dist/src/cli/vault.js +362 -0
  163. package/dist/src/cli/vault.js.map +1 -0
  164. package/dist/src/cli/verb-registry.js +2173 -0
  165. package/dist/src/cli/verb-registry.js.map +1 -0
  166. package/dist/src/cli/wordmark.js +52 -0
  167. package/dist/src/cli/wordmark.js.map +1 -0
  168. package/dist/src/core/advance-cycle.js +200 -0
  169. package/dist/src/core/advance-cycle.js.map +1 -0
  170. package/dist/src/core/agents-md.js +747 -0
  171. package/dist/src/core/agents-md.js.map +1 -0
  172. package/dist/src/core/attest.js +577 -0
  173. package/dist/src/core/attest.js.map +1 -0
  174. package/dist/src/core/audit.js +882 -0
  175. package/dist/src/core/audit.js.map +1 -0
  176. package/dist/src/core/budgets.js +449 -0
  177. package/dist/src/core/budgets.js.map +1 -0
  178. package/dist/src/core/checkpoint.js +738 -0
  179. package/dist/src/core/checkpoint.js.map +1 -0
  180. package/dist/src/core/child-env.js +86 -0
  181. package/dist/src/core/child-env.js.map +1 -0
  182. package/dist/src/core/clock.js +43 -0
  183. package/dist/src/core/clock.js.map +1 -0
  184. package/dist/src/core/command-class.js +2321 -0
  185. package/dist/src/core/command-class.js.map +1 -0
  186. package/dist/src/core/coverage-sources/adapter.js +71 -0
  187. package/dist/src/core/coverage-sources/adapter.js.map +1 -0
  188. package/dist/src/core/coverage-sources/gh.js +136 -0
  189. package/dist/src/core/coverage-sources/gh.js.map +1 -0
  190. package/dist/src/core/coverage-sources/git.js +269 -0
  191. package/dist/src/core/coverage-sources/git.js.map +1 -0
  192. package/dist/src/core/coverage.js +337 -0
  193. package/dist/src/core/coverage.js.map +1 -0
  194. package/dist/src/core/credential-spec.js +23 -0
  195. package/dist/src/core/credential-spec.js.map +1 -0
  196. package/dist/src/core/dark-session.js +714 -0
  197. package/dist/src/core/dark-session.js.map +1 -0
  198. package/dist/src/core/decision-refusal.js +265 -0
  199. package/dist/src/core/decision-refusal.js.map +1 -0
  200. package/dist/src/core/env-file.js +837 -0
  201. package/dist/src/core/env-file.js.map +1 -0
  202. package/dist/src/core/execute.js +1233 -0
  203. package/dist/src/core/execute.js.map +1 -0
  204. package/dist/src/core/frontmatter.js +100 -0
  205. package/dist/src/core/frontmatter.js.map +1 -0
  206. package/dist/src/core/gate-window.js +506 -0
  207. package/dist/src/core/gate-window.js.map +1 -0
  208. package/dist/src/core/gate.js +2947 -0
  209. package/dist/src/core/gate.js.map +1 -0
  210. package/dist/src/core/git-run.js +93 -0
  211. package/dist/src/core/git-run.js.map +1 -0
  212. package/dist/src/core/harness-version.js +210 -0
  213. package/dist/src/core/harness-version.js.map +1 -0
  214. package/dist/src/core/harness-wait.js +58 -0
  215. package/dist/src/core/harness-wait.js.map +1 -0
  216. package/dist/src/core/head-retry.js +121 -0
  217. package/dist/src/core/head-retry.js.map +1 -0
  218. package/dist/src/core/instance.js +319 -0
  219. package/dist/src/core/instance.js.map +1 -0
  220. package/dist/src/core/intake-limits.js +350 -0
  221. package/dist/src/core/intake-limits.js.map +1 -0
  222. package/dist/src/core/jcs.js +132 -0
  223. package/dist/src/core/jcs.js.map +1 -0
  224. package/dist/src/core/journal.js +200 -0
  225. package/dist/src/core/journal.js.map +1 -0
  226. package/dist/src/core/live-draw.js +703 -0
  227. package/dist/src/core/live-draw.js.map +1 -0
  228. package/dist/src/core/log-reconcile.js +136 -0
  229. package/dist/src/core/log-reconcile.js.map +1 -0
  230. package/dist/src/core/log.js +546 -0
  231. package/dist/src/core/log.js.map +1 -0
  232. package/dist/src/core/loop.js +476 -0
  233. package/dist/src/core/loop.js.map +1 -0
  234. package/dist/src/core/md-fence.js +74 -0
  235. package/dist/src/core/md-fence.js.map +1 -0
  236. package/dist/src/core/money.js +195 -0
  237. package/dist/src/core/money.js.map +1 -0
  238. package/dist/src/core/payload-census.js +146 -0
  239. package/dist/src/core/payload-census.js.map +1 -0
  240. package/dist/src/core/payload-store.js +340 -0
  241. package/dist/src/core/payload-store.js.map +1 -0
  242. package/dist/src/core/payload.js +80 -0
  243. package/dist/src/core/payload.js.map +1 -0
  244. package/dist/src/core/policy-diff.js +565 -0
  245. package/dist/src/core/policy-diff.js.map +1 -0
  246. package/dist/src/core/policy-expectations.js +394 -0
  247. package/dist/src/core/policy-expectations.js.map +1 -0
  248. package/dist/src/core/policy-explain.js +230 -0
  249. package/dist/src/core/policy-explain.js.map +1 -0
  250. package/dist/src/core/policy-load.js +524 -0
  251. package/dist/src/core/policy-load.js.map +1 -0
  252. package/dist/src/core/policy-match.js +467 -0
  253. package/dist/src/core/policy-match.js.map +1 -0
  254. package/dist/src/core/policy-proposal.js +458 -0
  255. package/dist/src/core/policy-proposal.js.map +1 -0
  256. package/dist/src/core/prompt-layout.js +422 -0
  257. package/dist/src/core/prompt-layout.js.map +1 -0
  258. package/dist/src/core/protected-path-guard.js +1087 -0
  259. package/dist/src/core/protected-path-guard.js.map +1 -0
  260. package/dist/src/core/registration.js +39 -0
  261. package/dist/src/core/registration.js.map +1 -0
  262. package/dist/src/core/reindex.js +336 -0
  263. package/dist/src/core/reindex.js.map +1 -0
  264. package/dist/src/core/sampler.js +388 -0
  265. package/dist/src/core/sampler.js.map +1 -0
  266. package/dist/src/core/sandbox.js +424 -0
  267. package/dist/src/core/sandbox.js.map +1 -0
  268. package/dist/src/core/seal.js +290 -0
  269. package/dist/src/core/seal.js.map +1 -0
  270. package/dist/src/core/state.js +1009 -0
  271. package/dist/src/core/state.js.map +1 -0
  272. package/dist/src/core/task-file.js +464 -0
  273. package/dist/src/core/task-file.js.map +1 -0
  274. package/dist/src/core/telegram-config.js +114 -0
  275. package/dist/src/core/telegram-config.js.map +1 -0
  276. package/dist/src/core/token.js +578 -0
  277. package/dist/src/core/token.js.map +1 -0
  278. package/dist/src/core/validate.js +0 -0
  279. package/dist/src/core/validate.js.map +1 -0
  280. package/dist/src/core/values.js +153 -0
  281. package/dist/src/core/values.js.map +1 -0
  282. package/dist/src/core/vault.js +612 -0
  283. package/dist/src/core/vault.js.map +1 -0
  284. package/dist/src/core/verified-snapshot.js +506 -0
  285. package/dist/src/core/verified-snapshot.js.map +1 -0
  286. package/dist/src/core/verify.js +549 -0
  287. package/dist/src/core/verify.js.map +1 -0
  288. package/dist/src/core/version.js +9 -0
  289. package/dist/src/core/version.js.map +1 -0
  290. package/dist/src/core/wysiwys.js +728 -0
  291. package/dist/src/core/wysiwys.js.map +1 -0
  292. package/dist/src/daemon/advance-child.js +78 -0
  293. package/dist/src/daemon/advance-child.js.map +1 -0
  294. package/dist/src/daemon/advance.js +849 -0
  295. package/dist/src/daemon/advance.js.map +1 -0
  296. package/dist/src/daemon/audit.js +90 -0
  297. package/dist/src/daemon/audit.js.map +1 -0
  298. package/dist/src/daemon/daemon.js +1988 -0
  299. package/dist/src/daemon/daemon.js.map +1 -0
  300. package/dist/src/daemon/dark-session.js +119 -0
  301. package/dist/src/daemon/dark-session.js.map +1 -0
  302. package/dist/src/daemon/draw-child.js +132 -0
  303. package/dist/src/daemon/draw-child.js.map +1 -0
  304. package/dist/src/daemon/draw.js +458 -0
  305. package/dist/src/daemon/draw.js.map +1 -0
  306. package/dist/src/daemon/git-evidence.js +345 -0
  307. package/dist/src/daemon/git-evidence.js.map +1 -0
  308. package/dist/src/daemon/projection.js +233 -0
  309. package/dist/src/daemon/projection.js.map +1 -0
  310. package/dist/src/daemon/prune.js +376 -0
  311. package/dist/src/daemon/prune.js.map +1 -0
  312. package/dist/src/mcp/http.js +343 -0
  313. package/dist/src/mcp/http.js.map +1 -0
  314. package/dist/src/mcp/server.js +594 -0
  315. package/dist/src/mcp/server.js.map +1 -0
  316. package/docs/cli-reference.md +5363 -0
  317. package/package.json +43 -4
  318. package/schema/.gitkeep +0 -0
  319. package/schema/LICENSE +117 -0
  320. package/schema/envelope.schema.json +137 -0
  321. package/schema/event.schema.json +1810 -0
  322. package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
  323. package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
  324. package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
  325. package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
  326. package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
  327. package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
  328. package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
  329. package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
  330. package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
  331. package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
  332. package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
  333. package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
  334. package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
  335. package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
  336. package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
  337. package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
  338. package/schema/fixtures/envelope/valid/canonical.json +25 -0
  339. package/schema/fixtures/envelope/valid/minimal.json +7 -0
  340. package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
  341. package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
  342. package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
  343. package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
  344. package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
  345. package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
  346. package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
  347. package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
  348. package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
  349. package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
  350. package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
  351. package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
  352. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
  353. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
  354. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
  355. package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
  356. package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
  357. package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
  358. package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
  359. package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
  360. package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
  361. package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
  362. package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
  363. package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
  364. package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
  365. package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
  366. package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
  367. package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
  368. package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
  369. package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
  370. package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
  371. package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
  372. package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
  373. package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
  374. package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
  375. package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
  376. package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
  377. package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
  378. package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
  379. package/schema/fixtures/event/invalid/missing-alg.json +14 -0
  380. package/schema/fixtures/event/invalid/missing-hash.json +14 -0
  381. package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
  382. package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
  383. package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
  384. package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
  385. package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
  386. package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
  387. package/schema/fixtures/event/invalid/short-hash.json +15 -0
  388. package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
  389. package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
  390. package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
  391. package/schema/fixtures/event/valid/approval-expired.json +15 -0
  392. package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
  393. package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
  394. package/schema/fixtures/event/valid/approval-granted.json +15 -0
  395. package/schema/fixtures/event/valid/approval-rejected.json +15 -0
  396. package/schema/fixtures/event/valid/approval-requested.json +19 -0
  397. package/schema/fixtures/event/valid/approval-revoked.json +15 -0
  398. package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
  399. package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
  400. package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
  401. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
  402. package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
  403. package/schema/fixtures/event/valid/audit-sampled.json +14 -0
  404. package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
  405. package/schema/fixtures/event/valid/envelope-drift.json +16 -0
  406. package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
  407. package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
  408. package/schema/fixtures/event/valid/execution-completed.json +15 -0
  409. package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
  410. package/schema/fixtures/event/valid/execution-failed.json +15 -0
  411. package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
  412. package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
  413. package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
  414. package/schema/fixtures/event/valid/execution-started.json +14 -0
  415. package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
  416. package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
  417. package/schema/fixtures/event/valid/gate-closed.json +14 -0
  418. package/schema/fixtures/event/valid/gate-opened.json +16 -0
  419. package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
  420. package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
  421. package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
  422. package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
  423. package/schema/fixtures/event/valid/payload-pruned.json +17 -0
  424. package/schema/fixtures/event/valid/policy-declined.json +16 -0
  425. package/schema/fixtures/event/valid/policy-proposed.json +35 -0
  426. package/schema/fixtures/event/valid/policy-updated.json +14 -0
  427. package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
  428. package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
  429. package/schema/fixtures/event/valid/route-accepted.json +15 -0
  430. package/schema/fixtures/event/valid/route-proposed.json +16 -0
  431. package/schema/fixtures/event/valid/spec-example.json +15 -0
  432. package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
  433. package/schema/fixtures/event/valid/task-registered.json +14 -0
  434. package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
  435. package/schema/fixtures/hash/known-answer.json +74 -0
  436. package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
  437. package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
  438. package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
  439. package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
  440. package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
  441. package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
  442. package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
  443. package/schema/fixtures/policy/invalid/missing-version.json +8 -0
  444. package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
  445. package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
  446. package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
  447. package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
  448. package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
  449. package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
  450. package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
  451. package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
  452. package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
  453. package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
  454. package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
  455. package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
  456. package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
  457. package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
  458. package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
  459. package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
  460. package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
  461. package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
  462. package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
  463. package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
  464. package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
  465. package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
  466. package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
  467. package/schema/fixtures/policy/valid/canonical.json +47 -0
  468. package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
  469. package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
  470. package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
  471. package/schema/fixtures/policy/valid/global-budgets.json +19 -0
  472. package/schema/fixtures/policy/valid/human-only.json +9 -0
  473. package/schema/fixtures/policy/valid/minimal.json +6 -0
  474. package/schema/fixtures/policy/valid/protected-paths.json +10 -0
  475. package/schema/fixtures/policy/valid/record-namespace.json +13 -0
  476. package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
  477. package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
  478. package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
  479. package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
  480. package/schema/fixtures/policy/valid/wildcards.json +15 -0
  481. package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
  482. package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
  483. package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
  484. package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
  485. package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
  486. package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
  487. package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
  488. package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
  489. package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
  490. package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
  491. package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
  492. package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
  493. package/schema/fixtures/policy-md/valid/canonical.md +50 -0
  494. package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
  495. package/schema/fixtures/policy-md/valid/minimal.md +3 -0
  496. package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
  497. package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
  498. package/schema/fixtures/policy-md/valid/with-values.md +79 -0
  499. package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
  500. package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
  501. package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
  502. package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
  503. package/schema/fixtures/sample-record/valid/minimal.json +4 -0
  504. package/schema/fixtures/sample-record/valid/with-note.json +5 -0
  505. package/schema/fixtures/values/invalid/class-shaped.json +9 -0
  506. package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
  507. package/schema/fixtures/values/invalid/non-string-item.json +4 -0
  508. package/schema/fixtures/values/invalid/over-cap.json +26 -0
  509. package/schema/fixtures/values/invalid/unknown-key.json +5 -0
  510. package/schema/fixtures/values/invalid/version-string.json +1 -0
  511. package/schema/fixtures/values/valid/empty-lists.json +7 -0
  512. package/schema/fixtures/values/valid/full.json +20 -0
  513. package/schema/fixtures/values/valid/minimal.json +1 -0
  514. package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
  515. package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
  516. package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
  517. package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
  518. package/schema/fixtures/values-md/valid/absent.md +50 -0
  519. package/schema/fixtures/values-md/valid/with-values.md +79 -0
  520. package/schema/policy.schema.json +481 -0
  521. package/schema/sample-record.schema.json +26 -0
  522. package/schema/values.schema.json +55 -0
@@ -0,0 +1,882 @@
1
+ /**
2
+ * The audit lifecycle (SPEC.md §5.2, §9.1, §12): `audit.sampled` →
3
+ * `audit.reviewed` → (on a denial) `reconciliation.required` →
4
+ * `reconciliation.satisfied`.
5
+ *
6
+ * ## What a retrospective denial can and cannot do (amended SPEC.md §5.2, APRV-127)
7
+ *
8
+ * The action already happened. The runtime cannot undo it, and any design that
9
+ * pretended otherwise would be lying to the person who denied it. What it can do
10
+ * is **oblige and record**: a denial appends an obligation naming the action, its
11
+ * class, and the review that denied, and the obligation stays open, and loud,
12
+ * until a HUMAN says it is discharged. Which obligation depends on whether the
13
+ * action was declared reversible — see {@link obligationFor}, including why a
14
+ * self-reported `reversible` may choose the shape and can never remove one.
15
+ *
16
+ * This is also why the autonomy split has a floor. `supervised-retro` refuses an
17
+ * action declaring `reversible: false` (`core/policy-match.ts`'s irreversibility
18
+ * floor pushes it to `manual`), because retrospective review of something that
19
+ * cannot be undone is regret with a paper trail. The floor is a floor and not a
20
+ * proof: it acts on a claim the acting party makes about itself, so it catches
21
+ * the honest declaration and never the lie. The obligation machinery here is what
22
+ * remains when the floor did not apply and the review said no anyway.
23
+ *
24
+ * Two halves, deliberately asymmetric:
25
+ *
26
+ * - **Sampling is the runtime's.** It is mechanical, it is retrospective, and it
27
+ * escalates a supervised action that already executed into a human's review
28
+ * queue. {@link sampleSupervised} appends `audit.sampled` with a `system:`
29
+ * actor. Selection is `core/sampler.ts`'s keyed HMAC, so no agent can predict
30
+ * or steer it.
31
+ * - **Review is a human's.** {@link reviewSample} appends `audit.reviewed` and
32
+ * refuses any actor that is not `human:<id>`, by the same rule that governs
33
+ * `grant`, `reject`, `revoke`, `policy attest`, and `execution resolve`. A
34
+ * runtime that could mark its own samples reviewed would be a supervision
35
+ * backlog that empties itself, which is precisely the failure the sampled
36
+ * backlog exists to make visible.
37
+ *
38
+ * ## Eligibility is derived, never self-reported (global invariant 4)
39
+ *
40
+ * A candidate is an `execution.started` record whose action's class, as recorded
41
+ * in the `task.registered` declaration the log already holds, resolves to
42
+ * `supervised` under the live policy. Two things follow, both load-bearing:
43
+ *
44
+ * - the class comes from the registration record rather than from the
45
+ * `execution.started` payload, and the autonomy comes from re-running
46
+ * `core/policy-match.ts` rather than from any field claiming an autonomy. No
47
+ * payload key an authoring party writes can move an action out of the
48
+ * candidate set;
49
+ * - eligibility is recomputed from the log every sweep, so it does not depend on
50
+ * any remembered flag, and a candidate cannot exclude itself by writing
51
+ * anything into its own event.
52
+ *
53
+ * The manual path is excluded because it never resolves `supervised`: a manual
54
+ * action's start is authorized by a token and its class resolves `manual`, so it
55
+ * is not a candidate and is not double-counted.
56
+ *
57
+ * ## Exactly once, without remembering anything
58
+ *
59
+ * Every sweep re-derives the whole candidate set and subtracts the subjects the
60
+ * log already carries an `audit.sampled` for, keyed on the subject record's
61
+ * `hash` (unique per record by construction, and stable across re-reads). A
62
+ * daemon restart, a second daemon, and a manual sweep all converge on the same
63
+ * set, and none of them can double-sample. Every append passes `expectedHead`,
64
+ * so a check made against one log cannot land on another (SPEC.md §11.1
65
+ * invariant 5).
66
+ *
67
+ * ## Time
68
+ *
69
+ * `audit.*` is gate-typed (SPEC.md §8), so no public function here takes a `ts`:
70
+ * the timestamp is read from the injected clock at the write boundary, and the
71
+ * party being audited does not author the clock it is judged by.
72
+ */
73
+ import { tick } from "./clock.js";
74
+ import { findDeclaration, indexDeclarations } from "./execute.js";
75
+ import { appendEvent, } from "./log.js";
76
+ import { loadPolicy } from "./policy-load.js";
77
+ import { resolve } from "./policy-match.js";
78
+ import { resolveSampler } from "./sampler.js";
79
+ import { payloadOf, readVerifiedRecords } from "./state.js";
80
+ /**
81
+ * SPEC.md §8: the sampler is the runtime, so its actor is `system:`. Distinct
82
+ * from `system:gate` (expiries) and `system:daemon` (envelope drift) so a reader
83
+ * can tell which part of the runtime spoke without reading the payload.
84
+ */
85
+ export const AUDIT_ACTOR = "system:audit";
86
+ /** `human:<id>`, the only actor a review may carry. */
87
+ const HUMAN_ACTOR = /^human:.+/u;
88
+ /**
89
+ * The closed set of audit refusal codes. Frozen public API in the same sense the
90
+ * gate's and the executor's are: a supervisor branches on these strings, so
91
+ * adding one is a spec change and renaming one is a breaking change.
92
+ */
93
+ export const AUDIT_REFUSAL_CODES = [
94
+ /** Review was attempted by an actor that is not `human:<id>`. */
95
+ "actor-not-human",
96
+ /** No `audit.sampled` record matches the subject named. */
97
+ "not-sampled",
98
+ /** That sample already has a later `audit.reviewed`. */
99
+ "already-reviewed",
100
+ /** An action key with more than one unreviewed sample; name the seq instead. */
101
+ "ambiguous-subject",
102
+ /** No `reconciliation.required` record at the seq named (APRV-127). */
103
+ "not-obliged",
104
+ /** That obligation already has a `reconciliation.satisfied` (APRV-127). */
105
+ "already-satisfied",
106
+ /**
107
+ * A verb that requires a reason was given none. Two shapes, one code: a
108
+ * reconciliation satisfied with a blank note (APRV-127), and a review whose
109
+ * `reaction` is `loved` or `disliked` with a blank note (APRV-239). Both are
110
+ * an assertion nobody can check, and both are evaluated after the actor check
111
+ * and before the log is read.
112
+ */
113
+ "note-required",
114
+ /**
115
+ * A review that says the action should not have happened and that the human
116
+ * liked or loved it (APRV-239). Evaluated beside `note-required`, before the
117
+ * log is read; nothing is appended.
118
+ *
119
+ * The two fields point opposite ways and only one of them is enforcement, so
120
+ * the safe reading is not "believe the verdict and drop the grade": a record
121
+ * carrying both would be read by a person later as evidence of whichever half
122
+ * suited them, and by an agent as a signal that a denial is survivable if the
123
+ * operator is pleased. The reviewer is asked to say which they meant.
124
+ */
125
+ "reaction-conflicts-verdict",
126
+ /**
127
+ * A `gated-revert` obligation whose satisfaction names no completed revert
128
+ * (APRV-127). The obligation is to undo the action THROUGH THE GATE, and the
129
+ * evidence of that is an `execution.completed` in this same log.
130
+ */
131
+ "revert-required",
132
+ /**
133
+ * The denial was recorded and its obligation was not (APRV-127). The log is
134
+ * NOT inconsistent — `audit.reviewed` stands and says `denied` — but the
135
+ * obligation it should have created is missing and must be created by
136
+ * reviewing again once the head settles.
137
+ */
138
+ "obligation-not-appended",
139
+ /** The log could not be read, or holds a line that is not a record. */
140
+ "log-unreadable",
141
+ /** The log's final line is unterminated (a crashed write). */
142
+ "log-torn-tail",
143
+ /** The chain does not verify; nothing is derived from an untrustworthy log. */
144
+ "log-corrupt",
145
+ /** The append itself failed; `append` carries the underlying error. */
146
+ "append-failed",
147
+ ];
148
+ function refuse(code, message, extra = {}) {
149
+ return { ok: false, code, message, ...extra };
150
+ }
151
+ function policyFor(options, cwd) {
152
+ const where = options.policy?.file !== undefined
153
+ ? { file: options.policy.file }
154
+ : { dir: options.policy?.dir ?? cwd };
155
+ if (options.schemaDir !== undefined)
156
+ where.schemaDir = options.schemaDir;
157
+ return loadPolicy(where);
158
+ }
159
+ /**
160
+ * Every `execution.started` whose action resolves `supervised` under `load`, in
161
+ * log order.
162
+ *
163
+ * Pure: no I/O, no clock, no environment. Records with no action key, and keys
164
+ * no `task.registered` record declares, are skipped rather than guessed at — an
165
+ * undeclared key has no class, and inventing one would put a fact in the sample
166
+ * that nobody wrote.
167
+ */
168
+ export function supervisedExecutions(records, load) {
169
+ const all = records;
170
+ const autonomyByClass = new Map();
171
+ const candidates = [];
172
+ // One forward pass over the log answers, for every key at once, the three
173
+ // questions this loop used to ask per candidate with a full scan each
174
+ // (APRV-211): which tasks declare the key (declaringTasks), what the last
175
+ // registration declared (findDeclaration), and whether a human was ever asked
176
+ // (hasApprovalCycle). Same records, same answers, same order — the index is a
177
+ // per-call derivation of this call's own verified records and nothing is
178
+ // remembered between sweeps. `tests/audit-index.test.ts` pins it against the
179
+ // per-key helpers, key by key, and against this function's previous algorithm.
180
+ const index = indexDeclarations(all);
181
+ for (const record of all) {
182
+ if (record.event !== "execution.started")
183
+ continue;
184
+ const actionKey = record.action_key;
185
+ if (typeof actionKey !== "string" || actionKey.length === 0)
186
+ continue;
187
+ // A key declared by more than one task is a refused collision (APRV-138);
188
+ // do not sample from an ambiguous declaration.
189
+ if ((index.declaringTasks.get(actionKey)?.length ?? 0) > 1)
190
+ continue;
191
+ // APRV-127. An action a human was already asked about is not a candidate for
192
+ // review of an unreviewed decision — there was a decision. The case is a
193
+ // `supervised-live` action the live draw selected: it executed on a grant,
194
+ // through the manual path, and its class still resolves `supervised`, so
195
+ // without this line it would be drawn a second time into a backlog asking a
196
+ // person to review the answer they themselves gave. Costs nothing for every
197
+ // other supervised action, which never carries an approval cycle.
198
+ if (index.requested.has(actionKey))
199
+ continue;
200
+ // `index.declarations` holds, per key, exactly what findDeclaration returns
201
+ // for it: the class comes from the `task.registered` record the log carries
202
+ // and from nowhere else.
203
+ const declared = index.declarations.get(actionKey) ?? null;
204
+ if (declared === null)
205
+ continue;
206
+ let autonomy = autonomyByClass.get(declared.class);
207
+ if (autonomy === undefined) {
208
+ autonomy = resolve(load, declared.class).autonomy;
209
+ autonomyByClass.set(declared.class, autonomy);
210
+ }
211
+ if (autonomy !== "supervised")
212
+ continue;
213
+ candidates.push({
214
+ seq: record.seq,
215
+ hash: record.hash,
216
+ ts: record.ts,
217
+ actionKey,
218
+ task: typeof record.task === "string" ? record.task : declared.task,
219
+ class: declared.class,
220
+ });
221
+ }
222
+ return candidates;
223
+ }
224
+ function stringOrNull(value) {
225
+ return typeof value === "string" && value.length > 0 ? value : null;
226
+ }
227
+ /**
228
+ * Every `audit.sampled` in the log, each tagged with the `audit.reviewed` that
229
+ * closes it.
230
+ *
231
+ * A review closes a sample only when it comes **after** it in the chain and
232
+ * names the same action key. An earlier review is a review of an earlier sample;
233
+ * treating it as covering this one would silently empty the backlog, which is
234
+ * exactly the failure a sampled-audit backlog exists to prevent. This mirrors
235
+ * `channels/render-queue.ts`'s matching rule, so the CLI and the queue
236
+ * projection never disagree about what is outstanding.
237
+ */
238
+ export function sampledSubjects(records) {
239
+ const subjects = [];
240
+ for (const record of records) {
241
+ if (record.event !== "audit.sampled")
242
+ continue;
243
+ const payload = payloadOf(record);
244
+ const actionKey = stringOrNull(record.action_key) ?? stringOrNull(payload["action_key"]);
245
+ const subjectSeq = payload["subject_seq"];
246
+ subjects.push({
247
+ seq: record.seq,
248
+ ts: record.ts,
249
+ actionKey,
250
+ task: stringOrNull(record.task) ?? stringOrNull(payload["task"]),
251
+ subjectHash: stringOrNull(payload["subject_hash"]),
252
+ subjectSeq: typeof subjectSeq === "number" && Number.isInteger(subjectSeq) ? subjectSeq : null,
253
+ reviewedSeq: null,
254
+ });
255
+ }
256
+ for (const subject of subjects) {
257
+ for (const record of records) {
258
+ if (record.event !== "audit.reviewed" || record.seq <= subject.seq)
259
+ continue;
260
+ const payload = payloadOf(record);
261
+ const key = stringOrNull(record.action_key) ?? stringOrNull(payload["action_key"]);
262
+ const reviewedSeq = payload["subject_seq"];
263
+ const namesThisSample = typeof reviewedSeq === "number" && reviewedSeq === subject.seq
264
+ ? true
265
+ : subject.actionKey !== null && key !== null && key === subject.actionKey;
266
+ if (!namesThisSample)
267
+ continue;
268
+ subject.reviewedSeq = record.seq;
269
+ break;
270
+ }
271
+ }
272
+ return subjects;
273
+ }
274
+ /** Samples with no later review, oldest first. The human's audit backlog. */
275
+ export function openSamples(records) {
276
+ return sampledSubjects(records).filter((subject) => subject.reviewedSeq === null);
277
+ }
278
+ /** The candidates a sweep would sample now: eligible, selected, not yet sampled. */
279
+ export function pendingSamples(records, load, sampler) {
280
+ if (!sampler.enabled)
281
+ return [];
282
+ const alreadySampled = new Set();
283
+ for (const subject of sampledSubjects(records)) {
284
+ if (subject.subjectHash !== null)
285
+ alreadySampled.add(subject.subjectHash);
286
+ }
287
+ // APRV-183. The draw is per class: a class declaring its own `retro_rate` is
288
+ // compared against that rate, and one that declares none against
289
+ // `audit.supervised_sample_rate`. Same secret, same HMAC, same input; only the
290
+ // threshold moves, so there is still exactly one selection mechanism.
291
+ return supervisedExecutions(records, load).filter((candidate) => !alreadySampled.has(candidate.hash) &&
292
+ sampler.selectsFor(candidate.class, candidate.hash));
293
+ }
294
+ /**
295
+ * Sample every supervised execution the log does not yet carry an
296
+ * `audit.sampled` for, and append one event per selection.
297
+ *
298
+ * Re-reads the verified log before every append so the head each
299
+ * compare-and-append is made against is the head the decision was made from. A
300
+ * `head-moved` refusal is collected and reported rather than retried: only the
301
+ * next sweep, which re-derives the whole question from the log as it now is,
302
+ * knows whether the candidate is still a candidate.
303
+ *
304
+ * Returns `ok` with an empty `appended` list when sampling is disabled; the
305
+ * reason travels on `sampler`. A disabled sampler is not a refusal, because
306
+ * nothing was asked for and nothing failed. See `core/sampler.ts` on why a
307
+ * missing secret disables sampling rather than escalating everything.
308
+ */
309
+ export function sampleSupervised(logPath, cwd, options = {}) {
310
+ const load = policyFor(options, cwd);
311
+ const sampler = resolveSampler(load, options.env ?? process.env);
312
+ if (!sampler.enabled)
313
+ return { ok: true, sampler, appended: [], refusals: [] };
314
+ const appended = [];
315
+ const refusals = [];
316
+ const validate = options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir };
317
+ for (;;) {
318
+ const read = readVerifiedRecords(logPath, validate);
319
+ if (!read.ok) {
320
+ // A log that cannot be read is reported whole: partial progress is already
321
+ // in the log (each append is its own record) and nothing is rolled back.
322
+ return appended.length === 0
323
+ ? refuse(read.code, read.message)
324
+ : { ok: true, sampler, appended, refusals: [...refusals, refuse(read.code, read.message)] };
325
+ }
326
+ const pending = pendingSamples(read.records, load, sampler);
327
+ const next = pending.find((candidate) => !appended.some((done) => done.candidate.hash === candidate.hash));
328
+ if (next === undefined)
329
+ break;
330
+ const result = appendSample(logPath, next, sampler, read.head, options);
331
+ if (!result.ok) {
332
+ refusals.push(result);
333
+ break;
334
+ }
335
+ appended.push({ record: result.record, candidate: next });
336
+ }
337
+ return { ok: true, sampler, appended, refusals };
338
+ }
339
+ function appendSample(logPath, candidate, sampler, head, options) {
340
+ const payload = {
341
+ // What was sampled, named by the chain's own identifiers so a reviewer (and
342
+ // a reproducing operator) can find the subject without a projection.
343
+ subject_seq: candidate.seq,
344
+ subject_hash: candidate.hash,
345
+ subject_event: "execution.started",
346
+ subject_ts: candidate.ts,
347
+ class: candidate.class,
348
+ // Why it was sampled. The selection VALUE is deliberately absent: it is
349
+ // derived from the operator's secret, an operator holding the secret can
350
+ // recompute it from subject_hash at will, and a value in the log is an
351
+ // oracle nobody needs.
352
+ reason: "supervised-sample",
353
+ selection: "hmac-sha256/event-hash",
354
+ // APRV-183: the rate this candidate was actually drawn at — the class's own
355
+ // `retro_rate` when it declared one, the global fallback otherwise. The
356
+ // record states the number the verdict was compared against, so a
357
+ // reproducing operator needs no second lookup and no guess about which key
358
+ // was in force.
359
+ rate: sampler.rateFor(candidate.class).rate ?? sampler.rate,
360
+ autonomy: "supervised",
361
+ };
362
+ const result = appendEvent(logPath, {
363
+ ts: tick(options),
364
+ event: "audit.sampled",
365
+ actor: AUDIT_ACTOR,
366
+ ...(candidate.task === null ? {} : { task: candidate.task }),
367
+ action_key: candidate.actionKey,
368
+ payload,
369
+ }, {
370
+ ...(options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir }),
371
+ expectedHead: head,
372
+ });
373
+ if (!result.ok) {
374
+ return refuse("append-failed", `audit.sampled for ${candidate.actionKey} was not appended (${result.error.code}): ${result.error.message}`, { append: result.error });
375
+ }
376
+ return { ok: true, record: result.record };
377
+ }
378
+ /** Parse the CLI's one positional: a bare integer is a seq, anything else a key. */
379
+ export function parseSubjectRef(text) {
380
+ return /^[1-9][0-9]*$/u.test(text)
381
+ ? { kind: "seq", seq: Number(text) }
382
+ : { kind: "action-key", actionKey: text };
383
+ }
384
+ /**
385
+ * The graded reaction a review or a grant MAY carry (amended SPEC.md §5.2,
386
+ * APRV-237/APRV-239), in the order the schema's enum lists them: worst to best.
387
+ *
388
+ * **This is not enforcement, and it lives here rather than in an enforcement
389
+ * module for that reason.** `verdict` is the field the runtime acts on;
390
+ * `reaction` is what the human thought, travelling human-to-agent, and SPEC.md
391
+ * §11.1 invariant 10 says no routing, class matching, sampling, budget, token,
392
+ * gate-window or execution decision may read it. `tests/values-inert.test.ts`
393
+ * enforces that as a static guard over the enforcement modules, which is why the
394
+ * tuple is exported from `core/audit.ts` (the projection's home) and imported by
395
+ * the surfaces that show it, and by nothing that decides.
396
+ *
397
+ * The vocabulary is closed on purpose. Four words are a grade a person can give
398
+ * in one tap and an agent can read back without interpretation; an open field
399
+ * would accumulate synonyms across surfaces until "meh" and "indifferent" were
400
+ * two different signals. The absence of the field is absence: it is never read
401
+ * as `indifferent`, which is a thing a person had to actually say.
402
+ */
403
+ export const REACTIONS = ["disliked", "indifferent", "liked", "loved"];
404
+ /** Whether a string is one of the four graded reactions. Used at the CLI boundary. */
405
+ export function isReaction(value) {
406
+ return REACTIONS.includes(value);
407
+ }
408
+ /**
409
+ * The two grades that require the human's own words (amended SPEC.md §5.2).
410
+ *
411
+ * `loved` and `disliked` are the reactions an agent is most likely to act on and
412
+ * least able to interpret alone: "disliked" with no words says something went
413
+ * wrong and nothing about what, which is the shape of a signal that gets guessed
414
+ * at. `liked` and `indifferent` are the ordinary readings and demand nothing,
415
+ * because a one-tap signal that opens a form is a signal that gets switched off.
416
+ */
417
+ const REACTIONS_REQUIRING_NOTE = new Set(["disliked", "loved"]);
418
+ /** A note that is present and is not only whitespace. Blank is not a note. */
419
+ function hasNote(note) {
420
+ return note !== null && note !== undefined && note.trim().length > 0;
421
+ }
422
+ /**
423
+ * The obligation a denial creates, chosen by the action's DECLARED
424
+ * reversibility (amended SPEC.md §5.2/§7, APRV-127).
425
+ *
426
+ * - `reversible: true` → `gated-revert`. The action can be undone, so the
427
+ * obligation is to undo it *through the gate*: the revert is itself a
428
+ * side-effecting action, and routing it through the gate is what closes the
429
+ * loop inside the log rather than inside a promise.
430
+ * - `reversible: false` → `policy-finding`. There is nothing to revert. What is
431
+ * left is the finding that the class should not have permitted this without a
432
+ * human, and the sanctioned response is tightening the class; the obligation
433
+ * is the review of that tightening.
434
+ * - **declared nothing** → `policy-finding`, the same as `false`. This is the
435
+ * fail-closed direction, and the reason is worth stating: obliging a revert of
436
+ * an action nobody said could be reverted would record an obligation that may
437
+ * be impossible to discharge, and an impossible obligation is one that gets
438
+ * closed dishonestly. A policy finding is always dischargeable, and it is the
439
+ * heavier of the two: it puts the CLASS on the table rather than one action.
440
+ *
441
+ * ## Self-reported, and only ever in the safe direction
442
+ *
443
+ * `reversible` is written by the party whose action is under review, so global
444
+ * invariant 4 applies: it may never reduce scrutiny. Here it does not. It
445
+ * selects the SHAPE of an obligation that exists either way; it cannot remove
446
+ * one, delay one, or decide whether the denial happened. The one thing a false
447
+ * `reversible: true` buys is a revert obligation instead of a policy finding —
448
+ * and the revert obligation is the one whose satisfaction this runtime checks
449
+ * against the log (`revert-required`), so the lie makes the claimant's own exit
450
+ * harder rather than easier.
451
+ */
452
+ export function obligationFor(reversible) {
453
+ return reversible === true ? "gated-revert" : "policy-finding";
454
+ }
455
+ /**
456
+ * Append `audit.reviewed` for one open sample.
457
+ *
458
+ * HUMAN-ONLY, by the same rule as `grant`/`reject`/`revoke`: the whole content
459
+ * of the event is that a person looked. An agent- or system-authored review
460
+ * would be the party under oversight closing its own audit item, and a backlog
461
+ * that can be emptied by the thing it supervises measures nothing.
462
+ *
463
+ * No attestation is required, for the reason `execution resolve` states: review
464
+ * records an observation and exercises no policy authority. It authorizes
465
+ * nothing, spends no budget, and mints no token.
466
+ *
467
+ * `--note` is optional and recorded verbatim when present. It is not mandatory
468
+ * the way `execution resolve`'s is, because that verb writes an *outcome* the
469
+ * runtime does not know while this one writes only "seen".
470
+ */
471
+ export function reviewSample(logPath, ref, actor, note, options = {}) {
472
+ const verdict = options.verdict ?? "ok";
473
+ if (!HUMAN_ACTOR.test(actor)) {
474
+ return refuse("actor-not-human", `audit review is human-only: the event's entire content is that a person looked at a sampled action, and a runtime that could mark its own samples reviewed would be a supervision backlog that empties itself. The actor must match human:<id>, got ${JSON.stringify(actor)}.`);
475
+ }
476
+ // APRV-239, and deliberately here: after the actor check and BEFORE the log is
477
+ // read. Both rules are properties of the two arguments in front of this
478
+ // function, so neither needs a log to decide, and a refusal that had already
479
+ // read (and verified) a log would report a log failure for an invocation that
480
+ // was malformed before it ever touched one. Nothing is appended on either
481
+ // path; the reviewer fixes the invocation and reviews again.
482
+ const reaction = options.reaction;
483
+ if (reaction !== undefined) {
484
+ if (verdict === "denied" && (reaction === "liked" || reaction === "loved")) {
485
+ return refuse("reaction-conflicts-verdict", `a denied review cannot also be ${reaction}: \`verdict\` says the action should not have happened and \`reaction\` says the human was pleased by it, and only the first of those is enforcement. A record carrying both reads afterwards as evidence of whichever half suits the reader, and reads to an agent as a denial being survivable when the operator is pleased. Nothing was appended. Say which one you meant: drop --deny, or use --reaction disliked or indifferent and put the nuance in --note.`);
486
+ }
487
+ if (REACTIONS_REQUIRING_NOTE.has(reaction) && !hasNote(note)) {
488
+ return refuse("note-required", `--reaction ${reaction} requires --note "<text>": it is the grade an agent is most likely to act on and least able to interpret alone, and "${reaction}" with no words says something happened and nothing about what. Blank is not a note. \`liked\` and \`indifferent\` need none. Nothing was appended.`);
489
+ }
490
+ }
491
+ const read = readVerifiedRecords(logPath, options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir });
492
+ if (!read.ok)
493
+ return refuse(read.code, read.message);
494
+ const subjects = sampledSubjects(read.records);
495
+ const located = locate(subjects, ref);
496
+ if (!located.ok)
497
+ return located;
498
+ const subject = located.subject;
499
+ const payload = {
500
+ subject_seq: subject.seq,
501
+ subject_event: "audit.sampled",
502
+ reviewed: true,
503
+ verdict,
504
+ };
505
+ if (subject.subjectHash !== null)
506
+ payload["sampled_subject_hash"] = subject.subjectHash;
507
+ if (note !== null && note.trim().length > 0)
508
+ payload["note"] = note;
509
+ // Written only when it was given. An omitted reaction leaves no key, which is
510
+ // the difference between "the human said nothing" and "the human said
511
+ // indifferent" — a distinction the read surfaces depend on.
512
+ if (reaction !== undefined)
513
+ payload["reaction"] = reaction;
514
+ const result = appendEvent(logPath, {
515
+ ts: tick(options),
516
+ event: "audit.reviewed",
517
+ actor,
518
+ ...(subject.task === null ? {} : { task: subject.task }),
519
+ ...(subject.actionKey === null ? {} : { action_key: subject.actionKey }),
520
+ payload,
521
+ }, {
522
+ ...(options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir }),
523
+ expectedHead: read.head,
524
+ });
525
+ if (!result.ok) {
526
+ return refuse("append-failed", `audit.reviewed for the sample at seq ${String(subject.seq)} was not appended (${result.error.code}): ${result.error.message}`, { append: result.error });
527
+ }
528
+ if (verdict !== "denied") {
529
+ return { ok: true, record: result.record, subject, obligation: null };
530
+ }
531
+ // APRV-127. The denial is recorded; now record what it obliges. Two events,
532
+ // not one, because they are two facts with two authors: a human concluded the
533
+ // action should not have happened, and the runtime derived — mechanically,
534
+ // from the declaration the log already holds — what must now be done about it.
535
+ // Collapsing them would let the reviewer's own words decide the obligation.
536
+ //
537
+ // Compare-and-append against the review itself: the obligation must land
538
+ // directly on the record it names, so nothing can slip between a denial and
539
+ // the obligation it creates.
540
+ const obliged = appendObligation(logPath, read.records, subject, result.record, options);
541
+ if (!obliged.ok)
542
+ return obliged;
543
+ return { ok: true, record: result.record, subject, obligation: obliged.record };
544
+ }
545
+ /**
546
+ * Append the `reconciliation.required` that a denial creates.
547
+ *
548
+ * Actor `system:audit`, not the reviewer. The obligation is a derivation, not an
549
+ * opinion: it follows from the denial and the action's declared reversibility by
550
+ * the rule {@link obligationFor} states, and the event schema refuses any actor
551
+ * that is not `system:`. A human-authored obligation would be one a human could
552
+ * word into something easier to discharge, and the party under oversight would
553
+ * then be describing its own homework.
554
+ */
555
+ function appendObligation(logPath, records, subject, review, options) {
556
+ const actionKey = subject.actionKey;
557
+ if (actionKey === null) {
558
+ return refuse("not-sampled", `the sample at seq ${String(subject.seq)} names no action key, so a denial of it can oblige nothing: a reconciliation names the action it concerns, and there is none to name. The denial itself was recorded at seq ${String(review.seq)}.`, { seq: subject.seq });
559
+ }
560
+ // The class and the reversibility come from the REGISTRATION, never from the
561
+ // review and never from the execution's own payload (global invariant 4). The
562
+ // party whose action was denied does not get to describe the action.
563
+ const declared = findDeclaration(records, actionKey);
564
+ const cls = declared?.class ?? null;
565
+ if (cls === null || cls.length === 0) {
566
+ return refuse("not-obliged", `action ${actionKey} has no task.registered declaration carrying a class, so the obligation its denial creates cannot name one. A policy finding tightens a CLASS; an obligation that names none is one nobody can act on. The denial itself was recorded at seq ${String(review.seq)}.`, { seq: review.seq });
567
+ }
568
+ const reversible = declared?.reversible ?? null;
569
+ const result = appendEvent(logPath, {
570
+ ts: tick(options),
571
+ event: "reconciliation.required",
572
+ actor: AUDIT_ACTOR,
573
+ ...(subject.task === null ? {} : { task: subject.task }),
574
+ action_key: actionKey,
575
+ payload: {
576
+ action_key: actionKey,
577
+ class: cls,
578
+ review_seq: review.seq,
579
+ obligation: obligationFor(reversible),
580
+ reversible,
581
+ // Restated so a reader of this record alone knows what the runtime could
582
+ // and could not do about it. The gate cannot undo anything; it obliges.
583
+ reason: "retrospective-denial",
584
+ },
585
+ }, {
586
+ ...(options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir }),
587
+ expectedHead: { seq: review.seq, hash: review.hash },
588
+ });
589
+ if (!result.ok) {
590
+ return refuse("obligation-not-appended", `the denial of ${actionKey} was recorded at seq ${String(review.seq)} and its reconciliation obligation was NOT appended (${result.error.code}): ${result.error.message}. The log is consistent — the review stands and says denied — but nothing yet records what the denial requires. Review the sample again once the head settles, or open the obligation by hand through a human-authored process; an unreconciled denial is exactly what \`approval status\` and \`approval doctor\` are meant to shout about.`, { seq: review.seq, append: result.error });
591
+ }
592
+ return { ok: true, record: result.record };
593
+ }
594
+ function obligationOf(record) {
595
+ const payload = payloadOf(record);
596
+ const actionKey = stringOrNull(record.action_key) ?? stringOrNull(payload["action_key"]);
597
+ const cls = stringOrNull(payload["class"]);
598
+ const reviewSeq = payload["review_seq"];
599
+ const shape = payload["obligation"];
600
+ if (actionKey === null || cls === null)
601
+ return null;
602
+ if (typeof reviewSeq !== "number" || !Number.isInteger(reviewSeq))
603
+ return null;
604
+ if (shape !== "gated-revert" && shape !== "policy-finding")
605
+ return null;
606
+ const reversible = payload["reversible"];
607
+ return {
608
+ seq: record.seq,
609
+ ts: record.ts,
610
+ actionKey,
611
+ task: stringOrNull(record.task) ?? stringOrNull(payload["task"]),
612
+ class: cls,
613
+ reviewSeq,
614
+ obligation: shape,
615
+ reversible: typeof reversible === "boolean" ? reversible : null,
616
+ satisfiedSeq: null,
617
+ };
618
+ }
619
+ /**
620
+ * Every reconciliation obligation the log carries, each tagged with the
621
+ * satisfaction that closes it.
622
+ *
623
+ * A satisfaction closes an obligation only when it comes **after** it in the
624
+ * chain and names its seq — the same "later, and names it" rule
625
+ * {@link sampledSubjects} applies to reviews, and for the same reason: a
626
+ * backlog that an earlier record could close is a backlog that empties itself.
627
+ *
628
+ * A malformed `reconciliation.required` (no action key, no class, no usable
629
+ * obligation shape) is SKIPPED rather than guessed at. Such a record cannot
630
+ * reach the log through this runtime — the event schema requires all three — so
631
+ * one that is there arrived some other way, and inventing the missing field
632
+ * would put a fact in the backlog that nobody wrote.
633
+ */
634
+ export function reconciliationObligations(records) {
635
+ const found = [];
636
+ for (const record of records) {
637
+ if (record.event !== "reconciliation.required")
638
+ continue;
639
+ const parsed = obligationOf(record);
640
+ if (parsed !== null)
641
+ found.push(parsed);
642
+ }
643
+ for (const item of found) {
644
+ for (const record of records) {
645
+ if (record.event !== "reconciliation.satisfied" || record.seq <= item.seq)
646
+ continue;
647
+ if (payloadOf(record)["obligation_seq"] !== item.seq)
648
+ continue;
649
+ item.satisfiedSeq = record.seq;
650
+ break;
651
+ }
652
+ }
653
+ return found;
654
+ }
655
+ /** Obligations with no later satisfaction, oldest first. The loud backlog. */
656
+ export function openObligations(records) {
657
+ return reconciliationObligations(records).filter((item) => item.satisfiedSeq === null);
658
+ }
659
+ /**
660
+ * Close one reconciliation obligation.
661
+ *
662
+ * **HUMAN-ONLY**, by the same rule that governs `grant`, `reject`, `revoke` and
663
+ * `audit.reviewed`, and enforced twice: here in code and again by the event
664
+ * schema. The entire content of the record is that a person judged the
665
+ * obligation discharged. A runtime that could satisfy its own obligations would
666
+ * be a reconciliation backlog that empties itself, which is precisely the
667
+ * silence an unreconciled denial exists to break.
668
+ *
669
+ * Two checks beyond the actor, and both are about evidence rather than trust:
670
+ *
671
+ * - **A note is required.** `audit.reviewed` may record only "seen"; this record
672
+ * asserts that something was DONE, and an assertion nobody described is one no
673
+ * auditor can check.
674
+ * - **A `gated-revert` obligation requires a completed revert IN THIS LOG.** The
675
+ * obligation was "undo it through the gate", so the discharge is a gated
676
+ * action that ran, and the runtime looks for its `execution.completed` rather
677
+ * than accepting a sentence saying it happened. That is what closes the loop
678
+ * in the chain. A `policy-finding` obligation has no such artifact — the
679
+ * sanctioned response is a policy amendment, which is a separate human
680
+ * ceremony with its own `policy.updated` record — so the note is the discharge
681
+ * there, and the note is required.
682
+ *
683
+ * No attestation is required, for the reason `audit review` and `execution
684
+ * resolve` state: this record exercises no policy authority, authorizes nothing,
685
+ * spends no budget, and mints no token.
686
+ */
687
+ export function satisfyObligation(logPath, obligationSeq, actor, input, options = {}) {
688
+ if (!HUMAN_ACTOR.test(actor)) {
689
+ return refuse("actor-not-human", `satisfying a reconciliation obligation is human-only: the event's entire content is that a PERSON judged the obligation discharged, and a runtime that could close its own obligations would be a reconciliation backlog that empties itself. The actor must match human:<id>, got ${JSON.stringify(actor)}.`);
690
+ }
691
+ const note = input.note.trim();
692
+ if (note.length === 0) {
693
+ return refuse("note-required", `a reconciliation.satisfied must say what was done. Unlike \`audit review\`, whose whole content may be "a person looked", this record asserts that an obligation was DISCHARGED, and a discharge nobody described is one no auditor can check. Pass --note "<what you did>".`);
694
+ }
695
+ const read = readVerifiedRecords(logPath, options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir });
696
+ if (!read.ok)
697
+ return refuse(read.code, read.message);
698
+ const obligation = reconciliationObligations(read.records).find((item) => item.seq === obligationSeq);
699
+ if (obligation === undefined) {
700
+ return refuse("not-obliged", `no reconciliation.required record at seq ${String(obligationSeq)}. \`approval audit reconcile\` names the OBLIGATION, not the action it concerns and not the review that created it. Run \`approval audit obligations\` for the open ones.`, { seq: obligationSeq });
701
+ }
702
+ if (obligation.satisfiedSeq !== null) {
703
+ return refuse("already-satisfied", `the obligation at seq ${String(obligation.seq)} was already satisfied at seq ${String(obligation.satisfiedSeq)}; a second satisfaction would record a second discharge of one obligation, which the log cannot tell apart from the first`, { seq: obligation.satisfiedSeq });
704
+ }
705
+ const revertKey = input.revertActionKey?.trim() ?? "";
706
+ if (obligation.obligation === "gated-revert") {
707
+ if (revertKey.length === 0) {
708
+ return refuse("revert-required", `the obligation at seq ${String(obligation.seq)} is a gated-revert: ${obligation.actionKey} was declared reversible, so the sanctioned response to its denial is to UNDO it through the gate. Name the revert with --revert <action-key>. The revert is itself a side-effecting action, and routing it through the gate is what closes this loop inside the log rather than inside a sentence.`, { seq: obligation.seq });
709
+ }
710
+ const completed = read.records.some((record) => record.event === "execution.completed" && record.action_key === revertKey);
711
+ if (!completed) {
712
+ return refuse("revert-required", `this log carries no execution.completed for ${JSON.stringify(revertKey)}, so the revert this obligation requires has not been shown to have run. Request the revert, have it granted, run it through \`approval run\`, and then satisfy the obligation naming it. The runtime checks the chain rather than the claim, because a discharge that could be asserted is a backlog that empties itself.`, { seq: obligation.seq });
713
+ }
714
+ }
715
+ const payload = {
716
+ obligation_seq: obligation.seq,
717
+ note,
718
+ action_key: obligation.actionKey,
719
+ class: obligation.class,
720
+ obligation: obligation.obligation,
721
+ };
722
+ if (obligation.obligation === "gated-revert")
723
+ payload["revert_action_key"] = revertKey;
724
+ const result = appendEvent(logPath, {
725
+ ts: tick(options),
726
+ event: "reconciliation.satisfied",
727
+ actor,
728
+ ...(obligation.task === null ? {} : { task: obligation.task }),
729
+ action_key: obligation.actionKey,
730
+ payload,
731
+ }, {
732
+ ...(options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir }),
733
+ expectedHead: read.head,
734
+ });
735
+ if (!result.ok) {
736
+ return refuse("append-failed", `reconciliation.satisfied for the obligation at seq ${String(obligation.seq)} was not appended (${result.error.code}): ${result.error.message}`, { append: result.error });
737
+ }
738
+ return { ok: true, record: result.record, obligation };
739
+ }
740
+ function locate(subjects, ref) {
741
+ if (ref.kind === "seq") {
742
+ const subject = subjects.find((entry) => entry.seq === ref.seq);
743
+ if (subject === undefined) {
744
+ return refuse("not-sampled", `no audit.sampled record at seq ${String(ref.seq)}; \`approval audit review\` names the SAMPLE, not the execution it sampled. Run \`approval audit list\` (or read .approval/QUEUE.md's sampled-audit backlog) for the open samples.`, { seq: ref.seq });
745
+ }
746
+ if (subject.reviewedSeq !== null) {
747
+ return refuse("already-reviewed", `the sample at seq ${String(subject.seq)} was already reviewed at seq ${String(subject.reviewedSeq)}; a second review would record a second human observation of the same item, which the log cannot tell apart from the first`, { seq: subject.reviewedSeq });
748
+ }
749
+ return { ok: true, subject };
750
+ }
751
+ const matching = subjects.filter((entry) => entry.actionKey === ref.actionKey);
752
+ if (matching.length === 0) {
753
+ return refuse("not-sampled", `no audit.sampled record names action ${JSON.stringify(ref.actionKey)}; only a SAMPLED action can be reviewed, and sampling is the runtime's decision, never a caller's`);
754
+ }
755
+ const open = matching.filter((entry) => entry.reviewedSeq === null);
756
+ if (open.length === 0) {
757
+ const last = matching[matching.length - 1];
758
+ return refuse("already-reviewed", `every audit.sampled for ${ref.actionKey} is reviewed (the latest, seq ${String(last?.seq ?? 0)}, at seq ${String(last?.reviewedSeq ?? 0)})`, last?.reviewedSeq === undefined || last.reviewedSeq === null ? {} : { seq: last.reviewedSeq });
759
+ }
760
+ if (open.length > 1) {
761
+ return refuse("ambiguous-subject", `action ${ref.actionKey} has ${String(open.length)} unreviewed samples (seq ${open
762
+ .map((entry) => String(entry.seq))
763
+ .join(", ")}); name the one you reviewed by its seq, because a review that could mean either would close the wrong item`);
764
+ }
765
+ return { ok: true, subject: open[0] };
766
+ }
767
+ /** The actor that appended a record, when it is a non-empty string. */
768
+ function actorOf(record) {
769
+ return typeof record.actor === "string" && record.actor.length > 0 ? record.actor : null;
770
+ }
771
+ /**
772
+ * Per action key, the actor of the `task.registered` that declared it, and
773
+ * failing that the actor of the `execution.started` that ran it.
774
+ *
775
+ * Registration first because it is the earliest and most specific statement of
776
+ * whose work this is: the party that put the action in a task envelope. The
777
+ * execution actor is the fallback for a log whose registration is missing (an
778
+ * imported or truncated log), and a key with neither is reported as `null`
779
+ * rather than guessed.
780
+ */
781
+ function agentActorsByKey(records) {
782
+ const registered = new Map();
783
+ const executed = new Map();
784
+ for (const record of records) {
785
+ if (record.event === "task.registered") {
786
+ const actor = actorOf(record);
787
+ if (actor === null)
788
+ continue;
789
+ const actions = payloadOf(record)["actions"];
790
+ if (!Array.isArray(actions))
791
+ continue;
792
+ for (const entry of actions) {
793
+ if (typeof entry !== "object" || entry === null)
794
+ continue;
795
+ const key = entry["idempotency_key"];
796
+ if (typeof key === "string" && key.length > 0)
797
+ registered.set(key, actor);
798
+ }
799
+ continue;
800
+ }
801
+ if (record.event !== "execution.started")
802
+ continue;
803
+ const key = record.action_key;
804
+ const actor = actorOf(record);
805
+ if (typeof key === "string" && key.length > 0 && actor !== null && !executed.has(key)) {
806
+ executed.set(key, actor);
807
+ }
808
+ }
809
+ for (const [key, actor] of executed) {
810
+ if (!registered.has(key))
811
+ registered.set(key, actor);
812
+ }
813
+ return registered;
814
+ }
815
+ function reactionOf(payload) {
816
+ const value = payload["reaction"];
817
+ return typeof value === "string" && isReaction(value) ? value : null;
818
+ }
819
+ /**
820
+ * Every reaction and every note a human wrote about an action, oldest first.
821
+ *
822
+ * The HUMAN-TO-AGENT direction of the log (amended SPEC.md §5.2). Two sources,
823
+ * because a human says what they thought in two places: at the gate, answering a
824
+ * request (`approval.granted`), and afterwards, reviewing a sampled action
825
+ * (`audit.reviewed`). Rejections and revocations carry no reaction at all, so
826
+ * they are not a source: their reason IS their note, and the record already says
827
+ * what happened.
828
+ *
829
+ * **An entry with neither a reaction nor a note is omitted.** A grant with no
830
+ * words is the ordinary case, most grants are, and listing thousands of them as
831
+ * blank rows would bury the handful where somebody actually said something.
832
+ * Absence of feedback is not feedback.
833
+ *
834
+ * Reads only the records it is given, and callers pass VERIFIED records: this is
835
+ * a projection in the sense the rest of this module uses the word, it writes
836
+ * nothing, decides nothing, and no enforcement path reads it (SPEC.md §11.1
837
+ * invariant 10).
838
+ */
839
+ export function humanFeedback(records) {
840
+ const index = indexDeclarations(records);
841
+ const agents = agentActorsByKey(records);
842
+ const subjectBySeq = new Map();
843
+ for (const subject of sampledSubjects(records)) {
844
+ if (subject.reviewedSeq !== null)
845
+ subjectBySeq.set(subject.reviewedSeq, subject);
846
+ }
847
+ const entries = [];
848
+ for (const record of records) {
849
+ const isReview = record.event === "audit.reviewed";
850
+ if (!isReview && record.event !== "approval.granted")
851
+ continue;
852
+ const payload = payloadOf(record);
853
+ const reaction = reactionOf(payload);
854
+ const note = stringOrNull(payload["note"]);
855
+ if (reaction === null && note === null)
856
+ continue;
857
+ const subject = isReview ? (subjectBySeq.get(record.seq) ?? null) : null;
858
+ const actionKey = stringOrNull(record.action_key) ??
859
+ stringOrNull(payload["action_key"]) ??
860
+ subject?.actionKey ??
861
+ null;
862
+ const declared = actionKey === null ? undefined : index.declarations.get(actionKey);
863
+ const rawVerdict = payload["verdict"];
864
+ entries.push({
865
+ seq: record.seq,
866
+ ts: record.ts,
867
+ source: isReview ? "review" : "decision",
868
+ event: record.event,
869
+ actor: actorOf(record) ?? "-",
870
+ reaction,
871
+ note,
872
+ verdict: isReview && (rawVerdict === "ok" || rawVerdict === "denied") ? rawVerdict : null,
873
+ actionKey,
874
+ task: stringOrNull(record.task) ?? subject?.task ?? declared?.task ?? null,
875
+ class: declared?.class ?? null,
876
+ agentActor: actionKey === null ? null : (agents.get(actionKey) ?? null),
877
+ sampleSeq: subject?.seq ?? null,
878
+ });
879
+ }
880
+ return entries;
881
+ }
882
+ //# sourceMappingURL=audit.js.map