hecks 2.9.0 → 3.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 (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +4 -2
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +7 -4
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,874 @@
1
+ //! Flat, domain-agnostic journal of every command this domain has ever
2
+ //! accepted, replayed in full on each invocation rather than filtered (ADR 0018).
3
+
4
+ use anyhow::Context;
5
+ use sha2::{Digest, Sha256};
6
+ use tokio_postgres::{Client, GenericClient};
7
+
8
+ pub async fn ensure_schema(client: &Client) -> anyhow::Result<()> {
9
+ client
10
+ .batch_execute(
11
+ "CREATE TABLE IF NOT EXISTS hecks_lambda_journal (
12
+ ordinal BIGSERIAL PRIMARY KEY,
13
+ verb TEXT NOT NULL,
14
+ args JSONB NOT NULL,
15
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
16
+ )",
17
+ )
18
+ .await?;
19
+ // Single-row cache of the kernel's last "instances" output; the
20
+ // `boolean PRIMARY KEY DEFAULT true CHECK (id)` trick enforces at
21
+ // most one row. See `load_snapshot`/`save_snapshot`.
22
+ client
23
+ .batch_execute(
24
+ "CREATE TABLE IF NOT EXISTS hecks_lambda_snapshot (
25
+ id boolean PRIMARY KEY DEFAULT true CHECK (id),
26
+ ordinal bigint NOT NULL,
27
+ seed jsonb NOT NULL
28
+ )",
29
+ )
30
+ .await?;
31
+ // Additive column; `DEFAULT false` makes every pre-existing row read
32
+ // as "not backfilled yet", the correct value with no separate migration.
33
+ client
34
+ .batch_execute(
35
+ "ALTER TABLE hecks_lambda_snapshot ADD COLUMN IF NOT EXISTS sagas_backfilled boolean NOT NULL DEFAULT false",
36
+ )
37
+ .await?;
38
+ // One row per in-flight saga instance (unlike the single-row snapshot
39
+ // above): sagas are numerous and mostly independent. No `ordinal`
40
+ // column — always written inside dispatch::handle's advisory-locked
41
+ // transaction, so plain overwrite-in-place is correct (see dispatch.rs).
42
+ client
43
+ .batch_execute(
44
+ "CREATE TABLE IF NOT EXISTS hecks_lambda_sagas (
45
+ process_manager TEXT NOT NULL,
46
+ correlation TEXT NOT NULL,
47
+ state TEXT NOT NULL,
48
+ memory JSONB NOT NULL,
49
+ PRIMARY KEY (process_manager, correlation)
50
+ )",
51
+ )
52
+ .await?;
53
+ // Additive column, same backfill idiom as `sagas_backfilled` above;
54
+ // `DEFAULT '[]'::jsonb` gives every pre-existing saga row an empty
55
+ // ledger instead of requiring a separate migration.
56
+ client
57
+ .batch_execute(
58
+ "ALTER TABLE hecks_lambda_sagas ADD COLUMN IF NOT EXISTS completed_compensations jsonb NOT NULL DEFAULT '[]'::jsonb",
59
+ )
60
+ .await?;
61
+ // Durable record of a cross-domain delivery that exhausted retries;
62
+ // see `record_dead_letter` below. Flat and domain-agnostic like
63
+ // `hecks_lambda_journal` — one deployed Lambda has only one source domain.
64
+ client
65
+ .batch_execute(
66
+ "CREATE TABLE IF NOT EXISTS hecks_cross_domain_dead_letters (
67
+ id BIGSERIAL PRIMARY KEY,
68
+ policy TEXT NOT NULL,
69
+ target_domain TEXT NOT NULL,
70
+ target_verb TEXT NOT NULL,
71
+ payload JSONB NOT NULL,
72
+ error TEXT NOT NULL,
73
+ attempts INT NOT NULL,
74
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
75
+ )",
76
+ )
77
+ .await?;
78
+ Ok(())
79
+ }
80
+
81
+ /// Durably records a cross-domain delivery that exhausted every retry.
82
+ /// Written post-commit — a failure here must never roll back the local command.
83
+ pub async fn record_dead_letter<C: GenericClient>(
84
+ client: &C,
85
+ policy: &str,
86
+ target_domain: &str,
87
+ target_verb: &str,
88
+ payload: &serde_json::Value,
89
+ error: &str,
90
+ attempts: i32,
91
+ ) -> anyhow::Result<()> {
92
+ client
93
+ .execute(
94
+ "INSERT INTO hecks_cross_domain_dead_letters \
95
+ (policy, target_domain, target_verb, payload, error, attempts) \
96
+ VALUES ($1, $2, $3, $4, $5, $6)",
97
+ &[&policy, &target_domain, &target_verb, payload, &error, &attempts],
98
+ )
99
+ .await?;
100
+ Ok(())
101
+ }
102
+
103
+ /// Every command ever accepted, in dispatch order, as `{"verb","args"}` steps.
104
+ /// Generic over `GenericClient` so callers can run it inside their own transaction.
105
+ pub async fn load_steps<C: GenericClient>(client: &C) -> anyhow::Result<Vec<serde_json::Value>> {
106
+ let rows = client
107
+ .query(
108
+ "SELECT verb, args FROM hecks_lambda_journal ORDER BY ordinal",
109
+ &[],
110
+ )
111
+ .await?;
112
+
113
+ Ok(rows
114
+ .iter()
115
+ .map(|row| {
116
+ let verb: String = row.get(0);
117
+ let args: serde_json::Value = row.get(1);
118
+ serde_json::json!({ "verb": verb, "args": args })
119
+ })
120
+ .collect())
121
+ }
122
+
123
+ /// Steps recorded after `ordinal` — what remains to replay on top of a
124
+ /// snapshot taken as of that ordinal (normally empty).
125
+ pub async fn load_steps_after<C: GenericClient>(client: &C, ordinal: i64) -> anyhow::Result<Vec<serde_json::Value>> {
126
+ let rows = client
127
+ .query(
128
+ "SELECT verb, args FROM hecks_lambda_journal WHERE ordinal > $1 ORDER BY ordinal",
129
+ &[&ordinal],
130
+ )
131
+ .await?;
132
+
133
+ Ok(rows
134
+ .iter()
135
+ .map(|row| {
136
+ let verb: String = row.get(0);
137
+ let args: serde_json::Value = row.get(1);
138
+ serde_json::json!({ "verb": verb, "args": args })
139
+ })
140
+ .collect())
141
+ }
142
+
143
+ /// The kernel's cached "instances" output as of `ordinal`, used to skip a
144
+ /// full replay from empty state.
145
+ pub struct Snapshot {
146
+ pub ordinal: i64,
147
+ pub seed: serde_json::Value,
148
+ /// One-time latch: false only for a snapshot written before saga
149
+ /// durability shipped, never a live signal about current saga state.
150
+ pub sagas_backfilled: bool,
151
+ }
152
+
153
+ /// The domain's cached snapshot, if one has ever been written. `None`
154
+ /// before the first accepted command, when there's nothing to seed from yet.
155
+ pub async fn load_snapshot<C: GenericClient>(client: &C) -> anyhow::Result<Option<Snapshot>> {
156
+ let rows = client
157
+ .query("SELECT ordinal, seed, sagas_backfilled FROM hecks_lambda_snapshot", &[])
158
+ .await?;
159
+ Ok(rows.first().map(|row| Snapshot { ordinal: row.get(0), seed: row.get(1), sagas_backfilled: row.get(2) }))
160
+ }
161
+
162
+ /// Returns the new journal row's ordinal, for `save_snapshot` to pair with it.
163
+ pub async fn append<C: GenericClient>(
164
+ client: &C,
165
+ verb: &str,
166
+ args: &serde_json::Value,
167
+ ) -> anyhow::Result<i64> {
168
+ let row = client
169
+ .query_one(
170
+ "INSERT INTO hecks_lambda_journal (verb, args) VALUES ($1, $2) RETURNING ordinal",
171
+ &[&verb, &args],
172
+ )
173
+ .await?;
174
+ Ok(row.get(0))
175
+ }
176
+
177
+ /// Upserts the one-row snapshot cache at `ordinal`. Always sets
178
+ /// `sagas_backfilled = true`; only a row written before that column
179
+ /// existed can ever read back `false`.
180
+ pub async fn save_snapshot<C: GenericClient>(client: &C, ordinal: i64, seed: &serde_json::Value) -> anyhow::Result<()> {
181
+ client
182
+ .execute(
183
+ "INSERT INTO hecks_lambda_snapshot (id, ordinal, seed, sagas_backfilled) VALUES (true, $1, $2, true) \
184
+ ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, seed = EXCLUDED.seed, sagas_backfilled = true",
185
+ &[&ordinal, seed],
186
+ )
187
+ .await?;
188
+ Ok(())
189
+ }
190
+
191
+ /// One live saga/process-manager instance, as stored in `hecks_lambda_sagas`.
192
+ pub struct SagaRow {
193
+ pub process_manager: String,
194
+ pub correlation: String,
195
+ pub state: String,
196
+ pub memory: serde_json::Value,
197
+ pub completed_compensations: serde_json::Value,
198
+ }
199
+
200
+ /// Every live saga instance for this domain, read once per invocation to
201
+ /// seed the kernel's in-memory `sagas` map.
202
+ pub async fn load_sagas<C: GenericClient>(client: &C) -> anyhow::Result<Vec<SagaRow>> {
203
+ let rows = client
204
+ .query(
205
+ "SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_lambda_sagas",
206
+ &[],
207
+ )
208
+ .await?;
209
+ Ok(rows
210
+ .into_iter()
211
+ .map(|row| SagaRow {
212
+ process_manager: row.get(0),
213
+ correlation: row.get(1),
214
+ state: row.get(2),
215
+ memory: row.get(3),
216
+ completed_compensations: row.get(4),
217
+ })
218
+ .collect())
219
+ }
220
+
221
+ /// Upserts one live saga instance's state. Must run in the same
222
+ /// transaction as `append`, never a post-commit connection.
223
+ pub async fn save_saga<C: GenericClient>(
224
+ client: &C,
225
+ process_manager: &str,
226
+ correlation: &str,
227
+ state: &str,
228
+ memory: &serde_json::Value,
229
+ completed_compensations: &serde_json::Value,
230
+ ) -> anyhow::Result<()> {
231
+ client
232
+ .execute(
233
+ "INSERT INTO hecks_lambda_sagas (process_manager, correlation, state, memory, completed_compensations) \
234
+ VALUES ($1, $2, $3, $4, $5) \
235
+ ON CONFLICT (process_manager, correlation) DO UPDATE \
236
+ SET state = EXCLUDED.state, memory = EXCLUDED.memory, completed_compensations = EXCLUDED.completed_compensations",
237
+ &[&process_manager, &correlation, &state, memory, completed_compensations],
238
+ )
239
+ .await?;
240
+ Ok(())
241
+ }
242
+
243
+ /// Removes one saga instance that ended.
244
+ pub async fn delete_saga<C: GenericClient>(
245
+ client: &C,
246
+ process_manager: &str,
247
+ correlation: &str,
248
+ ) -> anyhow::Result<()> {
249
+ client
250
+ .execute(
251
+ "DELETE FROM hecks_lambda_sagas WHERE process_manager = $1 AND correlation = $2",
252
+ &[&process_manager, &correlation],
253
+ )
254
+ .await?;
255
+ Ok(())
256
+ }
257
+
258
+ /// Which domain journal this binary writes as, and which era. Read from
259
+ /// `HECKS_DOMAIN`/main.rs's boot-time mint decision, never computed here.
260
+ pub struct LineageConfig {
261
+ pub domain: String,
262
+ /// `None` when lineage isn't provisioned for this domain (ADR 0034).
263
+ pub era: Option<i32>,
264
+ /// Aggregates mirrored into era-shaped head snapshots; `None` means all.
265
+ pub mirrored: Option<std::collections::BTreeSet<String>>,
266
+ }
267
+
268
+ impl LineageConfig {
269
+ /// Whether `qualified_aggregate` is mirrored into the era-shaped head
270
+ /// snapshots.
271
+ pub fn mirrors(&self, qualified_aggregate: &str) -> bool {
272
+ match &self.mirrored {
273
+ None => true,
274
+ Some(capable) => capable.contains(qualified_aggregate),
275
+ }
276
+ }
277
+ }
278
+
279
+ /// Ports `Naming.snake` (lib/hecks/naming.rb) verbatim — must match Ruby
280
+ /// exactly so both runtimes derive the same storage names.
281
+ pub fn snake(name: &str) -> String {
282
+ word_boundary(&acronym_boundary(name)).to_ascii_lowercase()
283
+ }
284
+
285
+ fn acronym_boundary(s: &str) -> String {
286
+ let chars: Vec<char> = s.chars().collect();
287
+ let mut out = String::new();
288
+ let mut i = 0;
289
+ while i < chars.len() {
290
+ if chars[i].is_ascii_uppercase() {
291
+ let mut j = i;
292
+ while j < chars.len() && chars[j].is_ascii_uppercase() {
293
+ j += 1;
294
+ }
295
+ let run_len = j - i;
296
+ if run_len >= 2 && j < chars.len() && chars[j].is_ascii_lowercase() {
297
+ out.extend(&chars[i..j - 1]);
298
+ out.push('_');
299
+ out.push(chars[j - 1]);
300
+ i = j;
301
+ continue;
302
+ }
303
+ out.extend(&chars[i..j]);
304
+ i = j;
305
+ continue;
306
+ }
307
+ out.push(chars[i]);
308
+ i += 1;
309
+ }
310
+ out
311
+ }
312
+
313
+ fn word_boundary(s: &str) -> String {
314
+ let chars: Vec<char> = s.chars().collect();
315
+ let mut out = String::new();
316
+ for (idx, &c) in chars.iter().enumerate() {
317
+ if idx > 0 && c.is_ascii_uppercase() {
318
+ let prev = chars[idx - 1];
319
+ if prev.is_ascii_lowercase() || prev.is_ascii_digit() {
320
+ out.push('_');
321
+ }
322
+ }
323
+ out.push(c);
324
+ }
325
+ out
326
+ }
327
+
328
+ /// `aggregate.storage_name` ported from ir/aggregate.rb — demodulizes a
329
+ /// qualified name like `"Banking::Customer"` to `"Customer"` before snaking it.
330
+ fn storage_name(qualified_aggregate: &str) -> String {
331
+ let short = qualified_aggregate.rsplit("::").next().unwrap_or(qualified_aggregate);
332
+ snake(short)
333
+ }
334
+
335
+ /// Quotes a Postgres identifier `PG::Connection.quote_ident`-style —
336
+ /// needed since table names can't be bound as query parameters.
337
+ pub(crate) fn quote_ident(name: &str) -> String {
338
+ format!("\"{}\"", name.replace('"', "\"\""))
339
+ }
340
+
341
+ fn journal_table(domain: &str) -> String {
342
+ format!("hecks_journal_{}", snake(domain))
343
+ }
344
+
345
+ /// Postgres's NAMEDATALEN-1 limit — an identifier over this length is
346
+ /// silently truncated, never refused, so two overlong names sharing a
347
+ /// prefix would otherwise collide.
348
+ const POSTGRES_IDENTIFIER_LIMIT: usize = 63;
349
+
350
+ /// Same algorithm as `Lineage#qualified_name` (lineage.rb): 63-byte limit,
351
+ /// SHA256 suffix, domain-qualified so two domains' storage names can't
352
+ /// collide (ADR 0059).
353
+ pub(crate) fn qualified_name(domain: &str, suffix: &str) -> String {
354
+ let full = format!("{}_{}", snake(domain), suffix);
355
+ if full.len() <= POSTGRES_IDENTIFIER_LIMIT {
356
+ return full;
357
+ }
358
+
359
+ let digest = format!("{:x}", Sha256::digest(full.as_bytes()));
360
+ let digest = &digest[0..8];
361
+ let keep = POSTGRES_IDENTIFIER_LIMIT - digest.len() - 1;
362
+ format!("{}_{}", &full[0..keep], digest)
363
+ }
364
+
365
+ fn head_snapshot_table(domain: &str, qualified_aggregate: &str, era: i32) -> String {
366
+ qualified_name(domain, &format!("{}_head_snapshot_{}", storage_name(qualified_aggregate), era))
367
+ }
368
+
369
+ /// The highest era ordinal on file for `domain`, not a bare existence
370
+ /// check — `hecks_eras` is append-only, so a stale checkout still has a row.
371
+ pub async fn current_era(client: &Client, domain: &str) -> anyhow::Result<Option<i32>> {
372
+ let rows = client
373
+ .query(
374
+ "SELECT ordinal FROM hecks_eras WHERE domain = $1 ORDER BY ordinal DESC LIMIT 1",
375
+ &[&domain],
376
+ )
377
+ .await?;
378
+ Ok(rows.first().map(|row| row.get(0)))
379
+ }
380
+
381
+ /// One `MutationRecord` from the kernel's JSON output, parsed generically
382
+ /// since rust/host never links the kernel crate directly (ADR 0012).
383
+ pub struct Mutation<'a> {
384
+ pub aggregate: &'a str,
385
+ pub id: &'a str,
386
+ pub operation: &'a str,
387
+ pub state: &'a serde_json::Value,
388
+ }
389
+
390
+ /// Mirrors `PostgresEra#append`'s two-step write: journal insert, then a
391
+ /// head-snapshot upsert that never regresses on a stale/reordered write.
392
+ pub async fn append_lineage_mutation<C: GenericClient>(
393
+ client: &C,
394
+ config: &LineageConfig,
395
+ mutation: &Mutation<'_>,
396
+ ) -> anyhow::Result<()> {
397
+ // ADR 0034 — the one chokepoint every lineage write passes through;
398
+ // dispatch::handle already checks `era.is_some()` for ordinary
399
+ // commands, so this bail! is the backstop for other callers.
400
+ let Some(era) = config.era else {
401
+ anyhow::bail!(
402
+ "cannot append a lineage mutation for {}::{}: no era is active — the lineage subsystem was never \
403
+ initialized for this domain",
404
+ config.domain,
405
+ mutation.aggregate
406
+ );
407
+ };
408
+
409
+ if mutation.operation != "save" {
410
+ anyhow::bail!(
411
+ "hecks_journal_{}: unsupported operation {:?} — only \"save\" is generated today",
412
+ snake(&config.domain),
413
+ mutation.operation
414
+ );
415
+ }
416
+
417
+ let journal = journal_table(&config.domain);
418
+ let snapshot = head_snapshot_table(&config.domain, mutation.aggregate, era);
419
+ let storage = storage_name(mutation.aggregate);
420
+
421
+ // Named per-statement: tokio_postgres's Display for a db error is
422
+ // just "db error" — the real message is on `source()`, which
423
+ // `main.rs` surfaces via `{:#}` formatting.
424
+ let row = client
425
+ .query_one(
426
+ &format!(
427
+ "INSERT INTO {} (era, aggregate, aggregate_id, operation, state) \
428
+ VALUES ($1, $2, $3, $4, $5) RETURNING ordinal",
429
+ quote_ident(&journal)
430
+ ),
431
+ &[&era, &storage, &mutation.id, &mutation.operation, &mutation.state],
432
+ )
433
+ .await
434
+ .with_context(|| format!("journalling {} #{} into {journal} at era {era}", mutation.aggregate, mutation.id))?;
435
+ let ordinal: i64 = row.get(0);
436
+
437
+ client
438
+ .execute(
439
+ &format!(
440
+ "INSERT INTO {snap} (id, ordinal, state) VALUES ($1, $2, $3) \
441
+ ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, state = EXCLUDED.state \
442
+ WHERE {snap}.ordinal < EXCLUDED.ordinal",
443
+ snap = quote_ident(&snapshot)
444
+ ),
445
+ &[&mutation.id, &ordinal, &mutation.state],
446
+ )
447
+ .await
448
+ .with_context(|| format!("upserting {} #{} into {snapshot}", mutation.aggregate, mutation.id))?;
449
+
450
+ Ok(())
451
+ }
452
+
453
+ // Not wired into dispatch::handle's replay seed — overlaying translated
454
+ // head state onto raw replayed steps would cause false "AlreadyExists" refusals.
455
+ pub(crate) fn head_view(domain: &str, storage_name: &str) -> String {
456
+ qualified_name(domain, &format!("{storage_name}_head"))
457
+ }
458
+
459
+ /// Every row for one lineage-capable aggregate, already translated to its
460
+ /// current shape (reads the same head view Ruby's `Adapters::Postgres` does).
461
+ pub async fn read_lineage_head_all<C: GenericClient>(
462
+ client: &C,
463
+ domain: &str,
464
+ storage_name: &str,
465
+ ) -> anyhow::Result<Vec<(String, serde_json::Value)>> {
466
+ let rows = client
467
+ .query(&format!("SELECT id, state FROM {}", quote_ident(&head_view(domain, storage_name))), &[])
468
+ .await?;
469
+ Ok(rows.into_iter().map(|row| (row.get(0), row.get(1))).collect())
470
+ }
471
+
472
+ /// One row by id, already translated to its current shape.
473
+ pub async fn read_lineage_head_by_id<C: GenericClient>(
474
+ client: &C,
475
+ domain: &str,
476
+ storage_name: &str,
477
+ id: &str,
478
+ ) -> anyhow::Result<Option<serde_json::Value>> {
479
+ let row = client
480
+ .query_opt(
481
+ &format!("SELECT state FROM {} WHERE id = $1", quote_ident(&head_view(domain, storage_name))),
482
+ &[&id],
483
+ )
484
+ .await?;
485
+ Ok(row.map(|r| r.get(0)))
486
+ }
487
+
488
+ /// One row of `hecks_eras`, mirroring era_store.rb's `eras` shape.
489
+ #[derive(Debug, Clone)]
490
+ pub struct HeldEra {
491
+ pub ordinal: i32,
492
+ pub hash: Option<String>,
493
+ pub label: Option<String>,
494
+ pub held_text: String,
495
+ pub watermark: Option<i64>,
496
+ }
497
+
498
+ /// Every held era for `domain`, each verified against its own digest — an
499
+ /// edited `held_text` refuses loudly instead of silently reporting no drift.
500
+ pub async fn held_eras<C: GenericClient>(client: &C, domain: &str) -> anyhow::Result<Vec<HeldEra>> {
501
+ let rows = client
502
+ .query(
503
+ "SELECT ordinal, hash, label, held_text, watermark, held_digest FROM hecks_eras \
504
+ WHERE domain = $1 ORDER BY ordinal",
505
+ &[&domain],
506
+ )
507
+ .await?;
508
+
509
+ rows.into_iter()
510
+ .map(|row| {
511
+ let ordinal: i32 = row.get("ordinal");
512
+ let held_text: String = row.get("held_text");
513
+ let held_digest: Option<String> = row.get("held_digest");
514
+
515
+ if let Some(stored) = &held_digest {
516
+ let digest = format!("{:x}", Sha256::digest(held_text.as_bytes()));
517
+ if &digest != stored {
518
+ anyhow::bail!(
519
+ "cannot boot {domain}: era {ordinal}'s held text does not match its own recorded digest \
520
+ — this row was edited outside the lineage tooling; run hecks reattest to acknowledge \
521
+ the change and re-seal it (Runtime::EraTamper's own recovery path)"
522
+ );
523
+ }
524
+ }
525
+
526
+ Ok(HeldEra {
527
+ ordinal,
528
+ hash: row.get("hash"),
529
+ label: row.get("label"),
530
+ held_text,
531
+ watermark: row.get("watermark"),
532
+ })
533
+ })
534
+ .collect()
535
+ }
536
+
537
+ /// A Layer-3 approval, as recorded — mirrors era_store.rb's `approval_for`.
538
+ #[derive(Debug, Clone)]
539
+ pub struct Approval {
540
+ pub edge_digest: String,
541
+ pub reviewed_ordinal: i64,
542
+ }
543
+
544
+ /// The latest approval for one shape-pair edge, if any (latest wins).
545
+ pub async fn approval_for<C: GenericClient>(
546
+ client: &C,
547
+ domain: &str,
548
+ from_label: &str,
549
+ to_label: &str,
550
+ ) -> anyhow::Result<Option<Approval>> {
551
+ let row = client
552
+ .query_opt(
553
+ "SELECT edge_digest, reviewed_ordinal FROM hecks_approvals \
554
+ WHERE domain = $1 AND from_label = $2 AND to_label = $3 \
555
+ ORDER BY approved_at DESC, reviewed_ordinal DESC LIMIT 1",
556
+ &[&domain, &from_label, &to_label],
557
+ )
558
+ .await?;
559
+
560
+ Ok(row.map(|row| Approval { edge_digest: row.get("edge_digest"), reviewed_ordinal: row.get("reviewed_ordinal") }))
561
+ }
562
+
563
+ /// Writes an approval into the journal, bound to the journal's tip as it stands, as
564
+ /// `LineageStore#record_approval!` does; how a committed approval joins the single history.
565
+ pub async fn record_approval<C: GenericClient>(
566
+ client: &C,
567
+ domain: &str,
568
+ from_label: &str,
569
+ to_label: &str,
570
+ edge_digest: &str,
571
+ ) -> anyhow::Result<()> {
572
+ let tip = last_ordinal(client, domain).await?;
573
+ client
574
+ .execute(
575
+ "INSERT INTO hecks_approvals (domain, from_label, to_label, edge_digest, reviewed_ordinal) \
576
+ VALUES ($1, $2, $3, $4, $5)",
577
+ &[&domain, &from_label, &to_label, &edge_digest, &tip],
578
+ )
579
+ .await?;
580
+ Ok(())
581
+ }
582
+
583
+ /// The journal's high-water ordinal — what a fresh approval binds to and
584
+ /// what a mint transaction captures as the new era's watermark.
585
+ pub async fn last_ordinal<C: GenericClient>(client: &C, domain: &str) -> anyhow::Result<i64> {
586
+ let row = client
587
+ .query_one(&format!("SELECT COALESCE(max(ordinal), 0) AS o FROM {}", quote_ident(&journal_table(domain))), &[])
588
+ .await?;
589
+ Ok(row.get("o"))
590
+ }
591
+
592
+ #[cfg(test)]
593
+ mod lineage_tests {
594
+ use super::*;
595
+ use tokio_postgres::NoTls;
596
+
597
+ // Mints a real era 2 via Ruby's LineageManager (not this crate's own
598
+ // writes) against a scratch DB with a genuinely fenced app role — so
599
+ // this proves staleness is refused by Postgres RLS, not a check here.
600
+ #[tokio::test]
601
+ async fn a_stale_era_write_is_refused_by_postgres_rls_not_this_crate() {
602
+ let db = "rust_host_rls_test";
603
+ let owner_role = "rust_host_rls_owner";
604
+ let app_role = "rust_host_rls_app";
605
+
606
+ let script = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mint_stale_era.rb");
607
+ let status = std::process::Command::new("ruby")
608
+ .arg(&script)
609
+ .arg(db)
610
+ .arg(owner_role)
611
+ .arg(app_role)
612
+ .status()
613
+ .expect("run mint_stale_era.rb -- is `ruby` on PATH?");
614
+ assert!(status.success(), "mint_stale_era.rb failed -- see its own stderr above");
615
+
616
+ // The fenced app role — the same connection shape a real deployed
617
+ // rust/host uses (never the table-owning role, which bypasses RLS).
618
+ let (client, connection) =
619
+ tokio_postgres::connect(&crate::test_pg::conninfo_as(&db, &app_role), NoTls)
620
+ .await
621
+ .expect("connect as the app role");
622
+ tokio::spawn(async move {
623
+ let _ = connection.await;
624
+ });
625
+
626
+ let state = serde_json::json!({ "cents": 100 });
627
+
628
+ // The era this checkout speaks -- allowed.
629
+ let current_era = LineageConfig { domain: "Ledger".to_string(), era: Some(2), mirrored: None };
630
+ let accepted = append_lineage_mutation(
631
+ &client,
632
+ &current_era,
633
+ &Mutation { aggregate: "Account", id: "current-era", operation: "save", state: &state },
634
+ )
635
+ .await;
636
+ assert!(accepted.is_ok(), "writing under the CURRENT era should succeed: {accepted:?}");
637
+
638
+ // The superseded era -- refused by Postgres's own RLS policy.
639
+ let stale_era = LineageConfig { domain: "Ledger".to_string(), era: Some(1), mirrored: None };
640
+ let refused = append_lineage_mutation(
641
+ &client,
642
+ &stale_era,
643
+ &Mutation { aggregate: "Account", id: "stale-era", operation: "save", state: &state },
644
+ )
645
+ .await;
646
+ assert!(refused.is_err(), "writing under a SUPERSEDED era should be refused");
647
+ let message = format!("{:#}", refused.unwrap_err()).to_lowercase();
648
+ assert!(
649
+ message.contains("row-level security") || message.contains("row level security"),
650
+ "the refusal should be Postgres's RLS policy specifically, not some other error: {message}"
651
+ );
652
+ }
653
+
654
+ // Proven against a real compute-migrated era Ruby minted, not a
655
+ // synthetic fixture, so a real hecks_approvals row is guaranteed
656
+ // to exist by the time this test reads it back.
657
+ #[tokio::test]
658
+ async fn held_eras_and_approval_for_read_exactly_what_ruby_s_own_mint_wrote() {
659
+ let db = "rust_host_era_read_test";
660
+ let owner_role = "rust_host_era_read_owner";
661
+ let app_role = "rust_host_era_read_app";
662
+
663
+ let script = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mint_and_seed_lineage_compute.rb");
664
+ let output = std::process::Command::new("ruby")
665
+ .arg(&script)
666
+ .arg(db)
667
+ .arg(owner_role)
668
+ .arg(app_role)
669
+ .output()
670
+ .expect("run mint_and_seed_lineage_compute.rb -- is `ruby` on PATH?");
671
+ assert!(
672
+ output.status.success(),
673
+ "mint_and_seed_lineage_compute.rb failed:\n{}",
674
+ String::from_utf8_lossy(&output.stderr)
675
+ );
676
+
677
+ // Owner-authenticated: hecks_eras/hecks_approvals aren't grantable
678
+ // to the app role by design — only the mint path reads them, not
679
+ // the RLS-fenced data path the test above exists to prove.
680
+ let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo_as(&db, &owner_role), NoTls)
681
+ .await
682
+ .expect("connect as owner");
683
+ tokio::spawn(async move {
684
+ let _ = connection.await;
685
+ });
686
+
687
+ let eras = held_eras(&client, "LedgerCompute").await.expect("held_eras");
688
+ assert_eq!(eras.len(), 2, "expected exactly the two eras mint_and_seed_lineage_compute.rb mints: {eras:?}");
689
+ assert_eq!(eras[0].ordinal, 1);
690
+ assert_eq!(eras[1].ordinal, 2);
691
+ let from_label = eras[0].label.clone().expect("era 1 has a minted label");
692
+ let to_label = eras[1].label.clone().expect("era 2 has a minted label");
693
+ assert_ne!(from_label, to_label, "a real compute edge names two DIFFERENT shapes");
694
+
695
+ let approval = approval_for(&client, "LedgerCompute", &from_label, &to_label)
696
+ .await
697
+ .expect("approval_for")
698
+ .expect("the real approval mint_and_seed_lineage_compute.rb recorded should still be readable");
699
+ assert!(!approval.edge_digest.is_empty());
700
+ // reviewed_ordinal binds to the journal's high-water mark at
701
+ // approval time; era 1's two prior writes mean it must be >= 2.
702
+ assert!(approval.reviewed_ordinal >= 2, "reviewed_ordinal should reflect era 1's real writes: {}", approval.reviewed_ordinal);
703
+
704
+ // last_ordinal must have advanced past the approval's own
705
+ // reviewed_ordinal by the one post-mint write.
706
+ let ordinal = last_ordinal(&client, "LedgerCompute").await.expect("last_ordinal");
707
+ assert!(ordinal > approval.reviewed_ordinal, "the journal should have advanced since the approval was reviewed: {ordinal} vs {}", approval.reviewed_ordinal);
708
+ }
709
+
710
+ // The boot-gate case the RLS test doesn't cover: this crate's own
711
+ // read, before any write is attempted. Proves `current_era` tells a
712
+ // genuinely-unminted domain apart from a minted-but-stale one, which
713
+ // a bare existence check can't.
714
+ #[tokio::test]
715
+ async fn current_era_tells_unminted_apart_from_stale_and_finds_the_live_ordinal() {
716
+ let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
717
+ .await
718
+ .expect("connect to postgres");
719
+ tokio::spawn(async move {
720
+ let _ = connection.await;
721
+ });
722
+ client
723
+ .batch_execute("CREATE TABLE IF NOT EXISTS hecks_eras (domain text, ordinal int)")
724
+ .await
725
+ .unwrap();
726
+ client
727
+ .batch_execute("DELETE FROM hecks_eras WHERE domain = 'CurrentEraFixture'")
728
+ .await
729
+ .unwrap();
730
+
731
+ // A domain hecks_eras has never heard of at all.
732
+ let unminted = current_era(&client, "CurrentEraFixture").await.unwrap();
733
+ assert_eq!(unminted, None, "a domain with no hecks_eras rows at all has no current era");
734
+
735
+ // Era 1 minted, then era 2 supersedes it; hecks_eras is
736
+ // append-only, so both rows remain on file.
737
+ client
738
+ .execute(
739
+ "INSERT INTO hecks_eras (domain, ordinal) VALUES ($1, 1)",
740
+ &[&"CurrentEraFixture"],
741
+ )
742
+ .await
743
+ .unwrap();
744
+ assert_eq!(
745
+ current_era(&client, "CurrentEraFixture").await.unwrap(),
746
+ Some(1),
747
+ "with only era 1 on file, era 1 is current"
748
+ );
749
+
750
+ client
751
+ .execute(
752
+ "INSERT INTO hecks_eras (domain, ordinal) VALUES ($1, 2)",
753
+ &[&"CurrentEraFixture"],
754
+ )
755
+ .await
756
+ .unwrap();
757
+ assert_eq!(
758
+ current_era(&client, "CurrentEraFixture").await.unwrap(),
759
+ Some(2),
760
+ "era 1's row is still on file, but era 2 is now the live one -- \
761
+ a bare existence check on era 1 would have missed that it's stale"
762
+ );
763
+ }
764
+
765
+ // Proven against an aggregate that isn't Member — the whole point of
766
+ // extracting `member_row_by_email`/`member_rows`'s query into these
767
+ // generic functions. Builds the head view by hand rather than
768
+ // reproducing Ruby's mint path; that correctness is lineage_spec.rb's job.
769
+ #[tokio::test]
770
+ async fn read_lineage_head_reads_any_aggregates_view_generically() {
771
+ let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
772
+ .await
773
+ .expect("connect to postgres");
774
+ tokio::spawn(async move {
775
+ let _ = connection.await;
776
+ });
777
+
778
+ // Domain-qualified (ADR 0059); snake("Fixtures") == "fixtures".
779
+ client.batch_execute("DROP VIEW IF EXISTS fixtures_widget_head").await.unwrap();
780
+ client.batch_execute("DROP TABLE IF EXISTS fixtures_widget_head_snapshot_1").await.unwrap();
781
+ client
782
+ .batch_execute(
783
+ "CREATE TABLE fixtures_widget_head_snapshot_1 (id text PRIMARY KEY, ordinal bigint NOT NULL, state jsonb NOT NULL)",
784
+ )
785
+ .await
786
+ .unwrap();
787
+ client
788
+ .batch_execute("CREATE VIEW fixtures_widget_head AS SELECT id, state FROM fixtures_widget_head_snapshot_1")
789
+ .await
790
+ .unwrap();
791
+
792
+ let config = LineageConfig { domain: "Fixtures".to_string(), era: Some(1), mirrored: None };
793
+ client
794
+ .batch_execute("CREATE TABLE IF NOT EXISTS hecks_journal_fixtures (ordinal bigserial PRIMARY KEY, era int NOT NULL, aggregate text NOT NULL, aggregate_id text NOT NULL, operation text NOT NULL, state jsonb)")
795
+ .await
796
+ .unwrap();
797
+ append_lineage_mutation(
798
+ &client,
799
+ &config,
800
+ &Mutation { aggregate: "Widget", id: "widget-1", operation: "save", state: &serde_json::json!({ "name": "Gadget" }) },
801
+ )
802
+ .await
803
+ .unwrap();
804
+ append_lineage_mutation(
805
+ &client,
806
+ &config,
807
+ &Mutation { aggregate: "Widget", id: "widget-2", operation: "save", state: &serde_json::json!({ "name": "Gizmo" }) },
808
+ )
809
+ .await
810
+ .unwrap();
811
+
812
+ let one = read_lineage_head_by_id(&client, "Fixtures", "widget", "widget-1").await.unwrap();
813
+ assert_eq!(one, Some(serde_json::json!({ "name": "Gadget" })));
814
+
815
+ let missing = read_lineage_head_by_id(&client, "Fixtures", "widget", "widget-nonexistent").await.unwrap();
816
+ assert_eq!(missing, None);
817
+
818
+ let mut all = read_lineage_head_all(&client, "Fixtures", "widget").await.unwrap();
819
+ all.sort_by(|a, b| a.0.cmp(&b.0));
820
+ assert_eq!(
821
+ all,
822
+ vec![
823
+ ("widget-1".to_string(), serde_json::json!({ "name": "Gadget" })),
824
+ ("widget-2".to_string(), serde_json::json!({ "name": "Gizmo" })),
825
+ ]
826
+ );
827
+ }
828
+
829
+ #[tokio::test]
830
+ async fn record_dead_letter_writes_a_real_durable_row() {
831
+ let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
832
+ .await
833
+ .expect("connect to postgres");
834
+ tokio::spawn(async move {
835
+ let _ = connection.await;
836
+ });
837
+ ensure_schema(&client).await.unwrap();
838
+ client.batch_execute("DELETE FROM hecks_cross_domain_dead_letters").await.unwrap();
839
+
840
+ record_dead_letter(
841
+ &client,
842
+ "ReviewOnFreeze",
843
+ "Compliance",
844
+ "Compliance::AccountFreezeReview.Open",
845
+ &serde_json::json!({ "number": { "value": "acct-1" } }),
846
+ "ResourceNotFoundException: function not found",
847
+ 3,
848
+ )
849
+ .await
850
+ .unwrap();
851
+
852
+ let rows = client
853
+ .query(
854
+ "SELECT policy, target_domain, target_verb, payload, error, attempts FROM hecks_cross_domain_dead_letters",
855
+ &[],
856
+ )
857
+ .await
858
+ .unwrap();
859
+ assert_eq!(rows.len(), 1, "should have written exactly one row");
860
+ let row = &rows[0];
861
+ let policy: String = row.get(0);
862
+ let target_domain: String = row.get(1);
863
+ let target_verb: String = row.get(2);
864
+ let payload: serde_json::Value = row.get(3);
865
+ let error: String = row.get(4);
866
+ let attempts: i32 = row.get(5);
867
+ assert_eq!(policy, "ReviewOnFreeze");
868
+ assert_eq!(target_domain, "Compliance");
869
+ assert_eq!(target_verb, "Compliance::AccountFreezeReview.Open");
870
+ assert_eq!(payload, serde_json::json!({ "number": { "value": "acct-1" } }));
871
+ assert_eq!(error, "ResourceNotFoundException: function not found");
872
+ assert_eq!(attempts, 3);
873
+ }
874
+ }