hecks 2.8.0 → 2.9.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 (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +17 -98
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +31 -141
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +9 -90
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +150 -678
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +2 -15
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +42 -134
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
@@ -5,98 +5,24 @@ module Hecks
5
5
  module Adapters
6
6
  class PostgresEra
7
7
  class Lineage
8
- # The read side of lineage: builds and maintains, per aggregate per
9
- # era, the head-snapshot table (`ensure_head_snapshot!`/
10
- # `backfill_head_snapshot!`) and the compiled materialized
11
- # view/plain view (`compile_head!`) that translate and reduce the
12
- # append-only journal into the single current-era head a query
13
- # actually reads.
8
+ # The read side of lineage: builds and maintains the head-snapshot
9
+ # table and the compiled view/matview a query actually reads.
14
10
  module HeadCompiler
15
- # ── head compilation ───────────────────────────────────────────
16
-
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
22
- # per live id, upserted transactionally by PostgresEra#append
23
- # alongside the journal insert it belongs to, never derived by
24
- # scanning history thereafter. Idempotent and unguarded by
25
- # `provisioner?` — a plain per-aggregate table, the same
26
- # privilege class as the per-aggregate views/matviews already
27
- # created this way, not the shared owner-only journal.
28
- #
29
- # Backfilled, not just created, the first time this table comes
30
- # into existence for a domain that already has journal history —
31
- # a fresh era (compile_head! at mint) never has any, but era 1
32
- # against an existing deployment (any domain running before this
33
- # snapshot table existed at all) does, and an empty table would
34
- # silently erase every already-written record from every read
35
- # the instant the head view starts pointing at it.
36
- #
37
- # Creation and backfill are deliberately two separate steps,
38
- # not one nested transaction — principle 1 (docs/implemented/postgres-era-
39
- # adapter-split-plan.md): no operation may hold a lock across a
40
- # scan whose duration scales with table size, and
41
- # `backfill_head_snapshot!` below is a chunked, resumable
42
- # scan (see `ResumableBackfill`), the opposite of something that
43
- # belongs inside one transaction. Creation alone (an empty
44
- # table, nothing to scan) still needs its short-held lock.
45
- # Backfill runs unconditionally after —
46
- # cheap and correct whether the table was just created, already
47
- # fully backfilled by a prior boot (one indexed point lookup via
48
- # `hecks_backfill_progress`, see ResumableBackfill), or left
49
- # mid-backfill by a crashed or still-racing concurrent boot
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
11
+ # Backfill runs unconditionally after create: an existing journal
12
+ # must never look empty just because its snapshot table is new.
56
13
  def ensure_head_snapshot!(storage_name, era)
57
14
  name = head_snapshot(storage_name, era)
58
15
  unless table_exists?(name)
59
- # Locked, not a bare CREATE TABLE IF NOT EXISTS — two
60
- # processes booting this aggregate for the very first time
61
- # concurrently must not race to `CREATE` (one wins, one gets a
62
- # real Postgres error). Re-checked under the lock: the fast
63
- # path above skips locking entirely once any boot has
64
- # already finished this once, which is every boot after the
65
- # first.
66
- #
67
- # Nestable — this runs both standalone (adapter boot, its
68
- # own transaction) and from `compile_head!` while
69
- # `mint_era!` is already mid-transaction (its manual
70
- # `BEGIN`, held open for the era row, every aggregate's
71
- # matview, and the advisory lock advance_era! relies on).
72
- # `@db.transaction` is a bare BEGIN/COMMIT with no savepoint
73
- # nesting (see H2 in docs/audits/2026-08-10-main-bug-
74
- # audit.md) — called while already inside a transaction, its
75
- # COMMIT would end that transaction early, releasing mint's
76
- # advisory lock and letting a later step run uncommitted.
77
- # `nested_transaction` tells the two cases apart and uses a
78
- # SAVEPOINT for the second, so the surrounding mint stays
79
- # one real transaction from BEGIN to its own COMMIT
80
- # regardless of how deep this is called from.
16
+ # Locked, not a bare CREATE, so two first-time boots can't race;
17
+ # nested_transaction lets this run standalone or inside mint's
18
+ # own already-open transaction without ending it early.
81
19
  nested_transaction("hecks_head_snapshot") do
82
20
  @db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_head_snapshot:' || $1))", [name])
83
21
  next if table_exists?(name)
84
22
 
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
90
- # matview's own (still-live-looking) `save` row with, so a
91
- # deleted ancestor-carried record would keep winning
92
- # `DISTINCT ON` forever. A delete upserts a tombstone row here
93
- # instead (`operation = 'delete'`, `state` NULL) — the
94
- # exact same ordinal-guarded upsert every write already
95
- # uses, so it participates in the union/`DISTINCT ON`
96
- # exactly like a save does, and out-ranks the ancestor row
97
- # by ordinal the same way a real re-save does
98
- # ("re-saves are masked correctly" — the audit's own
99
- # phrasing for why that half of the read path holds).
23
+ # A delete upserts a tombstone (operation = 'delete', state
24
+ # NULL) instead of removing the row, so it still outranks a
25
+ # stale ancestor `save` row by ordinal under DISTINCT ON.
100
26
  @db.exec(<<~SQL)
101
27
  CREATE TABLE #{quote(name)} (
102
28
  id text PRIMARY KEY,
@@ -107,38 +33,16 @@ module Hecks
107
33
  SQL
108
34
  end
109
35
  end
110
- # Self-healing for a table this ADR predates — same idiom as
111
- # `postgres/schema_builder.rb`'s own `hecks_version` backfill:
112
- # unconditional, runs on every boot, a no-op once the column
113
- # is there. A table created before H3's fix has no
114
- # `operation` column and a `state NOT NULL` constraint a
115
- # tombstone's NULL state would violate; both are corrected
116
- # here regardless of which branch above just ran.
36
+ # Self-healing for a table created before this column existed;
37
+ # a no-op once it and the relaxed constraint are already there.
117
38
  @db.exec("ALTER TABLE #{quote(name)} ADD COLUMN IF NOT EXISTS operation text NOT NULL DEFAULT 'save'")
118
39
  @db.exec("ALTER TABLE #{quote(name)} ALTER COLUMN state DROP NOT NULL")
119
40
  backfill_head_snapshot!(name, storage_name, era)
120
41
  end
121
42
 
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,
128
- # chunked through `ResumableBackfill#chunked_backfill!` instead
129
- # of one blocking `INSERT ... SELECT` over the whole journal:
130
- # each chunk reads the next page of distinct ids (a plain SELECT,
131
- # no lock held — an ordinary reader or writer is never blocked
132
- # by this running), then upserts it under the same short-held
133
- # advisory lock + ordinal guard every other upsert in this
134
- # adapter already uses. See `ResumableBackfill`'s own header for
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
43
+ # Chunked via ResumableBackfill instead of one blocking
44
+ # INSERT ... SELECT, so an ordinary reader or writer is never
45
+ # blocked while a backfill is in flight.
142
46
  def backfill_head_snapshot!(name, storage_name, era)
143
47
  chunked_backfill!(
144
48
  name,
@@ -155,9 +59,8 @@ module Hecks
155
59
  end,
156
60
  upsert: lambda do |rows|
157
61
  rows.each do |row|
158
- # Always `'save'` — `source_sql` above already filtered
159
- # to `operation = 'save'`, so nothing this backfill ever
160
- # sees is a delete tombstone.
62
+ # Always 'save' — source_sql above already filtered to
63
+ # operation = 'save' rows only.
161
64
  @db.exec_params(
162
65
  "INSERT INTO #{quote(name)} (id, ordinal, operation, state) VALUES ($1, $2, 'save', $3) " \
163
66
  "ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
@@ -169,40 +72,17 @@ module Hecks
169
72
  )
170
73
  end
171
74
 
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
75
+ # Matches any relation kind via to_regclass, not only a table,
76
+ # despite the name.
178
77
  def table_exists?(name)
179
78
  @db.exec_params("SELECT to_regclass($1) IS NOT NULL AS present", [name]).getvalue(0, 0) == "t"
180
79
  end
181
80
 
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
- #
185
- # A transaction wrapper safe to call from inside an already-open
186
- # transaction, unlike `PG::Connection#transaction` (bare
187
- # BEGIN/COMMIT, no savepoint nesting — see H2). Standalone
188
- # (`PQTRANS_IDLE`), this is `@db.transaction` itself: a real
189
- # transaction, committed or rolled back on the way out. Nested
190
- # (anything else — `PQTRANS_INTRANS` from a manual `BEGIN` like
191
- # mint_era!'s, or the gem's own `#transaction`), it's a SAVEPOINT
192
- # instead: released on success, rolled back to (then the error
193
- # re-raised) on failure, and either way the surrounding
194
- # transaction is never touched — no early COMMIT, no early
195
- # release of whatever advisory lock it holds.
81
+ # Runs the block inside a transaction, nesting via `SAVEPOINT` when one is already open.
196
82
  #
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
83
+ # `@db.transaction` is a bare `BEGIN`/`COMMIT` with no savepoint nesting,
84
+ # so calling it while already inside a transaction would commit
85
+ # early; this uses a `SAVEPOINT` instead whenever one is already open.
206
86
  def nested_transaction(name, &)
207
87
  return @db.transaction(&) if @db.transaction_status == PG::PQTRANS_IDLE
208
88
 
@@ -216,54 +96,23 @@ module Hecks
216
96
  end
217
97
  end
218
98
 
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
- #
222
- # Era 1: the head is the snapshot table itself, verbatim — no
223
- # per-read reduction over history left to do, because `append`
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
99
+ # Era 1's head is the snapshot table itself, verbatim — no ancestor
100
+ # tail to reduce.
229
101
  def ensure_first_head!(storage_name)
230
102
  ensure_head_snapshot!(storage_name, 1)
231
- # `WHERE operation = 'save'` — era 1 has no ancestor tail to
232
- # worry about, but the snapshot table can hold delete
233
- # tombstones too (H3, see `ensure_head_snapshot!`'s own
234
- # comment), and a tombstone's `state` is NULL: without this
235
- # filter a deleted id would still resolve here, just to a nil
236
- # state, instead of correctly falling out of the head at all.
103
+ # WHERE operation = 'save' excludes delete tombstones, whose
104
+ # state is NULL, so a deleted id falls out of the head instead
105
+ # of resolving to a nil state.
237
106
  @db.exec(<<~SQL)
238
107
  CREATE OR REPLACE VIEW #{quote(head_view(storage_name))} AS
239
108
  SELECT id, state FROM #{quote(head_snapshot(storage_name, 1))} WHERE operation = 'save'
240
109
  SQL
241
110
  end
242
111
 
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.
245
- #
246
- # Only the latest ancestor entry per ID is observable. The head,
247
- # the tail-merge, and the audit all reduce by
248
- # DISTINCT ON (aggregate_id) ORDER BY ordinal DESC before anyone
249
- # reads a state, so an entry with a newer sibling can never
250
- # reach a reader. Translating those siblings computes and stores
251
- # rows nothing can observe — on an append-only journal a record
252
- # edited a hundred times cost a hundred translations to serve
253
- # one.
254
- #
255
- # So the tail is reduced before the chain, not after. The
256
- # translated output is identical (the reducer is idempotent and
257
- # the survivor is the same row either way); only the work
258
- # changes, from |journal entries| to |distinct records|.
259
- #
260
- # `era` must survive the reduction: each edge's `CASE` reads it to
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
112
+ # Reduces the tail before chaining edges over it, not after: every
113
+ # reader already reduces to latest-per-id, so translating a
114
+ # superseded entry would be wasted work. `era` survives the
115
+ # reduction because each edge's case still needs it.
267
116
  def latest_per_id(tail)
268
117
  return tail if tail.to_s.empty?
269
118
 
@@ -271,68 +120,13 @@ module Hecks
271
120
  "FROM (#{tail}) tail_entries ORDER BY aggregate_id, ordinal DESC"
272
121
  end
273
122
 
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
278
- # history. Returns nil when it cannot apply, and the caller
279
- # falls back to the full chain (`chain_sql`).
280
- #
281
- # The algebra it rests on, both halves load-bearing:
282
- #
283
- # 1. The cuts do not move. ancestor_tail_sql cuts ancestor k at
284
- # W(k+1) — the watermark recorded when era k+1 was minted.
285
- # Those are the same literals in the era N-1 build and the
286
- # era N build, so era N-1's matview already carries exactly
287
- # the cut era N needs for eras 1..N-2. Only era N-1's own
288
- # rows are new, and they are cut at W(N). The watermark is
289
- # still a literal baked into a definition; it is simply
290
- # baked into the layer beneath. (This is why the tail-merge
291
- # must pass full: — it moves every watermark at once.)
292
- #
293
- # 2. Reducing is associative. reduce(A ∪ B) == reduce(reduce(A) ∪ B),
294
- # because the survivor is max-ordinal-per-id either way. So
295
- # reducing per layer is the same answer as reducing the
296
- # whole tail once.
297
- #
298
- # And every row on both sides of the union is already in era
299
- # N-1's shape — the matview because it was chained through edge
300
- # N-2, era N-1's own rows because that is the shape they were
301
- # written under — so the final edge applies uniformly, with no
302
- # per-era `CASE`. That equality is asserted, not argued: the spec
303
- # builds a third era both ways and diffs them.
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,
307
- # edges)` sizes `names[:storage]` to `edges.size + 1`, and the
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
310
- # call, and the audit's own "after" reading, both are). A caller
311
- # handed a shorter chain against the same `era` — exactly what
312
- # the audit's own "before" reading is, `chain[0..-2]` against the
313
- # unchanged target era — would index `names[:storage]` out of
314
- # its real bounds instead of falling back to `chain_sql` the way
315
- # every other mismatch here already does. Guarded structurally,
316
- # not by trusting every future caller to know this invariant.
317
- #
318
- # The guard clauses are the method: each rules out one way
319
- # the layered shortcut cannot honestly apply (era too young, too
320
- # few edges, a mismatched chain length, no prior era, no label,
321
- # no materialized prior view) before the SQL-building tail runs.
322
- # Splitting them out would separate each guard from the algebra
323
- # comment it protects, and thread `held`/`prior`/`prior_view`
324
- # back out as return values with no simpler shape than they
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
123
+ # Builds era N by layering its own last edge onto era N-1's existing
124
+ # matview, instead of re-deriving from raw history; returns nil
125
+ # (falling back to chain_sql) whenever that shortcut can't honestly
126
+ # apply. `edges.size != era - 1` guards names_by_era's own
127
+ # assumption that `edges` is the full chain reaching `era` — a
128
+ # shorter chain here would index names[:storage] out of bounds
129
+ # rather than safely falling back.
336
130
  # rubocop:disable-next Metrics/CyclomaticComplexity
337
131
  # rubocop:disable-next Metrics/PerceivedComplexity
338
132
  def layered_chain_sql(aggregate, era, edges)
@@ -371,32 +165,9 @@ module Hecks
371
165
  SQL
372
166
  end
373
167
 
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
168
+ # Chains the original edges in mint order rather than flattening them
169
+ # into one rule set: edge 1 renaming A→B then edge 2 renaming C→A
170
+ # has no single phase order that applies both correctly.
400
171
  def chain_sql(aggregate, era, edges)
401
172
  names = names_by_era(aggregate, edges)
402
173
  tail = latest_per_id(ancestor_tail_sql(names, era))
@@ -422,50 +193,15 @@ module Hecks
422
193
  SQL
423
194
  end
424
195
 
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
- #
428
- # The translated tail as it would stand in era `era`, latest
429
- # entry per id, saves only — what the audit holds up against the
430
- # bluebook and the edge. Reads through `head_body_sql`, the same
431
- # layered-or-full choice `compile_head!` makes at real mint time
432
- # (that method's own comment has the full reasoning) — so this
433
- # is provably what the real mint will materialize, not a second
434
- # implementation that could silently drift from it. Safe for
435
- # both real callers: the "after" reading (`edges` is the full
436
- # chain reaching `era`) and the "before" reading (`edges` one
437
- # shorter, same `era` — `audit.rb`'s own `samples_for`/`check`
438
- # callers) — `layered_chain_sql`'s own `edges.size != era - 1`
439
- # guard falls back to `chain_sql` exactly when the shorter
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
196
+ # Reads through head_body_sql — the same layered-or-full choice
197
+ # compile_head! makes at mint time — so a preview can't silently
198
+ # drift from what a real mint will materialize.
451
199
  def translated_latest(aggregate, era, edges)
452
200
  latest_of(head_body_sql(aggregate, era, edges))
453
201
  end
454
202
 
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
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
203
+ # The untranslated ancestor tail, latest entry per id — the "before"
204
+ # side of a per-rule preservation check.
469
205
  def ancestor_latest(aggregate, era, edges)
470
206
  names = names_by_era(aggregate, edges)
471
207
  tail = ancestor_tail_sql(names, era)
@@ -474,15 +210,8 @@ module Hecks
474
210
  latest_of("SELECT ordinal, era, aggregate_id, operation, state FROM (#{tail}) tail_rows")
475
211
  end
476
212
 
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
213
+ # Reduces to newest entry per aggregate id, dropping any id whose
214
+ # newest entry is a delete rather than returning it with a nil state.
486
215
  def latest_of(sql)
487
216
  rows = @db.exec(<<~SQL)
488
217
  SELECT aggregate_id, state FROM (
@@ -493,90 +222,20 @@ module Hecks
493
222
  rows.to_h { |row| [row["aggregate_id"], JSON.parse(row["state"])] }
494
223
  end
495
224
 
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
225
+ # The one place that picks layered vs full, so a preview
226
+ # (translated_latest) can never approve a branch the real mint
227
+ # doesn't take. `full:` mirrors compile_head!'s own default;
228
+ # tail_merge is the one caller that forces it true.
523
229
  def head_body_sql(aggregate, era, edges, full: false)
524
230
  return chain_sql(aggregate, era, edges) if full
525
231
 
526
232
  layered_chain_sql(aggregate, era, edges) || chain_sql(aggregate, era, edges)
527
233
  end
528
234
 
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
- #
533
- # Era N: materialize the translated ancestor tail (edge chain as
534
- # CTEs, watermarks baked in), then overlay this era's own live
535
- # rows — read from its snapshot table, not re-derived from raw
536
- # history — in a plain view.
537
- #
538
- # **The frozen-tail invariant**. This materialized view is correct
539
- # by construction only because both of these hold:
540
- # 1. journal rows are never updated or deleted (immutability
541
- # by privilege), and
542
- # 2. the watermark is baked into this definition as a literal,
543
- # so post-cut ancestor writes — which do keep arriving
544
- # while a fork is live — can never enter the head, even on
545
- # a full `REFRESH`.
546
- # The ancestor partition is append-only but not frozen. Any
547
- # future "optimization" that refreshes incrementally, reads the
548
- # watermark from hecks_eras at query time, or otherwise
549
- # re-derives the cut will silently leak the old world's post-cut
550
- # writes into the new head. Rebuilding the definition (mint,
551
- # merge) is the only way the cut may move.
552
- #
553
- # `full:` forces a rebuild from the raw journal. The tail-merge
554
- # needs it: it moves every watermark to the new tip, so every
555
- # ancestor matview's cut goes stale in the same statement, and
556
- # layering on one would carry a cut that does not exist.
557
- #
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
562
- # snapshot table exists to avoid (PostgresEra#append keeps it
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
566
- # (the matview only ever holds the reduced tail, never raw
567
- # history — see latest_per_id's own comment).
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
235
+ # The matview bakes each ancestor's watermark in as a literal;
236
+ # refreshing it incrementally or re-deriving the cut at query time
237
+ # would leak post-cut ancestor writes into the head. Only rebuilding
238
+ # the definition (mint, merge) may move the cut.
580
239
  def compile_head!(aggregate, era, label, edges, full: false)
581
240
  storage_name = aggregate.storage_name
582
241
  view = matview(storage_name, era, label)
@@ -585,38 +244,13 @@ module Hecks
585
244
  CREATE MATERIALIZED VIEW #{quote(view)} AS
586
245
  #{body}
587
246
  SQL
588
- # Additive, changes nothing about what can be pushed through
589
- # the reduction (that wall is structural, an index doesn't
590
- # move it — see this file's own module header) — only speeds
591
- # up the reduction itself, which every read still has to run
592
- # regardless of a field cache's own two-phase shortcut (a
593
- # multi-clause query with an uncached clause, an order_by-only
594
- # query, or simply every read before Track C's cache tables
595
- # exist for a given field all still hit this). Worth having
596
- # on any supported server; genuinely skip-scan-usable once
597
- # running on PG18+.
598
247
  @db.exec("CREATE INDEX IF NOT EXISTS #{quote("#{view}_reduce_idx")} ON #{quote(view)} (aggregate_id, ordinal DESC)")
599
248
 
600
- # era-qualified, always a fresh table for a newly-minted era —
601
- # no separate reset step needed; there is structurally nothing
602
- # in it yet for anyone to have written, until an append lands
603
- # under this era specifically.
604
249
  ensure_head_snapshot!(storage_name, era)
605
250
  @db.exec("DROP VIEW IF EXISTS #{quote(head_view(storage_name))}")
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
616
- # delete (`ensure_head_snapshot!`'s own comment) instead of
617
- # deleting the row, so reading its actual `operation` here
618
- # lets that tombstone outrank the stale ancestor `save` row
619
- # by ordinal, exactly the way a genuine re-save does.
251
+ # Reads the snapshot's own operation column rather than hardcoding
252
+ # 'save', so a tombstone here outranks a stale ancestor save row
253
+ # by ordinal instead of letting it resurrect a deleted record.
620
254
  @db.exec(<<~SQL)
621
255
  CREATE VIEW #{quote(head_view(storage_name))} AS
622
256
  SELECT id, state FROM (
@@ -630,20 +264,9 @@ module Hecks
630
264
  SQL
631
265
  end
632
266
 
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
637
- # walked backward from the current name, one edge at a time.
638
- # names[:current][i] is the declared (Pascal) name after edge i;
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
267
+ # Walks `was:` chains backward from the current name, so each
268
+ # ancestor era's rows can be found under the name of their time.
269
+ # names[:storage][e - 1] is the storage name for era e (1-based).
647
270
  def names_by_era(aggregate, edges)
648
271
  current = Array.new(edges.size + 1)
649
272
  current[edges.size] = aggregate.name
@@ -655,20 +278,8 @@ module Hecks
655
278
  { current: current, storage: storage }
656
279
  end
657
280
 
658
- # Builds the `UNION ALL` of one journal SELECT per ancestor era, the raw
659
- # input every translation chain starts from.
660
- #
661
- # Rows this aggregate contributed to every ancestor era, under
662
- # the name it had then, cut at the watermark recorded when the
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
281
+ # Cuts each ancestor era's rows at the watermark recorded when its
282
+ # successor was minted, so later writes to the old world don't leak in.
672
283
  def ancestor_tail_sql(names, era)
673
284
  watermarks = eras.to_h { |held| [held[:ordinal], held[:watermark]] }
674
285
  selects = (1...era).map do |ancestor|
@@ -680,28 +291,16 @@ module Hecks
680
291
  selects.join(" UNION ALL ")
681
292
  end
682
293
 
683
- # compile_rules/rekeyed?/id_case/compile_id_expression/
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
687
- # SQL export (feeding rust/host's own boot-time mint) can call
688
- # the exact same code this file's own callers do, rather than
689
- # a hand-ported duplicate. See that module's own header.
294
+ # The rule-compiling helpers live in Translation::RuleCompiler instead,
295
+ # with no database connection, so Exporter's build-time SQL export can
296
+ # call the exact same code these callers do.
690
297
 
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
298
+ # Matches a view or materialized view only; a table of the same
299
+ # name does not count.
698
300
  def view_exists?(name)
699
- # pg_class + pg_table_is_visible, not information_schema/
700
- # pg_matviews by bare name — same shared-instance reasoning
701
- # as provisioning.rb's own catalog lookups: this must resolve
702
- # the name the same way search_path would, or a sibling
703
- # domain's same-named view satisfies a check meant for this
704
- # domain's own.
301
+ # pg_table_is_visible, not information_schema, so this resolves
302
+ # the name the same way search_path would rather than matching a
303
+ # sibling domain's same-named view.
705
304
  @db.exec_params(
706
305
  "SELECT 1 FROM pg_class WHERE relname = $1 AND relkind IN ('v', 'm') " \
707
306
  "AND pg_table_is_visible(oid)",