hecks 1.2.0 → 1.4.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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -14,7 +14,11 @@ module Hecks
14
14
  module HeadCompiler
15
15
  # ── head compilation ───────────────────────────────────────────
16
16
 
17
- # The snapshot table backing one aggregate's CURRENT era: one row
17
+ # Creates one aggregate's head-snapshot table for an era if it is missing,
18
+ # brings an older table up to the current column shape, and backfills it
19
+ # from the journal until the backfill is recorded complete.
20
+ #
21
+ # The snapshot table backing one aggregate's current era: one row
18
22
  # per live id, upserted transactionally by PostgresEra#append
19
23
  # alongside the journal insert it belongs to, never derived by
20
24
  # scanning history thereafter. Idempotent and unguarded by
@@ -22,40 +26,45 @@ module Hecks
22
26
  # privilege class as the per-aggregate views/matviews already
23
27
  # created this way, not the shared owner-only journal.
24
28
  #
25
- # BACKFILLED, not just created, the FIRST time this table comes
29
+ # Backfilled, not just created, the first time this table comes
26
30
  # into existence for a domain that already has journal history —
27
31
  # a fresh era (compile_head! at mint) never has any, but era 1
28
- # against an EXISTING deployment (any domain running before this
32
+ # against an existing deployment (any domain running before this
29
33
  # snapshot table existed at all) does, and an empty table would
30
34
  # silently erase every already-written record from every read
31
35
  # the instant the head view starts pointing at it.
32
36
  #
33
- # CREATION and BACKFILL are deliberately two separate steps now,
37
+ # Creation and backfill are deliberately two separate steps,
34
38
  # not one nested transaction — principle 1 (docs/implemented/postgres-era-
35
39
  # adapter-split-plan.md): no operation may hold a lock across a
36
40
  # scan whose duration scales with table size, and
37
- # `backfill_head_snapshot!` below is now a CHUNKED, resumable
41
+ # `backfill_head_snapshot!` below is a chunked, resumable
38
42
  # scan (see `ResumableBackfill`), the opposite of something that
39
43
  # belongs inside one transaction. Creation alone (an empty
40
- # table, nothing to scan) still needs the short-held lock
41
- # unchanged from before. Backfill runs unconditionally after —
44
+ # table, nothing to scan) still needs its short-held lock.
45
+ # Backfill runs unconditionally after —
42
46
  # cheap and correct whether the table was just created, already
43
47
  # fully backfilled by a prior boot (one indexed point lookup via
44
48
  # `hecks_backfill_progress`, see ResumableBackfill), or left
45
49
  # mid-backfill by a crashed or still-racing concurrent boot
46
- # (picks up exactly where the last COMMITTED chunk left off).
50
+ # (picks up exactly where the last committed chunk left off).
51
+ #
52
+ # @param storage_name [String] the aggregate's snake-cased storage name in `era`
53
+ # @param era [Integer] ordinal of the era the snapshot belongs to
54
+ # @return [void]
55
+ # @raise [PG::Error] if Postgres refuses the DDL, a journal read, or an upsert
47
56
  def ensure_head_snapshot!(storage_name, era)
48
57
  name = head_snapshot(storage_name, era)
49
58
  unless table_exists?(name)
50
59
  # Locked, not a bare CREATE TABLE IF NOT EXISTS — two
51
60
  # processes booting this aggregate for the very first time
52
- # concurrently must not race to CREATE (one wins, one gets a
61
+ # concurrently must not race to `CREATE` (one wins, one gets a
53
62
  # real Postgres error). Re-checked under the lock: the fast
54
63
  # path above skips locking entirely once any boot has
55
64
  # already finished this once, which is every boot after the
56
65
  # first.
57
66
  #
58
- # NESTABLE — this runs both standalone (adapter boot, its
67
+ # Nestable — this runs both standalone (adapter boot, its
59
68
  # own transaction) and from `compile_head!` while
60
69
  # `mint_era!` is already mid-transaction (its manual
61
70
  # `BEGIN`, held open for the era row, every aggregate's
@@ -63,7 +72,7 @@ module Hecks
63
72
  # `@db.transaction` is a bare BEGIN/COMMIT with no savepoint
64
73
  # nesting (see H2 in docs/audits/2026-08-10-main-bug-
65
74
  # audit.md) — called while already inside a transaction, its
66
- # COMMIT would end THAT transaction early, releasing mint's
75
+ # COMMIT would end that transaction early, releasing mint's
67
76
  # advisory lock and letting a later step run uncommitted.
68
77
  # `nested_transaction` tells the two cases apart and uses a
69
78
  # SAVEPOINT for the second, so the surrounding mint stays
@@ -73,22 +82,21 @@ module Hecks
73
82
  @db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_head_snapshot:' || $1))", [name])
74
83
  next if table_exists?(name)
75
84
 
76
- # `operation` + a NULLABLE `state` — H3, docs/audits/2026-08-
77
- # 10-main-bug-audit.md: a delete used to just `DELETE FROM`
78
- # this table, leaving NO row at all for an id carried in
79
- # from an ancestor era. `compile_head!`'s union below then
80
- # had nothing current-era to outrank the ancestor
85
+ # `operation` + a nullable `state` — H3, docs/audits/2026-08-
86
+ # 10-main-bug-audit.md: a delete that just ran `DELETE FROM`
87
+ # this table would leave no row at all for an id carried in
88
+ # from an ancestor era. `compile_head!`'s union below would
89
+ # then have nothing current-era to outrank the ancestor
81
90
  # matview's own (still-live-looking) `save` row with, so a
82
- # deleted ancestor-carried record kept winning `DISTINCT
83
- # ON` forever. A delete now upserts a TOMBSTONE row here
91
+ # deleted ancestor-carried record would keep winning
92
+ # `DISTINCT ON` forever. A delete upserts a tombstone row here
84
93
  # instead (`operation = 'delete'`, `state` NULL) — the
85
94
  # exact same ordinal-guarded upsert every write already
86
95
  # uses, so it participates in the union/`DISTINCT ON`
87
96
  # exactly like a save does, and out-ranks the ancestor row
88
- # by ordinal the same way a real re-save already did
97
+ # by ordinal the same way a real re-save does
89
98
  # ("re-saves are masked correctly" — the audit's own
90
- # phrasing for why this half of the read path was never
91
- # broken).
99
+ # phrasing for why that half of the read path holds).
92
100
  @db.exec(<<~SQL)
93
101
  CREATE TABLE #{quote(name)} (
94
102
  id text PRIMARY KEY,
@@ -99,7 +107,7 @@ module Hecks
99
107
  SQL
100
108
  end
101
109
  end
102
- # SELF-HEALING for a table this ADR predates — same idiom as
110
+ # Self-healing for a table this ADR predates — same idiom as
103
111
  # `postgres/schema_builder.rb`'s own `hecks_version` backfill:
104
112
  # unconditional, runs on every boot, a no-op once the column
105
113
  # is there. A table created before H3's fix has no
@@ -111,8 +119,12 @@ module Hecks
111
119
  backfill_head_snapshot!(name, storage_name, era)
112
120
  end
113
121
 
114
- # The exact reduction era 1's OLD live view used to run on every
115
- # single read DISTINCT ON latest-per-id, saves only — now
122
+ # Fills a head-snapshot table with the latest saved state of every live id
123
+ # in one era's journal, one committed chunk at a time, resuming from the
124
+ # cursor in `hecks_backfill_progress` and doing nothing once complete.
125
+ #
126
+ # The reduction a live view over the raw journal would run on every
127
+ # single read — DISTINCT ON latest-per-id, saves only — run once,
116
128
  # chunked through `ResumableBackfill#chunked_backfill!` instead
117
129
  # of one blocking `INSERT ... SELECT` over the whole journal:
118
130
  # each chunk reads the next page of distinct ids (a plain SELECT,
@@ -120,7 +132,13 @@ module Hecks
120
132
  # by this running), then upserts it under the same short-held
121
133
  # advisory lock + ordinal guard every other upsert in this
122
134
  # adapter already uses. See `ResumableBackfill`'s own header for
123
- # why this is RESUMABLE, not merely safe-to-restart.
135
+ # why this is resumable, not merely safe-to-restart.
136
+ #
137
+ # @param name [String] unquoted snapshot table name, from `head_snapshot`
138
+ # @param storage_name [String] the aggregate's snake-cased storage name in `era`
139
+ # @param era [Integer] ordinal of the era whose journal rows are read
140
+ # @return [void]
141
+ # @raise [PG::Error] if Postgres refuses a journal read or an upsert
124
142
  def backfill_head_snapshot!(name, storage_name, era)
125
143
  chunked_backfill!(
126
144
  name,
@@ -151,10 +169,19 @@ module Hecks
151
169
  )
152
170
  end
153
171
 
172
+ # Tells whether a relation of this name resolves on the connection's search
173
+ # path, by `to_regclass` — any relation kind counts, not only a table.
174
+ #
175
+ # @param name [String] unquoted relation name
176
+ # @return [Boolean] true when `to_regclass` resolves the name
177
+ # @raise [PG::Error] if the lookup fails
154
178
  def table_exists?(name)
155
179
  @db.exec_params("SELECT to_regclass($1) IS NOT NULL AS present", [name]).getvalue(0, 0) == "t"
156
180
  end
157
181
 
182
+ # Runs the block atomically: in its own transaction when the connection is
183
+ # idle, under a savepoint when a transaction is already open.
184
+ #
158
185
  # A transaction wrapper safe to call from inside an already-open
159
186
  # transaction, unlike `PG::Connection#transaction` (bare
160
187
  # BEGIN/COMMIT, no savepoint nesting — see H2). Standalone
@@ -166,6 +193,16 @@ module Hecks
166
193
  # re-raised) on failure, and either way the surrounding
167
194
  # transaction is never touched — no early COMMIT, no early
168
195
  # release of whatever advisory lock it holds.
196
+ #
197
+ # @param name [String] savepoint name for the nested case, interpolated
198
+ # unquoted so it must be a plain SQL identifier; unused when standalone
199
+ # @yield the statements to run atomically
200
+ # @yieldparam connection [PG::Connection] the connection, standalone only;
201
+ # the nested case yields nothing
202
+ # @return [Object] the block's value when standalone, the `PG::Result` of
203
+ # `RELEASE SAVEPOINT` when nested; callers here ignore it
204
+ # @raise [StandardError] whatever the block raises, re-raised after the
205
+ # transaction or the savepoint is rolled back
169
206
  def nested_transaction(name, &)
170
207
  return @db.transaction(&) if @db.transaction_status == PG::PQTRANS_IDLE
171
208
 
@@ -179,13 +216,20 @@ module Hecks
179
216
  end
180
217
  end
181
218
 
219
+ # Ensures era 1's snapshot table for an aggregate, then points its head
220
+ # view at that table's saved rows, replacing any earlier definition.
221
+ #
182
222
  # Era 1: the head is the snapshot table itself, verbatim — no
183
223
  # per-read reduction over history left to do, because `append`
184
224
  # already keeps the snapshot current as of every write.
225
+ #
226
+ # @param storage_name [String] the aggregate's snake-cased storage name
227
+ # @return [void]
228
+ # @raise [PG::Error] if Postgres refuses the DDL or the snapshot backfill
185
229
  def ensure_first_head!(storage_name)
186
230
  ensure_head_snapshot!(storage_name, 1)
187
231
  # `WHERE operation = 'save'` — era 1 has no ancestor tail to
188
- # worry about, but the snapshot table can now hold delete
232
+ # worry about, but the snapshot table can hold delete
189
233
  # tombstones too (H3, see `ensure_head_snapshot!`'s own
190
234
  # comment), and a tombstone's `state` is NULL: without this
191
235
  # filter a deleted id would still resolve here, just to a nil
@@ -196,19 +240,10 @@ module Hecks
196
240
  SQL
197
241
  end
198
242
 
199
- # The compiled chain over the ancestor tail the matview's body,
200
- # also runnable as a LIVE query (the audit previews a pending
201
- # era through exactly this SQL before anything is minted).
243
+ # Wraps an ancestor-tail query so it keeps only the newest journal entry
244
+ # per aggregate id, deletes included, before any edge is applied.
202
245
  #
203
- # NEVER flatten the edges into one merged rule set, however
204
- # tempting the optimization looks. The two-line counterexample:
205
- # edge 1 renames A→B, edge 2 renames C→A. Flattened, a single
206
- # phase order either applies C→A before A→B (aliasing C's value
207
- # into B) or drops the recycled name entirely — there is NO
208
- # correct position for both rules in one pass. Chaining the
209
- # original edges in mint order reproduces the true execution
210
- # exactly and needs no such reasoning.
211
- # ONLY THE LATEST ANCESTOR ENTRY PER ID IS OBSERVABLE. The head,
246
+ # Only the latest ancestor entry per ID is observable. The head,
212
247
  # the tail-merge, and the audit all reduce by
213
248
  # DISTINCT ON (aggregate_id) ORDER BY ordinal DESC before anyone
214
249
  # reads a state, so an entry with a newer sibling can never
@@ -217,13 +252,18 @@ module Hecks
217
252
  # edited a hundred times cost a hundred translations to serve
218
253
  # one.
219
254
  #
220
- # So the tail is reduced BEFORE the chain, not after. The
255
+ # So the tail is reduced before the chain, not after. The
221
256
  # translated output is identical (the reducer is idempotent and
222
257
  # the survivor is the same row either way); only the work
223
258
  # changes, from |journal entries| to |distinct records|.
224
259
  #
225
- # `era` must survive the reduction: each edge's CASE reads it to
260
+ # `era` must survive the reduction: each edge's `CASE` reads it to
226
261
  # decide whether a row is old enough to need that edge applied.
262
+ #
263
+ # @param tail [String, nil] the `ancestor_tail_sql` SELECT, or an empty string
264
+ # (or nil) when the aggregate has no ancestor era
265
+ # @return [String, nil] a `SELECT DISTINCT ON (aggregate_id)` over `tail`, or
266
+ # `tail` itself, unchanged, when it is empty or nil
227
267
  def latest_per_id(tail)
228
268
  return tail if tail.to_s.empty?
229
269
 
@@ -231,15 +271,18 @@ module Hecks
231
271
  "FROM (#{tail}) tail_entries ORDER BY aggregate_id, ordinal DESC"
232
272
  end
233
273
 
234
- # THE LAYERED BUILD era N from era N-1's matview, not from raw
274
+ # Builds the ancestor-tail SQL for era N by applying only the last edge to
275
+ # era N-1's existing matview plus era N-1's own journal rows.
276
+ #
277
+ # **The layered build** — era N from era N-1's matview, not from raw
235
278
  # history. Returns nil when it cannot apply, and the caller
236
- # falls back to the full chain above.
279
+ # falls back to the full chain (`chain_sql`).
237
280
  #
238
281
  # The algebra it rests on, both halves load-bearing:
239
282
  #
240
- # 1. THE CUTS DO NOT MOVE. ancestor_tail_sql cuts ancestor k at
283
+ # 1. The cuts do not move. ancestor_tail_sql cuts ancestor k at
241
284
  # W(k+1) — the watermark recorded when era k+1 was minted.
242
- # Those are the SAME literals in the era N-1 build and the
285
+ # Those are the same literals in the era N-1 build and the
243
286
  # era N build, so era N-1's matview already carries exactly
244
287
  # the cut era N needs for eras 1..N-2. Only era N-1's own
245
288
  # rows are new, and they are cut at W(N). The watermark is
@@ -247,7 +290,7 @@ module Hecks
247
290
  # baked into the layer beneath. (This is why the tail-merge
248
291
  # must pass full: — it moves every watermark at once.)
249
292
  #
250
- # 2. REDUCING IS ASSOCIATIVE. reduce(A ∪ B) == reduce(reduce(A) ∪ B),
293
+ # 2. Reducing is associative. reduce(A ∪ B) == reduce(reduce(A) ∪ B),
251
294
  # because the survivor is max-ordinal-per-id either way. So
252
295
  # reducing per layer is the same answer as reducing the
253
296
  # whole tail once.
@@ -256,22 +299,23 @@ module Hecks
256
299
  # N-1's shape — the matview because it was chained through edge
257
300
  # N-2, era N-1's own rows because that is the shape they were
258
301
  # written under — so the final edge applies uniformly, with no
259
- # per-era CASE. That equality is asserted, not argued: the spec
302
+ # per-era `CASE`. That equality is asserted, not argued: the spec
260
303
  # builds a third era both ways and diffs them.
261
- # `edges.size != era - 1` — added alongside `head_body_sql`
262
- # below, not merely a pre-existing guard: `names_by_era(aggregate,
304
+ #
305
+ # `edges.size != era - 1` is a guard in its own right, not a
306
+ # restatement of the two before it: `names_by_era(aggregate,
263
307
  # edges)` sizes `names[:storage]` to `edges.size + 1`, and the
264
- # union above indexes it at `names[:storage][era - 2]` — CORRECT
265
- # only when `edges` is the FULL chain reaching `era` (mint's own
308
+ # union in the body indexes it at `names[:storage][era - 2]` — correct
309
+ # only when `edges` is the full chain reaching `era` (mint's own
266
310
  # call, and the audit's own "after" reading, both are). A caller
267
- # handed a SHORTER chain against the SAME `era` — exactly what
311
+ # handed a shorter chain against the same `era` — exactly what
268
312
  # the audit's own "before" reading is, `chain[0..-2]` against the
269
313
  # unchanged target era — would index `names[:storage]` out of
270
314
  # its real bounds instead of falling back to `chain_sql` the way
271
315
  # every other mismatch here already does. Guarded structurally,
272
316
  # not by trusting every future caller to know this invariant.
273
317
  #
274
- # The guard clauses above ARE the method: each rules out one way
318
+ # The guard clauses are the method: each rules out one way
275
319
  # the layered shortcut cannot honestly apply (era too young, too
276
320
  # few edges, a mismatched chain length, no prior era, no label,
277
321
  # no materialized prior view) before the SQL-building tail runs.
@@ -279,6 +323,16 @@ module Hecks
279
323
  # comment it protects, and thread `held`/`prior`/`prior_view`
280
324
  # back out as return values with no simpler shape than they
281
325
  # already have.
326
+ #
327
+ # @param aggregate [Bluebook::Aggregate] the aggregate as the target era declares it
328
+ # @param era [Integer] ordinal of the era being built
329
+ # @param edges [Array<Hash{Symbol => Bluebook::Translation}>] one
330
+ # `{ translation: }` per edge, in mint order; only the last is applied
331
+ # @return [String, nil] the layered SELECT, or nil when `era` is under 3, `edges`
332
+ # is not the full chain of `era - 1` edges, or era N-1 has no row, no label, or
333
+ # no existing matview
334
+ # @raise [Runtime::WiringError] if a held era's text fails its digest check
335
+ # @raise [PG::Error] if reading `hecks_eras` or the catalog fails
282
336
  # rubocop:disable-next Metrics/CyclomaticComplexity
283
337
  # rubocop:disable-next Metrics/PerceivedComplexity
284
338
  def layered_chain_sql(aggregate, era, edges)
@@ -317,6 +371,32 @@ module Hecks
317
371
  SQL
318
372
  end
319
373
 
374
+ # Builds the full translation query for an aggregate: the reduced ancestor
375
+ # tail as a CTE, then one CTE per edge in mint order, edge k applying its
376
+ # rules only to saved rows written in era k or earlier.
377
+ #
378
+ # The compiled chain over the ancestor tail — the matview's body,
379
+ # also runnable as a live query (the audit previews a pending
380
+ # era through exactly this SQL before anything is minted).
381
+ #
382
+ # Never flatten the edges into one merged rule set, however
383
+ # tempting the optimization looks. The two-line counterexample:
384
+ # edge 1 renames A→B, edge 2 renames C→A. Flattened, a single
385
+ # phase order either applies C→A before A→B (aliasing C's value
386
+ # into B) or drops the recycled name entirely — there is no
387
+ # correct position for both rules in one pass. Chaining the
388
+ # original edges in mint order reproduces the true execution
389
+ # exactly and needs no such reasoning.
390
+ #
391
+ # @param aggregate [Bluebook::Aggregate] the aggregate as the target era declares it
392
+ # @param era [Integer] ordinal of the era being built; ancestors are eras
393
+ # `1...era`
394
+ # @param edges [Array<Hash{Symbol => Bluebook::Translation}>] one
395
+ # `{ translation: }` per edge, in mint order
396
+ # @return [String] a `WITH tail AS (...), edge_1 AS (...), ...` SELECT yielding
397
+ # `ordinal, aggregate_id, operation, state`
398
+ # @raise [Runtime::WiringError] if a held era's text fails its digest check
399
+ # @raise [PG::Error] if reading `hecks_eras` fails
320
400
  def chain_sql(aggregate, era, edges)
321
401
  names = names_by_era(aggregate, edges)
322
402
  tail = latest_per_id(ancestor_tail_sql(names, era))
@@ -342,25 +422,50 @@ module Hecks
342
422
  SQL
343
423
  end
344
424
 
425
+ # Runs the head body as a live query and returns every surviving record's
426
+ # translated state, without creating or changing any relation.
427
+ #
345
428
  # The translated tail as it would stand in era `era`, latest
346
429
  # entry per id, saves only — what the audit holds up against the
347
- # bluebook and the edge. Reads through `head_body_sql`, the SAME
430
+ # bluebook and the edge. Reads through `head_body_sql`, the same
348
431
  # layered-or-full choice `compile_head!` makes at real mint time
349
432
  # (that method's own comment has the full reasoning) — so this
350
433
  # is provably what the real mint will materialize, not a second
351
434
  # implementation that could silently drift from it. Safe for
352
- # BOTH real callers: the "after" reading (`edges` is the full
435
+ # both real callers: the "after" reading (`edges` is the full
353
436
  # chain reaching `era`) and the "before" reading (`edges` one
354
437
  # shorter, same `era` — `audit.rb`'s own `samples_for`/`check`
355
438
  # callers) — `layered_chain_sql`'s own `edges.size != era - 1`
356
439
  # guard falls back to `chain_sql` exactly when the shorter
357
440
  # chain can't honestly answer the layered question.
441
+ #
442
+ # @param aggregate [Bluebook::Aggregate] the aggregate as the target era declares it
443
+ # @param era [Integer] ordinal of the era being previewed
444
+ # @param edges [Array<Hash{Symbol => Bluebook::Translation}>] one
445
+ # `{ translation: }` per edge, in mint order; the full chain, or one shorter
446
+ # @return [Hash{String => Hash}] parsed state keyed by aggregate id, for every
447
+ # id whose latest entry is a save; `{}` when nothing survives
448
+ # @raise [PG::Error] if the preview query fails, such as a convert rule
449
+ # meeting a value it does not map
450
+ # @raise [Runtime::WiringError] if a held era's text fails its digest check
358
451
  def translated_latest(aggregate, era, edges)
359
452
  latest_of(head_body_sql(aggregate, era, edges))
360
453
  end
361
454
 
362
- # The UNtranslated ancestor tail, latest entry per id the
455
+ # Reads the ancestor tail with no edge applied and returns each surviving
456
+ # record's state exactly as its own era stored it.
457
+ #
458
+ # The untranslated ancestor tail, latest entry per id — the
363
459
  # "before" side of every per-rule preservation check.
460
+ #
461
+ # @param aggregate [Bluebook::Aggregate] the aggregate as the target era declares it
462
+ # @param era [Integer] ordinal of the target era; ancestors are eras `1...era`
463
+ # @param edges [Array<Hash{Symbol => Bluebook::Translation}>] the edge chain,
464
+ # read only for the `was:` names the aggregate carried in each ancestor era
465
+ # @return [Hash{String => Hash}] parsed state keyed by aggregate id, for every
466
+ # id whose latest entry is a save; `{}` when there is no ancestor era
467
+ # @raise [PG::Error] if the query fails
468
+ # @raise [Runtime::WiringError] if a held era's text fails its digest check
364
469
  def ancestor_latest(aggregate, era, edges)
365
470
  names = names_by_era(aggregate, edges)
366
471
  tail = ancestor_tail_sql(names, era)
@@ -369,6 +474,15 @@ module Hecks
369
474
  latest_of("SELECT ordinal, era, aggregate_id, operation, state FROM (#{tail}) tail_rows")
370
475
  end
371
476
 
477
+ # Runs a journal-shaped query and reduces it the way the head does: newest
478
+ # entry per aggregate id by ordinal, dropping any id whose newest entry is
479
+ # a delete.
480
+ #
481
+ # @param sql [String] a SELECT yielding at least `ordinal`, `aggregate_id`,
482
+ # `operation` and `state`
483
+ # @return [Hash{String => Hash}] `JSON.parse`d state keyed by aggregate id;
484
+ # `{}` when no id survives
485
+ # @raise [PG::Error] if the query fails
372
486
  def latest_of(sql)
373
487
  rows = @db.exec(<<~SQL)
374
488
  SELECT aggregate_id, state FROM (
@@ -379,59 +493,90 @@ module Hecks
379
493
  rows.to_h { |row| [row["aggregate_id"], JSON.parse(row["state"])] }
380
494
  end
381
495
 
496
+ # Chooses the SQL a head's ancestor matview is built from: the layered
497
+ # build when it applies and `full` is off, otherwise the full chain.
498
+ #
499
+ # The one place that picks layered vs full — its own method so the
500
+ # audit's own preview (`translated_latest`, above: both
501
+ # `bin/translation_audit`'s standalone run and the real
502
+ # mint-time gate in coverage_check.rb#audit!) reads through the
503
+ # identical branch selection a real mint's own `compile_head!`
504
+ # will use, rather than a second, hand-kept-in-sync copy of this
505
+ # same "layered when eligible, else full" choice. A preview that
506
+ # called `chain_sql` unconditionally would let the real mint
507
+ # (`compile_head!`, `full: false` by default), for any era >= 3,
508
+ # materialize via the layered path while the
509
+ # preview that approved it never exercised that branch at all.
510
+ # `full:` mirrors `compile_head!`'s own default; tail_merge's
511
+ # `full: true` rebuild is the one caller that ever needs the
512
+ # non-layered branch unconditionally.
513
+ #
514
+ # @param aggregate [Bluebook::Aggregate] the aggregate as the target era declares it
515
+ # @param era [Integer] ordinal of the era being built
516
+ # @param edges [Array<Hash{Symbol => Bluebook::Translation}>] one
517
+ # `{ translation: }` per edge, in mint order
518
+ # @param full [Boolean] true skips the layered build and always chains from
519
+ # the raw journal
520
+ # @return [String] a SELECT yielding `ordinal, aggregate_id, operation, state`
521
+ # @raise [Runtime::WiringError] if a held era's text fails its digest check
522
+ # @raise [PG::Error] if reading `hecks_eras` or the catalog fails
523
+ def head_body_sql(aggregate, era, edges, full: false)
524
+ return chain_sql(aggregate, era, edges) if full
525
+
526
+ layered_chain_sql(aggregate, era, edges) || chain_sql(aggregate, era, edges)
527
+ end
528
+
529
+ # Builds one aggregate's head for a minted era: the ancestor matview, its
530
+ # reduce index, the era's snapshot table, and a head view recreated over
531
+ # the two. Runs inside the caller's mint or merge transaction.
532
+ #
382
533
  # Era N: materialize the translated ancestor tail (edge chain as
383
- # CTEs, watermarks baked in), then overlay this era's OWN live
534
+ # CTEs, watermarks baked in), then overlay this era's own live
384
535
  # rows — read from its snapshot table, not re-derived from raw
385
536
  # history — in a plain view.
386
537
  #
387
- # THE FROZEN-TAIL INVARIANT. This materialized view is correct
388
- # by construction only because BOTH of these hold:
538
+ # **The frozen-tail invariant**. This materialized view is correct
539
+ # by construction only because both of these hold:
389
540
  # 1. journal rows are never updated or deleted (immutability
390
541
  # by privilege), and
391
- # 2. the watermark is baked into this definition as a LITERAL,
392
- # so post-cut ancestor writes — which DO keep arriving
542
+ # 2. the watermark is baked into this definition as a literal,
543
+ # so post-cut ancestor writes — which do keep arriving
393
544
  # while a fork is live — can never enter the head, even on
394
- # a full REFRESH.
395
- # The ancestor partition is append-only but NOT frozen. Any
545
+ # a full `REFRESH`.
546
+ # The ancestor partition is append-only but not frozen. Any
396
547
  # future "optimization" that refreshes incrementally, reads the
397
548
  # watermark from hecks_eras at query time, or otherwise
398
549
  # re-derives the cut will silently leak the old world's post-cut
399
550
  # writes into the new head. Rebuilding the definition (mint,
400
551
  # merge) is the only way the cut may move.
552
+ #
401
553
  # `full:` forces a rebuild from the raw journal. The tail-merge
402
- # needs it: it moves EVERY watermark to the new tip, so every
554
+ # needs it: it moves every watermark to the new tip, so every
403
555
  # ancestor matview's cut goes stale in the same statement, and
404
556
  # layering on one would carry a cut that no longer exists.
405
557
  #
406
- # THE LIVE HALF used to be `WHERE era = era AND aggregate = name`
407
- # over the raw journal — a DISTINCT ON that re-reduced this era's
408
- # ENTIRE write history on every single read, the exact cost this
558
+ # The live half reads the snapshot table, not `WHERE era = era AND
559
+ # aggregate = name` over the raw journal — that would be a
560
+ # DISTINCT ON re-reducing this era's
561
+ # entire write history on every single read, the exact cost the
409
562
  # snapshot table exists to avoid (PostgresEra#append keeps it
410
- # current, transactionally, as of every write). The union below
411
- # is bounded by LIVE RECORD COUNT for this era instead of its
412
- # write count; the ancestor side was already bounded that way
563
+ # current, transactionally, as of every write). The head view's union
564
+ # is bounded by live record count for this era instead of its
565
+ # write count; the ancestor side is bounded the same way
413
566
  # (the matview only ever holds the reduced tail, never raw
414
567
  # history — see latest_per_id's own comment).
415
- # THE ONE PLACE THAT PICKS layered VS full — pulled out so the
416
- # audit's own preview (`translated_latest`, below: both
417
- # `bin/translation_audit`'s standalone run and the real
418
- # mint-time gate in coverage_check.rb#audit!) reads through the
419
- # IDENTICAL branch selection a real mint's own `compile_head!`
420
- # will use, rather than a second, hand-kept-in-sync copy of this
421
- # same "layered when eligible, else full" choice. Before this,
422
- # `translated_latest` called `chain_sql` unconditionally for
423
- # any era >= 3, the real mint (this method, `full: false` by
424
- # default) could materialize via the LAYERED path while the
425
- # preview that approved it never exercised that branch at all.
426
- # `full:` mirrors `compile_head!`'s own default; tail_merge's
427
- # `full: true` rebuild is the one caller that ever needs the
428
- # non-layered branch unconditionally.
429
- def head_body_sql(aggregate, era, edges, full: false)
430
- return chain_sql(aggregate, era, edges) if full
431
-
432
- layered_chain_sql(aggregate, era, edges) || chain_sql(aggregate, era, edges)
433
- end
434
-
568
+ #
569
+ # @param aggregate [Bluebook::Aggregate] the aggregate as era `era` declares it
570
+ # @param era [Integer] ordinal of the era whose head is built, 2 or later
571
+ # @param label [String] the era's minted label, part of the matview's name
572
+ # @param edges [Array<Hash{Symbol => Bluebook::Translation}>] one
573
+ # `{ translation: }` per edge, in mint order, reaching `era`
574
+ # @param full [Boolean] true builds the matview from the raw journal rather
575
+ # than layering on era N-1's matview; the tail-merge passes it
576
+ # @return [void]
577
+ # @raise [PG::Error] if Postgres refuses a statement, including a matview of
578
+ # that name already existing or a convert rule meeting an unmapped value
579
+ # @raise [Runtime::WiringError] if a held era's text fails its digest check
435
580
  def compile_head!(aggregate, era, label, edges, full: false)
436
581
  storage_name = aggregate.storage_name
437
582
  view = matview(storage_name, era, label)
@@ -440,10 +585,10 @@ module Hecks
440
585
  CREATE MATERIALIZED VIEW #{quote(view)} AS
441
586
  #{body}
442
587
  SQL
443
- # ADDITIVE, changes nothing about what can be pushed through
588
+ # Additive, changes nothing about what can be pushed through
444
589
  # the reduction (that wall is structural, an index doesn't
445
590
  # move it — see this file's own module header) — only speeds
446
- # up the reduction ITSELF, which every read still has to run
591
+ # up the reduction itself, which every read still has to run
447
592
  # regardless of a field cache's own two-phase shortcut (a
448
593
  # multi-clause query with an uncached clause, an order_by-only
449
594
  # query, or simply every read before Track C's cache tables
@@ -452,26 +597,26 @@ module Hecks
452
597
  # running on PG18+.
453
598
  @db.exec("CREATE INDEX IF NOT EXISTS #{quote("#{view}_reduce_idx")} ON #{quote(view)} (aggregate_id, ordinal DESC)")
454
599
 
455
- # era-qualified, always a FRESH table for a newly-minted era —
600
+ # era-qualified, always a fresh table for a newly-minted era —
456
601
  # no separate reset step needed; there is structurally nothing
457
602
  # in it yet for anyone to have written, until an append lands
458
603
  # under this era specifically.
459
604
  ensure_head_snapshot!(storage_name, era)
460
605
  @db.exec("DROP VIEW IF EXISTS #{quote(head_view(storage_name))}")
461
- # THE CURRENT-ERA SIDE READS ITS OWN `operation` COLUMN NOW
462
- # H3 (docs/audits/2026-08-10-main-bug-audit.md). This used to
463
- # hardcode `'save' AS operation` here, on the reasoning that a
464
- # delete simply removed its row from the snapshot table. But
465
- # for an id carried in from an ancestor era, that left the
466
- # ancestor matview's own `save` row as the ONLY row on either
467
- # side of this union — which then won `DISTINCT ON` and
468
- # `WHERE operation = 'save'` let it straight through, so a
469
- # deleted ancestor-carried record kept resurrecting forever.
470
- # The snapshot side now upserts a real tombstone row on
606
+ # The current-era side reads its own `operation` column
607
+ # H3 (docs/audits/2026-08-10-main-bug-audit.md) rather than
608
+ # hardcoding `'save' AS operation` on the reasoning that a
609
+ # delete simply removes its row from the snapshot table. For
610
+ # an id carried in from an ancestor era, a removed row leaves the
611
+ # ancestor matview's own `save` row as the only row on either
612
+ # side of this union — which then wins `DISTINCT ON` and
613
+ # `WHERE operation = 'save'` lets it straight through, so a
614
+ # deleted ancestor-carried record keeps resurrecting forever.
615
+ # The snapshot side upserts a real tombstone row on
471
616
  # delete (`ensure_head_snapshot!`'s own comment) instead of
472
617
  # deleting the row, so reading its actual `operation` here
473
618
  # lets that tombstone outrank the stale ancestor `save` row
474
- # by ordinal, exactly the way a genuine re-save already did.
619
+ # by ordinal, exactly the way a genuine re-save does.
475
620
  @db.exec(<<~SQL)
476
621
  CREATE VIEW #{quote(head_view(storage_name))} AS
477
622
  SELECT id, state FROM (
@@ -485,10 +630,20 @@ module Hecks
485
630
  SQL
486
631
  end
487
632
 
488
- # The aggregate's storage name AS OF each era: `was:` chains
633
+ # Resolves the name an aggregate carried in every era of the chain, so each
634
+ # ancestor era's journal rows can be found under the name of their time.
635
+ #
636
+ # The aggregate's storage name as of each era: `was:` chains
489
637
  # walked backward from the current name, one edge at a time.
490
638
  # names[:current][i] is the declared (Pascal) name after edge i;
491
- # names[:storage][e] the snake storage name DURING era e (1-based).
639
+ # names[:storage][i] its snake-cased storage name, so era e
640
+ # (1-based) is `names[:storage][e - 1]`.
641
+ #
642
+ # @param aggregate [Bluebook::Aggregate] the aggregate under its current name
643
+ # @param edges [Array<Hash{Symbol => Bluebook::Translation}>] one
644
+ # `{ translation: }` per edge, in mint order
645
+ # @return [Hash{Symbol => Array<String>}] `:current` and `:storage`, each of
646
+ # `edges.size + 1` names, index 0 being era 1
492
647
  def names_by_era(aggregate, edges)
493
648
  current = Array.new(edges.size + 1)
494
649
  current[edges.size] = aggregate.name
@@ -500,9 +655,20 @@ module Hecks
500
655
  { current: current, storage: storage }
501
656
  end
502
657
 
658
+ # Builds the `UNION ALL` of one journal SELECT per ancestor era, the raw
659
+ # input every translation chain starts from.
660
+ #
503
661
  # Rows this aggregate contributed to every ancestor era, under
504
- # the name it had THEN, cut at the watermark recorded when the
662
+ # the name it had then, cut at the watermark recorded when the
505
663
  # next era was minted.
664
+ #
665
+ # @param names [Hash{Symbol => Array<String>}] the `names_by_era` result; only
666
+ # `:storage` is read
667
+ # @param era [Integer] ordinal of the target era; ancestors are eras `1...era`
668
+ # @return [String] the unioned SELECT, or `""` when `era` is 1; an ancestor
669
+ # whose successor has no recorded watermark is read uncut
670
+ # @raise [Runtime::WiringError] if a held era's text fails its digest check
671
+ # @raise [PG::Error] if reading `hecks_eras` fails
506
672
  def ancestor_tail_sql(names, era)
507
673
  watermarks = eras.to_h { |held| [held[:ordinal], held[:watermark]] }
508
674
  selects = (1...era).map do |ancestor|
@@ -515,13 +681,20 @@ module Hecks
515
681
  end
516
682
 
517
683
  # compile_rules/rekeyed?/id_case/compile_id_expression/
518
- # compile_compute moved to Translation::RuleCompiler — the
519
- # PURE half of this compiler, with no database connection, no
520
- # watermark, no era chain. Extracted so Exporter's build-time
684
+ # compile_compute live in Translation::RuleCompiler — the
685
+ # pure half of this compiler, with no database connection, no
686
+ # watermark, no era chain. Separate so Exporter's build-time
521
687
  # SQL export (feeding rust/host's own boot-time mint) can call
522
688
  # the exact same code this file's own callers do, rather than
523
689
  # a hand-ported duplicate. See that module's own header.
524
690
 
691
+ # Tells whether a view or materialized view of this name is visible on the
692
+ # connection's search path; a table of the same name does not count.
693
+ #
694
+ # @param name [String] unquoted relation name
695
+ # @return [Boolean] true when a visible `pg_class` row of kind `v` or `m` has
696
+ # that name
697
+ # @raise [PG::Error] if the catalog lookup fails
525
698
  def view_exists?(name)
526
699
  # pg_class + pg_table_is_visible, not information_schema/
527
700
  # pg_matviews by bare name — same shared-instance reasoning