hecks 3.14.0 → 5.0.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 (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -1,9 +1,9 @@
1
1
  //! Ties the journal and the sandbox together: replays history, then the
2
2
  //! new step, persisting it only if the module accepts it.
3
3
 
4
+ use crate::host::Host;
4
5
  use crate::journal;
5
- use crate::journal::LineageConfig;
6
- use crate::lambda_client::{self, LambdaInvoker};
6
+ use crate::lambda_client::{self};
7
7
  use crate::wasm_runner;
8
8
  use std::path::Path;
9
9
  use tokio::sync::Mutex;
@@ -16,7 +16,7 @@ pub struct Outcome {
16
16
 
17
17
  /// Who a command is dispatched for: the role they state and, when the host knows who they are, the
18
18
  /// actor Governance holds assignments for. Both absent is an unidentified caller.
19
- #[derive(Clone, Copy, Debug, Default, PartialEq)]
19
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20
20
  pub struct Caller<'a> {
21
21
  pub role: Option<&'a str>,
22
22
  pub actor_id: Option<&'a str>,
@@ -27,7 +27,7 @@ pub struct Caller<'a> {
27
27
  pub const ANONYMOUS_ROLE: &str = "Anonymous";
28
28
 
29
29
  /// What the host does about the role a command declares (`HECKS_ROLE_ENFORCEMENT`).
30
- #[derive(Clone, Copy, Debug, PartialEq)]
30
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
31
31
  pub enum Enforcement {
32
32
  /// A caller with no role is unchecked, as before.
33
33
  Off,
@@ -56,9 +56,12 @@ impl Enforcement {
56
56
 
57
57
  /// The caller the kernel is asked to check: an unidentified caller becomes the anonymous role
58
58
  /// unless enforcement is off.
59
- pub fn effective<'a>(self, caller: Caller<'a>) -> Caller<'a> {
59
+ pub fn effective(self, caller: Caller<'_>) -> Caller<'_> {
60
60
  if self != Self::Off && caller.role.is_none() && caller.actor_id.is_none() {
61
- return Caller { role: Some(ANONYMOUS_ROLE), actor_id: None };
61
+ return Caller {
62
+ role: Some(ANONYMOUS_ROLE),
63
+ actor_id: None,
64
+ };
62
65
  }
63
66
  caller
64
67
  }
@@ -75,9 +78,13 @@ enum Origin {
75
78
 
76
79
  /// Whether the kernel refused the command for the caller's role.
77
80
  fn refused_for_role(result: &serde_json::Value) -> bool {
78
- result.get("refusals").and_then(|r| r.as_array()).is_some_and(|all| {
79
- all.iter().any(|r| r.get("kind").and_then(|k| k.as_str()) == Some("Unauthorized"))
80
- })
81
+ result
82
+ .get("refusals")
83
+ .and_then(|r| r.as_array())
84
+ .is_some_and(|all| {
85
+ all.iter()
86
+ .any(|r| r.get("kind").and_then(|k| k.as_str()) == Some("Unauthorized"))
87
+ })
81
88
  }
82
89
 
83
90
  // Distinguishes a module refusal from a kernel failure to run at all.
@@ -92,9 +99,16 @@ enum KernelAnswer {
92
99
  // refusals doesn't by itself mean the run succeeded.
93
100
  fn classify(result: &serde_json::Value) -> KernelAnswer {
94
101
  if let Some(error) = result.get("error") {
95
- return KernelAnswer::Failed(error.as_str().map(str::to_string).unwrap_or_else(|| error.to_string()));
102
+ return KernelAnswer::Failed(
103
+ error
104
+ .as_str()
105
+ .map_or_else(|| error.to_string(), str::to_string),
106
+ );
96
107
  }
97
- let refused = result.get("refusals").and_then(|r| r.as_array()).is_some_and(|r| !r.is_empty());
108
+ let refused = result
109
+ .get("refusals")
110
+ .and_then(|r| r.as_array())
111
+ .is_some_and(|r| !r.is_empty());
98
112
  if refused {
99
113
  KernelAnswer::Refused
100
114
  } else {
@@ -107,18 +121,26 @@ fn classify(result: &serde_json::Value) -> KernelAnswer {
107
121
  fn parse_kernel_output(output: &str) -> anyhow::Result<serde_json::Value> {
108
122
  let result: serde_json::Value = serde_json::from_str(output)?;
109
123
  match classify(&result) {
110
- KernelAnswer::Failed(message) => anyhow::bail!("the kernel failed before it could run the command: {message}"),
124
+ KernelAnswer::Failed(message) => {
125
+ anyhow::bail!("the kernel failed before it could run the command: {message}")
126
+ }
111
127
  KernelAnswer::Accepted | KernelAnswer::Refused => Ok(result),
112
128
  }
113
129
  }
114
130
 
115
131
  // Kept as one opaque object under the journal's `args` column; the kernel
116
132
  // unwraps `to`/`with` itself, so this needs no journal schema migration.
117
- pub fn routed_invocation(to: serde_json::Value, facts: serde_json::Value) -> anyhow::Result<serde_json::Value> {
133
+ pub fn routed_invocation(
134
+ to: serde_json::Value,
135
+ facts: serde_json::Value,
136
+ ) -> anyhow::Result<serde_json::Value> {
118
137
  if !facts.is_object() {
119
138
  anyhow::bail!("with must be an object of command facts");
120
139
  }
121
- Ok(serde_json::json!({ "to": to, "with": facts }))
140
+ let mut invocation = serde_json::Map::new();
141
+ invocation.insert("to".to_string(), to);
142
+ invocation.insert("with".to_string(), facts);
143
+ Ok(serde_json::Value::Object(invocation))
122
144
  }
123
145
 
124
146
  // No `to` route: used only when a command creates a new aggregate, so
@@ -127,66 +149,52 @@ pub fn facts_invocation(facts: serde_json::Value) -> anyhow::Result<serde_json::
127
149
  if !facts.is_object() {
128
150
  anyhow::bail!("with must be an object of command facts");
129
151
  }
130
- Ok(serde_json::json!({ "with": facts }))
152
+ let mut invocation = serde_json::Map::new();
153
+ invocation.insert("with".to_string(), facts);
154
+ Ok(serde_json::Value::Object(invocation))
131
155
  }
132
156
 
133
157
  // Route-aware entry point; `handle` below stays the compatibility path
134
158
  // for callers still sending the older mixed args shape.
135
- #[allow(clippy::too_many_arguments)]
136
159
  pub async fn handle_routed(
137
- client: &Mutex<Client>,
138
- wasm_path: &Path,
160
+ host: Host<'_>,
139
161
  verb: &str,
140
162
  to: serde_json::Value,
141
163
  facts: serde_json::Value,
142
164
  role: Option<&str>,
143
- config: &LineageConfig,
144
- invoker: &dyn LambdaInvoker,
145
165
  ) -> anyhow::Result<Outcome> {
146
- handle(client, wasm_path, verb, routed_invocation(to, facts)?, role, config, invoker).await
166
+ handle(host, verb, routed_invocation(to, facts)?, role).await
147
167
  }
148
168
 
149
169
  /// `handle_routed` for a caller the host can name.
150
- #[allow(clippy::too_many_arguments)]
151
170
  pub async fn handle_routed_as(
152
- client: &Mutex<Client>,
153
- wasm_path: &Path,
171
+ host: Host<'_>,
154
172
  verb: &str,
155
173
  to: serde_json::Value,
156
174
  facts: serde_json::Value,
157
175
  caller: Caller<'_>,
158
- config: &LineageConfig,
159
- invoker: &dyn LambdaInvoker,
160
176
  ) -> anyhow::Result<Outcome> {
161
- handle_as(client, wasm_path, verb, routed_invocation(to, facts)?, caller, config, invoker).await
177
+ handle_as(host, verb, routed_invocation(to, facts)?, caller).await
162
178
  }
163
179
 
164
180
  // Facts-only counterpart to `handle_routed`.
165
- #[allow(clippy::too_many_arguments)]
166
181
  pub async fn handle_facts(
167
- client: &Mutex<Client>,
168
- wasm_path: &Path,
182
+ host: Host<'_>,
169
183
  verb: &str,
170
184
  facts: serde_json::Value,
171
185
  role: Option<&str>,
172
- config: &LineageConfig,
173
- invoker: &dyn LambdaInvoker,
174
186
  ) -> anyhow::Result<Outcome> {
175
- handle(client, wasm_path, verb, facts_invocation(facts)?, role, config, invoker).await
187
+ handle(host, verb, facts_invocation(facts)?, role).await
176
188
  }
177
189
 
178
190
  /// `handle_facts` for a caller the host can name.
179
- #[allow(clippy::too_many_arguments)]
180
191
  pub async fn handle_facts_as(
181
- client: &Mutex<Client>,
182
- wasm_path: &Path,
192
+ host: Host<'_>,
183
193
  verb: &str,
184
194
  facts: serde_json::Value,
185
195
  caller: Caller<'_>,
186
- config: &LineageConfig,
187
- invoker: &dyn LambdaInvoker,
188
196
  ) -> anyhow::Result<Outcome> {
189
- handle_as(client, wasm_path, verb, facts_invocation(facts)?, caller, config, invoker).await
197
+ handle_as(host, verb, facts_invocation(facts)?, caller).await
190
198
  }
191
199
 
192
200
  // A Mutex, not a bare Arc<Client>: `handle` needs `Client::transaction`,
@@ -205,68 +213,95 @@ pub async fn handle_facts_as(
205
213
  // host-internal dispatch and is unchecked in every enforcement mode. A call that names a role is
206
214
  // still checked against it. Callers outside the host go through the `_as` entry points.
207
215
  pub async fn handle(
208
- client: &Mutex<Client>,
209
- wasm_path: &Path,
216
+ host: Host<'_>,
210
217
  verb: &str,
211
218
  args: serde_json::Value,
212
219
  role: Option<&str>,
213
- config: &LineageConfig,
214
- invoker: &dyn LambdaInvoker,
215
220
  ) -> anyhow::Result<Outcome> {
216
- let origin = if role.is_none() { Origin::HostInternal } else { Origin::External };
217
- dispatch_in(Enforcement::from_env(), origin, client, wasm_path, verb, args, Caller { role, actor_id: None }, config, invoker).await
221
+ let origin = if role.is_none() {
222
+ Origin::HostInternal
223
+ } else {
224
+ Origin::External
225
+ };
226
+ dispatch_in(
227
+ Enforcement::from_env(),
228
+ origin,
229
+ host,
230
+ verb,
231
+ args,
232
+ Caller {
233
+ role,
234
+ actor_id: None,
235
+ },
236
+ )
237
+ .await
218
238
  }
219
239
 
220
240
  /// Dispatches for a caller, applying `HECKS_ROLE_ENFORCEMENT`: unidentified callers are refused (or,
221
241
  /// in shadow, logged as would-be refusals and let through).
222
242
  pub async fn handle_as(
223
- client: &Mutex<Client>,
224
- wasm_path: &Path,
243
+ host: Host<'_>,
225
244
  verb: &str,
226
245
  args: serde_json::Value,
227
246
  caller: Caller<'_>,
228
- config: &LineageConfig,
229
- invoker: &dyn LambdaInvoker,
230
247
  ) -> anyhow::Result<Outcome> {
231
- dispatch_in(Enforcement::from_env(), Origin::External, client, wasm_path, verb, args, caller, config, invoker).await
248
+ dispatch_in(
249
+ Enforcement::from_env(),
250
+ Origin::External,
251
+ host,
252
+ verb,
253
+ args,
254
+ caller,
255
+ )
256
+ .await
232
257
  }
233
258
 
234
- #[allow(clippy::too_many_arguments)]
235
259
  async fn dispatch_in(
236
260
  mode: Enforcement,
237
261
  origin: Origin,
238
- client: &Mutex<Client>,
239
- wasm_path: &Path,
262
+ host: Host<'_>,
240
263
  verb: &str,
241
264
  args: serde_json::Value,
242
265
  caller: Caller<'_>,
243
- config: &LineageConfig,
244
- invoker: &dyn LambdaInvoker,
245
266
  ) -> anyhow::Result<Outcome> {
246
- let checked = if origin == Origin::HostInternal { caller } else { mode.effective(caller) };
247
- let outcome = run(client, wasm_path, verb, args.clone(), checked, config, invoker).await?;
267
+ let checked = if origin == Origin::HostInternal {
268
+ caller
269
+ } else {
270
+ mode.effective(caller)
271
+ };
272
+ let outcome = run(host, verb, args.clone(), checked).await?;
248
273
  // Only a caller that stated no role (unidentified, or an actor Governance holds no grant for)
249
274
  // is let through; a stated role that does not match stays refused, as it is under `Off`.
250
- if mode == Enforcement::Shadow && caller.role.is_none() && !outcome.accepted && refused_for_role(&outcome.result) {
251
- crate::log::error("would_refuse_role", serde_json::json!({
252
- "verb": verb, "role": checked.role, "actor_id": checked.actor_id,
253
- "refusals": outcome.result.get("refusals"),
254
- }));
255
- return run(client, wasm_path, verb, args, Caller::default(), config, invoker).await;
275
+ if mode == Enforcement::Shadow
276
+ && caller.role.is_none()
277
+ && !outcome.accepted
278
+ && refused_for_role(&outcome.result)
279
+ {
280
+ crate::log::error(
281
+ "would_refuse_role",
282
+ serde_json::json!({
283
+ "verb": verb, "role": checked.role, "actor_id": checked.actor_id,
284
+ "refusals": outcome.result.get("refusals"),
285
+ }),
286
+ );
287
+ return run(host, verb, args, Caller::default()).await;
256
288
  }
257
289
  Ok(outcome)
258
290
  }
259
291
 
260
- #[allow(clippy::too_many_arguments)]
261
292
  async fn run(
262
- client: &Mutex<Client>,
263
- wasm_path: &Path,
293
+ host: Host<'_>,
264
294
  verb: &str,
265
295
  args: serde_json::Value,
266
296
  caller: Caller<'_>,
267
- config: &LineageConfig,
268
- invoker: &dyn LambdaInvoker,
269
297
  ) -> anyhow::Result<Outcome> {
298
+ let Host {
299
+ client,
300
+ wasm_path,
301
+ config,
302
+ invoker,
303
+ ..
304
+ } = host;
270
305
  let role = caller.role;
271
306
  let mut guard = client.lock().await;
272
307
  let txn = guard.transaction().await?;
@@ -327,7 +362,9 @@ async fn run(
327
362
  let seed = if needs_saga_backfill {
328
363
  serde_json::json!({})
329
364
  } else {
330
- snapshot.as_ref().map(|s| s.seed.clone()).unwrap_or_else(|| serde_json::json!({}))
365
+ snapshot
366
+ .as_ref()
367
+ .map_or_else(|| serde_json::json!({}), |s| s.seed.clone())
331
368
  };
332
369
 
333
370
  // Read inside the same advisory-locked transaction: the lock covers
@@ -378,7 +415,10 @@ async fn run(
378
415
 
379
416
  // The kernel's "instances" output is already the exact seed
380
417
  // shape, so nothing here re-derives or filters it.
381
- let new_seed = result.get("instances").cloned().unwrap_or_else(|| serde_json::json!({}));
418
+ let new_seed = result
419
+ .get("instances")
420
+ .cloned()
421
+ .unwrap_or_else(|| serde_json::json!({}));
382
422
  journal::save_snapshot(&txn, ordinal, &new_seed).await?;
383
423
 
384
424
  // Persisted in the same transaction, so a saga checkpoint can
@@ -386,24 +426,42 @@ async fn run(
386
426
  // Reconciled against `saga_rows` (pre-run state): present now
387
427
  // is upserted, present before but absent now is deleted.
388
428
  if let Some(saga_snapshot) = result.get("saga_snapshot").and_then(|v| v.as_array()) {
389
- let mut still_present: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
429
+ let mut still_present: std::collections::HashSet<(String, String)> =
430
+ std::collections::HashSet::new();
390
431
  for entry in saga_snapshot {
391
432
  let process_manager = entry
392
433
  .get("process_manager")
393
434
  .and_then(|v| v.as_str())
394
- .ok_or_else(|| anyhow::anyhow!("saga_snapshot entry missing \"process_manager\": {entry}"))?;
435
+ .ok_or_else(|| {
436
+ anyhow::anyhow!("saga_snapshot entry missing \"process_manager\": {entry}")
437
+ })?;
395
438
  let correlation = entry
396
439
  .get("correlation")
397
440
  .and_then(|v| v.as_str())
398
- .ok_or_else(|| anyhow::anyhow!("saga_snapshot entry missing \"correlation\": {entry}"))?;
399
- let state = entry
400
- .get("state")
401
- .and_then(|v| v.as_str())
402
- .ok_or_else(|| anyhow::anyhow!("saga_snapshot entry missing \"state\": {entry}"))?;
403
- let memory = entry.get("memory").cloned().unwrap_or_else(|| serde_json::json!({}));
404
- let completed_compensations = entry.get("completed_compensations").cloned().unwrap_or_else(|| serde_json::json!([]));
405
-
406
- journal::save_saga(&txn, process_manager, correlation, state, &memory, &completed_compensations).await?;
441
+ .ok_or_else(|| {
442
+ anyhow::anyhow!("saga_snapshot entry missing \"correlation\": {entry}")
443
+ })?;
444
+ let state = entry.get("state").and_then(|v| v.as_str()).ok_or_else(|| {
445
+ anyhow::anyhow!("saga_snapshot entry missing \"state\": {entry}")
446
+ })?;
447
+ let memory = entry
448
+ .get("memory")
449
+ .cloned()
450
+ .unwrap_or_else(|| serde_json::json!({}));
451
+ let completed_compensations = entry
452
+ .get("completed_compensations")
453
+ .cloned()
454
+ .unwrap_or_else(|| serde_json::json!([]));
455
+
456
+ journal::save_saga(
457
+ &txn,
458
+ process_manager,
459
+ correlation,
460
+ state,
461
+ &memory,
462
+ &completed_compensations,
463
+ )
464
+ .await?;
407
465
  still_present.insert((process_manager.to_string(), correlation.to_string()));
408
466
  }
409
467
  for row in &saga_rows {
@@ -434,7 +492,9 @@ async fn run(
434
492
  let aggregate = mutation
435
493
  .get("aggregate")
436
494
  .and_then(|v| v.as_str())
437
- .ok_or_else(|| anyhow::anyhow!("mutation record missing \"aggregate\": {mutation}"))?;
495
+ .ok_or_else(|| {
496
+ anyhow::anyhow!("mutation record missing \"aggregate\": {mutation}")
497
+ })?;
438
498
  // `era.is_some()` means the lineage subsystem exists, not that
439
499
  // this aggregate has a mirror: `mint` only provisions head
440
500
  // snapshots for the capable set, so mirroring anything else
@@ -449,7 +509,9 @@ async fn run(
449
509
  let operation = mutation
450
510
  .get("operation")
451
511
  .and_then(|v| v.as_str())
452
- .ok_or_else(|| anyhow::anyhow!("mutation record missing \"operation\": {mutation}"))?;
512
+ .ok_or_else(|| {
513
+ anyhow::anyhow!("mutation record missing \"operation\": {mutation}")
514
+ })?;
453
515
  let state = mutation
454
516
  .get("state")
455
517
  .ok_or_else(|| anyhow::anyhow!("mutation record missing \"state\": {mutation}"))?;
@@ -457,7 +519,12 @@ async fn run(
457
519
  journal::append_lineage_mutation(
458
520
  &txn,
459
521
  config,
460
- &journal::Mutation { aggregate, id, operation, state },
522
+ &journal::Mutation {
523
+ aggregate,
524
+ id,
525
+ operation,
526
+ state,
527
+ },
461
528
  )
462
529
  .await?;
463
530
  }
@@ -495,7 +562,9 @@ async fn run(
495
562
  match lambda_client::deliver_with_retry(invoker, reaction).await {
496
563
  Ok(record) => {
497
564
  if let Some(response) = result.as_object_mut() {
498
- if let Some(reactions) = response.get_mut("reactions").and_then(|r| r.as_array_mut()) {
565
+ if let Some(reactions) =
566
+ response.get_mut("reactions").and_then(|r| r.as_array_mut())
567
+ {
499
568
  reactions.push(serde_json::json!({
500
569
  "policy": record.policy, "on": reaction.get("on").and_then(|v| v.as_str()).unwrap_or_default(),
501
570
  "trigger": record.target_verb, "delivered": record.delivered, "reason": record.reason,
@@ -506,10 +575,13 @@ async fn run(
506
575
  }
507
576
  Err(failure) => {
508
577
  let error_text = format!("{:#}", failure.error);
509
- crate::log::error("cross_domain_delivery_failed", serde_json::json!({
510
- "policy": failure.policy, "target_domain": failure.target_domain,
511
- "target_verb": failure.target_verb, "attempts": failure.attempts, "error": error_text,
512
- }));
578
+ crate::log::error(
579
+ "cross_domain_delivery_failed",
580
+ serde_json::json!({
581
+ "policy": failure.policy, "target_domain": failure.target_domain,
582
+ "target_verb": failure.target_verb, "attempts": failure.attempts, "error": error_text,
583
+ }),
584
+ );
513
585
  journal::record_dead_letter(
514
586
  &*guard,
515
587
  &failure.policy,
@@ -530,7 +602,10 @@ async fn run(
530
602
  return Err(error);
531
603
  }
532
604
  if let Some(response) = result.as_object_mut() {
533
- response.insert("cross_domain_deliveries".to_string(), serde_json::Value::Array(cross_domain_deliveries));
605
+ response.insert(
606
+ "cross_domain_deliveries".to_string(),
607
+ serde_json::Value::Array(cross_domain_deliveries),
608
+ );
534
609
  }
535
610
 
536
611
  log_command(verb, role, accepted, &result);
@@ -548,11 +623,21 @@ fn log_command(verb: &str, role: Option<&str>, accepted: bool, result: &serde_js
548
623
  }
549
624
  }
550
625
 
551
- fn command_fields(verb: &str, role: Option<&str>, accepted: bool, result: &serde_json::Value) -> serde_json::Value {
626
+ fn command_fields(
627
+ verb: &str,
628
+ role: Option<&str>,
629
+ accepted: bool,
630
+ result: &serde_json::Value,
631
+ ) -> serde_json::Value {
552
632
  let events: Vec<serde_json::Value> = result
553
633
  .get("events")
554
634
  .and_then(|v| v.as_array())
555
- .map(|items| items.iter().filter_map(|item| item.get("name").cloned()).collect())
635
+ .map(|items| {
636
+ items
637
+ .iter()
638
+ .filter_map(|item| item.get("name").cloned())
639
+ .collect()
640
+ })
556
641
  .unwrap_or_default();
557
642
  serde_json::json!({
558
643
  "verb": verb, "role": role, "accepted": accepted, "events": events,
@@ -586,7 +671,8 @@ pub async fn read(client: &Mutex<Client>, wasm_path: &Path) -> anyhow::Result<se
586
671
  let input = serde_json::json!({ "seed": &s.seed, "steps": steps_since }).to_string();
587
672
  let owned_wasm_path = wasm_path.to_path_buf();
588
673
  let output =
589
- tokio::task::spawn_blocking(move || wasm_runner::run(&owned_wasm_path, &input)).await??;
674
+ tokio::task::spawn_blocking(move || wasm_runner::run(&owned_wasm_path, &input))
675
+ .await??;
590
676
  return parse_kernel_output(&output);
591
677
  }
592
678
 
@@ -617,13 +703,41 @@ pub async fn query(
617
703
  crate::needs::fill_query_needs(domain_ir, question, &mut args, &crate::needs::ProcessClock);
618
704
  }
619
705
  let state = read(client, wasm_path).await?;
620
- let seed = state.get("instances").cloned().unwrap_or_else(|| serde_json::json!({}));
621
- let input = serde_json::json!({ "seed": seed, "steps": [{ "query": question, "args": args }] }).to_string();
706
+ let seed = state
707
+ .get("instances")
708
+ .cloned()
709
+ .unwrap_or_else(|| serde_json::json!({}));
710
+ let input = serde_json::json!({ "seed": seed, "steps": [{ "query": question, "args": args }] })
711
+ .to_string();
622
712
  let owned_wasm_path = wasm_path.to_path_buf();
623
- let output = tokio::task::spawn_blocking(move || wasm_runner::run(&owned_wasm_path, &input)).await??;
713
+ let output =
714
+ tokio::task::spawn_blocking(move || wasm_runner::run(&owned_wasm_path, &input)).await??;
624
715
  parse_kernel_output(&output)
625
716
  }
626
717
 
718
+ /// Answers `answer` (a kernel answer already computed and persisted) in the bare shape a caller
719
+ /// reads, by one more wasm run of the kernel's `{"present": answer}` input: each single-field
720
+ /// value object as its sole field. Nothing is stored or replayed.
721
+ pub async fn present(
722
+ wasm_path: &Path,
723
+ answer: serde_json::Value,
724
+ ) -> anyhow::Result<serde_json::Value> {
725
+ let owned_wasm_path = wasm_path.to_path_buf();
726
+ tokio::task::spawn_blocking(move || {
727
+ present_with(&answer, |input| wasm_runner::run(&owned_wasm_path, input))
728
+ })
729
+ .await?
730
+ }
731
+
732
+ /// Presents `answer` as `present` does, with `run` standing in for the kernel (text in, text out).
733
+ pub(crate) fn present_with(
734
+ answer: &serde_json::Value,
735
+ run: impl FnOnce(&str) -> anyhow::Result<String>,
736
+ ) -> anyhow::Result<serde_json::Value> {
737
+ let input = serde_json::json!({ "present": answer }).to_string();
738
+ parse_kernel_output(&run(&input)?)
739
+ }
740
+
627
741
  #[cfg(test)]
628
742
  pub(crate) mod tests {
629
743
 
@@ -642,6 +756,8 @@ pub(crate) mod tests {
642
756
  assert!(!fields.to_string().contains("a@b.c"));
643
757
  }
644
758
  use super::*;
759
+ use crate::journal::LineageConfig;
760
+ use crate::lambda_client::LambdaInvoker;
645
761
  use tokio_postgres::NoTls;
646
762
 
647
763
  // A throwaway Postgres database per test, uniquely named so
@@ -663,10 +779,9 @@ pub(crate) mod tests {
663
779
  .await
664
780
  .unwrap();
665
781
 
666
- let (client, conn) =
667
- tokio_postgres::connect(&crate::test_pg::conninfo(&name), NoTls)
668
- .await
669
- .expect("connect to scratch db");
782
+ let (client, conn) = tokio_postgres::connect(&crate::test_pg::conninfo(name), NoTls)
783
+ .await
784
+ .expect("connect to scratch db");
670
785
  tokio::spawn(async move {
671
786
  let _ = conn.await;
672
787
  });
@@ -677,11 +792,18 @@ pub(crate) mod tests {
677
792
  // Not Ruby's real provisioning — just enough structure for
678
793
  // `journal::current_era` and `append_lineage_mutation` to succeed,
679
794
  // without reproducing Ruby's full DDL.
680
- pub(crate) async fn provision_lineage(client: &Client, domain: &str, era: i32, aggregate_storage_names: &[&str]) {
795
+ pub(crate) async fn provision_lineage(
796
+ client: &Client,
797
+ domain: &str,
798
+ era: i32,
799
+ aggregate_storage_names: &[&str],
800
+ ) {
681
801
  // `int`, matching Ruby's real DDL: tokio_postgres requires the
682
802
  // Rust and Postgres types to match, not just be compatible.
683
803
  client
684
- .batch_execute("CREATE TABLE IF NOT EXISTS hecks_eras (domain text, ordinal int, held_text text)")
804
+ .batch_execute(
805
+ "CREATE TABLE IF NOT EXISTS hecks_eras (domain text, ordinal int, held_text text)",
806
+ )
685
807
  .await
686
808
  .unwrap();
687
809
  client
@@ -710,7 +832,10 @@ pub(crate) mod tests {
710
832
  for name in aggregate_storage_names {
711
833
  // Domain-qualified (ADR 0059), matching what a real write
712
834
  // targets.
713
- let snapshot_table = journal::qualified_name(domain, &format!("{}_head_snapshot_{era}", journal::snake(name)));
835
+ let snapshot_table = journal::qualified_name(
836
+ domain,
837
+ &format!("{}_head_snapshot_{era}", journal::snake(name)),
838
+ );
714
839
  client
715
840
  .batch_execute(&format!(
716
841
  "CREATE TABLE IF NOT EXISTS \"{snapshot_table}\" (id text PRIMARY KEY, ordinal bigint NOT NULL, \
@@ -748,15 +873,34 @@ pub(crate) mod tests {
748
873
 
749
874
  // Mirroring everything is the outage: no head snapshot exists to
750
875
  // upsert into.
751
- let everything = LineageConfig { domain: "Banking".to_string(), era: Some(1), mirrored: None };
752
- let refused = handle_facts(&client, &wasm, "Banking::Customer.Register", open.clone(), None, &everything, &invoker).await;
876
+ let everything = LineageConfig {
877
+ domain: "Banking".to_string(),
878
+ era: Some(1),
879
+ mirrored: None,
880
+ };
881
+ let refused = handle_facts(
882
+ Host::new(&client, &wasm, &everything, &invoker),
883
+ "Banking::Customer.Register",
884
+ open.clone(),
885
+ None,
886
+ )
887
+ .await;
753
888
  let message = match refused {
754
889
  Ok(_) => panic!("expected the missing-relation failure"),
755
890
  Err(e) => format!("{e:#}"),
756
891
  };
757
- assert!(message.contains("banking_customer_head_snapshot_1"), "names the relation it could not write: {message}");
758
- assert!(message.contains("does not exist"), "carries the database's own words: {message}");
759
- assert!(message.contains("Banking::Customer"), "names the record: {message}");
892
+ assert!(
893
+ message.contains("banking_customer_head_snapshot_1"),
894
+ "names the relation it could not write: {message}"
895
+ );
896
+ assert!(
897
+ message.contains("does not exist"),
898
+ "carries the database's own words: {message}"
899
+ );
900
+ assert!(
901
+ message.contains("Banking::Customer"),
902
+ "names the record: {message}"
903
+ );
760
904
 
761
905
  // Mirroring what the IR actually declares — nothing — writes
762
906
  // cleanly, into this crate's own journal.
@@ -765,17 +909,38 @@ pub(crate) mod tests {
765
909
  era: Some(1),
766
910
  mirrored: Some(std::collections::BTreeSet::new()),
767
911
  };
768
- let outcome = handle_facts(&client, &wasm, "Banking::Customer.Register", open, None, &declared, &invoker)
769
- .await
770
- .expect("writes");
912
+ let outcome = handle_facts(
913
+ Host::new(&client, &wasm, &declared, &invoker),
914
+ "Banking::Customer.Register",
915
+ open,
916
+ None,
917
+ )
918
+ .await
919
+ .expect("writes");
771
920
  assert!(outcome.accepted, "{}", outcome.result);
772
921
 
773
922
  let guard = client.lock().await;
774
- let journalled: i64 = guard.query_one("SELECT count(*) FROM hecks_lambda_journal", &[]).await.unwrap().get(0);
775
- assert_eq!(journalled, 1, "durable in this crate's own journal, which is what `read` replays");
776
- let mirror: Option<String> =
777
- guard.query_one("SELECT to_regclass('banking_customer_head_snapshot_1')::text", &[]).await.unwrap().get(0);
778
- assert!(mirror.is_none(), "and no mirror was invented for an aggregate the IR never called capable");
923
+ let journalled: i64 = guard
924
+ .query_one("SELECT count(*) FROM hecks_lambda_journal", &[])
925
+ .await
926
+ .unwrap()
927
+ .get(0);
928
+ assert_eq!(
929
+ journalled, 1,
930
+ "durable in this crate's own journal, which is what `read` replays"
931
+ );
932
+ let mirror: Option<String> = guard
933
+ .query_one(
934
+ "SELECT to_regclass('banking_customer_head_snapshot_1')::text",
935
+ &[],
936
+ )
937
+ .await
938
+ .unwrap()
939
+ .get(0);
940
+ assert!(
941
+ mirror.is_none(),
942
+ "and no mirror was invented for an aggregate the IR never called capable"
943
+ );
779
944
  }
780
945
 
781
946
  // The other half: an aggregate the IR does call capable is still
@@ -787,29 +952,62 @@ pub(crate) mod tests {
787
952
  let config = LineageConfig {
788
953
  domain: "Banking".to_string(),
789
954
  era: Some(1),
790
- mirrored: Some(["Banking::Customer".to_string()].into_iter().collect()),
955
+ mirrored: Some(std::iter::once("Banking::Customer".to_string()).collect()),
791
956
  };
792
957
 
793
958
  handle_facts(
794
- &client,
795
- &wasm_path(),
959
+ Host::new(
960
+ &client,
961
+ &wasm_path(),
962
+ &config,
963
+ &crate::lambda_client::NeverInvoker,
964
+ ),
796
965
  "Banking::Customer.Register",
797
966
  register("CUST-2"),
798
967
  None,
799
- &config,
800
- &crate::lambda_client::NeverInvoker,
801
968
  )
802
969
  .await
803
970
  .expect("writes");
804
971
 
805
972
  let guard = client.lock().await;
806
- let mirrored: i64 =
807
- guard.query_one("SELECT count(*) FROM banking_customer_head_snapshot_1", &[]).await.unwrap().get(0);
973
+ let mirrored: i64 = guard
974
+ .query_one("SELECT count(*) FROM banking_customer_head_snapshot_1", &[])
975
+ .await
976
+ .unwrap()
977
+ .get(0);
808
978
  assert_eq!(mirrored, 1);
809
979
  }
810
980
 
811
981
  fn test_config(domain: &str, era: i32) -> LineageConfig {
812
- LineageConfig { domain: domain.to_string(), era: Some(era), mirrored: None }
982
+ LineageConfig {
983
+ domain: domain.to_string(),
984
+ era: Some(era),
985
+ mirrored: None,
986
+ }
987
+ }
988
+
989
+ #[test]
990
+ fn present_hands_the_kernel_the_answer_under_present_and_answers_what_it_returns() {
991
+ let answer = serde_json::json!({"instances": {"Shop::Item#a": {"name": {"value": "x"}}}});
992
+ let mut sent = String::new();
993
+ let presented = present_with(&answer, |input| {
994
+ sent = input.to_string();
995
+ Ok(r#"{"instances":{"Shop::Item#a":{"name":"x"}}}"#.to_string())
996
+ })
997
+ .unwrap();
998
+ assert_eq!(
999
+ serde_json::from_str::<serde_json::Value>(&sent).unwrap(),
1000
+ serde_json::json!({ "present": answer })
1001
+ );
1002
+ assert_eq!(presented["instances"]["Shop::Item#a"]["name"], "x");
1003
+ }
1004
+
1005
+ #[test]
1006
+ fn present_is_an_error_when_the_kernel_fails() {
1007
+ let failed = present_with(&serde_json::json!({}), |_| {
1008
+ Ok(r#"{"error":"expected a top-level {\"steps\": [...]} object"}"#.to_string())
1009
+ });
1010
+ assert!(failed.is_err());
813
1011
  }
814
1012
 
815
1013
  pub(crate) fn wasm_path() -> std::path::PathBuf {
@@ -832,28 +1030,40 @@ pub(crate) mod tests {
832
1030
  provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
833
1031
  let config = test_config("Banking", 1);
834
1032
 
835
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0100"), None, &config, &lambda_client::NeverInvoker)
836
- .await
837
- .unwrap()
838
- .accepted
839
- .then_some(())
840
- .expect("registering a customer should succeed");
1033
+ handle(
1034
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1035
+ "Banking::Customer.Register",
1036
+ register("CUST-0100"),
1037
+ None,
1038
+ )
1039
+ .await
1040
+ .unwrap()
1041
+ .accepted
1042
+ .then_some(())
1043
+ .expect("registering a customer should succeed");
841
1044
 
842
1045
  // `Customer.Suspended` is `where status == "suspended"` — a
843
1046
  // freshly registered customer is active, so the query has to
844
1047
  // answer with a real, empty row set, not everything.
845
- let before = query(&client, &wasm_path(), "Banking::Customer.Suspended", serde_json::json!({})).await.unwrap();
1048
+ let before = query(
1049
+ &client,
1050
+ &wasm_path(),
1051
+ "Banking::Customer.Suspended",
1052
+ serde_json::json!({}),
1053
+ )
1054
+ .await
1055
+ .unwrap();
846
1056
  assert_eq!(before["queries"][0]["query"], "Banking::Customer.Suspended");
847
- assert_eq!(before["queries"][0]["rows"].as_array().expect("rows").len(), 0);
1057
+ assert_eq!(
1058
+ before["queries"][0]["rows"].as_array().expect("rows").len(),
1059
+ 0
1060
+ );
848
1061
 
849
1062
  handle(
850
- &client,
851
- &wasm_path(),
1063
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
852
1064
  "Banking::Customer.Suspend",
853
1065
  serde_json::json!({ "reference": "CUST-0100", "standing": { "value": "watch" } }),
854
1066
  None,
855
- &config,
856
- &lambda_client::NeverInvoker,
857
1067
  )
858
1068
  .await
859
1069
  .unwrap()
@@ -861,9 +1071,20 @@ pub(crate) mod tests {
861
1071
  .then_some(())
862
1072
  .expect("suspending a customer should succeed");
863
1073
 
864
- let after = query(&client, &wasm_path(), "Banking::Customer.Suspended", serde_json::json!({})).await.unwrap();
1074
+ let after = query(
1075
+ &client,
1076
+ &wasm_path(),
1077
+ "Banking::Customer.Suspended",
1078
+ serde_json::json!({}),
1079
+ )
1080
+ .await
1081
+ .unwrap();
865
1082
  let rows = after["queries"][0]["rows"].as_array().expect("rows");
866
- assert_eq!(rows.len(), 1, "the suspended customer should be the one row: {after}");
1083
+ assert_eq!(
1084
+ rows.len(),
1085
+ 1,
1086
+ "the suspended customer should be the one row: {after}"
1087
+ );
867
1088
  assert_eq!(rows[0]["id"], "CUST-0100");
868
1089
  }
869
1090
 
@@ -873,13 +1094,15 @@ pub(crate) mod tests {
873
1094
  provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
874
1095
 
875
1096
  let outcome = handle(
876
- &client,
877
- &wasm_path(),
1097
+ Host::new(
1098
+ &client,
1099
+ &wasm_path(),
1100
+ &test_config("Banking", 1),
1101
+ &lambda_client::NeverInvoker,
1102
+ ),
878
1103
  "Banking::Customer.Register",
879
1104
  register("CUST-0001"),
880
1105
  None,
881
- &test_config("Banking", 1),
882
- &lambda_client::NeverInvoker,
883
1106
  )
884
1107
  .await
885
1108
  .unwrap();
@@ -893,7 +1116,10 @@ pub(crate) mod tests {
893
1116
  // The lineage write itself: a row landed in the era-tagged,
894
1117
  // storage-name-keyed journal shape, not just the flat log above.
895
1118
  let journal_rows = guard
896
- .query("SELECT era, aggregate, aggregate_id, operation FROM hecks_journal_banking", &[])
1119
+ .query(
1120
+ "SELECT era, aggregate, aggregate_id, operation FROM hecks_journal_banking",
1121
+ &[],
1122
+ )
897
1123
  .await
898
1124
  .unwrap();
899
1125
  assert_eq!(journal_rows.len(), 1);
@@ -902,14 +1128,26 @@ pub(crate) mod tests {
902
1128
  let aggregate: String = row.get(1);
903
1129
  let aggregate_id: String = row.get(2);
904
1130
  let operation: String = row.get(3);
905
- assert_eq!((era, aggregate.as_str(), aggregate_id.as_str(), operation.as_str()), (1, "customer", "CUST-0001", "save"));
1131
+ assert_eq!(
1132
+ (
1133
+ era,
1134
+ aggregate.as_str(),
1135
+ aggregate_id.as_str(),
1136
+ operation.as_str()
1137
+ ),
1138
+ (1, "customer", "CUST-0001", "save")
1139
+ );
906
1140
 
907
1141
  // domain-qualified (docs/decisions/0059) — snake("Banking") == "banking".
908
1142
  let snapshot_rows = guard
909
1143
  .query("SELECT id FROM banking_customer_head_snapshot_1", &[])
910
1144
  .await
911
1145
  .unwrap();
912
- assert_eq!(snapshot_rows.len(), 1, "the head-snapshot table should carry exactly the one live record");
1146
+ assert_eq!(
1147
+ snapshot_rows.len(),
1148
+ 1,
1149
+ "the head-snapshot table should carry exactly the one live record"
1150
+ );
913
1151
  let id: String = snapshot_rows[0].get(0);
914
1152
  assert_eq!(id, "CUST-0001");
915
1153
  }
@@ -924,13 +1162,15 @@ pub(crate) mod tests {
924
1162
 
925
1163
  let before = crate::auth::httpdate_now();
926
1164
  let outcome = handle(
927
- &client,
928
- &wasm_path(),
1165
+ Host::new(
1166
+ &client,
1167
+ &wasm_path(),
1168
+ &test_config("Banking", 1),
1169
+ &lambda_client::NeverInvoker,
1170
+ ),
929
1171
  "Banking::Customer.Register",
930
1172
  register("CUST-OCCURRED-AT"),
931
1173
  None,
932
- &test_config("Banking", 1),
933
- &lambda_client::NeverInvoker,
934
1174
  )
935
1175
  .await
936
1176
  .unwrap();
@@ -939,11 +1179,16 @@ pub(crate) mod tests {
939
1179
  assert!(outcome.accepted, "{:?}", outcome.result["refusals"]);
940
1180
  let events = outcome.result["events"].as_array().unwrap();
941
1181
  assert_eq!(events.len(), 1);
942
- let occurred_at = events[0]["occurred_at"].as_str().expect("occurred_at should be a real string, not null or absent");
1182
+ let occurred_at = events[0]["occurred_at"]
1183
+ .as_str()
1184
+ .expect("occurred_at should be a real string, not null or absent");
943
1185
  // Lexical comparison is valid: this format's digit-then-letter
944
1186
  // layout sorts identically to chronological order for stamps
945
1187
  // less than 10,000 years apart.
946
- assert!(occurred_at >= before.as_str() && occurred_at <= after.as_str(), "{occurred_at} should fall between {before} and {after}");
1188
+ assert!(
1189
+ occurred_at >= before.as_str() && occurred_at <= after.as_str(),
1190
+ "{occurred_at} should fall between {before} and {after}"
1191
+ );
947
1192
  }
948
1193
 
949
1194
  #[tokio::test]
@@ -953,13 +1198,10 @@ pub(crate) mod tests {
953
1198
  let config = test_config("Banking", 1);
954
1199
 
955
1200
  let first = handle(
956
- &client,
957
- &wasm_path(),
1201
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
958
1202
  "Banking::Customer.Register",
959
1203
  register("CUST-0001"),
960
1204
  None,
961
- &config,
962
- &lambda_client::NeverInvoker,
963
1205
  )
964
1206
  .await
965
1207
  .unwrap();
@@ -973,13 +1215,10 @@ pub(crate) mod tests {
973
1215
  // succeeded independently: the second registration only refuses
974
1216
  // if the first's effect was genuinely rebuilt from Postgres.
975
1217
  let second = handle(
976
- &client,
977
- &wasm_path(),
1218
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
978
1219
  "Banking::Customer.Register",
979
1220
  register("CUST-0001"),
980
1221
  None,
981
- &config,
982
- &lambda_client::NeverInvoker,
983
1222
  )
984
1223
  .await
985
1224
  .unwrap();
@@ -1007,17 +1246,28 @@ pub(crate) mod tests {
1007
1246
  let config = test_config("Banking", 1);
1008
1247
 
1009
1248
  for reference in ["CUST-0004", "CUST-0005", "CUST-0006"] {
1010
- let outcome = handle(&client, &wasm_path(), "Banking::Customer.Register", register(reference), None, &config, &lambda_client::NeverInvoker)
1011
- .await
1012
- .unwrap();
1013
- assert!(outcome.accepted, "registering {reference} should succeed: {:?}", outcome.result);
1249
+ let outcome = handle(
1250
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1251
+ "Banking::Customer.Register",
1252
+ register(reference),
1253
+ None,
1254
+ )
1255
+ .await
1256
+ .unwrap();
1257
+ assert!(
1258
+ outcome.accepted,
1259
+ "registering {reference} should succeed: {:?}",
1260
+ outcome.result
1261
+ );
1014
1262
 
1015
1263
  let guard = client.lock().await;
1016
1264
  let snapshot = journal::load_snapshot(&*guard)
1017
1265
  .await
1018
1266
  .unwrap()
1019
1267
  .expect("a snapshot should exist after the first accepted command");
1020
- let tail = journal::load_steps_after(&*guard, snapshot.ordinal).await.unwrap();
1268
+ let tail = journal::load_steps_after(&*guard, snapshot.ordinal)
1269
+ .await
1270
+ .unwrap();
1021
1271
  assert!(tail.is_empty(), "the snapshot should already reflect every accepted command, leaving nothing to replay");
1022
1272
 
1023
1273
  // And the seed is right, not just non-empty: it should carry
@@ -1029,7 +1279,10 @@ pub(crate) mod tests {
1029
1279
  );
1030
1280
  }
1031
1281
 
1032
- let final_snapshot = journal::load_snapshot(&*client.lock().await).await.unwrap().unwrap();
1282
+ let final_snapshot = journal::load_snapshot(&*client.lock().await)
1283
+ .await
1284
+ .unwrap()
1285
+ .unwrap();
1033
1286
  let final_seed = final_snapshot.seed.as_object().unwrap();
1034
1287
  assert_eq!(final_seed.len(), 3, "the snapshot should accumulate all three registrations, not just the latest: {final_seed:?}");
1035
1288
  }
@@ -1037,8 +1290,17 @@ pub(crate) mod tests {
1037
1290
  #[test]
1038
1291
  fn a_top_level_error_is_a_kernel_failure_and_nothing_else_is() {
1039
1292
  let failed = serde_json::json!({"error": "invalid seed: Registration.status: missing from JSON args"});
1040
- assert_eq!(classify(&failed), KernelAnswer::Failed("invalid seed: Registration.status: missing from JSON args".to_string()));
1041
- assert_eq!(classify(&serde_json::json!({"error": {"code": 7}})), KernelAnswer::Failed("{\"code\":7}".to_string()), "a non-text error is still a failure");
1293
+ assert_eq!(
1294
+ classify(&failed),
1295
+ KernelAnswer::Failed(
1296
+ "invalid seed: Registration.status: missing from JSON args".to_string()
1297
+ )
1298
+ );
1299
+ assert_eq!(
1300
+ classify(&serde_json::json!({"error": {"code": 7}})),
1301
+ KernelAnswer::Failed("{\"code\":7}".to_string()),
1302
+ "a non-text error is still a failure"
1303
+ );
1042
1304
 
1043
1305
  let accepted = serde_json::json!({"instances": {}, "events": [], "refusals": []});
1044
1306
  assert_eq!(classify(&accepted), KernelAnswer::Accepted);
@@ -1058,9 +1320,18 @@ pub(crate) mod tests {
1058
1320
 
1059
1321
  #[test]
1060
1322
  fn parse_kernel_output_turns_a_failure_into_an_error_and_keeps_refusals_as_results() {
1061
- let error = parse_kernel_output(r#"{"error":"invalid seed: Customer.status: missing from JSON args"}"#).unwrap_err();
1062
- assert!(format!("{error:#}").contains("invalid seed: Customer.status: missing from JSON args"), "{error:#}");
1063
- let refused = parse_kernel_output(r#"{"instances":{},"refusals":[{"verb":"v","error":"no","kind":"K"}]}"#).unwrap();
1323
+ let error = parse_kernel_output(
1324
+ r#"{"error":"invalid seed: Customer.status: missing from JSON args"}"#,
1325
+ )
1326
+ .unwrap_err();
1327
+ assert!(
1328
+ format!("{error:#}").contains("invalid seed: Customer.status: missing from JSON args"),
1329
+ "{error:#}"
1330
+ );
1331
+ let refused = parse_kernel_output(
1332
+ r#"{"instances":{},"refusals":[{"verb":"v","error":"no","kind":"K"}]}"#,
1333
+ )
1334
+ .unwrap();
1064
1335
  assert_eq!(classify(&refused), KernelAnswer::Refused);
1065
1336
  assert!(parse_kernel_output("not json").is_err());
1066
1337
  }
@@ -1075,55 +1346,153 @@ pub(crate) mod tests {
1075
1346
  let config = test_config("Banking", 1);
1076
1347
  let noinvoke = lambda_client::NeverInvoker;
1077
1348
 
1078
- let first = handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0001"), None, &config, &noinvoke).await.unwrap();
1349
+ let first = handle(
1350
+ Host::new(&client, &wasm_path(), &config, &noinvoke),
1351
+ "Banking::Customer.Register",
1352
+ register("CUST-0001"),
1353
+ None,
1354
+ )
1355
+ .await
1356
+ .unwrap();
1079
1357
  assert!(first.accepted, "{:?}", first.result);
1080
1358
 
1081
- let whole_world = journal::load_snapshot(&*client.lock().await).await.unwrap().unwrap().seed;
1359
+ let whole_world = journal::load_snapshot(&*client.lock().await)
1360
+ .await
1361
+ .unwrap()
1362
+ .unwrap()
1363
+ .seed;
1082
1364
  let mut damaged = whole_world.clone();
1083
- let customer = damaged.as_object_mut().unwrap().values_mut().next().unwrap().as_object_mut().unwrap();
1084
- assert!(customer.remove("status").is_some(), "the fixture Customer carries a status");
1085
- client.lock().await.execute("UPDATE hecks_lambda_snapshot SET seed = $1", &[&damaged]).await.unwrap();
1365
+ let customer = damaged
1366
+ .as_object_mut()
1367
+ .unwrap()
1368
+ .values_mut()
1369
+ .next()
1370
+ .unwrap()
1371
+ .as_object_mut()
1372
+ .unwrap();
1373
+ assert!(
1374
+ customer.remove("status").is_some(),
1375
+ "the fixture Customer carries a status"
1376
+ );
1377
+ client
1378
+ .lock()
1379
+ .await
1380
+ .execute("UPDATE hecks_lambda_snapshot SET seed = $1", &[&damaged])
1381
+ .await
1382
+ .unwrap();
1086
1383
 
1087
1384
  let before = snapshot_and_journal(&client).await;
1088
- let failure = handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0002"), None, &config, &noinvoke).await;
1089
- let message = format!("{:#}", failure.err().expect("a kernel failure must be an error, not an accepted outcome"));
1090
- assert!(message.contains("invalid seed") && message.contains("status"), "{message}");
1385
+ let failure = handle(
1386
+ Host::new(&client, &wasm_path(), &config, &noinvoke),
1387
+ "Banking::Customer.Register",
1388
+ register("CUST-0002"),
1389
+ None,
1390
+ )
1391
+ .await;
1392
+ let message = format!(
1393
+ "{:#}",
1394
+ failure
1395
+ .err()
1396
+ .expect("a kernel failure must be an error, not an accepted outcome")
1397
+ );
1398
+ assert!(
1399
+ message.contains("invalid seed") && message.contains("status"),
1400
+ "{message}"
1401
+ );
1091
1402
 
1092
- assert_eq!(snapshot_and_journal(&client).await, before, "the failed command left the journal and the snapshot byte-identical");
1403
+ assert_eq!(
1404
+ snapshot_and_journal(&client).await,
1405
+ before,
1406
+ "the failed command left the journal and the snapshot byte-identical"
1407
+ );
1093
1408
  assert_eq!(before.2, 1, "and only the first command was ever journaled");
1094
- let lineage_rows: i64 = client.lock().await.query_one("SELECT count(*) FROM hecks_journal_banking", &[]).await.unwrap().get(0);
1095
- assert_eq!(lineage_rows, 1, "nothing was mirrored into the era journal either");
1409
+ let lineage_rows: i64 = client
1410
+ .lock()
1411
+ .await
1412
+ .query_one("SELECT count(*) FROM hecks_journal_banking", &[])
1413
+ .await
1414
+ .unwrap()
1415
+ .get(0);
1416
+ assert_eq!(
1417
+ lineage_rows, 1,
1418
+ "nothing was mirrored into the era journal either"
1419
+ );
1096
1420
 
1097
1421
  // A read that must run the kernel over the damaged snapshot fails
1098
1422
  // the same way, instead of answering an empty world.
1099
1423
  client
1100
1424
  .lock()
1101
1425
  .await
1102
- .execute("INSERT INTO hecks_lambda_journal (verb, args) VALUES ($1, $2)", &[&"Banking::Customer.Register", &register("CUST-0009")])
1426
+ .execute(
1427
+ "INSERT INTO hecks_lambda_journal (verb, args) VALUES ($1, $2)",
1428
+ &[&"Banking::Customer.Register", &register("CUST-0009")],
1429
+ )
1103
1430
  .await
1104
1431
  .unwrap();
1105
- let read_failure = read(&client, &wasm_path()).await.err().expect("a kernel failure is not an empty read");
1106
- assert!(format!("{read_failure:#}").contains("invalid seed"), "{read_failure:#}");
1107
- let query_failure = query(&client, &wasm_path(), "Banking::Customer.Suspended", serde_json::json!({})).await.err();
1432
+ let read_failure = read(&client, &wasm_path())
1433
+ .await
1434
+ .expect_err("a kernel failure is not an empty read");
1435
+ assert!(
1436
+ format!("{read_failure:#}").contains("invalid seed"),
1437
+ "{read_failure:#}"
1438
+ );
1439
+ let query_failure = query(
1440
+ &client,
1441
+ &wasm_path(),
1442
+ "Banking::Customer.Suspended",
1443
+ serde_json::json!({}),
1444
+ )
1445
+ .await
1446
+ .err();
1108
1447
  assert!(query_failure.is_some_and(|e| format!("{e:#}").contains("invalid seed")));
1109
1448
  client.lock().await.execute("DELETE FROM hecks_lambda_journal WHERE verb = 'Banking::Customer.Register' AND ordinal > 1", &[]).await.unwrap();
1110
1449
 
1111
1450
  // Once the snapshot is whole again the next command works, and the
1112
1451
  // snapshot afterwards holds the whole world, not just its own instance.
1113
- client.lock().await.execute("UPDATE hecks_lambda_snapshot SET seed = $1", &[&whole_world]).await.unwrap();
1114
- let second = handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0002"), None, &config, &noinvoke).await.unwrap();
1452
+ client
1453
+ .lock()
1454
+ .await
1455
+ .execute(
1456
+ "UPDATE hecks_lambda_snapshot SET seed = $1",
1457
+ &[&whole_world],
1458
+ )
1459
+ .await
1460
+ .unwrap();
1461
+ let second = handle(
1462
+ Host::new(&client, &wasm_path(), &config, &noinvoke),
1463
+ "Banking::Customer.Register",
1464
+ register("CUST-0002"),
1465
+ None,
1466
+ )
1467
+ .await
1468
+ .unwrap();
1115
1469
  assert!(second.accepted, "{:?}", second.result);
1116
- let after = journal::load_snapshot(&*client.lock().await).await.unwrap().unwrap().seed;
1470
+ let after = journal::load_snapshot(&*client.lock().await)
1471
+ .await
1472
+ .unwrap()
1473
+ .unwrap()
1474
+ .seed;
1117
1475
  let keys: Vec<&String> = after.as_object().unwrap().keys().collect();
1118
1476
  assert_eq!(keys.len(), 2, "the snapshot holds both customers: {keys:?}");
1119
- assert!(keys.iter().any(|k| k.contains("CUST-0001")) && keys.iter().any(|k| k.contains("CUST-0002")), "{keys:?}");
1477
+ assert!(
1478
+ keys.iter().any(|k| k.contains("CUST-0001"))
1479
+ && keys.iter().any(|k| k.contains("CUST-0002")),
1480
+ "{keys:?}"
1481
+ );
1120
1482
  }
1121
1483
 
1122
1484
  // The snapshot's ordinal and exact seed text, and how many commands are journaled.
1123
1485
  async fn snapshot_and_journal(client: &Mutex<Client>) -> (i64, String, i64) {
1124
1486
  let guard = client.lock().await;
1125
- let snapshot = guard.query_one("SELECT ordinal, seed::text FROM hecks_lambda_snapshot", &[]).await.unwrap();
1126
- let journaled: i64 = guard.query_one("SELECT count(*) FROM hecks_lambda_journal", &[]).await.unwrap().get(0);
1487
+ let snapshot = guard
1488
+ .query_one("SELECT ordinal, seed::text FROM hecks_lambda_snapshot", &[])
1489
+ .await
1490
+ .unwrap();
1491
+ let journaled: i64 = guard
1492
+ .query_one("SELECT count(*) FROM hecks_lambda_journal", &[])
1493
+ .await
1494
+ .unwrap()
1495
+ .get(0);
1127
1496
  (snapshot.get(0), snapshot.get(1), journaled)
1128
1497
  }
1129
1498
 
@@ -1141,8 +1510,18 @@ pub(crate) mod tests {
1141
1510
  let wasm_path = wasm_path();
1142
1511
 
1143
1512
  let (first, second) = tokio::join!(
1144
- handle(&client, &wasm_path, "Banking::Customer.Register", register("CUST-0002"), None, &config, &lambda_client::NeverInvoker),
1145
- handle(&client, &wasm_path, "Banking::Customer.Register", register("CUST-0002"), None, &config, &lambda_client::NeverInvoker),
1513
+ handle(
1514
+ Host::new(&client, &wasm_path, &config, &lambda_client::NeverInvoker),
1515
+ "Banking::Customer.Register",
1516
+ register("CUST-0002"),
1517
+ None
1518
+ ),
1519
+ handle(
1520
+ Host::new(&client, &wasm_path, &config, &lambda_client::NeverInvoker),
1521
+ "Banking::Customer.Register",
1522
+ register("CUST-0002"),
1523
+ None
1524
+ ),
1146
1525
  );
1147
1526
  let (first, second) = (first.unwrap(), second.unwrap());
1148
1527
 
@@ -1152,7 +1531,11 @@ pub(crate) mod tests {
1152
1531
  first.result, second.result
1153
1532
  );
1154
1533
  let steps = journal::load_steps(&*client.lock().await).await.unwrap();
1155
- assert_eq!(steps.len(), 1, "only the accepted registration should be persisted");
1534
+ assert_eq!(
1535
+ steps.len(),
1536
+ 1,
1537
+ "only the accepted registration should be persisted"
1538
+ );
1156
1539
  }
1157
1540
 
1158
1541
  #[tokio::test]
@@ -1161,13 +1544,15 @@ pub(crate) mod tests {
1161
1544
  provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
1162
1545
 
1163
1546
  handle(
1164
- &client,
1165
- &wasm_path(),
1547
+ Host::new(
1548
+ &client,
1549
+ &wasm_path(),
1550
+ &test_config("Banking", 1),
1551
+ &lambda_client::NeverInvoker,
1552
+ ),
1166
1553
  "Banking::Customer.Register",
1167
1554
  register("CUST-0003"),
1168
1555
  None,
1169
- &test_config("Banking", 1),
1170
- &lambda_client::NeverInvoker,
1171
1556
  )
1172
1557
  .await
1173
1558
  .unwrap()
@@ -1202,31 +1587,54 @@ pub(crate) mod tests {
1202
1587
 
1203
1588
  impl RecordingInvoker {
1204
1589
  fn new() -> Self {
1205
- Self { calls: std::sync::Mutex::new(Vec::new()) }
1590
+ Self {
1591
+ calls: std::sync::Mutex::new(Vec::new()),
1592
+ }
1206
1593
  }
1207
1594
  }
1208
1595
 
1209
1596
  #[async_trait::async_trait]
1210
1597
  impl LambdaInvoker for RecordingInvoker {
1211
- async fn invoke(&self, function_name: &str, payload: &str) -> anyhow::Result<lambda_client::InvokeOutcome> {
1212
- self.calls.lock().unwrap().push((function_name.to_string(), payload.to_string()));
1213
- Ok(lambda_client::InvokeOutcome { body: serde_json::json!({ "refusals": [] }), function_error: false })
1598
+ async fn invoke(
1599
+ &self,
1600
+ function_name: &str,
1601
+ payload: &str,
1602
+ ) -> anyhow::Result<lambda_client::InvokeOutcome> {
1603
+ self.calls
1604
+ .lock()
1605
+ .unwrap()
1606
+ .push((function_name.to_string(), payload.to_string()));
1607
+ Ok(lambda_client::InvokeOutcome {
1608
+ body: serde_json::json!({ "refusals": [] }),
1609
+ function_error: false,
1610
+ })
1214
1611
  }
1215
1612
  }
1216
1613
 
1217
1614
  #[tokio::test]
1218
1615
  async fn a_cross_domain_policy_delivers_through_the_real_rehydrate_replay_path() {
1219
1616
  let client = scratch_db("rust_host_dispatch_test_6").await;
1220
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "Account"]).await;
1617
+ provision_lineage(
1618
+ &*client.lock().await,
1619
+ "Banking",
1620
+ 1,
1621
+ &["Customer", "Account"],
1622
+ )
1623
+ .await;
1221
1624
  let config = test_config("Banking", 1);
1222
1625
  let invoker = RecordingInvoker::new();
1223
1626
 
1224
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0007"), None, &config, &invoker)
1225
- .await
1226
- .unwrap()
1227
- .accepted
1228
- .then_some(())
1229
- .expect("registration should succeed");
1627
+ handle(
1628
+ Host::new(&client, &wasm_path(), &config, &invoker),
1629
+ "Banking::Customer.Register",
1630
+ register("CUST-0007"),
1631
+ None,
1632
+ )
1633
+ .await
1634
+ .unwrap()
1635
+ .accepted
1636
+ .then_some(())
1637
+ .expect("registration should succeed");
1230
1638
 
1231
1639
  let open_args = serde_json::json!({
1232
1640
  "number": { "value": "acct-freeze-me" },
@@ -1234,24 +1642,44 @@ pub(crate) mod tests {
1234
1642
  "daily_limit": { "cents": 50000 },
1235
1643
  "customer": "CUST-0007",
1236
1644
  });
1237
- handle(&client, &wasm_path(), "Banking::Account.Open", open_args, None, &config, &invoker)
1238
- .await
1239
- .unwrap()
1240
- .accepted
1241
- .then_some(())
1242
- .expect("account open should succeed");
1645
+ handle(
1646
+ Host::new(&client, &wasm_path(), &config, &invoker),
1647
+ "Banking::Account.Open",
1648
+ open_args,
1649
+ None,
1650
+ )
1651
+ .await
1652
+ .unwrap()
1653
+ .accepted
1654
+ .then_some(())
1655
+ .expect("account open should succeed");
1243
1656
 
1244
1657
  // `FreezeAccount` announces `AccountFrozen`, matched by
1245
1658
  // `ReviewOnFreeze` (`across "Compliance"`) — the real trigger
1246
1659
  // this feature exists for.
1247
1660
  let freeze_args = serde_json::json!({ "number": { "value": "acct-freeze-me" } });
1248
- let outcome = handle(&client, &wasm_path(), "Banking::Account.FreezeAccount", freeze_args, None, &config, &invoker)
1249
- .await
1250
- .unwrap();
1251
- assert!(outcome.accepted, "freeze should succeed: {:?}", outcome.result);
1661
+ let outcome = handle(
1662
+ Host::new(&client, &wasm_path(), &config, &invoker),
1663
+ "Banking::Account.FreezeAccount",
1664
+ freeze_args,
1665
+ None,
1666
+ )
1667
+ .await
1668
+ .unwrap();
1669
+ assert!(
1670
+ outcome.accepted,
1671
+ "freeze should succeed: {:?}",
1672
+ outcome.result
1673
+ );
1252
1674
 
1253
- let deliveries = outcome.result["cross_domain_deliveries"].as_array().unwrap();
1254
- assert_eq!(deliveries.len(), 1, "exactly one cross-domain reaction should have fired: {deliveries:?}");
1675
+ let deliveries = outcome.result["cross_domain_deliveries"]
1676
+ .as_array()
1677
+ .unwrap();
1678
+ assert_eq!(
1679
+ deliveries.len(),
1680
+ 1,
1681
+ "exactly one cross-domain reaction should have fired: {deliveries:?}"
1682
+ );
1255
1683
  assert_eq!(deliveries[0]["policy"], "ReviewOnFreeze");
1256
1684
  assert_eq!(deliveries[0]["target_domain"], "Compliance");
1257
1685
  assert_eq!(deliveries[0]["delivered"], true);
@@ -1260,7 +1688,10 @@ pub(crate) mod tests {
1260
1688
  // ({policy, on, trigger, delivered, reason}), alongside the
1261
1689
  // differently-shaped "cross_domain_deliveries" entry above.
1262
1690
  let reactions = outcome.result["reactions"].as_array().unwrap();
1263
- let merged = reactions.iter().find(|r| r["policy"] == "ReviewOnFreeze").expect("ReviewOnFreeze should appear in \"reactions\" too");
1691
+ let merged = reactions
1692
+ .iter()
1693
+ .find(|r| r["policy"] == "ReviewOnFreeze")
1694
+ .expect("ReviewOnFreeze should appear in \"reactions\" too");
1264
1695
  assert_eq!(merged["on"], "AccountFrozen");
1265
1696
  assert_eq!(merged["trigger"], "Compliance::AccountFreezeReview.Open");
1266
1697
  assert_eq!(merged["delivered"], true);
@@ -1281,10 +1712,19 @@ pub(crate) mod tests {
1281
1712
  // Never re-delivered on a later replay: `pending_cross_domain`
1282
1713
  // only ever reads the last step's own reactions, so rehydrating
1283
1714
  // history here doesn't re-invoke the sibling Lambda.
1284
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0008"), None, &config, &invoker)
1285
- .await
1286
- .unwrap();
1287
- assert_eq!(invoker.calls.lock().unwrap().len(), 1, "replaying prior history must not re-deliver its cross-domain reaction");
1715
+ handle(
1716
+ Host::new(&client, &wasm_path(), &config, &invoker),
1717
+ "Banking::Customer.Register",
1718
+ register("CUST-0008"),
1719
+ None,
1720
+ )
1721
+ .await
1722
+ .unwrap();
1723
+ assert_eq!(
1724
+ invoker.calls.lock().unwrap().len(),
1725
+ 1,
1726
+ "replaying prior history must not re-deliver its cross-domain reaction"
1727
+ );
1288
1728
  }
1289
1729
 
1290
1730
  // Pins the fix: this is the first test that reaches `check_role`
@@ -1301,13 +1741,10 @@ pub(crate) mod tests {
1301
1741
  // The correct role succeeds, same as every other registration,
1302
1742
  // just now with a caller actually bound and matching.
1303
1743
  let matching = handle(
1304
- &client,
1305
- &wasm_path(),
1744
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1306
1745
  "Banking::Customer.Register",
1307
1746
  register("CUST-0009"),
1308
1747
  Some("Branch clerk"),
1309
- &config,
1310
- &lambda_client::NeverInvoker,
1311
1748
  )
1312
1749
  .await
1313
1750
  .unwrap();
@@ -1321,13 +1758,10 @@ pub(crate) mod tests {
1321
1758
  // caller stating the wrong role must be refused, not silently
1322
1759
  // admitted via the unchecked no-role path.
1323
1760
  let mismatched = handle(
1324
- &client,
1325
- &wasm_path(),
1761
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1326
1762
  "Banking::Customer.Register",
1327
1763
  register("CUST-0010"),
1328
1764
  Some("Teller"),
1329
- &config,
1330
- &lambda_client::NeverInvoker,
1331
1765
  )
1332
1766
  .await
1333
1767
  .unwrap();
@@ -1348,7 +1782,11 @@ pub(crate) mod tests {
1348
1782
  // And the refused command was never persisted — same discipline
1349
1783
  // every other refusal in this file is held to.
1350
1784
  let steps = journal::load_steps(&*client.lock().await).await.unwrap();
1351
- assert_eq!(steps.len(), 1, "only the correctly-authorized registration should be persisted");
1785
+ assert_eq!(
1786
+ steps.len(),
1787
+ 1,
1788
+ "only the correctly-authorized registration should be persisted"
1789
+ );
1352
1790
  }
1353
1791
 
1354
1792
  #[test]
@@ -1366,16 +1804,27 @@ pub(crate) mod tests {
1366
1804
  assert_eq!(Enforcement::Off.effective(nobody), nobody);
1367
1805
  for mode in [Enforcement::Shadow, Enforcement::Enforce] {
1368
1806
  assert_eq!(mode.effective(nobody).role, Some(ANONYMOUS_ROLE));
1369
- let stated = Caller { role: Some("Teller"), actor_id: None };
1807
+ let stated = Caller {
1808
+ role: Some("Teller"),
1809
+ actor_id: None,
1810
+ };
1370
1811
  assert_eq!(mode.effective(stated), stated, "a stated role is kept");
1371
- let identified = Caller { role: None, actor_id: Some("u1") };
1372
- assert_eq!(mode.effective(identified), identified, "an identified caller is kept");
1812
+ let identified = Caller {
1813
+ role: None,
1814
+ actor_id: Some("u1"),
1815
+ };
1816
+ assert_eq!(
1817
+ mode.effective(identified),
1818
+ identified,
1819
+ "an identified caller is kept"
1820
+ );
1373
1821
  }
1374
1822
  }
1375
1823
 
1376
1824
  // The unidentified caller is the case the host never refused: no role at all.
1377
1825
  #[tokio::test]
1378
- async fn an_unidentified_caller_is_refused_when_enforced_and_let_through_when_off_or_shadowed() {
1826
+ async fn an_unidentified_caller_is_refused_when_enforced_and_let_through_when_off_or_shadowed()
1827
+ {
1379
1828
  let client = scratch_db("rust_host_dispatch_test_enforce").await;
1380
1829
  provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
1381
1830
  let config = test_config("Banking", 1);
@@ -1383,26 +1832,52 @@ pub(crate) mod tests {
1383
1832
  let client = &client;
1384
1833
  let config = &config;
1385
1834
  async move {
1386
- dispatch_in(mode, Origin::External, client, &wasm_path(), "Banking::Customer.Register", register(id), Caller::default(), config, &lambda_client::NeverInvoker)
1387
- .await
1388
- .unwrap()
1835
+ dispatch_in(
1836
+ mode,
1837
+ Origin::External,
1838
+ Host::new(client, &wasm_path(), config, &lambda_client::NeverInvoker),
1839
+ "Banking::Customer.Register",
1840
+ register(id),
1841
+ Caller::default(),
1842
+ )
1843
+ .await
1844
+ .unwrap()
1389
1845
  }
1390
1846
  };
1391
1847
 
1392
1848
  let off = go(Enforcement::Off, "CUST-0021").await;
1393
- assert!(off.accepted, "off keeps the old behaviour: {:?}", off.result);
1849
+ assert!(
1850
+ off.accepted,
1851
+ "off keeps the old behaviour: {:?}",
1852
+ off.result
1853
+ );
1394
1854
 
1395
1855
  let enforced = go(Enforcement::Enforce, "CUST-0022").await;
1396
- assert!(!enforced.accepted, "an unidentified caller must be refused: {:?}", enforced.result);
1856
+ assert!(
1857
+ !enforced.accepted,
1858
+ "an unidentified caller must be refused: {:?}",
1859
+ enforced.result
1860
+ );
1397
1861
  assert!(refused_for_role(&enforced.result), "{:?}", enforced.result);
1398
1862
  let error = enforced.result["refusals"][0]["error"].as_str().unwrap();
1399
- assert!(error.contains("Branch clerk") && error.contains(ANONYMOUS_ROLE), "{error}");
1863
+ assert!(
1864
+ error.contains("Branch clerk") && error.contains(ANONYMOUS_ROLE),
1865
+ "{error}"
1866
+ );
1400
1867
 
1401
1868
  let shadowed = go(Enforcement::Shadow, "CUST-0023").await;
1402
- assert!(shadowed.accepted, "shadow lets the command through: {:?}", shadowed.result);
1869
+ assert!(
1870
+ shadowed.accepted,
1871
+ "shadow lets the command through: {:?}",
1872
+ shadowed.result
1873
+ );
1403
1874
 
1404
1875
  let steps = journal::load_steps(&*client.lock().await).await.unwrap();
1405
- assert_eq!(steps.len(), 2, "the enforced refusal was not persisted; off and shadow were");
1876
+ assert_eq!(
1877
+ steps.len(),
1878
+ 2,
1879
+ "the enforced refusal was not persisted; off and shadow were"
1880
+ );
1406
1881
  }
1407
1882
 
1408
1883
  // Shadow may only let through what Off would: a caller that stated a wrong role is refused.
@@ -1415,24 +1890,60 @@ pub(crate) mod tests {
1415
1890
  let client = &client;
1416
1891
  let config = &config;
1417
1892
  async move {
1418
- dispatch_in(mode, origin, client, &wasm_path(), "Banking::Customer.Register", register(id), caller, config, &lambda_client::NeverInvoker)
1419
- .await
1420
- .unwrap()
1893
+ dispatch_in(
1894
+ mode,
1895
+ origin,
1896
+ Host::new(client, &wasm_path(), config, &lambda_client::NeverInvoker),
1897
+ "Banking::Customer.Register",
1898
+ register(id),
1899
+ caller,
1900
+ )
1901
+ .await
1902
+ .unwrap()
1421
1903
  }
1422
1904
  };
1423
- let wrong = Caller { role: Some("Auditor"), actor_id: None };
1905
+ let wrong = Caller {
1906
+ role: Some("Auditor"),
1907
+ actor_id: None,
1908
+ };
1424
1909
 
1425
1910
  let shadow_wrong = go(Enforcement::Shadow, Origin::External, wrong, "CUST-0031").await;
1426
- assert!(!shadow_wrong.accepted, "shadow must not accept a stated wrong role: {:?}", shadow_wrong.result);
1427
- assert!(refused_for_role(&shadow_wrong.result), "{:?}", shadow_wrong.result);
1911
+ assert!(
1912
+ !shadow_wrong.accepted,
1913
+ "shadow must not accept a stated wrong role: {:?}",
1914
+ shadow_wrong.result
1915
+ );
1916
+ assert!(
1917
+ refused_for_role(&shadow_wrong.result),
1918
+ "{:?}",
1919
+ shadow_wrong.result
1920
+ );
1428
1921
  let off_wrong = go(Enforcement::Off, Origin::External, wrong, "CUST-0032").await;
1429
- assert!(!off_wrong.accepted, "off refuses it too: {:?}", off_wrong.result);
1922
+ assert!(
1923
+ !off_wrong.accepted,
1924
+ "off refuses it too: {:?}",
1925
+ off_wrong.result
1926
+ );
1430
1927
 
1431
- let nobody = go(Enforcement::Shadow, Origin::External, Caller::default(), "CUST-0033").await;
1432
- assert!(nobody.accepted, "shadow lets an unidentified caller through: {:?}", nobody.result);
1928
+ let nobody = go(
1929
+ Enforcement::Shadow,
1930
+ Origin::External,
1931
+ Caller::default(),
1932
+ "CUST-0033",
1933
+ )
1934
+ .await;
1935
+ assert!(
1936
+ nobody.accepted,
1937
+ "shadow lets an unidentified caller through: {:?}",
1938
+ nobody.result
1939
+ );
1433
1940
 
1434
1941
  let steps = journal::load_steps(&*client.lock().await).await.unwrap();
1435
- assert_eq!(steps.len(), 1, "only the unidentified caller's registration was persisted");
1942
+ assert_eq!(
1943
+ steps.len(),
1944
+ 1,
1945
+ "only the unidentified caller's registration was persisted"
1946
+ );
1436
1947
  }
1437
1948
 
1438
1949
  // A host-internal dispatch names no caller and is unchecked in every mode, while an external
@@ -1446,15 +1957,26 @@ pub(crate) mod tests {
1446
1957
  let client = &client;
1447
1958
  let config = &config;
1448
1959
  async move {
1449
- dispatch_in(mode, origin, client, &wasm_path(), "Banking::Customer.Register", register(id), Caller::default(), config, &lambda_client::NeverInvoker)
1450
- .await
1451
- .unwrap()
1960
+ dispatch_in(
1961
+ mode,
1962
+ origin,
1963
+ Host::new(client, &wasm_path(), config, &lambda_client::NeverInvoker),
1964
+ "Banking::Customer.Register",
1965
+ register(id),
1966
+ Caller::default(),
1967
+ )
1968
+ .await
1969
+ .unwrap()
1452
1970
  }
1453
1971
  };
1454
1972
 
1455
1973
  let external = go(Enforcement::Enforce, Origin::External, "CUST-0041").await;
1456
1974
  assert!(!external.accepted, "{:?}", external.result);
1457
- for (mode, id) in [(Enforcement::Enforce, "CUST-0042"), (Enforcement::Shadow, "CUST-0043"), (Enforcement::Off, "CUST-0044")] {
1975
+ for (mode, id) in [
1976
+ (Enforcement::Enforce, "CUST-0042"),
1977
+ (Enforcement::Shadow, "CUST-0043"),
1978
+ (Enforcement::Off, "CUST-0044"),
1979
+ ] {
1458
1980
  let internal = go(mode, Origin::HostInternal, id).await;
1459
1981
  assert!(internal.accepted, "{mode:?}: {:?}", internal.result);
1460
1982
  }
@@ -1466,29 +1988,58 @@ pub(crate) mod tests {
1466
1988
  // after `begin_saga` already wrote into `hecks_lambda_sagas`,
1467
1989
  // proving that write rolls back with the rest of the transaction.
1468
1990
  let client = scratch_db("rust_host_dispatch_test_9").await;
1469
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "Account"]).await;
1991
+ provision_lineage(
1992
+ &*client.lock().await,
1993
+ "Banking",
1994
+ 1,
1995
+ &["Customer", "Account"],
1996
+ )
1997
+ .await;
1470
1998
  let config = test_config("Banking", 1);
1471
1999
 
1472
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0012"), None, &config, &lambda_client::NeverInvoker)
1473
- .await.unwrap().accepted.then_some(()).expect("registration should succeed");
2000
+ handle(
2001
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
2002
+ "Banking::Customer.Register",
2003
+ register("CUST-0012"),
2004
+ None,
2005
+ )
2006
+ .await
2007
+ .unwrap()
2008
+ .accepted
2009
+ .then_some(())
2010
+ .expect("registration should succeed");
1474
2011
 
1475
2012
  let open_src = serde_json::json!({
1476
2013
  "number": { "value": "src-rollback" }, "kind": { "name": "current" },
1477
2014
  "daily_limit": { "cents": 50000 }, "customer": "CUST-0012",
1478
2015
  });
1479
- handle(&client, &wasm_path(), "Banking::Account.Open", open_src, None, &config, &lambda_client::NeverInvoker)
1480
- .await.unwrap().accepted.then_some(()).expect("source account open should succeed");
2016
+ handle(
2017
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
2018
+ "Banking::Account.Open",
2019
+ open_src,
2020
+ None,
2021
+ )
2022
+ .await
2023
+ .unwrap()
2024
+ .accepted
2025
+ .then_some(())
2026
+ .expect("source account open should succeed");
1481
2027
  let open_dst = serde_json::json!({
1482
2028
  "number": { "value": "dst-rollback" }, "kind": { "name": "current" },
1483
2029
  "daily_limit": { "cents": 50000 }, "customer": "CUST-0012",
1484
2030
  });
1485
- handle(&client, &wasm_path(), "Banking::Account.Open", open_dst, None, &config, &lambda_client::NeverInvoker)
1486
- .await.unwrap().accepted.then_some(()).expect("destination account open should succeed");
1487
2031
  handle(
1488
- &client, &wasm_path(), "Banking::Account.Credit",
1489
- serde_json::json!({ "number": "src-rollback", "amount": { "cents": 1000 }, "narrative": { "text": "opening balance" } }),
1490
- None, &config, &lambda_client::NeverInvoker,
2032
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
2033
+ "Banking::Account.Open",
2034
+ open_dst,
2035
+ None,
1491
2036
  )
2037
+ .await
2038
+ .unwrap()
2039
+ .accepted
2040
+ .then_some(())
2041
+ .expect("destination account open should succeed");
2042
+ handle(Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker), "Banking::Account.Credit", serde_json::json!({ "number": "src-rollback", "amount": { "cents": 1000 }, "narrative": { "text": "opening balance" } }), None)
1492
2043
  .await.unwrap().accepted.then_some(()).expect("credit should succeed");
1493
2044
 
1494
2045
  // `Transfer.Request`'s own mutation is the first one this step
@@ -1498,11 +2049,26 @@ pub(crate) mod tests {
1498
2049
  "reference": { "value": "tr-rollback" }, "amount": { "cents": 200 },
1499
2050
  "narrative": { "text": "rollback test" }, "source": "src-rollback", "destination": "dst-rollback",
1500
2051
  });
1501
- let outcome = handle(&client, &wasm_path(), "Banking::Transfer.Request", transfer_args, None, &config, &lambda_client::NeverInvoker).await;
1502
- assert!(outcome.is_err(), "the unprovisioned Transfer lineage table should make this whole invocation fail");
2052
+ let outcome = handle(
2053
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
2054
+ "Banking::Transfer.Request",
2055
+ transfer_args,
2056
+ None,
2057
+ )
2058
+ .await;
2059
+ assert!(
2060
+ outcome.is_err(),
2061
+ "the unprovisioned Transfer lineage table should make this whole invocation fail"
2062
+ );
1503
2063
 
1504
2064
  let guard = client.lock().await;
1505
- let saga_rows = guard.query("SELECT process_manager, correlation FROM hecks_lambda_sagas", &[]).await.unwrap();
2065
+ let saga_rows = guard
2066
+ .query(
2067
+ "SELECT process_manager, correlation FROM hecks_lambda_sagas",
2068
+ &[],
2069
+ )
2070
+ .await
2071
+ .unwrap();
1506
2072
  assert_eq!(
1507
2073
  saga_rows.len(), 0,
1508
2074
  "a saga write inside a transaction that ultimately fails must roll back with it, not commit independently: {saga_rows:?}"
@@ -1512,49 +2078,102 @@ pub(crate) mod tests {
1512
2078
  #[tokio::test]
1513
2079
  async fn a_pre_existing_snapshot_without_the_sagas_backfilled_latch_forces_one_full_replay() {
1514
2080
  let client = scratch_db("rust_host_dispatch_test_10").await;
1515
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "Account", "Transfer"]).await;
2081
+ provision_lineage(
2082
+ &*client.lock().await,
2083
+ "Banking",
2084
+ 1,
2085
+ &["Customer", "Account", "Transfer"],
2086
+ )
2087
+ .await;
1516
2088
  let config = test_config("Banking", 1);
1517
2089
  // `ReviewOnFreeze` fires on every `FreezeAccount` in this domain
1518
2090
  // (proven above), so `NeverInvoker` would panic here.
1519
2091
  let invoker = RecordingInvoker::new();
1520
2092
 
1521
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0013"), None, &config, &invoker)
1522
- .await.unwrap().accepted.then_some(()).expect("registration should succeed");
2093
+ handle(
2094
+ Host::new(&client, &wasm_path(), &config, &invoker),
2095
+ "Banking::Customer.Register",
2096
+ register("CUST-0013"),
2097
+ None,
2098
+ )
2099
+ .await
2100
+ .unwrap()
2101
+ .accepted
2102
+ .then_some(())
2103
+ .expect("registration should succeed");
1523
2104
 
1524
2105
  let open_src = serde_json::json!({
1525
2106
  "number": { "value": "src-backfill" }, "kind": { "name": "current" },
1526
2107
  "daily_limit": { "cents": 50000 }, "customer": "CUST-0013",
1527
2108
  });
1528
- handle(&client, &wasm_path(), "Banking::Account.Open", open_src, None, &config, &invoker)
1529
- .await.unwrap().accepted.then_some(()).expect("source account open should succeed");
2109
+ handle(
2110
+ Host::new(&client, &wasm_path(), &config, &invoker),
2111
+ "Banking::Account.Open",
2112
+ open_src,
2113
+ None,
2114
+ )
2115
+ .await
2116
+ .unwrap()
2117
+ .accepted
2118
+ .then_some(())
2119
+ .expect("source account open should succeed");
1530
2120
  let open_dst = serde_json::json!({
1531
2121
  "number": { "value": "dst-backfill" }, "kind": { "name": "current" },
1532
2122
  "daily_limit": { "cents": 50000 }, "customer": "CUST-0013",
1533
2123
  });
1534
- handle(&client, &wasm_path(), "Banking::Account.Open", open_dst, None, &config, &invoker)
1535
- .await.unwrap().accepted.then_some(()).expect("destination account open should succeed");
1536
2124
  handle(
1537
- &client, &wasm_path(), "Banking::Account.Credit",
1538
- serde_json::json!({ "number": "src-backfill", "amount": { "cents": 1000 }, "narrative": { "text": "opening balance" } }),
1539
- None, &config, &invoker,
2125
+ Host::new(&client, &wasm_path(), &config, &invoker),
2126
+ "Banking::Account.Open",
2127
+ open_dst,
2128
+ None,
1540
2129
  )
2130
+ .await
2131
+ .unwrap()
2132
+ .accepted
2133
+ .then_some(())
2134
+ .expect("destination account open should succeed");
2135
+ handle(Host::new(&client, &wasm_path(), &config, &invoker), "Banking::Account.Credit", serde_json::json!({ "number": "src-backfill", "amount": { "cents": 1000 }, "narrative": { "text": "opening balance" } }), None)
1541
2136
  .await.unwrap().accepted.then_some(()).expect("credit should succeed");
1542
2137
 
1543
2138
  // Freezing the destination first means the credit leg refuses
1544
2139
  // and Settlement compensates, leaving the saga stuck "reversed"
1545
2140
  // — exactly the mid-flight shape this test needs to recover.
1546
- handle(&client, &wasm_path(), "Banking::Account.FreezeAccount", serde_json::json!({ "number": { "value": "dst-backfill" } }), None, &config, &invoker)
1547
- .await.unwrap().accepted.then_some(()).expect("freeze should succeed");
2141
+ handle(
2142
+ Host::new(&client, &wasm_path(), &config, &invoker),
2143
+ "Banking::Account.FreezeAccount",
2144
+ serde_json::json!({ "number": { "value": "dst-backfill" } }),
2145
+ None,
2146
+ )
2147
+ .await
2148
+ .unwrap()
2149
+ .accepted
2150
+ .then_some(())
2151
+ .expect("freeze should succeed");
1548
2152
  let transfer_args = serde_json::json!({
1549
2153
  "reference": { "value": "tr-backfill" }, "amount": { "cents": 200 },
1550
2154
  "narrative": { "text": "backfill test" }, "source": "src-backfill", "destination": "dst-backfill",
1551
2155
  });
1552
- handle(&client, &wasm_path(), "Banking::Transfer.Request", transfer_args, None, &config, &invoker)
1553
- .await.unwrap().accepted.then_some(()).expect("the Transfer.Request COMMAND succeeds; the saga's own credit leg is what refuses");
2156
+ handle(
2157
+ Host::new(&client, &wasm_path(), &config, &invoker),
2158
+ "Banking::Transfer.Request",
2159
+ transfer_args,
2160
+ None,
2161
+ )
2162
+ .await
2163
+ .unwrap()
2164
+ .accepted
2165
+ .then_some(())
2166
+ .expect("the Transfer.Request COMMAND succeeds; the saga's own credit leg is what refuses");
1554
2167
 
1555
2168
  {
1556
2169
  let guard = client.lock().await;
1557
- let before = guard.query("SELECT process_manager, correlation, state FROM hecks_lambda_sagas", &[]).await.unwrap();
2170
+ let before = guard
2171
+ .query(
2172
+ "SELECT process_manager, correlation, state FROM hecks_lambda_sagas",
2173
+ &[],
2174
+ )
2175
+ .await
2176
+ .unwrap();
1558
2177
  assert_eq!(before.len(), 1, "the reversed Settlement instance should already be tracked before the simulated reset: {before:?}");
1559
2178
  }
1560
2179
 
@@ -1563,18 +2182,42 @@ pub(crate) mod tests {
1563
2182
  // mid-flight.
1564
2183
  {
1565
2184
  let guard = client.lock().await;
1566
- guard.execute("UPDATE hecks_lambda_snapshot SET sagas_backfilled = false", &[]).await.unwrap();
1567
- guard.execute("DELETE FROM hecks_lambda_sagas", &[]).await.unwrap();
2185
+ guard
2186
+ .execute(
2187
+ "UPDATE hecks_lambda_snapshot SET sagas_backfilled = false",
2188
+ &[],
2189
+ )
2190
+ .await
2191
+ .unwrap();
2192
+ guard
2193
+ .execute("DELETE FROM hecks_lambda_sagas", &[])
2194
+ .await
2195
+ .unwrap();
1568
2196
  }
1569
2197
 
1570
2198
  // One more command should trigger a full replay, whose saga
1571
2199
  // cascade re-running as a side effect repopulates
1572
2200
  // `hecks_lambda_sagas`.
1573
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0014"), None, &config, &invoker)
1574
- .await.unwrap().accepted.then_some(()).expect("second registration should succeed");
2201
+ handle(
2202
+ Host::new(&client, &wasm_path(), &config, &invoker),
2203
+ "Banking::Customer.Register",
2204
+ register("CUST-0014"),
2205
+ None,
2206
+ )
2207
+ .await
2208
+ .unwrap()
2209
+ .accepted
2210
+ .then_some(())
2211
+ .expect("second registration should succeed");
1575
2212
 
1576
2213
  let guard = client.lock().await;
1577
- let saga_rows = guard.query("SELECT process_manager, correlation, state FROM hecks_lambda_sagas", &[]).await.unwrap();
2214
+ let saga_rows = guard
2215
+ .query(
2216
+ "SELECT process_manager, correlation, state FROM hecks_lambda_sagas",
2217
+ &[],
2218
+ )
2219
+ .await
2220
+ .unwrap();
1578
2221
  assert_eq!(saga_rows.len(), 1, "the backfill replay should have recovered the in-flight Settlement instance: {saga_rows:?}");
1579
2222
  let process_manager: String = saga_rows[0].get(0);
1580
2223
  let correlation: String = saga_rows[0].get(1);
@@ -1583,7 +2226,11 @@ pub(crate) mod tests {
1583
2226
  assert_eq!(correlation, "tr-backfill");
1584
2227
  assert_eq!(state, "reversed");
1585
2228
 
1586
- let latched: bool = guard.query_one("SELECT sagas_backfilled FROM hecks_lambda_snapshot", &[]).await.unwrap().get(0);
2229
+ let latched: bool = guard
2230
+ .query_one("SELECT sagas_backfilled FROM hecks_lambda_snapshot", &[])
2231
+ .await
2232
+ .unwrap()
2233
+ .get(0);
1587
2234
  assert!(latched, "after the backfill replay, the latch should be set so this doesn't repeat every invocation");
1588
2235
  }
1589
2236
 
@@ -1595,31 +2242,49 @@ pub(crate) mod tests {
1595
2242
 
1596
2243
  impl AlwaysFailingInvoker {
1597
2244
  fn new() -> Self {
1598
- Self { calls: std::sync::Mutex::new(0) }
2245
+ Self {
2246
+ calls: std::sync::Mutex::new(0),
2247
+ }
1599
2248
  }
1600
2249
  }
1601
2250
 
1602
2251
  #[async_trait::async_trait]
1603
2252
  impl LambdaInvoker for AlwaysFailingInvoker {
1604
- async fn invoke(&self, _function_name: &str, _payload: &str) -> anyhow::Result<lambda_client::InvokeOutcome> {
2253
+ async fn invoke(
2254
+ &self,
2255
+ _function_name: &str,
2256
+ _payload: &str,
2257
+ ) -> anyhow::Result<lambda_client::InvokeOutcome> {
1605
2258
  *self.calls.lock().unwrap() += 1;
1606
2259
  anyhow::bail!("ResourceNotFoundException: function not found")
1607
2260
  }
1608
2261
  }
1609
2262
 
1610
2263
  #[tokio::test]
1611
- async fn a_cross_domain_delivery_that_exhausts_every_retry_dead_letters_and_still_fails_the_invocation() {
2264
+ async fn a_cross_domain_delivery_that_exhausts_every_retry_dead_letters_and_still_fails_the_invocation(
2265
+ ) {
1612
2266
  let client = scratch_db("rust_host_dispatch_test_8").await;
1613
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "Account"]).await;
2267
+ provision_lineage(
2268
+ &*client.lock().await,
2269
+ "Banking",
2270
+ 1,
2271
+ &["Customer", "Account"],
2272
+ )
2273
+ .await;
1614
2274
  let config = test_config("Banking", 1);
1615
2275
  let invoker = AlwaysFailingInvoker::new();
1616
2276
 
1617
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0011"), None, &config, &invoker)
1618
- .await
1619
- .unwrap()
1620
- .accepted
1621
- .then_some(())
1622
- .expect("registration should succeed");
2277
+ handle(
2278
+ Host::new(&client, &wasm_path(), &config, &invoker),
2279
+ "Banking::Customer.Register",
2280
+ register("CUST-0011"),
2281
+ None,
2282
+ )
2283
+ .await
2284
+ .unwrap()
2285
+ .accepted
2286
+ .then_some(())
2287
+ .expect("registration should succeed");
1623
2288
 
1624
2289
  let open_args = serde_json::json!({
1625
2290
  "number": { "value": "acct-freeze-dead-letter" },
@@ -1627,18 +2292,29 @@ pub(crate) mod tests {
1627
2292
  "daily_limit": { "cents": 50000 },
1628
2293
  "customer": "CUST-0011",
1629
2294
  });
1630
- handle(&client, &wasm_path(), "Banking::Account.Open", open_args, None, &config, &invoker)
1631
- .await
1632
- .unwrap()
1633
- .accepted
1634
- .then_some(())
1635
- .expect("account open should succeed");
2295
+ handle(
2296
+ Host::new(&client, &wasm_path(), &config, &invoker),
2297
+ "Banking::Account.Open",
2298
+ open_args,
2299
+ None,
2300
+ )
2301
+ .await
2302
+ .unwrap()
2303
+ .accepted
2304
+ .then_some(())
2305
+ .expect("account open should succeed");
1636
2306
 
1637
2307
  // `ReviewOnFreeze` fires here — every attempt against
1638
2308
  // `AlwaysFailingInvoker` fails, so `deliver_with_retry` exhausts
1639
2309
  // `MAX_DELIVERY_ATTEMPTS` before `handle` ever sees a result.
1640
2310
  let freeze_args = serde_json::json!({ "number": { "value": "acct-freeze-dead-letter" } });
1641
- let outcome = handle(&client, &wasm_path(), "Banking::Account.FreezeAccount", freeze_args, None, &config, &invoker).await;
2311
+ let outcome = handle(
2312
+ Host::new(&client, &wasm_path(), &config, &invoker),
2313
+ "Banking::Account.FreezeAccount",
2314
+ freeze_args,
2315
+ None,
2316
+ )
2317
+ .await;
1642
2318
 
1643
2319
  assert!(
1644
2320
  outcome.is_err(),
@@ -1660,7 +2336,11 @@ pub(crate) mod tests {
1660
2336
  .query("SELECT policy, target_domain, target_verb, attempts FROM hecks_cross_domain_dead_letters", &[])
1661
2337
  .await
1662
2338
  .unwrap();
1663
- assert_eq!(dead_letters.len(), 1, "exactly one exhausted delivery should have been dead-lettered: {dead_letters:?}");
2339
+ assert_eq!(
2340
+ dead_letters.len(),
2341
+ 1,
2342
+ "exactly one exhausted delivery should have been dead-lettered: {dead_letters:?}"
2343
+ );
1664
2344
  let policy: String = dead_letters[0].get(0);
1665
2345
  let target_domain: String = dead_letters[0].get(1);
1666
2346
  let target_verb: String = dead_letters[0].get(2);
@@ -1679,16 +2359,27 @@ pub(crate) mod tests {
1679
2359
  #[tokio::test]
1680
2360
  async fn a_failed_cross_domain_delivery_does_not_drop_a_sibling_reaction_from_the_same_step() {
1681
2361
  let client = scratch_db("rust_host_dispatch_test_11").await;
1682
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "SafeDepositBox"]).await;
2362
+ provision_lineage(
2363
+ &*client.lock().await,
2364
+ "Banking",
2365
+ 1,
2366
+ &["Customer", "SafeDepositBox"],
2367
+ )
2368
+ .await;
1683
2369
  let config = test_config("Banking", 1);
1684
2370
  let invoker = AlwaysFailingInvoker::new();
1685
2371
 
1686
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0022"), None, &config, &invoker)
1687
- .await
1688
- .unwrap()
1689
- .accepted
1690
- .then_some(())
1691
- .expect("registration should succeed");
2372
+ handle(
2373
+ Host::new(&client, &wasm_path(), &config, &invoker),
2374
+ "Banking::Customer.Register",
2375
+ register("CUST-0022"),
2376
+ None,
2377
+ )
2378
+ .await
2379
+ .unwrap()
2380
+ .accepted
2381
+ .then_some(())
2382
+ .expect("registration should succeed");
1692
2383
 
1693
2384
  let rent_args = serde_json::json!({
1694
2385
  "branch_code": { "value": "downtown" },
@@ -1696,22 +2387,34 @@ pub(crate) mod tests {
1696
2387
  "size": { "value": "small" },
1697
2388
  "customer": "CUST-0022",
1698
2389
  });
1699
- handle(&client, &wasm_path(), "Banking::SafeDepositBox.Rent", rent_args, None, &config, &invoker)
1700
- .await
1701
- .unwrap()
1702
- .accepted
1703
- .then_some(())
1704
- .expect("rent should succeed");
2390
+ handle(
2391
+ Host::new(&client, &wasm_path(), &config, &invoker),
2392
+ "Banking::SafeDepositBox.Rent",
2393
+ rent_args,
2394
+ None,
2395
+ )
2396
+ .await
2397
+ .unwrap()
2398
+ .accepted
2399
+ .then_some(())
2400
+ .expect("rent should succeed");
1705
2401
 
1706
2402
  // Both ReviewOnBoxSurrender (-> Compliance) and FlagKeyReturn
1707
2403
  // (-> Notifications) fire here; both deliveries exhaust every
1708
2404
  // retry against AlwaysFailingInvoker.
1709
2405
  let surrender_args = serde_json::json!({ "branch_code": { "value": "downtown" }, "box_number": { "value": 12 } });
1710
- let outcome =
1711
- handle(&client, &wasm_path(), "Banking::SafeDepositBox.Surrender", surrender_args, None, &config, &invoker)
1712
- .await;
2406
+ let outcome = handle(
2407
+ Host::new(&client, &wasm_path(), &config, &invoker),
2408
+ "Banking::SafeDepositBox.Surrender",
2409
+ surrender_args,
2410
+ None,
2411
+ )
2412
+ .await;
1713
2413
 
1714
- assert!(outcome.is_err(), "still fails the invocation visibly, same contract as a single failed delivery");
2414
+ assert!(
2415
+ outcome.is_err(),
2416
+ "still fails the invocation visibly, same contract as a single failed delivery"
2417
+ );
1715
2418
  assert_eq!(
1716
2419
  *invoker.calls.lock().unwrap(),
1717
2420
  lambda_client::MAX_DELIVERY_ATTEMPTS * 2,
@@ -1721,7 +2424,10 @@ pub(crate) mod tests {
1721
2424
 
1722
2425
  let guard = client.lock().await;
1723
2426
  let dead_letters = guard
1724
- .query("SELECT policy FROM hecks_cross_domain_dead_letters ORDER BY policy", &[])
2427
+ .query(
2428
+ "SELECT policy FROM hecks_cross_domain_dead_letters ORDER BY policy",
2429
+ &[],
2430
+ )
1725
2431
  .await
1726
2432
  .unwrap();
1727
2433
  let policies: Vec<String> = dead_letters.iter().map(|row| row.get(0)).collect();