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,728 @@
1
+ /**
2
+ * WYSIWYS: the canonical rendering of a payload (APRV-119), and the structural
3
+ * views it is built from (APRV-100, APRV-124, APRV-126, APRV-144).
4
+ *
5
+ * ## What you see is what you sign
6
+ *
7
+ * The prompt a human approves is a deterministic function of the payload bytes
8
+ * and the action class, and of nothing else. {@link canonicalRender} is that
9
+ * function. Two channels, two runtimes, or two versions of this one cannot show
10
+ * two humans two different readings of the same payload without the difference
11
+ * being detectable: the rendering carries its own `display_hash`, the gate
12
+ * records that hash on `approval.requested`, and a rendering that disagrees is a
13
+ * rendering whose hash disagrees.
14
+ *
15
+ * The threat this closes is signoff social engineering. An approval surface that
16
+ * renders benign text while the hashed payload is malicious leaves the human
17
+ * signing blind. `payload_hash` binds the bytes; `display_hash` binds the
18
+ * reading of them.
19
+ *
20
+ * Four properties, and they are the whole module:
21
+ *
22
+ * 1. **Pure.** No clock, no locale, no environment, no randomness, no IO. The
23
+ * only inputs are the payload value and the class string, and
24
+ * `tests/wysiwys.test.ts` reads this file's own source and fails on a
25
+ * reference to any of them.
26
+ * 2. **Closed field set per kind.** A payload is recognised as one of four kinds
27
+ * (`command`, `file-change`, `email`, `opaque`) by its STRUCTURE, and each
28
+ * kind renders a fixed list of fields. A shape carrying one key its kind does
29
+ * not render is not that kind: it falls through to `opaque`, where the
30
+ * canonical JSON is shown whole. Nothing is hidden by being unrecognised.
31
+ * 3. **Absent renders explicitly.** A field the payload does not carry is
32
+ * printed as {@link ABSENT}, never omitted. An omitted line and a line whose
33
+ * value happens to be empty are different facts, and a reader who cannot tell
34
+ * them apart is reading a rendering that lost information.
35
+ * 4. **Claimed material stays outside.** Everything inside the canonical block
36
+ * is derived from the bound bytes. Summaries, cost estimates, rationale,
37
+ * confidence, and model-written glosses are rendered OUTSIDE it, under the
38
+ * channel's own claimed heading (SPEC.md §9).
39
+ *
40
+ * ## Why this lives in `src/core/`, not `src/channels/`
41
+ *
42
+ * It is deterministic core in the sense CLAUDE.md means: pure, exhaustively
43
+ * tested, and consulted by the gate. `core/gate.ts` computes `display_hash` at
44
+ * the write boundary from the same function every channel renders with, so the
45
+ * log states what rendering the approver was shown. A renderer under
46
+ * `src/channels/` would have to be imported BY core to do that, inverting the
47
+ * direction the codebase is built on. `channels/payload-view.ts` is the
48
+ * channel-side facade over this module and holds the one function that needs a
49
+ * channel type.
50
+ *
51
+ * ## The reading aids this absorbed (APRV-100, APRV-124, APRV-126)
52
+ *
53
+ * SPEC.md §10.4 requires a channel to present, for a `manual` action, "the full
54
+ * payload or a faithful rendering of it". Until now every channel used the one
55
+ * rendering `channels/tagging.ts` builds: pretty-printed JSON. That is faithful
56
+ * and it is exact, and for an email it is close to unreadable — the observed
57
+ * failure (2026-08-18, examples/email-demo.md) is a body arriving on a phone as
58
+ * a single line carrying literal `\n` sequences, which is precisely the text a
59
+ * human is being asked to take responsibility for.
60
+ *
61
+ * So this module adds a second rendering *on top of* the first, never instead
62
+ * of it:
63
+ *
64
+ * - A payload whose **structure** matches the email adapter's payload shape
65
+ * ({@link ../adapters/email.js}) is rendered field by field, with the body as
66
+ * the human will read it: real line breaks, no JSON escapes.
67
+ * - A payload whose structure is a **shell command** (APRV-126: the shape
68
+ * `cli/hook.ts` builds for a `Bash` tool call) is rendered as the command a
69
+ * shell would receive, over as many lines as it really has, with `cwd` on its
70
+ * own line beneath it. Same reason again: `"command": "gh pr create --body
71
+ * 'a\nb'"` on a phone is one paragraph of escaped JSON, and the observed
72
+ * failure (2026-08-20) is a human being asked to take responsibility for it.
73
+ * - A payload whose structure is a **file change** (APRV-124: the shape
74
+ * `cli/hook.ts` builds for an `Edit` or a `Write` tool call) is rendered as a
75
+ * diff, so the approver reads the change rather than the fact that a file was
76
+ * touched. Same reason as the email case: `"before": "a\nb"` on a phone is
77
+ * bytes nobody can check.
78
+ * - Every other shape falls to `opaque`, whose view IS the canonical JSON: the
79
+ * bytes whole, pretty-printed, exactly the rendering every payload had before
80
+ * the structural views existed.
81
+ *
82
+ * Three properties this file is careful about:
83
+ *
84
+ * 1. **Detection is structural.** Nothing here reads a self-declared "kind" or
85
+ * "type" field, because that field would be authored by the party under
86
+ * oversight, and a channel that lets an agent choose its own presentation
87
+ * has handed the agent the display. A payload is email-shaped when its keys
88
+ * and its value types say so, and for no other reason.
89
+ * 2. **Nothing is hidden.** The shape is accepted only when *every* key is one
90
+ * this module renders, so the field-by-field view never omits a byte of the
91
+ * payload — a `bcc` or a `content_type` that the reader could not see would
92
+ * be the same failure as a truncated payload, wearing a friendlier face.
93
+ * 3. **This is claimed content.** The payload is authored by the requesting
94
+ * agent. The block says so in its first line, and the computed binding (the
95
+ * `sha256` label each channel already prints around this region) stays where
96
+ * it is. Making the payload *legible* must not make it look *verified*.
97
+ * A `tool` or a `rule` value inside a file-change payload is rendered for
98
+ * the reader and is never what selects the rendering: the shape is, exactly
99
+ * as for an email.
100
+ * 4. **Two different byte strings never look the same.** A reading aid that
101
+ * interprets escape sequences has to answer the question it creates: if a
102
+ * real line break becomes a line break, what does the two-byte sequence
103
+ * backslash-`n` become? Rendering both as a line break would let an agent
104
+ * write one payload and have the approver read another. So the rendering is
105
+ * INJECTIVE by construction ({@link markEscapes}), and the property is
106
+ * tested by generating pairs of distinct byte strings.
107
+ * 5. **The view is the whole reading (APRV-162, `approval.md/wysiwys/2`).** A
108
+ * structured kind's view is the canonical rendering entire; no canonical-JSON
109
+ * appendix follows it. The completeness argument is property 2 above: kind
110
+ * detection is a closed field set, one unrecognised key sends the payload to
111
+ * `opaque` whose view is the whole JSON, so a structural view that renders at
112
+ * all renders every byte. The views therefore do not fold. A fold was
113
+ * survivable only while the appendix restated the hidden lines underneath it;
114
+ * with the appendix gone it would hide bytes from the only reading a human
115
+ * gets, which is the failure this module exists to remove.
116
+ *
117
+ * The output is plain text with real newlines. Escaping belongs to the channel:
118
+ * `telegram.ts` and `web.ts` each pass this through their own `escapeHtml` and
119
+ * their own `<pre>`, so the injection surface is exactly what it was before.
120
+ */
121
+ import { createHash } from "node:crypto";
122
+ import { classifyCommand, commandSegmentWords, isProtectedPath, } from "./command-class.js";
123
+ import { payloadHash } from "./payload.js";
124
+ /**
125
+ * How a field the payload does not carry is rendered.
126
+ *
127
+ * Never an omission. A closed field set that silently drops its absent members
128
+ * is not a closed field set: the reader cannot tell "no `cc`" from "a `cc` this
129
+ * renderer does not know how to show", and those are the two cases the whole
130
+ * design exists to keep apart.
131
+ */
132
+ export const ABSENT = "(absent)";
133
+ /** Keys the email adapter's payload may carry (`adapters/email.ts`). */
134
+ const STRING_KEYS = ["from", "subject", "body", "content_type"];
135
+ const ADDRESS_LIST_KEYS = ["to", "cc", "bcc"];
136
+ /**
137
+ * The keys that must be present for a value to read as an email at all.
138
+ *
139
+ * A recipient, a subject and a body: the triple that makes the JSON rendering
140
+ * unreadable in the first place. `from` is optional here even though the
141
+ * adapter requires it, because this module answers "will a human read this
142
+ * better field by field?", not "will the adapter accept it?" — the adapter
143
+ * enforces its own shape at execution, and duplicating that judgement here
144
+ * would only mean a payload the adapter rejects gets rendered badly first.
145
+ */
146
+ const REQUIRED_KEYS = ["to", "subject", "body"];
147
+ /** Display order. `body` last: it is the only multi-line field. */
148
+ const FIELD_ORDER = ["from", "to", "cc", "bcc", "subject", "content_type", "body"];
149
+ /** The heading and delimiters. Exported because the tests pin them. */
150
+ export const EMAIL_VIEW_HEADING = "email — rendered field by field; every value below is CLAIMED, authored by the requesting party";
151
+ export const BODY_BEGIN = "--- body begins ---";
152
+ export const BODY_END = "--- body ends ---";
153
+ export const CANONICAL_JSON_HEADING = "--- the same bytes, canonical JSON ---";
154
+ function isStringArray(value) {
155
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
156
+ }
157
+ /**
158
+ * Recognise an email-shaped payload, structurally.
159
+ *
160
+ * Returns the fields in display order, or `null` when the value is any other
161
+ * shape — including an email-ish object carrying one key this module does not
162
+ * know how to show, which falls back to JSON rather than hiding it.
163
+ */
164
+ export function emailPayloadFields(value) {
165
+ if (typeof value !== "object" || value === null || Array.isArray(value))
166
+ return null;
167
+ const record = value;
168
+ const known = new Set([...STRING_KEYS, ...ADDRESS_LIST_KEYS]);
169
+ for (const key of Object.keys(record)) {
170
+ if (!known.has(key))
171
+ return null;
172
+ }
173
+ for (const key of REQUIRED_KEYS) {
174
+ if (!(key in record))
175
+ return null;
176
+ }
177
+ for (const key of STRING_KEYS) {
178
+ if (key in record && typeof record[key] !== "string")
179
+ return null;
180
+ }
181
+ for (const key of ADDRESS_LIST_KEYS) {
182
+ if (!(key in record))
183
+ continue;
184
+ const entry = record[key];
185
+ if (typeof entry !== "string" && !isStringArray(entry))
186
+ return null;
187
+ }
188
+ const fields = [];
189
+ for (const key of FIELD_ORDER) {
190
+ if (!(key in record))
191
+ continue;
192
+ const entry = record[key];
193
+ fields.push({
194
+ label: key,
195
+ text: typeof entry === "string" ? entry : entry.join(", "),
196
+ });
197
+ }
198
+ return fields;
199
+ }
200
+ // ---------------------------------------------------------------------------
201
+ // File changes (APRV-124)
202
+ // ---------------------------------------------------------------------------
203
+ /**
204
+ * Keys a file-change payload may carry (`cli/hook.ts`).
205
+ *
206
+ * `tool` and `rule` are rendered, never consulted: `tool` is the harness's name
207
+ * for the call, and `rule` is the hook's own qualifier (`protected-path` or
208
+ * `protected-path-proposal`, APRV-124). Which rendering this module uses is
209
+ * decided by the presence of `file` and of the change itself, below.
210
+ */
211
+ const CHANGE_LABEL_KEYS = ["tool", "rule", "file"];
212
+ const CHANGE_TEXT_KEYS = ["before", "after", "content"];
213
+ const CHANGE_BOOL_KEYS = ["replace_all"];
214
+ /** The heading and delimiters of the diff view. Exported because tests pin them. */
215
+ export const EDIT_VIEW_HEADING = "file change — the change itself, not the touch; every value below is CLAIMED, authored by the requesting party";
216
+ export const DIFF_BEGIN = "--- change begins ---";
217
+ export const DIFF_END = "--- change ends ---";
218
+ /** The qualifier a proposal-tier touch renders (APRV-124). */
219
+ export const PROPOSAL_QUALIFIER = "this edit targets a file inside an AGENT WORKTREE: it is a branch PROPOSAL, not the live file. Merging it to the live checkout is a separate gated action.";
220
+ export const LIVE_QUALIFIER = "this edit targets the LIVE checkout, not a branch proposal.";
221
+ /** The qualifier a protected-name touch outside the gated checkout renders (APRV-161). */
222
+ export const ELSEWHERE_QUALIFIER = "this edit targets a file NAMED like a policy file, OUTSIDE the gated checkout: it is not the live policy. It gates because the name is protected wherever it sits.";
223
+ /**
224
+ * Recognise a file-change payload, structurally.
225
+ *
226
+ * Accepted when the payload names a `file` and carries either both sides of an
227
+ * edit (`before` and `after`) or a whole-file `content`, and every other key is
228
+ * one this module renders. Anything else — including a payload that carries
229
+ * only `before`, where the reader would be shown half a change — is `null` and
230
+ * falls back to JSON.
231
+ */
232
+ export function changePayloadView(value) {
233
+ if (typeof value !== "object" || value === null || Array.isArray(value))
234
+ return null;
235
+ const record = value;
236
+ const known = new Set([...CHANGE_LABEL_KEYS, ...CHANGE_TEXT_KEYS, ...CHANGE_BOOL_KEYS]);
237
+ for (const key of Object.keys(record)) {
238
+ if (!known.has(key))
239
+ return null;
240
+ }
241
+ if (typeof record["file"] !== "string")
242
+ return null;
243
+ for (const key of [...CHANGE_LABEL_KEYS, ...CHANGE_TEXT_KEYS]) {
244
+ if (key in record && typeof record[key] !== "string")
245
+ return null;
246
+ }
247
+ for (const key of CHANGE_BOOL_KEYS) {
248
+ if (key in record && typeof record[key] !== "boolean")
249
+ return null;
250
+ }
251
+ const before = record["before"];
252
+ const after = record["after"];
253
+ const content = record["content"];
254
+ const isEdit = typeof before === "string" && typeof after === "string";
255
+ const isWrite = typeof content === "string" && before === undefined && after === undefined;
256
+ if (!isEdit && !isWrite)
257
+ return null;
258
+ if (isEdit && content !== undefined)
259
+ return null;
260
+ const labels = [];
261
+ for (const key of CHANGE_LABEL_KEYS) {
262
+ const entry = record[key];
263
+ if (typeof entry === "string")
264
+ labels.push({ label: key, text: entry });
265
+ }
266
+ for (const key of CHANGE_BOOL_KEYS) {
267
+ const entry = record[key];
268
+ if (typeof entry === "boolean")
269
+ labels.push({ label: key, text: entry ? "true" : "false" });
270
+ }
271
+ return {
272
+ labels,
273
+ before: isEdit ? before : null,
274
+ after: isEdit ? after : content,
275
+ };
276
+ }
277
+ /**
278
+ * `text` as prefixed diff lines, whole (APRV-162).
279
+ *
280
+ * No budget and no fold: this view is the entire canonical rendering of the
281
+ * payload, so a line it stops showing is a line nobody sees. Length is the
282
+ * channel's problem (`telegram.ts` chunks, never truncates).
283
+ */
284
+ function diffLines(text, marker) {
285
+ return text.split("\n").map((line) => `${marker}${line}`);
286
+ }
287
+ function lineCount(text) {
288
+ return text === "" ? 0 : text.split("\n").length;
289
+ }
290
+ /**
291
+ * The closed field set of a file change, in display order (APRV-119).
292
+ *
293
+ * Every one of these is printed for every file change, {@link ABSENT} where the
294
+ * payload carries none. `replace_all` is the field this matters most for: an
295
+ * edit that replaces one occurrence and an edit that replaces all of them are
296
+ * different actions, and a rendering that showed the flag only when it was set
297
+ * would leave the reader unable to tell "replace_all: false" from "a renderer
298
+ * that does not show this".
299
+ */
300
+ const CHANGE_FIELD_ORDER = [...CHANGE_LABEL_KEYS, ...CHANGE_BOOL_KEYS];
301
+ /** The diff view: labels, then one block per side, `-` removed, `+` added. */
302
+ function changeRegionText(view, hash) {
303
+ const lines = [EDIT_VIEW_HEADING];
304
+ for (const key of CHANGE_FIELD_ORDER) {
305
+ const field = view.labels.find((entry) => entry.label === key);
306
+ lines.push(`${key}: ${field === undefined ? ABSENT : field.text}`);
307
+ }
308
+ // The tier qualifier, rendered for EVERY tier. Printing it only for the
309
+ // qualified tiers would make "live" the silent default, and the silent
310
+ // default is the one that reaches the real file. Each tier has its own
311
+ // wording: LIVE_QUALIFIER is a claim about the file the edit lands in, so a
312
+ // protected-name touch elsewhere (APRV-161) must not borrow it.
313
+ const rule = view.labels.find((field) => field.label === "rule");
314
+ if (rule !== undefined && rule.text === "protected-name-elsewhere") {
315
+ lines.push(`note: ${ELSEWHERE_QUALIFIER}`);
316
+ }
317
+ else if (rule !== undefined && rule.text.startsWith("protected-path")) {
318
+ lines.push(`note: ${rule.text.endsWith("-proposal") ? PROPOSAL_QUALIFIER : LIVE_QUALIFIER}`);
319
+ }
320
+ lines.push(DIFF_BEGIN);
321
+ if (view.before === null) {
322
+ lines.push(`the whole file as it will be written (${String(lineCount(view.after))} lines); the bytes it replaces are not part of what is being approved`);
323
+ lines.push(...diffLines(view.after, "+"));
324
+ }
325
+ else {
326
+ lines.push(`replacing ${String(lineCount(view.before))} line(s) with ${String(lineCount(view.after))}`);
327
+ lines.push(...diffLines(view.before, "-"));
328
+ lines.push(...diffLines(view.after, "+"));
329
+ }
330
+ lines.push(DIFF_END);
331
+ lines.push(rawBytesLine(hash));
332
+ return lines;
333
+ }
334
+ // ---------------------------------------------------------------------------
335
+ // Shell commands (APRV-126)
336
+ // ---------------------------------------------------------------------------
337
+ /** Keys a command payload may carry (`cli/hook.ts`: `{command, cwd}`). */
338
+ const COMMAND_KEYS = ["command", "cwd"];
339
+ /** The heading and delimiters of the command view. Exported because tests pin them. */
340
+ export const COMMAND_VIEW_HEADING = "command — rendered; the hash binds the RAW BYTES, not this view. Every value below is CLAIMED, authored by the requesting party";
341
+ export const COMMAND_BEGIN = "--- command begins ---";
342
+ export const COMMAND_END = "--- command ends ---";
343
+ /**
344
+ * The delimiters around a marked escape sequence.
345
+ *
346
+ * Guillemets rather than brackets: `[` and `]` are ordinary shell and regex
347
+ * characters, so a marker built from them would be indistinguishable from the
348
+ * command's own text at a glance, which is the failure this marker exists to
349
+ * prevent.
350
+ */
351
+ export const ESCAPE_OPEN = "«";
352
+ export const ESCAPE_CLOSE = "»";
353
+ /** The legend printed above every command block, so the marker needs no lore. */
354
+ export const ESCAPE_LEGEND = `escapes: ${ESCAPE_OPEN}\\n${ESCAPE_CLOSE} is the two LITERAL bytes backslash-n; a real line break is a line break`;
355
+ /**
356
+ * The escape letters that name a character the reader could otherwise mistake
357
+ * for the real thing.
358
+ *
359
+ * `\` is in the set for the same reason: `\\n` and `\n` are different bytes and
360
+ * a reader must not have to count backslashes to tell them apart.
361
+ */
362
+ const MARKED_ESCAPES = new Set(["n", "r", "t", "\\"]);
363
+ /**
364
+ * One line of a command, with literal escape sequences marked.
365
+ *
366
+ * INJECTIVE, and the proof is short enough to keep here. The rendering is a
367
+ * left-to-right tokenizer over two tokens: a backslash followed by a letter in
368
+ * {@link MARKED_ESCAPES} becomes `«\c»`, and every other character is itself.
369
+ * A `«\c»` in the OUTPUT can therefore only have come from that first token,
370
+ * because a backslash followed by such a letter in the input is never emitted
371
+ * bare — so reading the output back left to right recovers the input exactly,
372
+ * and a left inverse is all injectivity needs.
373
+ *
374
+ * Real newlines are handled by the caller, which splits on them before calling
375
+ * this: a line break in the output comes from a line break in the input, and
376
+ * from nothing else.
377
+ */
378
+ export function markEscapes(line) {
379
+ let out = "";
380
+ for (let index = 0; index < line.length; index += 1) {
381
+ const character = line[index];
382
+ const next = index + 1 < line.length ? line[index + 1] : "";
383
+ if (character === "\\" && MARKED_ESCAPES.has(next)) {
384
+ out += `${ESCAPE_OPEN}\\${next}${ESCAPE_CLOSE}`;
385
+ index += 1;
386
+ continue;
387
+ }
388
+ out += character;
389
+ }
390
+ return out;
391
+ }
392
+ /**
393
+ * Recognise a command payload, structurally.
394
+ *
395
+ * Accepted when the payload carries a string `command` and nothing this module
396
+ * cannot show. `cwd` is optional here even though `cli/hook.ts` always sets it:
397
+ * the question this answers is "will a human read this better as a command?",
398
+ * and a payload missing its directory reads better either way.
399
+ */
400
+ export function commandPayloadView(value) {
401
+ if (typeof value !== "object" || value === null || Array.isArray(value))
402
+ return null;
403
+ const record = value;
404
+ const known = new Set(COMMAND_KEYS);
405
+ for (const key of Object.keys(record)) {
406
+ if (!known.has(key))
407
+ return null;
408
+ }
409
+ if (typeof record["command"] !== "string")
410
+ return null;
411
+ const cwd = record["cwd"];
412
+ if (cwd !== undefined && typeof cwd !== "string")
413
+ return null;
414
+ return { command: record["command"], cwd: typeof cwd === "string" ? cwd : null };
415
+ }
416
+ /**
417
+ * Where the exact bytes live, and how to get them back (APRV-126, APRV-162).
418
+ *
419
+ * Carried by every structural view, not the command view alone: with no
420
+ * canonical-JSON appendix underneath, this line is the reader's only route from
421
+ * a rendering back to the bytes it was derived from.
422
+ *
423
+ * The store is content-addressed by this very hash and re-verified on every
424
+ * read (`core/payload-store.ts`), so the line is an instruction, never a claim:
425
+ * following it produces the bytes or produces a refusal, and never something
426
+ * else wearing the same name.
427
+ */
428
+ export function rawBytesLine(hash) {
429
+ return `raw bytes: .approval/payloads/${hash}.json — re-verified against this sha256 on every read`;
430
+ }
431
+ /** The command view: the command over its real lines, then `cwd`. */
432
+ function commandRegionText(view, hash) {
433
+ const lines = [COMMAND_VIEW_HEADING, ESCAPE_LEGEND];
434
+ const commandLines = view.command.split("\n");
435
+ const count = commandLines.length;
436
+ lines.push(`command (${String(count)} line${count === 1 ? "" : "s"}):`);
437
+ lines.push(COMMAND_BEGIN);
438
+ lines.push(...commandLines.map(markEscapes));
439
+ lines.push(COMMAND_END);
440
+ // On its own line, beneath the command, because it is the other half of what
441
+ // the command does and a `cd` the reader never saw is the whole difference
442
+ // between a repository and someone else's.
443
+ lines.push(`cwd: ${view.cwd === null ? "(none declared)" : markEscapes(view.cwd)}`);
444
+ lines.push(rawBytesLine(hash));
445
+ return lines;
446
+ }
447
+ // ---------------------------------------------------------------------------
448
+ // The command breakdown (APRV-144)
449
+ // ---------------------------------------------------------------------------
450
+ /** What separates two segments of the breakdown. Exported: the tests pin it. */
451
+ export const BREAKDOWN_SEPARATOR = " · ";
452
+ /** Characters one segment of the breakdown may take before it folds. */
453
+ export const BREAKDOWN_SEGMENT_BUDGET = 40;
454
+ /** Segments the breakdown shows before it says how many it did not. */
455
+ export const BREAKDOWN_MAX_SEGMENTS = 8;
456
+ /**
457
+ * The words of one segment worth showing: its arguments, minus the flags and
458
+ * minus the words that are probably flag VALUES.
459
+ *
460
+ * The classifier does not know which flags take a value, and neither does this
461
+ * (`RuleContext.positionals` has the same blind spot, on purpose). What it does
462
+ * is drop the word after a bare SHORT flag, which is what turns
463
+ * `git commit -m "APRV-…"` into `git commit` rather than into a segment whose
464
+ * only visible argument is a commit message.
465
+ *
466
+ * Short and not long, deliberately. A short flag that takes a value almost
467
+ * always takes it as the next word (`-m msg`, `-H header`, `-o file`); a long
468
+ * one almost always carries it inline (`--message=…`) or takes none at all, so
469
+ * the word after `--force-with-lease` is `origin` and dropping it would hide
470
+ * the destination of a push. Both mistakes cost a word on a reading aid whose
471
+ * raw bytes sit underneath it in full, and nothing reads this back — but the
472
+ * long-flag case is the one that would have hidden something worth seeing.
473
+ */
474
+ function salientArgs(args) {
475
+ const kept = [];
476
+ let afterValueFlag = false;
477
+ for (const arg of args) {
478
+ const flag = arg.startsWith("-") && arg !== "-";
479
+ if (!flag && !afterValueFlag)
480
+ kept.push(arg);
481
+ afterValueFlag = flag && !arg.startsWith("--") && !arg.includes("=");
482
+ }
483
+ return kept;
484
+ }
485
+ /** One segment as `bin sub arg…`, collapsed to one line and folded at the budget. */
486
+ function breakdownSegment(bin, args) {
487
+ const text = [bin, ...salientArgs(args)].join(" ").replace(/\s+/gu, " ").trim();
488
+ return text.length <= BREAKDOWN_SEGMENT_BUDGET
489
+ ? text
490
+ : `${text.slice(0, BREAKDOWN_SEGMENT_BUDGET - 1)}…`;
491
+ }
492
+ /**
493
+ * What a compound command does, segment by segment (APRV-144).
494
+ *
495
+ * `git add … · git commit · git push origin main:records-… · gh pr create`.
496
+ *
497
+ * The observed complaint (Carter, 2026-08-25) is that the claimed summary of a
498
+ * shell action is `truncate(command, 160)`, which for a chained command is the
499
+ * first clause and a path prefix: the approver reads where the command starts
500
+ * and never what it ends by doing. This is the deterministic half of the
501
+ * answer. It is derived from {@link commandSegmentWords} — the classifier's own
502
+ * tokenizer, never a second one — so a channel showing it cannot describe a
503
+ * command differently from the module that chose its class.
504
+ *
505
+ * `null` for a string the tokenizer refuses (the same input the classifier
506
+ * answers `unparseable` for) and for one with no segment carrying a binary: an
507
+ * aid that cannot be derived is absent, never guessed.
508
+ */
509
+ export function commandBreakdown(command) {
510
+ const segments = commandSegmentWords(command);
511
+ if (segments === null || segments.length === 0)
512
+ return null;
513
+ const shown = segments.slice(0, BREAKDOWN_MAX_SEGMENTS);
514
+ const parts = shown.map((segment) => breakdownSegment(segment.bin, segment.args));
515
+ const hidden = segments.length - shown.length;
516
+ if (hidden > 0)
517
+ parts.push(`… ${String(hidden)} more`);
518
+ return parts.join(BREAKDOWN_SEPARATOR);
519
+ }
520
+ // ---------------------------------------------------------------------------
521
+ // The protected path (APRV-143)
522
+ // ---------------------------------------------------------------------------
523
+ /** The rule names a file-tool touch of a protected path reports (`cli/hook.ts`). */
524
+ const PROTECTED_RULE_NAMES = [
525
+ "protected-path",
526
+ "protected-path-proposal",
527
+ "protected-name-elsewhere",
528
+ ];
529
+ /**
530
+ * Which protected path selected this payload's class, when one did (APRV-143).
531
+ *
532
+ * A prompt that says `class: policy.edit` and stops there tells the approver
533
+ * that *some* rule fired and leaves them to find the file. Both gated shapes
534
+ * can say which:
535
+ *
536
+ * - a shell payload is re-classified here, by the same
537
+ * {@link classifyCommand} the hook decided with, and the segment that took
538
+ * `policy.edit` carries the word it matched (`ClassifiedSegment.path`);
539
+ * - a file-tool payload names its target in `file`, and
540
+ * {@link isProtectedPath} is re-run over it rather than trusted: the answer
541
+ * is recomputed from the bound bytes, so this stays a computed field. The
542
+ * payload's own `rule` is used as the label only when it is one of the three
543
+ * the hook writes, which is what keeps the worktree-proposal and
544
+ * protected-name-elsewhere tiers legible (APRV-124, APRV-161).
545
+ *
546
+ * `extra` is `policy.protected_paths`, passed exactly as every enforcement path
547
+ * passes it; omitting it narrows the answer and never widens it.
548
+ */
549
+ export function protectedPathView(value, extra = []) {
550
+ const command = commandPayloadView(value);
551
+ if (command !== null) {
552
+ const classified = classifyCommand(command.command, extra);
553
+ if (!classified.ok)
554
+ return null;
555
+ for (const segment of classified.segments) {
556
+ if (segment.path !== undefined)
557
+ return { path: segment.path, rule: segment.rule };
558
+ }
559
+ return null;
560
+ }
561
+ if (typeof value !== "object" || value === null || Array.isArray(value))
562
+ return null;
563
+ const record = value;
564
+ const file = record["file"];
565
+ if (typeof file !== "string" || !isProtectedPath(file, extra))
566
+ return null;
567
+ const rule = record["rule"];
568
+ return {
569
+ path: file,
570
+ rule: typeof rule === "string" && PROTECTED_RULE_NAMES.includes(rule) ? rule : "protected-path",
571
+ };
572
+ }
573
+ /** The email view: every field of the closed set, then the body in delimiters. */
574
+ function emailRegionText(fields, hash) {
575
+ const lines = [EMAIL_VIEW_HEADING];
576
+ for (const key of FIELD_ORDER) {
577
+ if (key === "body")
578
+ continue;
579
+ const field = fields.find((entry) => entry.label === key);
580
+ lines.push(`${key}: ${field === undefined ? ABSENT : field.text}`);
581
+ }
582
+ // `body` is required for the shape to be recognised at all, so its absence
583
+ // here is unreachable; it is rendered as an absence anyway rather than as an
584
+ // empty block, because a delimiter pair around nothing reads as "the body is
585
+ // empty" and that is a different claim.
586
+ const body = fields.find((field) => field.label === "body");
587
+ if (body === undefined) {
588
+ lines.push(`body: ${ABSENT}`, rawBytesLine(hash));
589
+ return lines;
590
+ }
591
+ const count = body.text === "" ? 0 : body.text.split("\n").length;
592
+ lines.push(`body (${count} line${count === 1 ? "" : "s"}):`, BODY_BEGIN, body.text, BODY_END);
593
+ lines.push(rawBytesLine(hash));
594
+ return lines;
595
+ }
596
+ // ---------------------------------------------------------------------------
597
+ // The canonical renderer (APRV-119)
598
+ // ---------------------------------------------------------------------------
599
+ /**
600
+ * The renderer's identity, printed inside every canonical block.
601
+ *
602
+ * Inside the text, and therefore inside {@link CanonicalRendering.display_hash}:
603
+ * a version that rode alongside the hash rather than inside it would let two
604
+ * renderer versions produce the same digest for two different readings, which is
605
+ * the one thing the digest exists to make impossible. Any change to the bytes
606
+ * this module emits — a new field, a reworded heading, a line that used to be
607
+ * folded away — is a new version, and a reader comparing a stored
608
+ * `display_hash` against a re-render can see which renderer wrote it. A record
609
+ * written under an earlier version re-derives under the renderer its own hashed
610
+ * text names, never under this one.
611
+ *
612
+ * `/2` (APRV-162): the structural views render whole and carry no canonical-JSON
613
+ * appendix; `opaque` is unchanged, its view being that JSON.
614
+ */
615
+ export const CANONICAL_RENDERER_VERSION = "approval.md/wysiwys/2";
616
+ /**
617
+ * The `approval.requested` payload field carrying {@link
618
+ * CanonicalRendering.display_hash} (APRV-119).
619
+ *
620
+ * Written by the gate at the write boundary, exactly as `ts` and `policy_sha256`
621
+ * are, and for the same reason: the requesting party must not be able to name
622
+ * the rendering it claims a human was shown. {@link RequestInput} carries no
623
+ * field for it.
624
+ */
625
+ export const DISPLAY_HASH_FIELD = "display_hash";
626
+ /** The delimiters of the canonical block. Exported because the tests pin them. */
627
+ export const CANONICAL_BEGIN = "--- canonical rendering begins ---";
628
+ export const CANONICAL_END = "--- canonical rendering ends ---";
629
+ /** The heading of the `opaque` kind: no structural view, the bytes whole. */
630
+ export const OPAQUE_VIEW_HEADING = "payload — no structural view applies to this shape; every byte of it is in the canonical JSON below, and every value is CLAIMED, authored by the requesting party";
631
+ /**
632
+ * The kinds a payload can be rendered as.
633
+ *
634
+ * Closed, and decided by structure alone. `opaque` is not a failure: it is the
635
+ * kind whose closed field set is "the whole canonical JSON", which is the
636
+ * rendering every payload had before the structural views existed.
637
+ */
638
+ export const CANONICAL_KINDS = ["command", "file-change", "email", "opaque"];
639
+ /** The structural view for a payload, and the kind that selected it. */
640
+ function canonicalBody(payload, hash) {
641
+ const command = commandPayloadView(payload);
642
+ if (command !== null)
643
+ return { kind: "command", lines: commandRegionText(command, hash) };
644
+ const change = changePayloadView(payload);
645
+ if (change !== null)
646
+ return { kind: "file-change", lines: changeRegionText(change, hash) };
647
+ const fields = emailPayloadFields(payload);
648
+ if (fields !== null)
649
+ return { kind: "email", lines: emailRegionText(fields, hash) };
650
+ // The `opaque` view IS the canonical JSON, and it is the only view that
651
+ // carries it (APRV-162): under a structured kind the same bytes would be a
652
+ // second reading of a payload the view already showed entire, and two
653
+ // readings is one more than a human checks.
654
+ return {
655
+ kind: "opaque",
656
+ lines: [OPAQUE_VIEW_HEADING, "", CANONICAL_JSON_HEADING, canonicalJson(payload)],
657
+ };
658
+ }
659
+ /**
660
+ * The `opaque` view: the payload, pretty-printed.
661
+ *
662
+ * Indented rather than RFC 8785 canonical, deliberately. The line above it
663
+ * states the RFC 8785 digest, which is the value the approval binds to and the
664
+ * value any second implementation can recompute; what this is for is a human
665
+ * reading a shape no structural view knows, and a single 4KB line is not that.
666
+ * The two never disagree, because both are derived from the same value in the
667
+ * same call.
668
+ */
669
+ function canonicalJson(payload) {
670
+ return JSON.stringify(payload, null, 2) ?? String(payload);
671
+ }
672
+ /**
673
+ * Render a payload the way every channel MUST present it (APRV-119).
674
+ *
675
+ * A pure function of `(payload, actionClass)`. Same arguments, byte-identical
676
+ * `text` and `display_hash`, in this process or another, today or next year
677
+ * under the same {@link CANONICAL_RENDERER_VERSION}.
678
+ *
679
+ * The block states its own renderer, class, kind and payload digest before it
680
+ * shows anything, so a reader who is handed the text alone can tell what
681
+ * produced it and what it binds to. Then the view for the kind, which is the
682
+ * whole reading: it renders every byte of the payload or the payload is
683
+ * `opaque` and the view is its JSON (APRV-162).
684
+ *
685
+ * Throws `JcsError` for a payload RFC 8785 cannot serialize (a cycle, a NaN).
686
+ * That is {@link payloadHash}'s contract and it is the right one here too: a
687
+ * payload that cannot be bound to must not acquire a plausible-looking rendering
688
+ * of itself. Every caller in this repository renders material that has already
689
+ * been hash-checked against the log's binding, so the throw is unreachable on
690
+ * the paths a human ever sees.
691
+ */
692
+ export function canonicalRender(payload, actionClass) {
693
+ const hash = payloadHash(payload);
694
+ const body = canonicalBody(payload, hash);
695
+ const text = [
696
+ CANONICAL_BEGIN,
697
+ `renderer: ${CANONICAL_RENDERER_VERSION}`,
698
+ `class: ${actionClass}`,
699
+ `payload kind: ${body.kind}`,
700
+ `payload sha256: ${hash}`,
701
+ "",
702
+ ...body.lines,
703
+ CANONICAL_END,
704
+ ].join("\n");
705
+ return {
706
+ version: CANONICAL_RENDERER_VERSION,
707
+ kind: body.kind,
708
+ text,
709
+ display_hash: createHash("sha256").update(text, "utf8").digest("hex"),
710
+ };
711
+ }
712
+ /**
713
+ * The `display_hash` of a payload, or `null` when there is none to compute.
714
+ *
715
+ * The gate's entry point (`core/gate.ts`), where a payload that cannot be
716
+ * canonicalized must not abort a request that has already passed every check
717
+ * that matters. A missing `display_hash` costs a reader one cross-check; a
718
+ * throw here would cost them the request.
719
+ */
720
+ export function displayHashOf(payload, actionClass) {
721
+ try {
722
+ return canonicalRender(payload, actionClass).display_hash;
723
+ }
724
+ catch {
725
+ return null;
726
+ }
727
+ }
728
+ //# sourceMappingURL=wysiwys.js.map