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,17 +1,17 @@
1
1
  //! Mints Postgres eras, porting `lib/hecks/adapters/driven/postgres_era/
2
2
  //! lineage/*.rb`'s SQL verbatim; this crate has no bluebook parser (ADR 0012).
3
3
 
4
- use anyhow::Context;
5
4
  use crate::journal::quote_ident;
6
5
  use crate::reference_transform;
7
6
  use crate::reference_validate;
8
7
  use crate::storage_shape;
8
+ use anyhow::Context;
9
9
  use serde_json::Value;
10
10
  use sha2::{Digest, Sha256};
11
11
  use tokio_postgres::GenericClient;
12
12
 
13
13
  fn journal_table(domain: &str) -> String {
14
- format!("hecks_journal_{}", storage_shape_snake(domain))
14
+ format!("hecks_journal_{}", crate::journal::snake(domain))
15
15
  }
16
16
 
17
17
  fn sequence(domain: &str) -> String {
@@ -36,12 +36,6 @@ fn head_view(domain: &str, storage_name: &str) -> String {
36
36
  crate::journal::qualified_name(domain, &format!("{storage_name}_head"))
37
37
  }
38
38
 
39
- // `Naming.snake` — same pure syntactic transform `crate::journal::snake`
40
- // already ports; reused directly rather than duplicated a second time.
41
- fn storage_shape_snake(name: &str) -> String {
42
- crate::journal::snake(name)
43
- }
44
-
45
39
  fn text_literal(text: &str) -> String {
46
40
  format!("'{}'", text.replace('\'', "''"))
47
41
  }
@@ -59,7 +53,6 @@ pub struct Aggregate {
59
53
  pub struct EdgeAggregate {
60
54
  pub name: String,
61
55
  pub was: Option<String>,
62
- pub has_compute_or_rekey: bool,
63
56
  pub compiled_state_expression: String,
64
57
  pub compiled_id_expression: Option<String>,
65
58
  }
@@ -87,24 +80,37 @@ pub fn parse_edges(ir: &Value, domain: &str) -> Vec<Edge> {
87
80
  list.iter()
88
81
  .filter(|edge| edge.get("domain").and_then(Value::as_str) == Some(domain))
89
82
  .map(|edge| Edge {
90
- from: edge.get("from").and_then(Value::as_str).unwrap_or_default().to_string(),
91
- to: edge.get("to").and_then(Value::as_str).unwrap_or_default().to_string(),
83
+ from: edge
84
+ .get("from")
85
+ .and_then(Value::as_str)
86
+ .unwrap_or_default()
87
+ .to_string(),
88
+ to: edge
89
+ .get("to")
90
+ .and_then(Value::as_str)
91
+ .unwrap_or_default()
92
+ .to_string(),
92
93
  aggregates: edge
93
94
  .get("aggregates")
94
95
  .and_then(Value::as_array)
95
96
  .map(|aggs| {
96
97
  aggs.iter()
97
98
  .map(|agg| EdgeAggregate {
98
- name: agg.get("name").and_then(Value::as_str).unwrap_or_default().to_string(),
99
+ name: agg
100
+ .get("name")
101
+ .and_then(Value::as_str)
102
+ .unwrap_or_default()
103
+ .to_string(),
99
104
  was: agg.get("was").and_then(Value::as_str).map(str::to_string),
100
- has_compute_or_rekey: !agg.get("computes").and_then(Value::as_array).map(Vec::is_empty).unwrap_or(true)
101
- || !agg.get("rekeys").and_then(Value::as_array).map(Vec::is_empty).unwrap_or(true),
102
105
  compiled_state_expression: agg
103
106
  .get("compiled_state_expression")
104
107
  .and_then(Value::as_str)
105
108
  .unwrap_or("state")
106
109
  .to_string(),
107
- compiled_id_expression: agg.get("compiled_id_expression").and_then(Value::as_str).map(str::to_string),
110
+ compiled_id_expression: agg
111
+ .get("compiled_id_expression")
112
+ .and_then(Value::as_str)
113
+ .map(str::to_string),
108
114
  })
109
115
  .collect()
110
116
  })
@@ -146,7 +152,11 @@ pub enum BootDecision {
146
152
  HoldFirst,
147
153
  /// The latest held era differs from this binary's shape — walk the
148
154
  /// edge chain from it and mint the next ordinal.
149
- Mint { ordinal: i32, from_ordinal: i32, from_label: String },
155
+ Mint {
156
+ ordinal: i32,
157
+ from_ordinal: i32,
158
+ from_label: String,
159
+ },
150
160
  /// The latest held era has no label yet — this crate has no parser to
151
161
  /// name it, so it refuses rather than guess whether it matches.
152
162
  LatestUnnamed { ordinal: i32 },
@@ -154,33 +164,39 @@ pub enum BootDecision {
154
164
 
155
165
  impl BootDecision {
156
166
  /// The outcome's name, as the boot log reports it.
157
- pub fn name(&self) -> &'static str {
167
+ pub const fn name(&self) -> &'static str {
158
168
  match self {
159
- BootDecision::UseExisting { .. } => "use_existing",
160
- BootDecision::HoldFirst => "hold_first",
161
- BootDecision::Mint { .. } => "mint",
162
- BootDecision::LatestUnnamed { .. } => "latest_unnamed",
169
+ Self::UseExisting { .. } => "use_existing",
170
+ Self::HoldFirst => "hold_first",
171
+ Self::Mint { .. } => "mint",
172
+ Self::LatestUnnamed { .. } => "latest_unnamed",
163
173
  }
164
174
  }
165
175
  }
166
176
 
167
177
  /// `held` must already be in ordinal order (as `journal::held_eras` returns it).
168
178
  pub fn decide_boot_action(held: &[crate::journal::HeldEra], my_label: &str) -> BootDecision {
169
- if let Some(matching) = held.iter().find(|held_era| held_era.label.as_deref() == Some(my_label)) {
170
- return BootDecision::UseExisting { ordinal: matching.ordinal };
179
+ if let Some(matching) = held
180
+ .iter()
181
+ .find(|held_era| held_era.label.as_deref() == Some(my_label))
182
+ {
183
+ return BootDecision::UseExisting {
184
+ ordinal: matching.ordinal,
185
+ };
171
186
  }
172
- if held.is_empty() {
187
+ let Some(latest) = held.last() else {
173
188
  return BootDecision::HoldFirst;
174
- }
175
- let latest = held.last().expect("checked non-empty above");
176
- match &latest.label {
177
- None => BootDecision::LatestUnnamed { ordinal: latest.ordinal },
178
- Some(label) => BootDecision::Mint {
189
+ };
190
+ latest.label.as_ref().map_or(
191
+ BootDecision::LatestUnnamed {
192
+ ordinal: latest.ordinal,
193
+ },
194
+ |label| BootDecision::Mint {
179
195
  ordinal: latest.ordinal + 1,
180
196
  from_ordinal: latest.ordinal,
181
197
  from_label: label.clone(),
182
198
  },
183
- }
199
+ )
184
200
  }
185
201
 
186
202
  async fn provisioner<C: GenericClient>(client: &C, journal: &str) -> anyhow::Result<bool> {
@@ -190,13 +206,14 @@ async fn provisioner<C: GenericClient>(client: &C, journal: &str) -> anyhow::Res
190
206
  &[&journal],
191
207
  )
192
208
  .await?;
193
- Ok(match row {
194
- None => true,
195
- Some(row) => row.get::<_, bool>("owned"),
196
- })
209
+ Ok(row.is_none_or(|row| row.get::<_, bool>("owned")))
197
210
  }
198
211
 
199
- async fn partition_attached<C: GenericClient>(client: &C, partition_name: &str, journal: &str) -> anyhow::Result<bool> {
212
+ async fn partition_attached<C: GenericClient>(
213
+ client: &C,
214
+ partition_name: &str,
215
+ journal: &str,
216
+ ) -> anyhow::Result<bool> {
200
217
  let row = client
201
218
  .query_opt(
202
219
  "SELECT 1 FROM pg_inherits i \
@@ -210,24 +227,36 @@ async fn partition_attached<C: GenericClient>(client: &C, partition_name: &str,
210
227
  Ok(row.is_some())
211
228
  }
212
229
 
213
- async fn ensure_partition<C: GenericClient>(client: &C, domain: &str, era: i32) -> anyhow::Result<()> {
230
+ async fn ensure_partition<C: GenericClient>(
231
+ client: &C,
232
+ domain: &str,
233
+ era: i32,
234
+ ) -> anyhow::Result<()> {
214
235
  let journal = journal_table(domain);
215
236
  let part = partition(domain, era);
216
237
  if partition_attached(client, &part, &journal).await? {
217
238
  return Ok(());
218
239
  }
219
240
  client
220
- .batch_execute(&format!("CREATE TABLE IF NOT EXISTS {} (LIKE {} INCLUDING DEFAULTS)", quote_ident(&part), quote_ident(&journal)))
241
+ .batch_execute(&format!(
242
+ "CREATE TABLE IF NOT EXISTS {} (LIKE {} INCLUDING DEFAULTS)",
243
+ quote_ident(&part),
244
+ quote_ident(&journal)
245
+ ))
221
246
  .await?;
222
247
  client
223
- .batch_execute(&format!("ALTER TABLE {} ATTACH PARTITION {} FOR VALUES IN ({era})", quote_ident(&journal), quote_ident(&part)))
248
+ .batch_execute(&format!(
249
+ "ALTER TABLE {} ATTACH PARTITION {} FOR VALUES IN ({era})",
250
+ quote_ident(&journal),
251
+ quote_ident(&part)
252
+ ))
224
253
  .await?;
225
254
  Ok(())
226
255
  }
227
256
 
228
257
  async fn install_transforms<C: GenericClient>(client: &C) -> anyhow::Result<()> {
229
258
  const FUNCTIONS: &[&str] = &[
230
- r#"CREATE OR REPLACE FUNCTION hecks_tr_extract(state jsonb, path text[], OUT remaining jsonb, OUT value jsonb, OUT present boolean)
259
+ r"CREATE OR REPLACE FUNCTION hecks_tr_extract(state jsonb, path text[], OUT remaining jsonb, OUT value jsonb, OUT present boolean)
231
260
  LANGUAGE plpgsql IMMUTABLE AS $fn$
232
261
  DECLARE parent jsonb; leaf text;
233
262
  BEGIN
@@ -253,8 +282,8 @@ BEGIN
253
282
  remaining := jsonb_set(state, path[1:array_upper(path, 1) - 1], parent);
254
283
  END IF;
255
284
  END IF;
256
- END $fn$"#,
257
- r#"CREATE OR REPLACE FUNCTION hecks_tr_insert(state jsonb, path text[], value jsonb, rule_label text) RETURNS jsonb
285
+ END $fn$",
286
+ r"CREATE OR REPLACE FUNCTION hecks_tr_insert(state jsonb, path text[], value jsonb, rule_label text) RETURNS jsonb
258
287
  LANGUAGE plpgsql IMMUTABLE AS $fn$
259
288
  BEGIN
260
289
  IF array_length(path, 1) = 1 THEN
@@ -268,22 +297,22 @@ BEGIN
268
297
  state := state || jsonb_build_object(path[1], '{}'::jsonb);
269
298
  END IF;
270
299
  RETURN jsonb_set(state, path, value);
271
- END $fn$"#,
272
- r#"CREATE OR REPLACE FUNCTION hecks_tr_rename(state jsonb, old_name text, new_name text) RETURNS jsonb
300
+ END $fn$",
301
+ r"CREATE OR REPLACE FUNCTION hecks_tr_rename(state jsonb, old_name text, new_name text) RETURNS jsonb
273
302
  LANGUAGE sql IMMUTABLE AS $fn$
274
303
  SELECT CASE WHEN state ? old_name
275
304
  THEN (state - old_name) || jsonb_build_object(new_name, state -> old_name)
276
305
  ELSE state END
277
- $fn$"#,
278
- r#"CREATE OR REPLACE FUNCTION hecks_tr_move(state jsonb, from_path text[], to_path text[], rule_label text) RETURNS jsonb
306
+ $fn$",
307
+ r"CREATE OR REPLACE FUNCTION hecks_tr_move(state jsonb, from_path text[], to_path text[], rule_label text) RETURNS jsonb
279
308
  LANGUAGE plpgsql IMMUTABLE AS $fn$
280
309
  DECLARE extracted record;
281
310
  BEGIN
282
311
  SELECT * INTO extracted FROM hecks_tr_extract(state, from_path);
283
312
  IF NOT extracted.present THEN RETURN state; END IF;
284
313
  RETURN hecks_tr_insert(extracted.remaining, to_path, extracted.value, rule_label);
285
- END $fn$"#,
286
- r#"CREATE OR REPLACE FUNCTION hecks_tr_convert(state jsonb, from_path text[], to_path text[], pairs jsonb, from_label text, rule_label text) RETURNS jsonb
314
+ END $fn$",
315
+ r"CREATE OR REPLACE FUNCTION hecks_tr_convert(state jsonb, from_path text[], to_path text[], pairs jsonb, from_label text, rule_label text) RETURNS jsonb
287
316
  LANGUAGE plpgsql IMMUTABLE AS $fn$
288
317
  DECLARE extracted record; pair jsonb;
289
318
  BEGIN
@@ -296,14 +325,14 @@ BEGIN
296
325
  END LOOP;
297
326
  RAISE EXCEPTION 'cannot translate %: % has no mapping in its convert''s values: table. Add % => ... to cover it.',
298
327
  from_label, extracted.value, extracted.value;
299
- END $fn$"#,
300
- r#"CREATE OR REPLACE FUNCTION hecks_tr_drop(state jsonb, path text[]) RETURNS jsonb
328
+ END $fn$",
329
+ r"CREATE OR REPLACE FUNCTION hecks_tr_drop(state jsonb, path text[]) RETURNS jsonb
301
330
  LANGUAGE plpgsql IMMUTABLE AS $fn$
302
331
  DECLARE extracted record;
303
332
  BEGIN
304
333
  SELECT * INTO extracted FROM hecks_tr_extract(state, path);
305
334
  RETURN extracted.remaining;
306
- END $fn$"#,
335
+ END $fn$",
307
336
  ];
308
337
  for sql in FUNCTIONS {
309
338
  client.batch_execute(sql).await?;
@@ -327,9 +356,15 @@ pub async fn ensure_base<C: GenericClient>(client: &C, domain: &str) -> anyhow::
327
356
  PRIMARY KEY (domain, ordinal))",
328
357
  )
329
358
  .await?;
330
- client.batch_execute("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_digest text").await?;
331
- client.batch_execute("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_projection jsonb").await?;
332
- client.batch_execute("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS canon_form int").await?;
359
+ client
360
+ .batch_execute("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_digest text")
361
+ .await?;
362
+ client
363
+ .batch_execute("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_projection jsonb")
364
+ .await?;
365
+ client
366
+ .batch_execute("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS canon_form int")
367
+ .await?;
333
368
  client
334
369
  .batch_execute(
335
370
  "CREATE TABLE IF NOT EXISTS hecks_era_texts (\
@@ -340,18 +375,19 @@ pub async fn ensure_base<C: GenericClient>(client: &C, domain: &str) -> anyhow::
340
375
  .await?;
341
376
  client
342
377
  .batch_execute(
343
- "CREATE TABLE IF NOT EXISTS hecks_approvals (\
344
- domain text NOT NULL, from_label text NOT NULL, to_label text NOT NULL, \
345
- edge_digest text NOT NULL, reviewed_ordinal bigint NOT NULL, \
346
- approved_at timestamptz NOT NULL DEFAULT now())",
378
+ "CREATE TABLE IF NOT EXISTS hecks_backfill_progress (\
379
+ target text PRIMARY KEY, cursor text, completed boolean NOT NULL DEFAULT false, \
380
+ updated_at timestamptz NOT NULL DEFAULT now())",
347
381
  )
348
382
  .await?;
349
- client.batch_execute("CREATE TABLE IF NOT EXISTS hecks_backfill_progress (\
350
- target text PRIMARY KEY, cursor text, completed boolean NOT NULL DEFAULT false, \
351
- updated_at timestamptz NOT NULL DEFAULT now())").await?;
352
383
 
353
384
  let seq = sequence(domain);
354
- client.batch_execute(&format!("CREATE SEQUENCE IF NOT EXISTS {}", quote_ident(&seq))).await?;
385
+ client
386
+ .batch_execute(&format!(
387
+ "CREATE SEQUENCE IF NOT EXISTS {}",
388
+ quote_ident(&seq)
389
+ ))
390
+ .await?;
355
391
  client
356
392
  .batch_execute(&format!(
357
393
  "CREATE TABLE IF NOT EXISTS {} (\
@@ -373,10 +409,14 @@ pub async fn ensure_base<C: GenericClient>(client: &C, domain: &str) -> anyhow::
373
409
  &[&journal],
374
410
  )
375
411
  .await?
376
- .map(|row| row.get::<_, bool>(0))
377
- .unwrap_or(true);
412
+ .is_none_or(|row| row.get::<_, bool>(0));
378
413
  if relacl_null {
379
- client.batch_execute(&format!("REVOKE UPDATE, DELETE ON {} FROM PUBLIC", quote_ident(&journal))).await?;
414
+ client
415
+ .batch_execute(&format!(
416
+ "REVOKE UPDATE, DELETE ON {} FROM PUBLIC",
417
+ quote_ident(&journal)
418
+ ))
419
+ .await?;
380
420
  }
381
421
 
382
422
  let current = client
@@ -385,12 +425,27 @@ pub async fn ensure_base<C: GenericClient>(client: &C, domain: &str) -> anyhow::
385
425
  &[&journal],
386
426
  )
387
427
  .await?;
388
- let (rowsecurity, forcerowsecurity) = current.map(|row| (row.get::<_, bool>("relrowsecurity"), row.get::<_, bool>("relforcerowsecurity"))).unwrap_or((false, false));
428
+ let (rowsecurity, forcerowsecurity) = current.map_or((false, false), |row| {
429
+ (
430
+ row.get::<_, bool>("relrowsecurity"),
431
+ row.get::<_, bool>("relforcerowsecurity"),
432
+ )
433
+ });
389
434
  if !rowsecurity {
390
- client.batch_execute(&format!("ALTER TABLE {} ENABLE ROW LEVEL SECURITY", quote_ident(&journal))).await?;
435
+ client
436
+ .batch_execute(&format!(
437
+ "ALTER TABLE {} ENABLE ROW LEVEL SECURITY",
438
+ quote_ident(&journal)
439
+ ))
440
+ .await?;
391
441
  }
392
442
  if !forcerowsecurity {
393
- client.batch_execute(&format!("ALTER TABLE {} FORCE ROW LEVEL SECURITY", quote_ident(&journal))).await?;
443
+ client
444
+ .batch_execute(&format!(
445
+ "ALTER TABLE {} FORCE ROW LEVEL SECURITY",
446
+ quote_ident(&journal)
447
+ ))
448
+ .await?;
394
449
  }
395
450
 
396
451
  install_transforms(client).await?;
@@ -399,15 +454,27 @@ pub async fn ensure_base<C: GenericClient>(client: &C, domain: &str) -> anyhow::
399
454
 
400
455
  const CHUNK_SIZE: i64 = 5_000;
401
456
 
402
- async fn backfill_progress<C: GenericClient>(client: &C, target: &str) -> anyhow::Result<(Option<String>, bool)> {
403
- let row = client.query_opt("SELECT cursor, completed FROM hecks_backfill_progress WHERE target = $1", &[&target]).await?;
404
- Ok(match row {
405
- None => (None, false),
406
- Some(row) => (row.get("cursor"), row.get("completed")),
407
- })
457
+ async fn backfill_progress<C: GenericClient>(
458
+ client: &C,
459
+ target: &str,
460
+ ) -> anyhow::Result<(Option<String>, bool)> {
461
+ let row = client
462
+ .query_opt(
463
+ "SELECT cursor, completed FROM hecks_backfill_progress WHERE target = $1",
464
+ &[&target],
465
+ )
466
+ .await?;
467
+ Ok(row.map_or((None, false), |row| {
468
+ (row.get("cursor"), row.get("completed"))
469
+ }))
408
470
  }
409
471
 
410
- async fn upsert_backfill_progress<C: GenericClient>(client: &C, target: &str, cursor: Option<&str>, completed: bool) -> anyhow::Result<()> {
472
+ async fn upsert_backfill_progress<C: GenericClient>(
473
+ client: &C,
474
+ target: &str,
475
+ cursor: Option<&str>,
476
+ completed: bool,
477
+ ) -> anyhow::Result<()> {
411
478
  client
412
479
  .execute(
413
480
  "INSERT INTO hecks_backfill_progress (target, cursor, completed, updated_at) VALUES ($1, $2, $3, now()) \
@@ -420,8 +487,16 @@ async fn upsert_backfill_progress<C: GenericClient>(client: &C, target: &str, cu
420
487
 
421
488
  /// One chunk of `head_snapshot`'s backfill, SAVEPOINT-nested since this
422
489
  /// always runs inside the mint transaction `mint_era` already opened.
423
- async fn run_backfill_chunk<C: GenericClient>(client: &C, domain: &str, storage_name: &str, era: i32, target: &str) -> anyhow::Result<bool> {
424
- client.batch_execute("SAVEPOINT hecks_backfill_chunk").await?;
490
+ async fn run_backfill_chunk<C: GenericClient>(
491
+ client: &C,
492
+ domain: &str,
493
+ storage_name: &str,
494
+ era: i32,
495
+ target: &str,
496
+ ) -> anyhow::Result<bool> {
497
+ client
498
+ .batch_execute("SAVEPOINT hecks_backfill_chunk")
499
+ .await?;
425
500
  let result: anyhow::Result<bool> = async {
426
501
  client.execute("SELECT pg_advisory_xact_lock(hashtext('hecks_field_cache:' || $1))", &[&target]).await?;
427
502
  let (cursor, completed) = backfill_progress(client, target).await?;
@@ -463,20 +538,35 @@ async fn run_backfill_chunk<C: GenericClient>(client: &C, domain: &str, storage_
463
538
  .await?;
464
539
  }
465
540
  let done = (rows.len() as i64) < CHUNK_SIZE;
466
- let last_cursor: String = rows.last().unwrap().get("id");
467
- upsert_backfill_progress(client, target, Some(&last_cursor), done).await?;
541
+ if let Some(last) = rows.last() {
542
+ let last_cursor: String = last.get("id");
543
+ upsert_backfill_progress(client, target, Some(&last_cursor), done).await?;
544
+ }
468
545
  Ok(done)
469
546
  }
470
547
  .await;
471
548
 
472
549
  match &result {
473
- Ok(_) => client.batch_execute("RELEASE SAVEPOINT hecks_backfill_chunk").await?,
474
- Err(_) => client.batch_execute("ROLLBACK TO SAVEPOINT hecks_backfill_chunk").await?,
550
+ Ok(_) => {
551
+ client
552
+ .batch_execute("RELEASE SAVEPOINT hecks_backfill_chunk")
553
+ .await?;
554
+ }
555
+ Err(_) => {
556
+ client
557
+ .batch_execute("ROLLBACK TO SAVEPOINT hecks_backfill_chunk")
558
+ .await?;
559
+ }
475
560
  }
476
561
  result
477
562
  }
478
563
 
479
- async fn backfill_head_snapshot<C: GenericClient>(client: &C, domain: &str, storage_name: &str, era: i32) -> anyhow::Result<()> {
564
+ async fn backfill_head_snapshot<C: GenericClient>(
565
+ client: &C,
566
+ domain: &str,
567
+ storage_name: &str,
568
+ era: i32,
569
+ ) -> anyhow::Result<()> {
480
570
  let target = head_snapshot(domain, storage_name, era);
481
571
  loop {
482
572
  if run_backfill_chunk(client, domain, storage_name, era, &target).await? {
@@ -501,36 +591,61 @@ pub async fn adopt_head_snapshots<C: GenericClient>(
501
591
  aggregates: &[Aggregate],
502
592
  era: i32,
503
593
  ) -> anyhow::Result<()> {
504
- client.batch_execute("BEGIN").await.context("opening a transaction to adopt head snapshots")?;
594
+ client
595
+ .batch_execute("BEGIN")
596
+ .await
597
+ .context("opening a transaction to adopt head snapshots")?;
505
598
  let result: anyhow::Result<()> = async {
506
599
  for aggregate in aggregates {
507
- create_head_snapshot(client, domain, &aggregate.storage_name, era).await.with_context(|| {
508
- format!("provisioning {domain}'s {} head snapshot for era {era}", aggregate.storage_name)
509
- })?;
600
+ create_head_snapshot(client, domain, &aggregate.storage_name, era)
601
+ .await
602
+ .with_context(|| {
603
+ format!(
604
+ "provisioning {domain}'s {} head snapshot for era {era}",
605
+ aggregate.storage_name
606
+ )
607
+ })?;
510
608
  }
511
609
  Ok(())
512
610
  }
513
611
  .await;
514
612
  match &result {
515
- Ok(_) => client.batch_execute("COMMIT").await?,
613
+ Ok(()) => client.batch_execute("COMMIT").await?,
516
614
  Err(_) => client.batch_execute("ROLLBACK").await?,
517
615
  }
518
616
  result
519
617
  }
520
618
 
521
- async fn ensure_head_snapshot<C: GenericClient>(client: &C, domain: &str, storage_name: &str, era: i32) -> anyhow::Result<()> {
619
+ async fn ensure_head_snapshot<C: GenericClient>(
620
+ client: &C,
621
+ domain: &str,
622
+ storage_name: &str,
623
+ era: i32,
624
+ ) -> anyhow::Result<()> {
522
625
  create_head_snapshot(client, domain, storage_name, era).await?;
523
626
  backfill_head_snapshot(client, domain, storage_name, era).await
524
627
  }
525
628
 
526
629
  /// The create half, shared by minting and adopting. Columns match Ruby's
527
630
  /// own shape, so a Ruby runtime can still compile a head view over this table.
528
- async fn create_head_snapshot<C: GenericClient>(client: &C, domain: &str, storage_name: &str, era: i32) -> anyhow::Result<()> {
631
+ async fn create_head_snapshot<C: GenericClient>(
632
+ client: &C,
633
+ domain: &str,
634
+ storage_name: &str,
635
+ era: i32,
636
+ ) -> anyhow::Result<()> {
529
637
  let name = head_snapshot(domain, storage_name, era);
530
638
  if !table_exists(client, &name).await? {
531
- client.batch_execute("SAVEPOINT hecks_head_snapshot").await?;
639
+ client
640
+ .batch_execute("SAVEPOINT hecks_head_snapshot")
641
+ .await?;
532
642
  let result: anyhow::Result<()> = async {
533
- client.execute("SELECT pg_advisory_xact_lock(hashtext('hecks_head_snapshot:' || $1))", &[&name]).await?;
643
+ client
644
+ .execute(
645
+ "SELECT pg_advisory_xact_lock(hashtext('hecks_head_snapshot:' || $1))",
646
+ &[&name],
647
+ )
648
+ .await?;
534
649
  if !table_exists(client, &name).await? {
535
650
  client
536
651
  .batch_execute(&format!(
@@ -544,8 +659,16 @@ async fn create_head_snapshot<C: GenericClient>(client: &C, domain: &str, storag
544
659
  }
545
660
  .await;
546
661
  match &result {
547
- Ok(_) => client.batch_execute("RELEASE SAVEPOINT hecks_head_snapshot").await?,
548
- Err(_) => client.batch_execute("ROLLBACK TO SAVEPOINT hecks_head_snapshot").await?,
662
+ Ok(()) => {
663
+ client
664
+ .batch_execute("RELEASE SAVEPOINT hecks_head_snapshot")
665
+ .await?;
666
+ }
667
+ Err(_) => {
668
+ client
669
+ .batch_execute("ROLLBACK TO SAVEPOINT hecks_head_snapshot")
670
+ .await?;
671
+ }
549
672
  }
550
673
  result?;
551
674
  }
@@ -559,18 +682,30 @@ fn names_by_era(current_name: &str, edges: &[&Edge]) -> (Vec<String>, Vec<String
559
682
  current[edges.len()] = current_name.to_string();
560
683
  for index in (0..edges.len()).rev() {
561
684
  let declared = edges[index].for_aggregate(&current[index + 1]);
562
- current[index] = declared.and_then(|d| d.was.clone()).unwrap_or_else(|| current[index + 1].clone());
685
+ current[index] = declared
686
+ .and_then(|d| d.was.clone())
687
+ .unwrap_or_else(|| current[index + 1].clone());
563
688
  }
564
- let storage = current.iter().map(|name| storage_shape_snake(name)).collect();
689
+ let storage = current
690
+ .iter()
691
+ .map(|name| crate::journal::snake(name))
692
+ .collect();
565
693
  (current, storage)
566
694
  }
567
695
 
568
- fn ancestor_tail_sql(domain: &str, era: i32, storage_names: &[String], watermarks: &std::collections::HashMap<i32, Option<i64>>) -> String {
696
+ fn ancestor_tail_sql(
697
+ domain: &str,
698
+ era: i32,
699
+ storage_names: &[String],
700
+ watermarks: &std::collections::HashMap<i32, Option<i64>>,
701
+ ) -> String {
569
702
  let journal = quote_ident(&journal_table(domain));
570
703
  (1..era)
571
704
  .map(|ancestor| {
572
705
  let cut = watermarks.get(&(ancestor + 1)).copied().flatten();
573
- let cut_clause = cut.map(|c| format!(" AND ordinal <= {c}")).unwrap_or_default();
706
+ let cut_clause = cut
707
+ .map(|c| format!(" AND ordinal <= {c}"))
708
+ .unwrap_or_default();
574
709
  format!(
575
710
  "SELECT ordinal, era, aggregate, aggregate_id, operation, state FROM {journal} \
576
711
  WHERE era = {ancestor} AND aggregate = {}{cut_clause}",
@@ -590,7 +725,13 @@ fn latest_per_id(tail: &str) -> String {
590
725
 
591
726
  // Each edge CTE is `MATERIALIZED` — unfenced, Postgres re-inlines every edge's
592
727
  // expression at each read of `state`, and the plan grows exponentially with the chain.
593
- fn chain_sql(domain: &str, current_name: &str, era: i32, edges: &[&Edge], watermarks: &std::collections::HashMap<i32, Option<i64>>) -> String {
728
+ fn chain_sql(
729
+ domain: &str,
730
+ current_name: &str,
731
+ era: i32,
732
+ edges: &[&Edge],
733
+ watermarks: &std::collections::HashMap<i32, Option<i64>>,
734
+ ) -> String {
594
735
  chain_sql_with(domain, current_name, era, edges, watermarks, true)
595
736
  }
596
737
 
@@ -602,7 +743,11 @@ fn chain_sql_with(
602
743
  watermarks: &std::collections::HashMap<i32, Option<i64>>,
603
744
  materialize_edges: bool,
604
745
  ) -> String {
605
- let fence = if materialize_edges { "MATERIALIZED " } else { "" };
746
+ let fence = if materialize_edges {
747
+ "MATERIALIZED "
748
+ } else {
749
+ ""
750
+ };
606
751
  let (current_names, storage_names) = names_by_era(current_name, edges);
607
752
  let tail = latest_per_id(&ancestor_tail_sql(domain, era, &storage_names, watermarks));
608
753
 
@@ -611,15 +756,18 @@ fn chain_sql_with(
611
756
  .enumerate()
612
757
  .map(|(index, edge)| {
613
758
  let declared = edge.for_aggregate(&current_names[index + 1]);
614
- let expression = declared.map(|d| d.compiled_state_expression.clone()).unwrap_or_else(|| "state".to_string());
759
+ let expression = declared.map_or_else(|| "state".to_string(), |d| d.compiled_state_expression.clone());
615
760
  let guard = format!("era <= {} AND operation = 'save'", index + 1);
616
- let id_column = match declared.filter(|d| d.compiled_id_expression.is_some()) {
617
- Some(d) => format!(
618
- "CASE WHEN {guard} THEN {} ELSE aggregate_id END AS aggregate_id",
619
- d.compiled_id_expression.as_deref().unwrap()
620
- ),
621
- None => "aggregate_id".to_string(),
622
- };
761
+ let id_column = declared
762
+ .and_then(|d| d.compiled_id_expression.as_deref())
763
+ .map_or_else(
764
+ || "aggregate_id".to_string(),
765
+ |id_expression| {
766
+ format!(
767
+ "CASE WHEN {guard} THEN {id_expression} ELSE aggregate_id END AS aggregate_id"
768
+ )
769
+ },
770
+ );
623
771
  let from = if index == 0 { "tail".to_string() } else { format!("edge_{index}") };
624
772
  format!(
625
773
  "edge_{} AS {fence}(SELECT ordinal, era, {id_column}, operation, CASE WHEN {guard} THEN {expression} ELSE state END AS state FROM {from})",
@@ -632,7 +780,10 @@ fn chain_sql_with(
632
780
  }
633
781
 
634
782
  /// Reduces a chain SQL to one row per id: the latest entry, only if it was a save.
635
- async fn latest_of<C: GenericClient>(client: &C, inner_sql: &str) -> anyhow::Result<std::collections::HashMap<String, Value>> {
783
+ async fn latest_of<C: GenericClient>(
784
+ client: &C,
785
+ inner_sql: &str,
786
+ ) -> anyhow::Result<std::collections::HashMap<String, Value>> {
636
787
  let sql = format!(
637
788
  "SELECT aggregate_id, state FROM (\
638
789
  SELECT DISTINCT ON (aggregate_id) aggregate_id, operation, state \
@@ -640,7 +791,15 @@ async fn latest_of<C: GenericClient>(client: &C, inner_sql: &str) -> anyhow::Res
640
791
  ) latest WHERE operation = 'save'"
641
792
  );
642
793
  let rows = client.query(&sql, &[]).await?;
643
- Ok(rows.into_iter().map(|row| (row.get::<_, String>("aggregate_id"), row.get::<_, Value>("state"))).collect())
794
+ Ok(rows
795
+ .into_iter()
796
+ .map(|row| {
797
+ (
798
+ row.get::<_, String>("aggregate_id"),
799
+ row.get::<_, Value>("state"),
800
+ )
801
+ })
802
+ .collect())
644
803
  }
645
804
 
646
805
  /// The translated tail at era `era`, latest per id, saves only — the audit's `after`.
@@ -652,7 +811,11 @@ async fn translated_latest<C: GenericClient>(
652
811
  edges: &[&Edge],
653
812
  watermarks: &std::collections::HashMap<i32, Option<i64>>,
654
813
  ) -> anyhow::Result<std::collections::HashMap<String, Value>> {
655
- latest_of(client, &chain_sql(domain, current_name, era, edges, watermarks)).await
814
+ latest_of(
815
+ client,
816
+ &chain_sql(domain, current_name, era, edges, watermarks),
817
+ )
818
+ .await
656
819
  }
657
820
 
658
821
  /// The untranslated ancestor tail, latest per id — the "before" side of the
@@ -670,12 +833,21 @@ async fn ancestor_latest<C: GenericClient>(
670
833
  if tail.is_empty() {
671
834
  return Ok(std::collections::HashMap::new());
672
835
  }
673
- latest_of(client, &format!("SELECT ordinal, era, aggregate_id, operation, state FROM ({tail}) tail_rows")).await
836
+ latest_of(
837
+ client,
838
+ &format!("SELECT ordinal, era, aggregate_id, operation, state FROM ({tail}) tail_rows"),
839
+ )
840
+ .await
674
841
  }
675
842
 
676
843
  /// One aggregate's raw rule JSON within one edge; `reference_transform::translate`
677
844
  /// needs the declarative rules themselves, not just the compiled SQL.
678
- fn raw_declared<'a>(raw_edges: &'a [Value], domain: &str, edge: &Edge, aggregate_name: &str) -> Option<&'a Value> {
845
+ fn raw_declared<'a>(
846
+ raw_edges: &'a [Value],
847
+ domain: &str,
848
+ edge: &Edge,
849
+ aggregate_name: &str,
850
+ ) -> Option<&'a Value> {
679
851
  raw_edges
680
852
  .iter()
681
853
  .find(|candidate| {
@@ -684,7 +856,10 @@ fn raw_declared<'a>(raw_edges: &'a [Value], domain: &str, edge: &Edge, aggregate
684
856
  && candidate.get("to").and_then(Value::as_str) == Some(edge.to.as_str())
685
857
  })
686
858
  .and_then(|raw_edge| raw_edge.get("aggregates").and_then(Value::as_array))
687
- .and_then(|aggs| aggs.iter().find(|agg| agg.get("name").and_then(Value::as_str) == Some(aggregate_name)))
859
+ .and_then(|aggs| {
860
+ aggs.iter()
861
+ .find(|agg| agg.get("name").and_then(Value::as_str) == Some(aggregate_name))
862
+ })
688
863
  }
689
864
 
690
865
  /// Layer 2 of the mint audit for one aggregate: per-rule value preservation
@@ -693,6 +868,7 @@ fn raw_declared<'a>(raw_edges: &'a [Value], domain: &str, edge: &Edge, aggregate
693
868
  ///
694
869
  /// `declared` is the raw rule JSON for the edge being minted this boot only —
695
870
  /// earlier edges in a multi-hop drift were already minted and audited.
871
+ #[allow(clippy::too_many_arguments)]
696
872
  async fn audit_layer_two<C: GenericClient>(
697
873
  client: &C,
698
874
  domain: &str,
@@ -705,14 +881,28 @@ async fn audit_layer_two<C: GenericClient>(
705
881
  violations: &mut Vec<String>,
706
882
  ) -> anyhow::Result<()> {
707
883
  let before = if edges.len() > 1 {
708
- translated_latest(client, domain, &aggregate.name, ordinal, &edges[..edges.len() - 1], watermarks).await?
884
+ translated_latest(
885
+ client,
886
+ domain,
887
+ &aggregate.name,
888
+ ordinal,
889
+ &edges[..edges.len() - 1],
890
+ watermarks,
891
+ )
892
+ .await?
709
893
  } else {
710
894
  ancestor_latest(client, domain, &aggregate.name, ordinal, edges, watermarks).await?
711
895
  };
712
896
 
713
- let last_edge = edges.last().expect("mint always audits at least one edge");
897
+ let Some(last_edge) = edges.last() else {
898
+ return Ok(()); // nothing to audit across zero edges
899
+ };
714
900
  let declared = raw_declared(raw_edges, domain, last_edge, &aggregate.name);
715
- let rekeyed = declared.map(|d| !d.get("rekeys").and_then(Value::as_array).map(Vec::is_empty).unwrap_or(true)).unwrap_or(false);
901
+ let rekeyed = declared.is_some_and(|d| {
902
+ !d.get("rekeys")
903
+ .and_then(Value::as_array)
904
+ .is_none_or(Vec::is_empty)
905
+ });
716
906
 
717
907
  if rekeyed {
718
908
  if before.len() != after.len() {
@@ -725,8 +915,14 @@ async fn audit_layer_two<C: GenericClient>(
725
915
  ));
726
916
  }
727
917
  } else {
728
- let mut lost: Vec<&String> = before.keys().filter(|id| !after.contains_key(*id)).collect();
729
- let mut gained: Vec<&String> = after.keys().filter(|id| !before.contains_key(*id)).collect();
918
+ let mut lost: Vec<&String> = before
919
+ .keys()
920
+ .filter(|id| !after.contains_key(*id))
921
+ .collect();
922
+ let mut gained: Vec<&String> = after
923
+ .keys()
924
+ .filter(|id| !before.contains_key(*id))
925
+ .collect();
730
926
  if !lost.is_empty() || !gained.is_empty() {
731
927
  lost.sort();
732
928
  gained.sort();
@@ -737,31 +933,20 @@ async fn audit_layer_two<C: GenericClient>(
737
933
  }
738
934
  }
739
935
 
740
- let Some(declared) = declared else { return Ok(()) };
936
+ let Some(declared) = declared else {
937
+ return Ok(());
938
+ };
741
939
  if rekeyed {
742
940
  return Ok(());
743
941
  }
744
942
 
745
- let compute_tops: std::collections::HashSet<String> = declared
746
- .get("computes")
747
- .and_then(Value::as_array)
748
- .map(|computes| {
749
- computes
750
- .iter()
751
- .flat_map(|c| [c.get("from"), c.get("to")])
752
- .flatten()
753
- .filter_map(Value::as_str)
754
- .map(|path| path.split('.').next().unwrap_or(path).to_string())
755
- .collect()
756
- })
757
- .unwrap_or_default();
758
-
759
943
  for (id, state) in &before {
760
- let Some(actual_after) = after.get(id) else { continue };
944
+ let Some(actual_after) = after.get(id) else {
945
+ continue;
946
+ };
761
947
  let expected = reference_transform::translate(declared, state)?;
762
- let expected = strip_keys(&expected, &compute_tops);
763
- let actual = strip_keys(actual_after, &compute_tops);
764
- if expected != actual {
948
+ let actual = actual_after;
949
+ if &expected != actual {
765
950
  let mut diverged: Vec<String> = expected
766
951
  .as_object()
767
952
  .into_iter()
@@ -772,23 +957,22 @@ async fn audit_layer_two<C: GenericClient>(
772
957
  .collect();
773
958
  diverged.sort();
774
959
  diverged.dedup();
775
- violations.push(format!("{}#{id}: the translated state diverges from the reference transform at {}", aggregate.name, diverged.join(", ")));
960
+ violations.push(format!(
961
+ "{}#{id}: the translated state diverges from the reference transform at {}",
962
+ aggregate.name,
963
+ diverged.join(", ")
964
+ ));
776
965
  }
777
966
  }
778
967
  Ok(())
779
968
  }
780
969
 
781
- fn strip_keys(state: &Value, keys: &std::collections::HashSet<String>) -> Value {
782
- match state {
783
- Value::Object(map) => Value::Object(map.iter().filter(|(k, _)| !keys.contains(*k)).map(|(k, v)| (k.clone(), v.clone())).collect()),
784
- other => other.clone(),
785
- }
786
- }
787
-
788
970
  /// The whole mint-time audit, over every lineage-capable aggregate. Runs
789
971
  /// before anything is minted, so a refusal leaves no half-born era.
790
972
  ///
791
973
  /// `ir` is the whole `ir.json` value; Layer 1 looks up each aggregate's node from it.
974
+ #[allow(clippy::implicit_hasher)] // reason: callers build the map with the default hasher; no caller needs another
975
+ #[allow(clippy::too_many_arguments)]
792
976
  pub async fn audit_before_mint<C: GenericClient>(
793
977
  client: &C,
794
978
  domain: &str,
@@ -802,13 +986,21 @@ pub async fn audit_before_mint<C: GenericClient>(
802
986
  let ir_aggregates: std::collections::HashMap<&str, &Value> = ir
803
987
  .get("aggregates")
804
988
  .and_then(Value::as_array)
805
- .map(|list| list.iter().filter_map(|agg| agg.get("name").and_then(Value::as_str).map(|n| (n, agg))).collect())
989
+ .map(|list| {
990
+ list.iter()
991
+ .filter_map(|agg| agg.get("name").and_then(Value::as_str).map(|n| (n, agg)))
992
+ .collect()
993
+ })
806
994
  .unwrap_or_default();
807
995
 
808
996
  let mut violations = Vec::new();
809
997
  for aggregate in aggregates {
810
- let phase = crate::log::phase_with("audit_aggregate", serde_json::json!({ "aggregate": &aggregate.storage_name }));
811
- let after = translated_latest(client, domain, &aggregate.name, ordinal, edges, watermarks).await?;
998
+ let phase = crate::log::phase_with(
999
+ "audit_aggregate",
1000
+ serde_json::json!({ "aggregate": &aggregate.storage_name }),
1001
+ );
1002
+ let after =
1003
+ translated_latest(client, domain, &aggregate.name, ordinal, edges, watermarks).await?;
812
1004
 
813
1005
  // Layer 1 — structural (types/patterns/admits/lifecycle) — over
814
1006
  // every record `after` actually holds, matching Ruby's own
@@ -821,13 +1013,27 @@ pub async fn audit_before_mint<C: GenericClient>(
821
1013
 
822
1014
  // Layer 2 — per-rule value preservation against the reference
823
1015
  // transform, id-set/count conservation.
824
- audit_layer_two(client, domain, aggregate, ordinal, edges, raw_edges, watermarks, &after, &mut violations).await?;
1016
+ audit_layer_two(
1017
+ client,
1018
+ domain,
1019
+ aggregate,
1020
+ ordinal,
1021
+ edges,
1022
+ raw_edges,
1023
+ watermarks,
1024
+ &after,
1025
+ &mut violations,
1026
+ )
1027
+ .await?;
825
1028
  phase.end_with(serde_json::json!({ "records": after.len() }));
826
1029
  }
827
1030
  if violations.is_empty() {
828
1031
  return Ok(());
829
1032
  }
830
- anyhow::bail!("cannot mint era {ordinal} of {domain}: the audit refused —\n - {}", violations.join("\n - "));
1033
+ anyhow::bail!(
1034
+ "cannot mint era {ordinal} of {domain}: the audit refused —\n - {}",
1035
+ violations.join("\n - ")
1036
+ );
831
1037
  }
832
1038
 
833
1039
  /// `compile_head!` — one aggregate's matview + head-snapshot + head
@@ -845,7 +1051,12 @@ async fn compile_head<C: GenericClient>(
845
1051
  let view = matview(domain, storage_name, era, label);
846
1052
  let body = chain_sql(domain, &aggregate.name, era, edges, watermarks);
847
1053
 
848
- client.batch_execute(&format!("CREATE MATERIALIZED VIEW {} AS\n{body}", quote_ident(&view))).await?;
1054
+ client
1055
+ .batch_execute(&format!(
1056
+ "CREATE MATERIALIZED VIEW {} AS\n{body}",
1057
+ quote_ident(&view)
1058
+ ))
1059
+ .await?;
849
1060
  client
850
1061
  .batch_execute(&format!(
851
1062
  "CREATE INDEX IF NOT EXISTS {} ON {} (aggregate_id, ordinal DESC)",
@@ -856,7 +1067,12 @@ async fn compile_head<C: GenericClient>(
856
1067
 
857
1068
  ensure_head_snapshot(client, domain, storage_name, era).await?;
858
1069
 
859
- client.batch_execute(&format!("DROP VIEW IF EXISTS {}", quote_ident(&head_view(domain, storage_name)))).await?;
1070
+ client
1071
+ .batch_execute(&format!(
1072
+ "DROP VIEW IF EXISTS {}",
1073
+ quote_ident(&head_view(domain, storage_name))
1074
+ ))
1075
+ .await?;
860
1076
  client
861
1077
  .batch_execute(&format!(
862
1078
  "CREATE VIEW {} AS \
@@ -874,8 +1090,15 @@ async fn compile_head<C: GenericClient>(
874
1090
  .await?;
875
1091
  Ok(())
876
1092
  }
877
- /// Mints era 1: fresh domain, no edge, never needs the approval gate.
878
- pub async fn hold_first<C: GenericClient>(client: &C, domain: &str, held_text: &str, ir: &Value, aggregates: &[Aggregate], role: Option<&str>) -> anyhow::Result<()> {
1093
+ /// Mints era 1: fresh domain, no edge to translate.
1094
+ pub async fn hold_first<C: GenericClient>(
1095
+ client: &C,
1096
+ domain: &str,
1097
+ held_text: &str,
1098
+ ir: &Value,
1099
+ aggregates: &[Aggregate],
1100
+ role: Option<&str>,
1101
+ ) -> anyhow::Result<()> {
879
1102
  ensure_base(client, domain).await?;
880
1103
 
881
1104
  // Own transaction: `ensure_first_head`'s `ensure_head_snapshot` backfill
@@ -883,7 +1106,7 @@ pub async fn hold_first<C: GenericClient>(client: &C, domain: &str, held_text: &
883
1106
  client.batch_execute("BEGIN").await?;
884
1107
  let result = hold_first_body(client, domain, held_text, ir, aggregates, role).await;
885
1108
  match &result {
886
- Ok(_) => client.batch_execute("COMMIT").await?,
1109
+ Ok(()) => client.batch_execute("COMMIT").await?,
887
1110
  Err(_) => {
888
1111
  let _ = client.batch_execute("ROLLBACK").await;
889
1112
  }
@@ -891,7 +1114,14 @@ pub async fn hold_first<C: GenericClient>(client: &C, domain: &str, held_text: &
891
1114
  result
892
1115
  }
893
1116
 
894
- async fn hold_first_body<C: GenericClient>(client: &C, domain: &str, held_text: &str, ir: &Value, aggregates: &[Aggregate], role: Option<&str>) -> anyhow::Result<()> {
1117
+ async fn hold_first_body<C: GenericClient>(
1118
+ client: &C,
1119
+ domain: &str,
1120
+ held_text: &str,
1121
+ ir: &Value,
1122
+ aggregates: &[Aggregate],
1123
+ role: Option<&str>,
1124
+ ) -> anyhow::Result<()> {
895
1125
  let hash = storage_shape::mint_hash(ir);
896
1126
  let label = storage_shape::mint_label(ir);
897
1127
  let digest = format!("{:x}", Sha256::digest(held_text.as_bytes()));
@@ -917,7 +1147,11 @@ async fn hold_first_body<C: GenericClient>(client: &C, domain: &str, held_text:
917
1147
  /// `ensure_first_head!` — era 1's head is a plain view over its own
918
1148
  /// (immediately-created) snapshot table, no chain to compile, no
919
1149
  /// translation possible from nothing.
920
- async fn ensure_first_head<C: GenericClient>(client: &C, domain: &str, storage_name: &str) -> anyhow::Result<()> {
1150
+ async fn ensure_first_head<C: GenericClient>(
1151
+ client: &C,
1152
+ domain: &str,
1153
+ storage_name: &str,
1154
+ ) -> anyhow::Result<()> {
921
1155
  ensure_head_snapshot(client, domain, storage_name, 1).await?;
922
1156
  client
923
1157
  .batch_execute(&format!(
@@ -929,7 +1163,13 @@ async fn ensure_first_head<C: GenericClient>(client: &C, domain: &str, storage_n
929
1163
  Ok(())
930
1164
  }
931
1165
 
932
- async fn archive_text<C: GenericClient>(client: &C, domain: &str, ordinal: i32, text: &str, digest: &str) -> anyhow::Result<()> {
1166
+ async fn archive_text<C: GenericClient>(
1167
+ client: &C,
1168
+ domain: &str,
1169
+ ordinal: i32,
1170
+ text: &str,
1171
+ digest: &str,
1172
+ ) -> anyhow::Result<()> {
933
1173
  client
934
1174
  .execute(
935
1175
  "INSERT INTO hecks_era_texts (domain, ordinal, digest, held_text) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
@@ -939,25 +1179,57 @@ async fn archive_text<C: GenericClient>(client: &C, domain: &str, ordinal: i32,
939
1179
  Ok(())
940
1180
  }
941
1181
 
942
- pub(crate) async fn advance_era<C: GenericClient>(client: &C, domain: &str, ordinal: i32) -> anyhow::Result<()> {
1182
+ pub(crate) async fn advance_era<C: GenericClient>(
1183
+ client: &C,
1184
+ domain: &str,
1185
+ ordinal: i32,
1186
+ ) -> anyhow::Result<()> {
943
1187
  let journal = quote_ident(&journal_table(domain));
944
- client.batch_execute(&format!("DROP POLICY IF EXISTS hecks_current_era ON {journal}")).await?;
1188
+ client
1189
+ .batch_execute(&format!(
1190
+ "DROP POLICY IF EXISTS hecks_current_era ON {journal}"
1191
+ ))
1192
+ .await?;
945
1193
  client
946
1194
  .batch_execute(&format!("CREATE POLICY hecks_current_era ON {journal} FOR INSERT TO PUBLIC WITH CHECK (era = {ordinal})"))
947
1195
  .await?;
948
- client.batch_execute(&format!("DROP POLICY IF EXISTS hecks_read_all ON {journal}")).await?;
949
- client.batch_execute(&format!("CREATE POLICY hecks_read_all ON {journal} FOR SELECT TO PUBLIC USING (true)")).await?;
1196
+ client
1197
+ .batch_execute(&format!(
1198
+ "DROP POLICY IF EXISTS hecks_read_all ON {journal}"
1199
+ ))
1200
+ .await?;
1201
+ client
1202
+ .batch_execute(&format!(
1203
+ "CREATE POLICY hecks_read_all ON {journal} FOR SELECT TO PUBLIC USING (true)"
1204
+ ))
1205
+ .await?;
950
1206
  Ok(())
951
1207
  }
952
1208
 
953
- async fn grant_role<C: GenericClient>(client: &C, domain: &str, role: &str, aggregates: &[Aggregate], era: Option<i32>) -> anyhow::Result<()> {
1209
+ async fn grant_role<C: GenericClient>(
1210
+ client: &C,
1211
+ domain: &str,
1212
+ role: &str,
1213
+ aggregates: &[Aggregate],
1214
+ era: Option<i32>,
1215
+ ) -> anyhow::Result<()> {
954
1216
  let journal = journal_table(domain);
955
1217
  if !provisioner(client, &journal).await? {
956
1218
  return Ok(());
957
1219
  }
958
1220
  let quoted_role = quote_ident(role);
959
- client.batch_execute(&format!("GRANT INSERT, SELECT ON {} TO {quoted_role}", quote_ident(&journal))).await?;
960
- client.batch_execute(&format!("GRANT USAGE ON SEQUENCE {} TO {quoted_role}", quote_ident(&sequence(domain)))).await?;
1221
+ client
1222
+ .batch_execute(&format!(
1223
+ "GRANT INSERT, SELECT ON {} TO {quoted_role}",
1224
+ quote_ident(&journal)
1225
+ ))
1226
+ .await?;
1227
+ client
1228
+ .batch_execute(&format!(
1229
+ "GRANT USAGE ON SEQUENCE {} TO {quoted_role}",
1230
+ quote_ident(&sequence(domain))
1231
+ ))
1232
+ .await?;
961
1233
  let Some(era) = era else { return Ok(()) };
962
1234
  for aggregate in aggregates {
963
1235
  let storage_name = &aggregate.storage_name;
@@ -969,7 +1241,12 @@ async fn grant_role<C: GenericClient>(client: &C, domain: &str, role: &str, aggr
969
1241
  .await?;
970
1242
  let view = head_view(domain, storage_name);
971
1243
  if table_or_view_exists(client, &view).await? {
972
- client.batch_execute(&format!("GRANT SELECT ON {} TO {quoted_role}", quote_ident(&view))).await?;
1244
+ client
1245
+ .batch_execute(&format!(
1246
+ "GRANT SELECT ON {} TO {quoted_role}",
1247
+ quote_ident(&view)
1248
+ ))
1249
+ .await?;
973
1250
  }
974
1251
  }
975
1252
  Ok(())
@@ -982,8 +1259,8 @@ async fn table_or_view_exists<C: GenericClient>(client: &C, name: &str) -> anyho
982
1259
  Ok(row.is_some())
983
1260
  }
984
1261
 
985
- /// The one transaction that makes era `ordinal` real. Approval-gate checking
986
- /// is the caller's job; this function mints once asked to.
1262
+ /// The one transaction that makes era `ordinal` real; the caller decides when
1263
+ /// to mint, and this function mints once asked to.
987
1264
  #[allow(clippy::too_many_arguments)]
988
1265
  pub async fn mint_era<C: GenericClient>(
989
1266
  client: &C,
@@ -998,10 +1275,23 @@ pub async fn mint_era<C: GenericClient>(
998
1275
  lifecycle_defaults: &[LifecycleDefault],
999
1276
  ) -> anyhow::Result<()> {
1000
1277
  client.batch_execute("BEGIN").await?;
1001
- let result = mint_era_body(client, domain, ordinal, hash, label, held_text, aggregates, edges, role, lifecycle_defaults).await;
1278
+ let result = mint_era_body(
1279
+ client,
1280
+ domain,
1281
+ ordinal,
1282
+ hash,
1283
+ label,
1284
+ held_text,
1285
+ aggregates,
1286
+ edges,
1287
+ role,
1288
+ lifecycle_defaults,
1289
+ )
1290
+ .await;
1002
1291
  match &result {
1003
- Ok(_) => {
1004
- let phase = crate::log::phase_with("mint_commit", serde_json::json!({ "era": ordinal }));
1292
+ Ok(()) => {
1293
+ let phase =
1294
+ crate::log::phase_with("mint_commit", serde_json::json!({ "era": ordinal }));
1005
1295
  client.batch_execute("COMMIT").await?;
1006
1296
  phase.end();
1007
1297
  }
@@ -1011,7 +1301,6 @@ pub async fn mint_era<C: GenericClient>(
1011
1301
  }
1012
1302
  result
1013
1303
  }
1014
-
1015
1304
  #[allow(clippy::too_many_arguments)]
1016
1305
  async fn mint_era_body<C: GenericClient>(
1017
1306
  client: &C,
@@ -1025,12 +1314,25 @@ async fn mint_era_body<C: GenericClient>(
1025
1314
  role: Option<&str>,
1026
1315
  lifecycle_defaults: &[LifecycleDefault],
1027
1316
  ) -> anyhow::Result<()> {
1028
- client.batch_execute("SET LOCAL lock_timeout = '10s'").await?;
1029
1317
  client
1030
- .execute(&format!("SELECT pg_advisory_xact_lock(hashtext('hecks_eras:' || {}))", text_literal(domain)), &[])
1318
+ .batch_execute("SET LOCAL lock_timeout = '10s'")
1319
+ .await?;
1320
+ client
1321
+ .execute(
1322
+ &format!(
1323
+ "SELECT pg_advisory_xact_lock(hashtext('hecks_eras:' || {}))",
1324
+ text_literal(domain)
1325
+ ),
1326
+ &[],
1327
+ )
1031
1328
  .await?;
1032
1329
 
1033
- let already_minted = client.query_opt("SELECT 1 FROM hecks_eras WHERE domain = $1 AND ordinal = $2", &[&domain, &ordinal]).await?;
1330
+ let already_minted = client
1331
+ .query_opt(
1332
+ "SELECT 1 FROM hecks_eras WHERE domain = $1 AND ordinal = $2",
1333
+ &[&domain, &ordinal],
1334
+ )
1335
+ .await?;
1034
1336
  if already_minted.is_some() {
1035
1337
  anyhow::bail!("era {ordinal} of {domain} is already minted — nothing to do");
1036
1338
  }
@@ -1050,11 +1352,26 @@ async fn mint_era_body<C: GenericClient>(
1050
1352
  // Every held era's watermark, for `ancestor_tail_sql`'s cut — includes
1051
1353
  // the row just inserted, which bounds era `ordinal - 1`'s tail read.
1052
1354
  let held = crate::journal::held_eras(client, domain).await?;
1053
- let watermarks: std::collections::HashMap<i32, Option<i64>> = held.iter().map(|era| (era.ordinal, era.watermark)).collect();
1355
+ let watermarks: std::collections::HashMap<i32, Option<i64>> = held
1356
+ .iter()
1357
+ .map(|era| (era.ordinal, era.watermark))
1358
+ .collect();
1054
1359
 
1055
1360
  for aggregate in aggregates {
1056
- let phase = crate::log::phase_with("compile_head", serde_json::json!({ "aggregate": &aggregate.storage_name, "era": ordinal }));
1057
- compile_head(client, domain, aggregate, ordinal, label, edges, &watermarks).await?;
1361
+ let phase = crate::log::phase_with(
1362
+ "compile_head",
1363
+ serde_json::json!({ "aggregate": &aggregate.storage_name, "era": ordinal }),
1364
+ );
1365
+ compile_head(
1366
+ client,
1367
+ domain,
1368
+ aggregate,
1369
+ ordinal,
1370
+ label,
1371
+ edges,
1372
+ &watermarks,
1373
+ )
1374
+ .await?;
1058
1375
  phase.end();
1059
1376
  }
1060
1377
 
@@ -1073,7 +1390,7 @@ async fn mint_era_body<C: GenericClient>(
1073
1390
 
1074
1391
  /// A lifecycle field an aggregate's instances must carry, and its default
1075
1392
  /// for new instances. `key_prefix` matches the kernel's `instances()` dump key shape.
1076
- #[derive(Debug, Clone, PartialEq)]
1393
+ #[derive(Debug, Clone, PartialEq, Eq)]
1077
1394
  pub struct LifecycleDefault {
1078
1395
  pub key_prefix: String,
1079
1396
  pub field: String,
@@ -1108,13 +1425,22 @@ pub fn lifecycle_defaults(ir: &Value) -> Vec<LifecycleDefault> {
1108
1425
  /// Fills a missing lifecycle field into every seeded instance; returns how
1109
1426
  /// many changed. Idempotent, since the kernel's `from_json` requires the field on load.
1110
1427
  pub fn fill_lifecycle_defaults(seed: &mut Value, defaults: &[LifecycleDefault]) -> usize {
1111
- let Some(instances) = seed.as_object_mut() else { return 0 };
1428
+ let Some(instances) = seed.as_object_mut() else {
1429
+ return 0;
1430
+ };
1112
1431
  let mut changed = 0;
1113
1432
  for (key, instance) in instances.iter_mut() {
1114
- let Some(default) = defaults.iter().find(|d| key.starts_with(&d.key_prefix)) else { continue };
1115
- let Some(fields) = instance.as_object_mut() else { continue };
1433
+ let Some(default) = defaults.iter().find(|d| key.starts_with(&d.key_prefix)) else {
1434
+ continue;
1435
+ };
1436
+ let Some(fields) = instance.as_object_mut() else {
1437
+ continue;
1438
+ };
1116
1439
  if fields.get(&default.field).is_none_or(Value::is_null) {
1117
- fields.insert(default.field.clone(), Value::String(default.default.clone()));
1440
+ fields.insert(
1441
+ default.field.clone(),
1442
+ Value::String(default.default.clone()),
1443
+ );
1118
1444
  changed += 1;
1119
1445
  }
1120
1446
  }
@@ -1125,38 +1451,69 @@ pub fn fill_lifecycle_defaults(seed: &mut Value, defaults: &[LifecycleDefault])
1125
1451
  /// transaction — without this, a request right after the mint can hit
1126
1452
  /// `invalid seed` on the old-shaped snapshot. Takes the same advisory lock a
1127
1453
  /// dispatch holds, so an in-flight dispatch saves first.
1128
- async fn fill_snapshot_lifecycle_defaults<C: GenericClient>(client: &C, domain: &str, defaults: &[LifecycleDefault]) -> anyhow::Result<usize> {
1454
+ async fn fill_snapshot_lifecycle_defaults<C: GenericClient>(
1455
+ client: &C,
1456
+ domain: &str,
1457
+ defaults: &[LifecycleDefault],
1458
+ ) -> anyhow::Result<usize> {
1129
1459
  if defaults.is_empty() {
1130
1460
  return Ok(0);
1131
1461
  }
1132
- let table_exists: bool = client.query_one("SELECT to_regclass('hecks_lambda_snapshot') IS NOT NULL", &[]).await?.get(0);
1462
+ let table_exists: bool = client
1463
+ .query_one(
1464
+ "SELECT to_regclass('hecks_lambda_snapshot') IS NOT NULL",
1465
+ &[],
1466
+ )
1467
+ .await?
1468
+ .get(0);
1133
1469
  if !table_exists {
1134
1470
  return Ok(0);
1135
1471
  }
1136
- client.execute("SELECT pg_advisory_xact_lock(hashtext('hecks_lambda_journal.' || $1::text))", &[&domain]).await?;
1137
- let Some(row) = client.query_opt("SELECT seed FROM hecks_lambda_snapshot FOR UPDATE", &[]).await? else { return Ok(0) };
1472
+ client
1473
+ .execute(
1474
+ "SELECT pg_advisory_xact_lock(hashtext('hecks_lambda_journal.' || $1::text))",
1475
+ &[&domain],
1476
+ )
1477
+ .await?;
1478
+ let Some(row) = client
1479
+ .query_opt("SELECT seed FROM hecks_lambda_snapshot FOR UPDATE", &[])
1480
+ .await?
1481
+ else {
1482
+ return Ok(0);
1483
+ };
1138
1484
  let mut seed: Value = row.get(0);
1139
1485
  let changed = fill_lifecycle_defaults(&mut seed, defaults);
1140
1486
  if changed > 0 {
1141
- client.execute("UPDATE hecks_lambda_snapshot SET seed = $1", &[&seed]).await.context("filling lifecycle defaults into the snapshot")?;
1487
+ client
1488
+ .execute("UPDATE hecks_lambda_snapshot SET seed = $1", &[&seed])
1489
+ .await
1490
+ .context("filling lifecycle defaults into the snapshot")?;
1142
1491
  }
1143
1492
  Ok(changed)
1144
1493
  }
1145
1494
 
1146
1495
  #[cfg(test)]
1496
+ #[allow(clippy::items_after_statements)] // reason: tests declare a one-off fixture next to the statement that uses it
1147
1497
  mod tests {
1148
1498
 
1149
1499
  /// A throwaway database of its own — `mint.rs` also compiles into
1150
1500
  /// `bin/mint_harness`, which has no `dispatch` module to borrow a helper from.
1151
1501
  async fn own_scratch_db(name: &str) -> tokio_postgres::Client {
1152
1502
  let (admin, connection) =
1153
- tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls).await.expect("connect as admin");
1503
+ tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
1504
+ .await
1505
+ .expect("connect as admin");
1154
1506
  tokio::spawn(async move {
1155
1507
  let _ = connection.await;
1156
1508
  });
1157
- let _ = admin.batch_execute(&format!("DROP DATABASE IF EXISTS {name} WITH (FORCE)")).await;
1158
- admin.batch_execute(&format!("CREATE DATABASE {name}")).await.expect("create scratch db");
1159
- let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo(&name), NoTls)
1509
+ let _ = admin
1510
+ .batch_execute(&format!("DROP DATABASE IF EXISTS {name} WITH (FORCE)"))
1511
+ .await;
1512
+ admin
1513
+ .batch_execute(&format!("CREATE DATABASE {name}"))
1514
+ .await
1515
+ .expect("create scratch db");
1516
+ let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo(name), NoTls)
1160
1517
  .await
1161
1518
  .expect("connect to scratch db");
1162
1519
  tokio::spawn(async move {
@@ -1175,23 +1532,42 @@ mod tests {
1175
1532
  super::adopt_head_snapshots(
1176
1533
  &guard,
1177
1534
  "SampleApp",
1178
- &[super::Aggregate { name: "StateStyle".to_string(), storage_name: "state_style".to_string() }],
1535
+ &[super::Aggregate {
1536
+ name: "StateStyle".to_string(),
1537
+ storage_name: "state_style".to_string(),
1538
+ }],
1179
1539
  1,
1180
1540
  )
1181
1541
  .await
1182
1542
  .expect("era 1 exists, as Ruby left it");
1183
1543
 
1184
1544
  let missing = super::head_snapshot("SampleApp", "state_style", 2);
1185
- assert!(!super::table_exists(&guard, &missing).await.expect("a lookup"), "era 2 starts absent");
1545
+ assert!(
1546
+ !super::table_exists(&guard, &missing)
1547
+ .await
1548
+ .expect("a lookup"),
1549
+ "era 2 starts absent"
1550
+ );
1186
1551
 
1187
- let aggregates =
1188
- vec![super::Aggregate { name: "StateStyle".to_string(), storage_name: "state_style".to_string() }];
1189
- super::adopt_head_snapshots(&guard, "SampleApp", &aggregates, 2).await.expect("adopts");
1552
+ let aggregates = vec![super::Aggregate {
1553
+ name: "StateStyle".to_string(),
1554
+ storage_name: "state_style".to_string(),
1555
+ }];
1556
+ super::adopt_head_snapshots(&guard, "SampleApp", &aggregates, 2)
1557
+ .await
1558
+ .expect("adopts");
1190
1559
 
1191
- assert!(super::table_exists(&guard, &missing).await.expect("a lookup"), "era 2 is provisioned now");
1560
+ assert!(
1561
+ super::table_exists(&guard, &missing)
1562
+ .await
1563
+ .expect("a lookup"),
1564
+ "era 2 is provisioned now"
1565
+ );
1192
1566
 
1193
1567
  // Idempotent — every boot runs it, the same way Ruby's does.
1194
- super::adopt_head_snapshots(&guard, "SampleApp", &aggregates, 2).await.expect("adopts again");
1568
+ super::adopt_head_snapshots(&guard, "SampleApp", &aggregates, 2)
1569
+ .await
1570
+ .expect("adopts again");
1195
1571
  }
1196
1572
 
1197
1573
  /// The columns match Ruby's, so a table this crate creates is one Ruby can
@@ -1199,8 +1575,13 @@ mod tests {
1199
1575
  #[tokio::test]
1200
1576
  async fn a_head_snapshot_this_crate_creates_carries_rubys_own_columns() {
1201
1577
  let guard = own_scratch_db("hecks_host_head_snapshot_columns").await;
1202
- let aggregates = vec![super::Aggregate { name: "Widget".to_string(), storage_name: "widget".to_string() }];
1203
- super::adopt_head_snapshots(&guard, "Fixtures", &aggregates, 1).await.expect("adopts");
1578
+ let aggregates = vec![super::Aggregate {
1579
+ name: "Widget".to_string(),
1580
+ storage_name: "widget".to_string(),
1581
+ }];
1582
+ super::adopt_head_snapshots(&guard, "Fixtures", &aggregates, 1)
1583
+ .await
1584
+ .expect("adopts");
1204
1585
 
1205
1586
  let name = super::head_snapshot("Fixtures", "widget", 1);
1206
1587
  let rows = guard
@@ -1214,19 +1595,40 @@ mod tests {
1214
1595
  let columns: Vec<String> = rows.iter().map(|r| r.get::<_, String>(0)).collect();
1215
1596
  assert_eq!(columns, vec!["id", "ordinal", "operation", "state"]);
1216
1597
 
1217
- let operation = rows.iter().find(|r| r.get::<_, String>(0) == "operation").expect("operation");
1598
+ let operation = rows
1599
+ .iter()
1600
+ .find(|r| r.get::<_, String>(0) == "operation")
1601
+ .expect("operation");
1218
1602
  assert_eq!(operation.get::<_, String>(1), "NO", "operation is NOT NULL");
1219
- assert!(operation.get::<_, Option<String>>(2).unwrap_or_default().contains("save"), "defaults to 'save'");
1603
+ assert!(
1604
+ operation
1605
+ .get::<_, Option<String>>(2)
1606
+ .unwrap_or_default()
1607
+ .contains("save"),
1608
+ "defaults to 'save'"
1609
+ );
1220
1610
 
1221
- let state = rows.iter().find(|r| r.get::<_, String>(0) == "state").expect("state");
1222
- assert_eq!(state.get::<_, String>(1), "YES", "state is nullable, for a delete tombstone");
1611
+ let state = rows
1612
+ .iter()
1613
+ .find(|r| r.get::<_, String>(0) == "state")
1614
+ .expect("state");
1615
+ assert_eq!(
1616
+ state.get::<_, String>(1),
1617
+ "YES",
1618
+ "state is nullable, for a delete tombstone"
1619
+ );
1223
1620
  }
1224
1621
  use super::*;
1225
1622
  use crate::journal;
1226
1623
  use tokio_postgres::NoTls;
1227
1624
 
1228
1625
  fn rule(name: &str, was: Option<&str>, expression: &str) -> EdgeAggregate {
1229
- EdgeAggregate { name: name.to_string(), was: was.map(str::to_string), has_compute_or_rekey: false, compiled_state_expression: expression.to_string(), compiled_id_expression: None }
1626
+ EdgeAggregate {
1627
+ name: name.to_string(),
1628
+ was: was.map(str::to_string),
1629
+ compiled_state_expression: expression.to_string(),
1630
+ compiled_id_expression: None,
1631
+ }
1230
1632
  }
1231
1633
 
1232
1634
  // A multi-hop chain: era1 "Pizza" -> era2 renamed to "Order" -> era3 an
@@ -1234,8 +1636,20 @@ mod tests {
1234
1636
  // correctly for chain_sql's per-ancestor filter to read the right era.
1235
1637
  #[test]
1236
1638
  fn names_by_era_walks_a_multi_hop_rename_chain_backward_correctly() {
1237
- let edge_1_to_2 = Edge { from: "aaa".to_string(), to: "bbb".to_string(), aggregates: vec![rule("Order", Some("Pizza"), "hecks_tr_rename(state, 'x', 'y')")] };
1238
- let edge_2_to_3 = Edge { from: "bbb".to_string(), to: "ccc".to_string(), aggregates: vec![rule("Order", None, "hecks_tr_rename(state, 'y', 'z')")] };
1639
+ let edge_1_to_2 = Edge {
1640
+ from: "aaa".to_string(),
1641
+ to: "bbb".to_string(),
1642
+ aggregates: vec![rule(
1643
+ "Order",
1644
+ Some("Pizza"),
1645
+ "hecks_tr_rename(state, 'x', 'y')",
1646
+ )],
1647
+ };
1648
+ let edge_2_to_3 = Edge {
1649
+ from: "bbb".to_string(),
1650
+ to: "ccc".to_string(),
1651
+ aggregates: vec![rule("Order", None, "hecks_tr_rename(state, 'y', 'z')")],
1652
+ };
1239
1653
  let edges: Vec<&Edge> = vec![&edge_1_to_2, &edge_2_to_3];
1240
1654
 
1241
1655
  let (current, storage) = names_by_era("Order", &edges);
@@ -1243,25 +1657,53 @@ mod tests {
1243
1657
  // era 1 (index 0): the old name, "Pizza" -- read from edge_1_to_2's
1244
1658
  // own `was:`. era 2 (index 1) and era 3 (index 2): "Order",
1245
1659
  // unchanged since nothing renamed it again.
1246
- assert_eq!(current, vec!["Pizza".to_string(), "Order".to_string(), "Order".to_string()]);
1247
- assert_eq!(storage, vec!["pizza".to_string(), "order".to_string(), "order".to_string()]);
1660
+ assert_eq!(
1661
+ current,
1662
+ vec![
1663
+ "Pizza".to_string(),
1664
+ "Order".to_string(),
1665
+ "Order".to_string()
1666
+ ]
1667
+ );
1668
+ assert_eq!(
1669
+ storage,
1670
+ vec![
1671
+ "pizza".to_string(),
1672
+ "order".to_string(),
1673
+ "order".to_string()
1674
+ ]
1675
+ );
1248
1676
  }
1249
1677
 
1250
1678
  #[test]
1251
1679
  fn edge_chain_finds_the_real_path_and_refuses_a_broken_one() {
1252
- let edge = Edge { from: "aaa".to_string(), to: "bbb".to_string(), aggregates: vec![] };
1253
- let edges = vec![edge.clone()];
1680
+ let edge = Edge {
1681
+ from: "aaa".to_string(),
1682
+ to: "bbb".to_string(),
1683
+ aggregates: vec![],
1684
+ };
1685
+ let edges = vec![edge];
1254
1686
 
1255
- let found = edge_chain(&edges, &["aaa".to_string(), "bbb".to_string()]).expect("a real edge connects aaa to bbb");
1687
+ let found = edge_chain(&edges, &["aaa".to_string(), "bbb".to_string()])
1688
+ .expect("a real edge connects aaa to bbb");
1256
1689
  assert_eq!(found.len(), 1);
1257
1690
  assert_eq!(found[0].to, "bbb");
1258
1691
 
1259
1692
  let broken = edge_chain(&edges, &["aaa".to_string(), "zzz".to_string()]);
1260
- assert!(broken.is_err(), "no edge leads aaa to zzz -- must refuse, not silently skip");
1693
+ assert!(
1694
+ broken.is_err(),
1695
+ "no edge leads aaa to zzz -- must refuse, not silently skip"
1696
+ );
1261
1697
  }
1262
1698
 
1263
1699
  fn held(ordinal: i32, label: Option<&str>) -> journal::HeldEra {
1264
- journal::HeldEra { ordinal, hash: label.map(|_| "irrelevant".to_string()), label: label.map(str::to_string), held_text: String::new(), watermark: None }
1700
+ journal::HeldEra {
1701
+ ordinal,
1702
+ hash: label.map(|_| "irrelevant".to_string()),
1703
+ label: label.map(str::to_string),
1704
+ held_text: String::new(),
1705
+ watermark: None,
1706
+ }
1265
1707
  }
1266
1708
 
1267
1709
  // The four branches `main.rs`'s boot gate calls into — pure, so each
@@ -1275,21 +1717,34 @@ mod tests {
1275
1717
  // even if it's not the latest one (an operator could roll back
1276
1718
  // to an older binary whose shape a prior era already covers).
1277
1719
  let already_named = vec![held(1, Some("aaa111")), held(2, Some("bbb222"))];
1278
- assert_eq!(decide_boot_action(&already_named, "aaa111"), BootDecision::UseExisting { ordinal: 1 });
1279
- assert_eq!(decide_boot_action(&already_named, "bbb222"), BootDecision::UseExisting { ordinal: 2 });
1720
+ assert_eq!(
1721
+ decide_boot_action(&already_named, "aaa111"),
1722
+ BootDecision::UseExisting { ordinal: 1 }
1723
+ );
1724
+ assert_eq!(
1725
+ decide_boot_action(&already_named, "bbb222"),
1726
+ BootDecision::UseExisting { ordinal: 2 }
1727
+ );
1280
1728
 
1281
1729
  // Held, but the latest doesn't match this binary's shape -- mint
1282
1730
  // the next ordinal from the latest's own label.
1283
1731
  let drifted = vec![held(1, Some("aaa111"))];
1284
1732
  assert_eq!(
1285
1733
  decide_boot_action(&drifted, "ccc333"),
1286
- BootDecision::Mint { ordinal: 2, from_ordinal: 1, from_label: "aaa111".to_string() }
1734
+ BootDecision::Mint {
1735
+ ordinal: 2,
1736
+ from_ordinal: 1,
1737
+ from_label: "aaa111".to_string()
1738
+ }
1287
1739
  );
1288
1740
 
1289
1741
  // The latest held era has no label yet (Ruby minted it, never
1290
1742
  // named it) -- refuse rather than guess whether it matches.
1291
1743
  let unnamed = vec![held(1, Some("aaa111")), held(2, None)];
1292
- assert_eq!(decide_boot_action(&unnamed, "ccc333"), BootDecision::LatestUnnamed { ordinal: 2 });
1744
+ assert_eq!(
1745
+ decide_boot_action(&unnamed, "ccc333"),
1746
+ BootDecision::LatestUnnamed { ordinal: 2 }
1747
+ );
1293
1748
  }
1294
1749
 
1295
1750
  // Proves Rust needs no Ruby to mint at all: real Postgres, zero Ruby
@@ -1305,21 +1760,47 @@ mod tests {
1305
1760
  tokio::spawn(async move {
1306
1761
  let _ = admin_connection.await;
1307
1762
  });
1308
- let _ = admin_client.batch_execute(&format!("DROP DATABASE IF EXISTS {db} WITH (FORCE)")).await;
1309
- admin_client.batch_execute(&format!("CREATE DATABASE {db}")).await.expect("create scratch db");
1310
- let _ = admin_client.batch_execute(&format!("DROP ROLE IF EXISTS {owner}")).await;
1311
- admin_client.batch_execute(&format!("CREATE ROLE {owner} {}", crate::test_pg::login_clause())).await.expect("create owner role");
1312
- admin_client.batch_execute(&format!("GRANT CONNECT ON DATABASE {db} TO {owner}")).await.expect("grant connect");
1313
-
1314
- let grant = tokio_postgres::connect(&crate::test_pg::conninfo(&db), NoTls).await;
1315
- let (grant_client, grant_connection) = grant.expect("connect to scratch db as superuser to grant schema rights");
1763
+ let _ = admin_client
1764
+ .batch_execute(&format!("DROP DATABASE IF EXISTS {db} WITH (FORCE)"))
1765
+ .await;
1766
+ admin_client
1767
+ .batch_execute(&format!("CREATE DATABASE {db}"))
1768
+ .await
1769
+ .expect("create scratch db");
1770
+ let _ = admin_client
1771
+ .batch_execute(&format!("DROP ROLE IF EXISTS {owner}"))
1772
+ .await;
1773
+ admin_client
1774
+ .batch_execute(&format!(
1775
+ "CREATE ROLE {owner} {}",
1776
+ crate::test_pg::login_clause()
1777
+ ))
1778
+ .await
1779
+ .expect("create owner role");
1780
+ admin_client
1781
+ .batch_execute(&format!("GRANT CONNECT ON DATABASE {db} TO {owner}"))
1782
+ .await
1783
+ .expect("grant connect");
1784
+
1785
+ let grant = tokio_postgres::connect(&crate::test_pg::conninfo(db), NoTls).await;
1786
+ let (grant_client, grant_connection) =
1787
+ grant.expect("connect to scratch db as superuser to grant schema rights");
1316
1788
  tokio::spawn(async move {
1317
1789
  let _ = grant_connection.await;
1318
1790
  });
1319
- grant_client.batch_execute(&format!("GRANT USAGE, CREATE ON SCHEMA public TO {owner}")).await.expect("grant schema rights");
1320
- grant_client.batch_execute(&format!("ALTER DATABASE {db} OWNER TO {owner}")).await.expect("make owner the db owner");
1791
+ grant_client
1792
+ .batch_execute(&format!("GRANT USAGE, CREATE ON SCHEMA public TO {owner}"))
1793
+ .await
1794
+ .expect("grant schema rights");
1795
+ grant_client
1796
+ .batch_execute(&format!("ALTER DATABASE {db} OWNER TO {owner}"))
1797
+ .await
1798
+ .expect("make owner the db owner");
1321
1799
 
1322
- let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo_as(&db, &owner), NoTls).await.expect("connect as owner");
1800
+ let (client, connection) =
1801
+ tokio_postgres::connect(&crate::test_pg::conninfo_as(db, owner), NoTls)
1802
+ .await
1803
+ .expect("connect as owner");
1323
1804
  tokio::spawn(async move {
1324
1805
  let _ = connection.await;
1325
1806
  });
@@ -1348,21 +1829,47 @@ mod tests {
1348
1829
  let v2_ir = shape("amount");
1349
1830
 
1350
1831
  // era 1: Rust mints it, no Ruby, no edge needed.
1351
- let aggregate = Aggregate { name: "Widget".to_string(), storage_name: "widget".to_string() };
1352
- hold_first(&client, domain, "v1 source text (opaque to this crate)", &v1_ir, &[aggregate.clone()], None).await.expect("hold_first");
1832
+ let aggregate = Aggregate {
1833
+ name: "Widget".to_string(),
1834
+ storage_name: "widget".to_string(),
1835
+ };
1836
+ hold_first(
1837
+ &client,
1838
+ domain,
1839
+ "v1 source text (opaque to this crate)",
1840
+ &v1_ir,
1841
+ std::slice::from_ref(&aggregate),
1842
+ None,
1843
+ )
1844
+ .await
1845
+ .expect("hold_first");
1353
1846
 
1354
- let config = journal::LineageConfig { domain: domain.to_string(), era: Some(1), mirrored: None };
1847
+ let config = journal::LineageConfig {
1848
+ domain: domain.to_string(),
1849
+ era: Some(1),
1850
+ mirrored: None,
1851
+ };
1355
1852
  journal::append_lineage_mutation(
1356
1853
  &client,
1357
1854
  &config,
1358
- &journal::Mutation { aggregate: "Widget", id: "w1", operation: "save", state: &serde_json::json!({"cost": {"cents": 100}, "kind": {"label": "w1"}}) },
1855
+ &journal::Mutation {
1856
+ aggregate: "Widget",
1857
+ id: "w1",
1858
+ operation: "save",
1859
+ state: &serde_json::json!({"cost": {"cents": 100}, "kind": {"label": "w1"}}),
1860
+ },
1359
1861
  )
1360
1862
  .await
1361
1863
  .expect("write w1 under era 1");
1362
1864
  journal::append_lineage_mutation(
1363
1865
  &client,
1364
1866
  &config,
1365
- &journal::Mutation { aggregate: "Widget", id: "w2", operation: "save", state: &serde_json::json!({"cost": {"cents": 200}, "kind": {"label": "w2"}}) },
1867
+ &journal::Mutation {
1868
+ aggregate: "Widget",
1869
+ id: "w2",
1870
+ operation: "save",
1871
+ state: &serde_json::json!({"cost": {"cents": 200}, "kind": {"label": "w2"}}),
1872
+ },
1366
1873
  )
1367
1874
  .await
1368
1875
  .expect("write w2 under era 1");
@@ -1378,31 +1885,53 @@ mod tests {
1378
1885
  aggregates: vec![EdgeAggregate {
1379
1886
  name: "Widget".to_string(),
1380
1887
  was: None,
1381
- has_compute_or_rekey: false,
1382
1888
  compiled_state_expression: "hecks_tr_rename(state, 'cost', 'amount')".to_string(),
1383
1889
  compiled_id_expression: None,
1384
1890
  }],
1385
1891
  };
1386
1892
 
1387
- mint_era(&client, domain, 2, &storage_shape::mint_hash(&v2_ir), &to_label, "v2 source text", &[aggregate], &[&edge], None, &[])
1388
- .await
1389
- .expect("mint_era");
1893
+ mint_era(
1894
+ &client,
1895
+ domain,
1896
+ 2,
1897
+ &storage_shape::mint_hash(&v2_ir),
1898
+ &to_label,
1899
+ "v2 source text",
1900
+ &[aggregate],
1901
+ &[&edge],
1902
+ None,
1903
+ &[],
1904
+ )
1905
+ .await
1906
+ .expect("mint_era");
1390
1907
 
1391
1908
  // Read back through the same generic function real deployment traffic
1392
1909
  // uses, proving the whole chain, not just that SQL ran.
1393
- let mut rows = journal::read_lineage_head_all(&client, domain, "widget").await.expect("read_lineage_head_all");
1910
+ let mut rows = journal::read_lineage_head_all(&client, domain, "widget")
1911
+ .await
1912
+ .expect("read_lineage_head_all");
1394
1913
  rows.sort_by(|a, b| a.0.cmp(&b.0));
1395
1914
  assert_eq!(
1396
1915
  rows,
1397
1916
  vec![
1398
- ("w1".to_string(), serde_json::json!({"amount": {"cents": 100}, "kind": {"label": "w1"}})),
1399
- ("w2".to_string(), serde_json::json!({"amount": {"cents": 200}, "kind": {"label": "w2"}})),
1917
+ (
1918
+ "w1".to_string(),
1919
+ serde_json::json!({"amount": {"cents": 100}, "kind": {"label": "w1"}})
1920
+ ),
1921
+ (
1922
+ "w2".to_string(),
1923
+ serde_json::json!({"amount": {"cents": 200}, "kind": {"label": "w2"}})
1924
+ ),
1400
1925
  ]
1401
1926
  );
1402
1927
  }
1403
1928
 
1404
1929
  fn registration_default() -> LifecycleDefault {
1405
- LifecycleDefault { key_prefix: "Studio::Registration#".to_string(), field: "status".to_string(), default: "active".to_string() }
1930
+ LifecycleDefault {
1931
+ key_prefix: "Studio::Registration#".to_string(),
1932
+ field: "status".to_string(),
1933
+ default: "active".to_string(),
1934
+ }
1406
1935
  }
1407
1936
 
1408
1937
  // A seed as the kernel's `instances()` dump writes it: one entry per
@@ -1429,16 +1958,25 @@ mod tests {
1429
1958
  #[test]
1430
1959
  fn a_missing_lifecycle_field_gets_the_default() {
1431
1960
  let mut seed = old_seed();
1432
- assert_eq!(fill_lifecycle_defaults(&mut seed, &[registration_default()]), 2);
1961
+ assert_eq!(
1962
+ fill_lifecycle_defaults(&mut seed, &[registration_default()]),
1963
+ 2
1964
+ );
1433
1965
  assert_eq!(seed["Studio::Registration#r1"]["status"], "active");
1434
1966
  assert_eq!(seed["Studio::Registration#r2"]["status"], "active");
1435
- assert_eq!(seed["Studio::Registration#r1"]["attendee"]["email"], "ada@example.com", "everything else on the instance is kept");
1967
+ assert_eq!(
1968
+ seed["Studio::Registration#r1"]["attendee"]["email"], "ada@example.com",
1969
+ "everything else on the instance is kept"
1970
+ );
1436
1971
  }
1437
1972
 
1438
1973
  #[test]
1439
1974
  fn a_null_lifecycle_field_is_filled_too() {
1440
1975
  let mut seed = serde_json::json!({"Studio::Registration#r1": {"status": null}});
1441
- assert_eq!(fill_lifecycle_defaults(&mut seed, &[registration_default()]), 1);
1976
+ assert_eq!(
1977
+ fill_lifecycle_defaults(&mut seed, &[registration_default()]),
1978
+ 1
1979
+ );
1442
1980
  assert_eq!(seed["Studio::Registration#r1"]["status"], "active");
1443
1981
  }
1444
1982
 
@@ -1446,7 +1984,10 @@ mod tests {
1446
1984
  fn an_existing_lifecycle_value_is_left_alone() {
1447
1985
  let mut seed = old_seed();
1448
1986
  seed["Studio::Registration#r2"]["status"] = serde_json::json!("archived");
1449
- assert_eq!(fill_lifecycle_defaults(&mut seed, &[registration_default()]), 1);
1987
+ assert_eq!(
1988
+ fill_lifecycle_defaults(&mut seed, &[registration_default()]),
1989
+ 1
1990
+ );
1450
1991
  assert_eq!(seed["Studio::Registration#r2"]["status"], "archived");
1451
1992
  assert_eq!(seed["Studio::Registration#r1"]["status"], "active");
1452
1993
  }
@@ -1474,18 +2015,30 @@ mod tests {
1474
2015
  let mut seed = old_seed();
1475
2016
  fill_lifecycle_defaults(&mut seed, &[registration_default()]);
1476
2017
  let once = seed.clone();
1477
- assert_eq!(fill_lifecycle_defaults(&mut seed, &[registration_default()]), 0);
2018
+ assert_eq!(
2019
+ fill_lifecycle_defaults(&mut seed, &[registration_default()]),
2020
+ 0
2021
+ );
1478
2022
  assert_eq!(seed, once);
1479
2023
  }
1480
2024
 
1481
2025
  #[test]
1482
2026
  fn a_seed_that_is_not_an_object_or_holds_a_non_object_instance_is_left_alone() {
1483
2027
  let mut empty = serde_json::json!({});
1484
- assert_eq!(fill_lifecycle_defaults(&mut empty, &[registration_default()]), 0);
2028
+ assert_eq!(
2029
+ fill_lifecycle_defaults(&mut empty, &[registration_default()]),
2030
+ 0
2031
+ );
1485
2032
  let mut not_a_seed = serde_json::json!([1, 2]);
1486
- assert_eq!(fill_lifecycle_defaults(&mut not_a_seed, &[registration_default()]), 0);
2033
+ assert_eq!(
2034
+ fill_lifecycle_defaults(&mut not_a_seed, &[registration_default()]),
2035
+ 0
2036
+ );
1487
2037
  let mut odd = serde_json::json!({"Studio::Registration#r1": "scalar"});
1488
- assert_eq!(fill_lifecycle_defaults(&mut odd, &[registration_default()]), 0);
2038
+ assert_eq!(
2039
+ fill_lifecycle_defaults(&mut odd, &[registration_default()]),
2040
+ 0
2041
+ );
1489
2042
  assert_eq!(odd["Studio::Registration#r1"], "scalar");
1490
2043
  }
1491
2044
 
@@ -1503,7 +2056,11 @@ mod tests {
1503
2056
  assert_eq!(
1504
2057
  lifecycle_defaults(&ir),
1505
2058
  vec![
1506
- LifecycleDefault { key_prefix: "Studio::Event#".to_string(), field: "status".to_string(), default: "open".to_string() },
2059
+ LifecycleDefault {
2060
+ key_prefix: "Studio::Event#".to_string(),
2061
+ field: "status".to_string(),
2062
+ default: "open".to_string()
2063
+ },
1507
2064
  registration_default(),
1508
2065
  ]
1509
2066
  );
@@ -1515,7 +2072,9 @@ mod tests {
1515
2072
  #[tokio::test]
1516
2073
  async fn minting_an_era_fills_lifecycle_defaults_into_the_stored_snapshot() {
1517
2074
  let client = own_scratch_db("hecks_host_mint_snapshot_fill").await;
1518
- journal::ensure_schema(&client).await.expect("ensure_schema");
2075
+ journal::ensure_schema(&client)
2076
+ .await
2077
+ .expect("ensure_schema");
1519
2078
  ensure_base(&client, "Studio").await.expect("ensure_base");
1520
2079
 
1521
2080
  fn shape(with_status: bool) -> Value {
@@ -1527,14 +2086,29 @@ mod tests {
1527
2086
  "entities": []
1528
2087
  });
1529
2088
  if with_status {
1530
- aggregate["lifecycle"] = serde_json::json!({"field": "status", "default": "active", "transitions": []});
2089
+ aggregate["lifecycle"] =
2090
+ serde_json::json!({"field": "status", "default": "active", "transitions": []});
1531
2091
  }
1532
2092
  serde_json::json!({"name": "Studio", "aggregates": [aggregate]})
1533
2093
  }
1534
2094
  let (v1_ir, v2_ir) = (shape(false), shape(true));
1535
- let aggregate = Aggregate { name: "Registration".to_string(), storage_name: "registration".to_string() };
1536
- hold_first(&client, "Studio", "v1 source", &v1_ir, &[aggregate.clone()], None).await.expect("hold_first");
1537
- journal::save_snapshot(&client, 7, &old_seed()).await.expect("save_snapshot");
2095
+ let aggregate = Aggregate {
2096
+ name: "Registration".to_string(),
2097
+ storage_name: "registration".to_string(),
2098
+ };
2099
+ hold_first(
2100
+ &client,
2101
+ "Studio",
2102
+ "v1 source",
2103
+ &v1_ir,
2104
+ std::slice::from_ref(&aggregate),
2105
+ None,
2106
+ )
2107
+ .await
2108
+ .expect("hold_first");
2109
+ journal::save_snapshot(&client, 7, &old_seed())
2110
+ .await
2111
+ .expect("save_snapshot");
1538
2112
 
1539
2113
  let to_label = storage_shape::mint_label(&v2_ir);
1540
2114
  let edge = Edge {
@@ -1543,36 +2117,76 @@ mod tests {
1543
2117
  aggregates: vec![EdgeAggregate {
1544
2118
  name: "Registration".to_string(),
1545
2119
  was: None,
1546
- has_compute_or_rekey: false,
1547
- compiled_state_expression: "state || jsonb_build_object('status', 'active')".to_string(),
2120
+ compiled_state_expression: "state || jsonb_build_object('status', 'active')"
2121
+ .to_string(),
1548
2122
  compiled_id_expression: None,
1549
2123
  }],
1550
2124
  };
1551
2125
  let defaults = lifecycle_defaults(&v2_ir);
1552
2126
  assert_eq!(defaults.len(), 1);
1553
2127
  // The defaults name "Studio::Registration#", matching the seed's keys.
1554
- mint_era(&client, "Studio", 2, &storage_shape::mint_hash(&v2_ir), &to_label, "v2 source", &[aggregate], &[&edge], None, &defaults)
1555
- .await
1556
- .expect("mint_era");
2128
+ mint_era(
2129
+ &client,
2130
+ "Studio",
2131
+ 2,
2132
+ &storage_shape::mint_hash(&v2_ir),
2133
+ &to_label,
2134
+ "v2 source",
2135
+ &[aggregate],
2136
+ &[&edge],
2137
+ None,
2138
+ &defaults,
2139
+ )
2140
+ .await
2141
+ .expect("mint_era");
1557
2142
 
1558
- let snapshot = journal::load_snapshot(&client).await.unwrap().expect("the snapshot row is still there");
1559
- assert_eq!(snapshot.ordinal, 7, "the snapshot's ordinal is not touched, only its instances");
2143
+ let snapshot = journal::load_snapshot(&client)
2144
+ .await
2145
+ .unwrap()
2146
+ .expect("the snapshot row is still there");
2147
+ assert_eq!(
2148
+ snapshot.ordinal, 7,
2149
+ "the snapshot's ordinal is not touched, only its instances"
2150
+ );
1560
2151
  assert_eq!(snapshot.seed["Studio::Registration#r1"]["status"], "active");
1561
2152
  assert_eq!(snapshot.seed["Studio::Registration#r2"]["status"], "active");
1562
- assert!(snapshot.seed["Studio::Event#yoga"]["status"] == "open" && snapshot.seed["Payments::Payment#r1"]["status"] == "succeeded");
2153
+ assert!(
2154
+ snapshot.seed["Studio::Event#yoga"]["status"] == "open"
2155
+ && snapshot.seed["Payments::Payment#r1"]["status"] == "succeeded"
2156
+ );
1563
2157
  }
1564
2158
 
1565
2159
  #[tokio::test]
1566
2160
  async fn filling_a_database_with_no_snapshot_table_or_row_is_a_no_op() {
1567
2161
  let client = own_scratch_db("hecks_host_mint_snapshot_fill_absent").await;
1568
- assert_eq!(fill_snapshot_lifecycle_defaults(&client, "Studio", &[registration_default()]).await.unwrap(), 0, "no table");
1569
- journal::ensure_schema(&client).await.expect("ensure_schema");
1570
- assert_eq!(fill_snapshot_lifecycle_defaults(&client, "Studio", &[registration_default()]).await.unwrap(), 0, "no row");
2162
+ assert_eq!(
2163
+ fill_snapshot_lifecycle_defaults(&client, "Studio", &[registration_default()])
2164
+ .await
2165
+ .unwrap(),
2166
+ 0,
2167
+ "no table"
2168
+ );
2169
+ journal::ensure_schema(&client)
2170
+ .await
2171
+ .expect("ensure_schema");
2172
+ assert_eq!(
2173
+ fill_snapshot_lifecycle_defaults(&client, "Studio", &[registration_default()])
2174
+ .await
2175
+ .unwrap(),
2176
+ 0,
2177
+ "no row"
2178
+ );
1571
2179
  }
1572
2180
 
1573
2181
  /// The compiled expression a backfill rule writes for one path, wrapped around `inner`.
1574
2182
  fn backfill_expression(inner: &str, path: &[&str], default_json: &str) -> String {
1575
- let array = format!("ARRAY[{}]::text[]", path.iter().map(|p| format!("'{p}'")).collect::<Vec<_>>().join(", "));
2183
+ let array = format!(
2184
+ "ARRAY[{}]::text[]",
2185
+ path.iter()
2186
+ .map(|p| format!("'{p}'"))
2187
+ .collect::<Vec<_>>()
2188
+ .join(", ")
2189
+ );
1576
2190
  let label = path.join(".");
1577
2191
  format!(
1578
2192
  "(SELECT CASE WHEN (hecks_tr_extract(__s, {array})).present THEN __s ELSE hecks_tr_insert(__s, {array}, '{default_json}'::jsonb, 'backfill {label}') END FROM (SELECT ({inner}) AS __s) __outer)"
@@ -1599,7 +2213,6 @@ mod tests {
1599
2213
  aggregates: vec![EdgeAggregate {
1600
2214
  name: aggregate.to_string(),
1601
2215
  was: None,
1602
- has_compute_or_rekey: false,
1603
2216
  compiled_state_expression: expression.to_string(),
1604
2217
  compiled_id_expression: None,
1605
2218
  }],
@@ -1612,14 +2225,30 @@ mod tests {
1612
2225
  #[test]
1613
2226
  fn every_cte_of_the_chain_is_materialized() {
1614
2227
  let watermarks = std::collections::HashMap::new();
1615
- let edges: Vec<Edge> = (1..=6).map(|n| edge_of(&format!("e{n}"), &format!("e{}", n + 1), "Registration", "state")).collect();
2228
+ let edges: Vec<Edge> = (1..=6)
2229
+ .map(|n| {
2230
+ edge_of(
2231
+ &format!("e{n}"),
2232
+ &format!("e{}", n + 1),
2233
+ "Registration",
2234
+ "state",
2235
+ )
2236
+ })
2237
+ .collect();
1616
2238
  for n in 1..=6usize {
1617
2239
  let chain: Vec<&Edge> = edges[..n].iter().collect();
1618
2240
  let sql = chain_sql("Chain", "Registration", (n + 1) as i32, &chain, &watermarks);
1619
2241
  assert!(sql.starts_with("WITH tail AS MATERIALIZED ("), "{sql}");
1620
- assert_eq!(sql.matches("AS MATERIALIZED (").count(), n + 1, "the tail and each of the {n} edges: {sql}");
2242
+ assert_eq!(
2243
+ sql.matches("AS MATERIALIZED (").count(),
2244
+ n + 1,
2245
+ "the tail and each of the {n} edges: {sql}"
2246
+ );
1621
2247
  for edge in 1..=n {
1622
- assert!(sql.contains(&format!("edge_{edge} AS MATERIALIZED (")), "edge {edge}: {sql}");
2248
+ assert!(
2249
+ sql.contains(&format!("edge_{edge} AS MATERIALIZED (")),
2250
+ "edge {edge}: {sql}"
2251
+ );
1623
2252
  }
1624
2253
  }
1625
2254
  }
@@ -1630,9 +2259,23 @@ mod tests {
1630
2259
  #[tokio::test]
1631
2260
  async fn the_chain_plan_grows_with_the_edges_not_exponentially() {
1632
2261
  let client = own_scratch_db("hecks_host_mint_chain_plan_size").await;
1633
- journal::ensure_schema(&client).await.expect("ensure_schema");
1634
- let aggregates = vec![Aggregate { name: "Registration".to_string(), storage_name: "registration".to_string() }];
1635
- hold_first(&client, "Plan", "era 1", &serde_json::json!({"name": "Plan", "aggregates": []}), &aggregates, None).await.expect("hold_first");
2262
+ journal::ensure_schema(&client)
2263
+ .await
2264
+ .expect("ensure_schema");
2265
+ let aggregates = vec![Aggregate {
2266
+ name: "Registration".to_string(),
2267
+ storage_name: "registration".to_string(),
2268
+ }];
2269
+ hold_first(
2270
+ &client,
2271
+ "Plan",
2272
+ "era 1",
2273
+ &serde_json::json!({"name": "Plan", "aggregates": []}),
2274
+ &aggregates,
2275
+ None,
2276
+ )
2277
+ .await
2278
+ .expect("hold_first");
1636
2279
 
1637
2280
  let mut heavy = "hecks_tr_drop(state, ARRAY['attendee', 'name']::text[])".to_string();
1638
2281
  for field in ["first_name", "last_name", "phone", "how_heard", "aim"] {
@@ -1640,24 +2283,53 @@ mod tests {
1640
2283
  }
1641
2284
  let mut edges = vec![edge_of("e1", "e2", "Registration", &heavy)];
1642
2285
  for n in 2..=11 {
1643
- edges.push(edge_of(&format!("e{n}"), &format!("e{}", n + 1), "Registration", "state"));
2286
+ edges.push(edge_of(
2287
+ &format!("e{n}"),
2288
+ &format!("e{}", n + 1),
2289
+ "Registration",
2290
+ "state",
2291
+ ));
1644
2292
  }
1645
- edges.push(edge_of("e12", "e13", "Registration", &backfill_expression("state", &["status"], "\"active\"")));
2293
+ edges.push(edge_of(
2294
+ "e12",
2295
+ "e13",
2296
+ "Registration",
2297
+ &backfill_expression("state", &["status"], "\"active\""),
2298
+ ));
1646
2299
 
1647
2300
  let watermarks = std::collections::HashMap::new();
1648
2301
  let plan_lines = |chain: &[&Edge], materialize: bool| {
1649
- let sql = chain_sql_with("Plan", "Registration", (chain.len() + 1) as i32, chain, &watermarks, materialize);
2302
+ let sql = chain_sql_with(
2303
+ "Plan",
2304
+ "Registration",
2305
+ (chain.len() + 1) as i32,
2306
+ chain,
2307
+ &watermarks,
2308
+ materialize,
2309
+ );
1650
2310
  let client = &client;
1651
- async move { client.query(&format!("EXPLAIN (COSTS OFF) {sql}"), &[]).await.expect("explain").len() }
2311
+ async move {
2312
+ client
2313
+ .query(&format!("EXPLAIN (COSTS OFF) {sql}"), &[])
2314
+ .await
2315
+ .expect("explain")
2316
+ .len()
2317
+ }
1652
2318
  };
1653
2319
 
1654
2320
  let all: Vec<&Edge> = edges.iter().collect();
1655
2321
  let fenced_12 = plan_lines(&all, true).await;
1656
2322
  let fenced_6 = plan_lines(&all[..6], true).await;
1657
- assert!(fenced_12 < 2 * fenced_6.max(500), "12 fenced edges plan to {fenced_12} lines, 6 to {fenced_6}");
2323
+ assert!(
2324
+ fenced_12 < 2 * fenced_6.max(500),
2325
+ "12 fenced edges plan to {fenced_12} lines, 6 to {fenced_6}"
2326
+ );
1658
2327
  let inlined_5 = plan_lines(&all[..5], false).await;
1659
2328
  let fenced_5 = plan_lines(&all[..5], true).await;
1660
- assert!(inlined_5 > 10 * fenced_5, "unfenced 5 edges: {inlined_5} plan lines; fenced: {fenced_5}");
2329
+ assert!(
2330
+ inlined_5 > 10 * fenced_5,
2331
+ "unfenced 5 edges: {inlined_5} plan lines; fenced: {fenced_5}"
2332
+ );
1661
2333
  }
1662
2334
 
1663
2335
  /// Materializing each edge's CTE must not change one row of any chain: five
@@ -1666,14 +2338,24 @@ mod tests {
1666
2338
  #[tokio::test]
1667
2339
  async fn materializing_the_edge_ctes_returns_exactly_the_rows_the_inlined_chain_does() {
1668
2340
  let client = own_scratch_db("hecks_host_mint_chain_differential").await;
1669
- journal::ensure_schema(&client).await.expect("ensure_schema");
2341
+ journal::ensure_schema(&client)
2342
+ .await
2343
+ .expect("ensure_schema");
1670
2344
  let domain = "Chain";
1671
2345
  let aggregates = vec![
1672
- Aggregate { name: "Registration".to_string(), storage_name: "registration".to_string() },
1673
- Aggregate { name: "Event".to_string(), storage_name: "event".to_string() },
2346
+ Aggregate {
2347
+ name: "Registration".to_string(),
2348
+ storage_name: "registration".to_string(),
2349
+ },
2350
+ Aggregate {
2351
+ name: "Event".to_string(),
2352
+ storage_name: "event".to_string(),
2353
+ },
1674
2354
  ];
1675
2355
  let ir = serde_json::json!({"name": domain, "aggregates": []});
1676
- hold_first(&client, domain, "era 1", &ir, &aggregates, None).await.expect("hold_first");
2356
+ hold_first(&client, domain, "era 1", &ir, &aggregates, None)
2357
+ .await
2358
+ .expect("hold_first");
1677
2359
 
1678
2360
  // Edge 1 backfills four nested paths; edges 2 and 3 carry no rule (as
1679
2361
  // most of a real deployment's edges do); edge 4 backfills one more.
@@ -1681,17 +2363,23 @@ mod tests {
1681
2363
  for field in ["first_name", "last_name", "phone", "aim"] {
1682
2364
  nested = backfill_expression(&nested, &["attendee", field], "\"(none)\"");
1683
2365
  }
1684
- let edges = vec![
2366
+ let edges = [
1685
2367
  edge_of("e1", "e2", "Registration", &nested),
1686
2368
  edge_of("e2", "e3", "Event", "state"),
1687
2369
  edge_of("e3", "e4", "Event", "state"),
1688
- edge_of("e4", "e5", "Registration", &backfill_expression("state", &["status"], "\"active\"")),
2370
+ edge_of(
2371
+ "e4",
2372
+ "e5",
2373
+ "Registration",
2374
+ &backfill_expression("state", &["status"], "\"active\""),
2375
+ ),
1689
2376
  ];
1690
2377
 
1691
2378
  let journal_name = "hecks_journal_chain";
1692
2379
  let insert = |era: i32, aggregate: &str, id: &str, operation: &str, state: Value| {
1693
2380
  let client = &client;
1694
- let (aggregate, id, operation) = (aggregate.to_string(), id.to_string(), operation.to_string());
2381
+ let (aggregate, id, operation) =
2382
+ (aggregate.to_string(), id.to_string(), operation.to_string());
1695
2383
  async move {
1696
2384
  client
1697
2385
  .execute(
@@ -1704,36 +2392,86 @@ mod tests {
1704
2392
  };
1705
2393
  for era in 1..=5i32 {
1706
2394
  // era 1 carries the old attendee shape, later eras the new one
1707
- let attendee = |n: &str| if era == 1 { serde_json::json!({"name": n, "email": "a@example.com"}) } else { serde_json::json!({"first_name": n, "email": "a@example.com"}) };
2395
+ let attendee = |n: &str| {
2396
+ if era == 1 {
2397
+ serde_json::json!({"name": n, "email": "a@example.com"})
2398
+ } else {
2399
+ serde_json::json!({"first_name": n, "email": "a@example.com"})
2400
+ }
2401
+ };
1708
2402
  for i in 0..6 {
1709
2403
  let id = format!("r{}", (era as usize + i) % 5);
1710
- insert(era, "registration", &id, "save", serde_json::json!({"attendee": attendee(&format!("n{era}{i}"))})).await;
2404
+ insert(
2405
+ era,
2406
+ "registration",
2407
+ &id,
2408
+ "save",
2409
+ serde_json::json!({"attendee": attendee(&format!("n{era}{i}"))}),
2410
+ )
2411
+ .await;
1711
2412
  }
1712
- insert(era, "event", &format!("e{era}"), "save", serde_json::json!({"name": {"value": format!("event {era}")}})).await;
1713
- insert(era, "event", "e1", "save", serde_json::json!({"name": {"value": "renamed"}})).await;
2413
+ insert(
2414
+ era,
2415
+ "event",
2416
+ &format!("e{era}"),
2417
+ "save",
2418
+ serde_json::json!({"name": {"value": format!("event {era}")}}),
2419
+ )
2420
+ .await;
2421
+ insert(
2422
+ era,
2423
+ "event",
2424
+ "e1",
2425
+ "save",
2426
+ serde_json::json!({"name": {"value": "renamed"}}),
2427
+ )
2428
+ .await;
1714
2429
  if era == 3 {
1715
2430
  insert(era, "registration", "r0", "delete", Value::Null).await;
1716
2431
  }
1717
2432
  if era < 5 {
1718
2433
  let chain: Vec<&Edge> = edges[..era as usize].iter().collect();
1719
- mint_era(&client, domain, era + 1, &"h".repeat(64), &format!("e{}", era + 1), "text", &aggregates, &chain, None, &[]).await.expect("mint_era");
2434
+ mint_era(
2435
+ &client,
2436
+ domain,
2437
+ era + 1,
2438
+ &"h".repeat(64),
2439
+ &format!("e{}", era + 1),
2440
+ "text",
2441
+ &aggregates,
2442
+ &chain,
2443
+ None,
2444
+ &[],
2445
+ )
2446
+ .await
2447
+ .expect("mint_era");
1720
2448
  }
1721
2449
  }
1722
2450
 
1723
2451
  let held = journal::held_eras(&client, domain).await.unwrap();
1724
- let watermarks: std::collections::HashMap<i32, Option<i64>> = held.iter().map(|h| (h.ordinal, h.watermark)).collect();
2452
+ let watermarks: std::collections::HashMap<i32, Option<i64>> =
2453
+ held.iter().map(|h| (h.ordinal, h.watermark)).collect();
1725
2454
  let mut compared = 0;
1726
2455
  for aggregate in &aggregates {
1727
2456
  for n in 1..=edges.len() {
1728
2457
  let chain: Vec<&Edge> = edges[..n].iter().collect();
1729
2458
  let era = (n + 1) as i32;
1730
- let inlined = chain_sql_with(domain, &aggregate.name, era, &chain, &watermarks, false);
1731
- let fenced = chain_sql_with(domain, &aggregate.name, era, &chain, &watermarks, true);
2459
+ let inlined =
2460
+ chain_sql_with(domain, &aggregate.name, era, &chain, &watermarks, false);
2461
+ let fenced =
2462
+ chain_sql_with(domain, &aggregate.name, era, &chain, &watermarks, true);
1732
2463
  assert_ne!(inlined, fenced, "the two forms really differ");
1733
2464
  assert!(fenced.contains("AS MATERIALIZED (") && !inlined.contains("MATERIALIZED"));
1734
2465
 
1735
- let (a, b) = (raw_rows(&client, &inlined).await, raw_rows(&client, &fenced).await);
1736
- assert!(!a.is_empty(), "{} chain of {n}: the fixture yields rows", aggregate.name);
2466
+ let (a, b) = (
2467
+ raw_rows(&client, &inlined).await,
2468
+ raw_rows(&client, &fenced).await,
2469
+ );
2470
+ assert!(
2471
+ !a.is_empty(),
2472
+ "{} chain of {n}: the fixture yields rows",
2473
+ aggregate.name
2474
+ );
1737
2475
  assert_eq!(a, b, "{} chain of {n}: raw rows", aggregate.name);
1738
2476
  assert_eq!(
1739
2477
  latest_of(&client, &inlined).await.unwrap(),
@@ -1746,9 +2484,25 @@ mod tests {
1746
2484
  }
1747
2485
  assert_eq!(compared, 8);
1748
2486
  // and the fixture exercised what it claims to: a backfilled status reached the head
1749
- let translated = translated_latest(&client, domain, "Registration", 5, &edges.iter().collect::<Vec<_>>(), &watermarks).await.unwrap();
1750
- assert!(translated.values().all(|state| state["status"] == "active"), "{translated:?}");
1751
- assert_eq!(translated.len(), 5, "five registrations were ever saved: {translated:?}");
2487
+ let translated = translated_latest(
2488
+ &client,
2489
+ domain,
2490
+ "Registration",
2491
+ 5,
2492
+ &edges.iter().collect::<Vec<_>>(),
2493
+ &watermarks,
2494
+ )
2495
+ .await
2496
+ .unwrap();
2497
+ assert!(
2498
+ translated.values().all(|state| state["status"] == "active"),
2499
+ "{translated:?}"
2500
+ );
2501
+ assert_eq!(
2502
+ translated.len(),
2503
+ 5,
2504
+ "five registrations were ever saved: {translated:?}"
2505
+ );
1752
2506
  }
1753
2507
 
1754
2508
  // Layer 2, proven live both directions: an edge whose declared rules agree
@@ -1764,21 +2518,47 @@ mod tests {
1764
2518
  tokio::spawn(async move {
1765
2519
  let _ = admin_connection.await;
1766
2520
  });
1767
- let _ = admin_client.batch_execute(&format!("DROP DATABASE IF EXISTS {db} WITH (FORCE)")).await;
1768
- admin_client.batch_execute(&format!("CREATE DATABASE {db}")).await.expect("create scratch db");
1769
- let _ = admin_client.batch_execute(&format!("DROP ROLE IF EXISTS {owner}")).await;
1770
- admin_client.batch_execute(&format!("CREATE ROLE {owner} {}", crate::test_pg::login_clause())).await.expect("create owner role");
1771
- admin_client.batch_execute(&format!("GRANT CONNECT ON DATABASE {db} TO {owner}")).await.expect("grant connect");
1772
-
1773
- let grant = tokio_postgres::connect(&crate::test_pg::conninfo(&db), NoTls).await;
1774
- let (grant_client, grant_connection) = grant.expect("connect to scratch db as superuser to grant schema rights");
2521
+ let _ = admin_client
2522
+ .batch_execute(&format!("DROP DATABASE IF EXISTS {db} WITH (FORCE)"))
2523
+ .await;
2524
+ admin_client
2525
+ .batch_execute(&format!("CREATE DATABASE {db}"))
2526
+ .await
2527
+ .expect("create scratch db");
2528
+ let _ = admin_client
2529
+ .batch_execute(&format!("DROP ROLE IF EXISTS {owner}"))
2530
+ .await;
2531
+ admin_client
2532
+ .batch_execute(&format!(
2533
+ "CREATE ROLE {owner} {}",
2534
+ crate::test_pg::login_clause()
2535
+ ))
2536
+ .await
2537
+ .expect("create owner role");
2538
+ admin_client
2539
+ .batch_execute(&format!("GRANT CONNECT ON DATABASE {db} TO {owner}"))
2540
+ .await
2541
+ .expect("grant connect");
2542
+
2543
+ let grant = tokio_postgres::connect(&crate::test_pg::conninfo(db), NoTls).await;
2544
+ let (grant_client, grant_connection) =
2545
+ grant.expect("connect to scratch db as superuser to grant schema rights");
1775
2546
  tokio::spawn(async move {
1776
2547
  let _ = grant_connection.await;
1777
2548
  });
1778
- grant_client.batch_execute(&format!("GRANT USAGE, CREATE ON SCHEMA public TO {owner}")).await.expect("grant schema rights");
1779
- grant_client.batch_execute(&format!("ALTER DATABASE {db} OWNER TO {owner}")).await.expect("make owner the db owner");
2549
+ grant_client
2550
+ .batch_execute(&format!("GRANT USAGE, CREATE ON SCHEMA public TO {owner}"))
2551
+ .await
2552
+ .expect("grant schema rights");
2553
+ grant_client
2554
+ .batch_execute(&format!("ALTER DATABASE {db} OWNER TO {owner}"))
2555
+ .await
2556
+ .expect("make owner the db owner");
1780
2557
 
1781
- let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo_as(&db, &owner), NoTls).await.expect("connect as owner");
2558
+ let (client, connection) =
2559
+ tokio_postgres::connect(&crate::test_pg::conninfo_as(db, owner), NoTls)
2560
+ .await
2561
+ .expect("connect as owner");
1782
2562
  tokio::spawn(async move {
1783
2563
  let _ = connection.await;
1784
2564
  });
@@ -1803,17 +2583,43 @@ mod tests {
1803
2583
  }
1804
2584
  let v1_ir = shape("cost");
1805
2585
  let v2_ir = shape("amount");
1806
- let aggregate = Aggregate { name: "Widget".to_string(), storage_name: "widget".to_string() };
2586
+ let aggregate = Aggregate {
2587
+ name: "Widget".to_string(),
2588
+ storage_name: "widget".to_string(),
2589
+ };
1807
2590
  let from_label = storage_shape::mint_label(&v1_ir);
1808
2591
  let to_label = storage_shape::mint_label(&v2_ir);
1809
2592
 
1810
- async fn seed_era_one(client: &tokio_postgres::Client, domain: &str, v1_ir: &Value, aggregate: &Aggregate) {
1811
- hold_first(client, domain, "v1 source text", v1_ir, std::slice::from_ref(aggregate), None).await.expect("hold_first");
1812
- let config = journal::LineageConfig { domain: domain.to_string(), era: Some(1), mirrored: None };
2593
+ async fn seed_era_one(
2594
+ client: &tokio_postgres::Client,
2595
+ domain: &str,
2596
+ v1_ir: &Value,
2597
+ aggregate: &Aggregate,
2598
+ ) {
2599
+ hold_first(
2600
+ client,
2601
+ domain,
2602
+ "v1 source text",
2603
+ v1_ir,
2604
+ std::slice::from_ref(aggregate),
2605
+ None,
2606
+ )
2607
+ .await
2608
+ .expect("hold_first");
2609
+ let config = journal::LineageConfig {
2610
+ domain: domain.to_string(),
2611
+ era: Some(1),
2612
+ mirrored: None,
2613
+ };
1813
2614
  journal::append_lineage_mutation(
1814
2615
  client,
1815
2616
  &config,
1816
- &journal::Mutation { aggregate: "Widget", id: "w1", operation: "save", state: &serde_json::json!({"cost": {"cents": 100}, "kind": {"label": "w1"}}) },
2617
+ &journal::Mutation {
2618
+ aggregate: "Widget",
2619
+ id: "w1",
2620
+ operation: "save",
2621
+ state: &serde_json::json!({"cost": {"cents": 100}, "kind": {"label": "w1"}}),
2622
+ },
1817
2623
  )
1818
2624
  .await
1819
2625
  .expect("write w1 under era 1");
@@ -1829,7 +2635,6 @@ mod tests {
1829
2635
  aggregates: vec![EdgeAggregate {
1830
2636
  name: "Widget".to_string(),
1831
2637
  was: None,
1832
- has_compute_or_rekey: false,
1833
2638
  compiled_state_expression: "hecks_tr_rename(state, 'cost', 'amount')".to_string(),
1834
2639
  compiled_id_expression: None,
1835
2640
  }],
@@ -1838,10 +2643,20 @@ mod tests {
1838
2643
  "domain": honest_domain, "from": from_label, "to": to_label,
1839
2644
  "aggregates": [{"name": "Widget", "renames": {"cost": "amount"}}]
1840
2645
  })];
1841
- let watermarks: std::collections::HashMap<i32, Option<i64>> = [(1, None)].into_iter().collect();
1842
- audit_before_mint(&client, honest_domain, &v2_ir, &[aggregate.clone()], 2, &[&honest_edge], &honest_raw_edges, &watermarks)
1843
- .await
1844
- .expect("an honest edge's audit must pass silently");
2646
+ let watermarks: std::collections::HashMap<i32, Option<i64>> =
2647
+ std::iter::once((1, None)).collect();
2648
+ audit_before_mint(
2649
+ &client,
2650
+ honest_domain,
2651
+ &v2_ir,
2652
+ std::slice::from_ref(&aggregate),
2653
+ 2,
2654
+ &[&honest_edge],
2655
+ &honest_raw_edges,
2656
+ &watermarks,
2657
+ )
2658
+ .await
2659
+ .expect("an honest edge's audit must pass silently");
1845
2660
 
1846
2661
  // The lying edge: declared `renames:` claims a different destination
1847
2662
  // than the compiled SQL produces — the exact bug this audit exists to
@@ -1855,11 +2670,23 @@ mod tests {
1855
2670
  // still renames it to `amount`. A real divergence.
1856
2671
  "aggregates": [{"name": "Widget", "renames": {"cost": "price"}}]
1857
2672
  })];
1858
- let error = audit_before_mint(&client, lying_domain, &v2_ir, &[aggregate], 2, &[&lying_edge], &lying_raw_edges, &watermarks)
1859
- .await
1860
- .expect_err("a declared rename that disagrees with the compiled SQL must refuse");
2673
+ let error = audit_before_mint(
2674
+ &client,
2675
+ lying_domain,
2676
+ &v2_ir,
2677
+ &[aggregate],
2678
+ 2,
2679
+ &[&lying_edge],
2680
+ &lying_raw_edges,
2681
+ &watermarks,
2682
+ )
2683
+ .await
2684
+ .expect_err("a declared rename that disagrees with the compiled SQL must refuse");
1861
2685
  let message = format!("{error:#}");
1862
2686
  assert!(message.contains("the audit refused"), "{message}");
1863
- assert!(message.contains("diverges from the reference transform"), "{message}");
2687
+ assert!(
2688
+ message.contains("diverges from the reference transform"),
2689
+ "{message}"
2690
+ );
1864
2691
  }
1865
2692
  }