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
@@ -13,13 +13,33 @@ module Hecks
13
13
  module MintTransaction
14
14
  # ── the mint transaction ───────────────────────────────────────
15
15
  #
16
+ # Makes a new era real, or reports that a concurrent minter already did.
17
+ #
16
18
  # One transaction: the new era row (held text + minted name +
17
19
  # cut watermark), the new partition, and every aggregate's
18
20
  # recompiled matview + head view. The advisory lock is the
19
21
  # writer fence — two concurrent minters serialize, and the
20
22
  # second finds the era already held. Populating the matview
21
23
  # inside the transaction means a convert meeting an unmapped
22
- # value REFUSES the whole mint, loudly, before anything boots.
24
+ # value refuses the whole mint, loudly, before anything boots.
25
+ #
26
+ # @param ordinal [Integer] ordinal of the era to mint, one past the newest held era
27
+ # @param hash [String] the new era's shape hash, SHA-256 hex
28
+ # @param label [String] the new era's short label, a prefix of `hash`
29
+ # @param held_text [String] the bluebook source text to freeze as this era
30
+ # @param aggregates [Array<Bluebook::Aggregate>] the current bluebook's aggregates, each
31
+ # of which gets its head recompiled
32
+ # @param edges [Array<Hash{Symbol => Bluebook::Translation}>] the full edge chain in mint
33
+ # order, one `{ translation: }` Hash per step, as `LineageManager.edge_chain` builds it
34
+ # @param role [String, nil] app role to grant base and head privileges to; nil grants
35
+ # nothing
36
+ # @param projection [Hash{String => Object}, nil] the bluebook's storage-shape
37
+ # projection, as `Runtime::StorageShape.project` returns it; nil stores none
38
+ # @return [Boolean] true once the era is committed; false, with everything rolled back,
39
+ # when the domain already holds `ordinal`
40
+ # @raise [Runtime::WiringError] if another mint or merge holds the domain lock for over
41
+ # 10s, if Postgres refuses any statement (a convert meeting an unmapped value
42
+ # included), or if a held era's text fails its integrity check
23
43
  def mint_era!(ordinal:, hash:, label:, held_text:, aggregates:, edges:, role: nil, projection: nil)
24
44
  @db.exec("BEGIN")
25
45
  # A concurrent minter blocks briefly, then refuses with a name
@@ -43,31 +63,31 @@ module Hecks
43
63
  )
44
64
  archive_text!(ordinal, held_text)
45
65
  ensure_partition!(ordinal)
46
- # AFTER compile_head!, not before — this era's own snapshot
66
+ # After compile_head!, not before — this era's own snapshot
47
67
  # table (a possibly-renamed name, per aggregate) is what
48
68
  # grant_role! grants on, and compile_head! is what creates it.
49
69
  # Granting first would GRANT on a relation that does not
50
70
  # exist yet for any aggregate renamed in this very edge.
51
71
  aggregates.each { |aggregate| compile_head!(aggregate, ordinal, label, edges) }
52
72
  grant_role!(role, aggregates: aggregates, era: ordinal) if role
53
- # LAST, right before COMMIT — not merely unconditional. Once
54
- # acquired, a lock is held until the TRANSACTION ends, not
73
+ # **Last, right before `COMMIT`** — not merely unconditional. Once
74
+ # acquired, a lock is held until the transaction ends, not
55
75
  # just for the statement that took it — so advance_era!'s
56
76
  # DROP POLICY/CREATE POLICY (AccessExclusiveLock, same family
57
77
  # as ALTER TABLE, and unavoidably so: Postgres has no lighter
58
78
  # form for changing a policy, unlike the partition attach
59
79
  # below) blocks every concurrent writer for as long as it sits
60
- # BEFORE the expensive step. Ordered here, that block is the
80
+ # before the expensive step. Ordered here, that block is the
61
81
  # width of a few catalog statements plus the commit itself,
62
- # not the width of compile_head!'s matview build. Measured:
63
- # moving this above compile_head! (an earlier ordering, caught
64
- # only once a genuine concurrent-write test was built rather
65
- # than assumed) reintroduced exactly the mint-stops-the-world
66
- # cost ensure_partition!'s build-then-ATTACH exists to avoid.
82
+ # not the width of compile_head!'s matview build. Measured, not
83
+ # assumed: placed above compile_head!, this reintroduces
84
+ # exactly the mint-stops-the-world cost ensure_partition!'s
85
+ # build-then-ATTACH exists to avoid a cost only a genuine
86
+ # concurrent-write test shows.
67
87
  #
68
- # UNCONDITIONAL regardless of position — this is the line that
88
+ # Unconditional regardless of position — this is the line that
69
89
  # drops writing to the old schema. It does not wait for a role
70
- # to be configured on THIS boot, because the cutoff is a fact
90
+ # to be configured on this boot, because the cutoff is a fact
71
91
  # about the era, not about who happened to mint it: an old
72
92
  # checkout's role, granted by some earlier boot this one knows
73
93
  # nothing about, must lose write access the instant this
@@ -93,10 +113,12 @@ module Hecks
93
113
  raise Runtime::WiringError, "cannot mint era #{ordinal} of #{@domain}: #{e.message.strip}"
94
114
  end
95
115
 
96
- # Base privileges for a deployment's app role a NON-owner,
116
+ # Grants an app role what it needs to append to the journal and read and write heads.
117
+ #
118
+ # Base privileges for a deployment's app role — a non-owner,
97
119
  # which may append and read once the shared era fence below
98
120
  # admits it, and owns nothing. Idempotent, and unconcerned with
99
- # WHICH era is current: that is advance_era!'s job, not this
121
+ # which era is current: that is advance_era!'s job, not this
100
122
  # one's, so a role can be onboarded at any time without
101
123
  # disturbing who may write what right now.
102
124
  #
@@ -107,10 +129,10 @@ module Hecks
107
129
  # `aggregates:`/`era:` cover the read-cache side of the same
108
130
  # story: unlike the journal (immutable, owner-provisioned once),
109
131
  # each aggregate's head_snapshot table is a table an app role
110
- # must itself INSERT/UPDATE/DELETE into — PostgresEra#append writes
132
+ # must itself `INSERT`/`UPDATE`/`DELETE` into — PostgresEra#append writes
111
133
  # it directly, not through a view — so it needs real DML grants,
112
134
  # not just the SELECT a derived read surface would need. `era:`
113
- # is the ordinal THIS role is about to write under (the one
135
+ # is the ordinal this role is about to write under (the one
114
136
  # hold_first!/mint_era! just made current, or the superseded one
115
137
  # a stale checkout still speaks) — head_snapshot is era-scoped,
116
138
  # so granting on the wrong era's table would grant on a table
@@ -119,6 +141,15 @@ module Hecks
119
141
  # held-but-superseded checkout, since a role connecting to it
120
142
  # for the first time (a new instance of an old checkout) has no
121
143
  # privileges yet either.
144
+ #
145
+ # @param role [String] name of the Postgres role to grant to
146
+ # @param aggregates [Array<Bluebook::Aggregate>] aggregates whose head snapshot table,
147
+ # and head view where one exists, the role is granted on; ignored when `era` is nil
148
+ # @param era [Integer, nil] ordinal of the era whose snapshot tables to grant on; nil
149
+ # grants only the journal and sequence privileges
150
+ # @return [void]
151
+ # @raise [PG::Error] if Postgres refuses a grant, such as one naming a role or a
152
+ # snapshot table that does not exist
122
153
  def grant_role!(role, aggregates: [], era: nil)
123
154
  return unless provisioner?
124
155
 
@@ -137,7 +168,9 @@ module Hecks
137
168
  end
138
169
  end
139
170
 
140
- # THE current-era fence ONE policy, shared by every granted
171
+ # Replaces the journal's row policies: one era accepts INSERTs, every row stays readable.
172
+ #
173
+ # The current-era fence — one policy, shared by every granted
141
174
  # role, not one per role. Advancing it is what drops writing to
142
175
  # the old schema the instant the new one materializes: the
143
176
  # moment this commits, no role — old or new, whether or not it
@@ -145,28 +178,33 @@ module Hecks
145
178
  # era named here. There is no persisted fork: a checkout that
146
179
  # keeps a role fenced to a stale era does not exist in this
147
180
  # design, because there is no such thing as a role fenced to an
148
- # era at all — only the ONE era everyone currently shares.
181
+ # era at all — only the one era everyone currently shares.
149
182
  #
150
- # A ROW POLICY, not a partition grant. Postgres checks INSERT
151
- # privilege on the partitioned PARENT for a routed insert and
183
+ # A row policy, not a partition grant. Postgres checks INSERT
184
+ # privilege on the partitioned parent for a routed insert and
152
185
  # never consults the partition's own grants, so a per-partition
153
186
  # GRANT/REVOKE is inert in both directions: grant only on the
154
187
  # partition and nobody can write at all; grant on the parent and
155
- # they may write into EVERY era, ancestors included. Measured,
188
+ # they may write into every era, ancestors included. Measured,
156
189
  # not reasoned about — see the spec, which writes through the
157
190
  # fence rather than asserting the catalog.
158
191
  #
159
192
  # The table owner bypasses RLS by default, and that is load
160
193
  # bearing: mint and merge run as the owner and must be able to
161
194
  # write any era (the merge re-enters a winner's state into the
162
- # CURRENT era, and compile_head! reads every ancestor).
195
+ # current era, and compile_head! reads every ancestor).
163
196
  #
164
- # CALL ONLY WITH THE NEW CURRENT ORDINAL — from hold_first! (era
165
- # 1) or mint_era! (era N). Calling this with a SUPERSEDED
197
+ # Call only with the new current ordinal — from hold_first! (era
198
+ # 1) or mint_era! (era N). Calling this with a superseded
166
199
  # ordinal — from a boot that merely recognizes an old checkout —
167
200
  # would roll the fence backward and silently reopen the old
168
- # schema for everyone. That path grants a role's PRIVILEGES
201
+ # schema for everyone. That path grants a role's privileges
169
202
  # (grant_role!) and stops there on purpose.
203
+ #
204
+ # @param ordinal [Integer] ordinal of the era that becomes the only writable one
205
+ # @return [void]
206
+ # @raise [PG::Error] if Postgres refuses the policy change, as it does for a role that
207
+ # does not own the journal
170
208
  def advance_era!(ordinal)
171
209
  @db.exec("DROP POLICY IF EXISTS hecks_current_era ON #{quoted_journal}")
172
210
  @db.exec(
@@ -9,7 +9,12 @@ module Hecks
9
9
  # journal partition (`ensure_partition!`) — the build-then-ATTACH
10
10
  # dance that keeps a mint from stopping every writer.
11
11
  module Provisioning
12
- # Provisioning is the OWNER's job, and a deployment's app role is
12
+ # Builds or tops up this domain's lineage schema when the connected role
13
+ # owns the journal (or no journal exists yet); for any other role it only
14
+ # attempts the `formerly_known_as` rename bridge, when one is declared, and
15
+ # leaves the schema as the owner built it.
16
+ #
17
+ # Provisioning is the owner's job, and a deployment's app role is
13
18
  # deliberately not the owner — it may append and read, and it
14
19
  # owns nothing. By the time such a role connects, the base is
15
20
  # already built, so its boot verifies rather than builds.
@@ -23,10 +28,15 @@ module Hecks
23
28
  # every guarded ALTER (RLS, REVOKE) is guarded and placed
24
29
  # exactly where it is for measured, documented reasons (see the
25
30
  # inline comments on each: catalog-lock avoidance, RLS timing,
26
- # FORCE semantics). Splitting this into smaller methods would
31
+ # `FORCE` semantics). Splitting this into smaller methods would
27
32
  # only hide that single ordered sequence behind several call
28
33
  # sites, with nothing gained — each statement already reads as
29
34
  # one step, and the length here is DDL, not branching logic.
35
+ #
36
+ # @return [void]
37
+ # @raise [Runtime::WiringError] if the `formerly_known_as` rename cannot take
38
+ # its locks within 10s or Postgres refuses one of its statements
39
+ # @raise [PG::Error] if Postgres refuses a provisioning statement
30
40
  # rubocop:disable-next Metrics/MethodLength
31
41
  def ensure_base!
32
42
  rename_domain! if @formerly_known_as
@@ -72,7 +82,7 @@ module Hecks
72
82
  )
73
83
  SQL
74
84
  @db.exec("CREATE SEQUENCE IF NOT EXISTS #{quote(sequence)}")
75
- # GENERATED ALWAYS AS IDENTITY is the intent, but identity
85
+ # `GENERATED ALWAYS AS IDENTITY` is the intent, but identity
76
86
  # columns on partitioned tables need Postgres 17 — an owned
77
87
  # sequence default is the same spanning ordinal on any
78
88
  # supported server.
@@ -91,29 +101,30 @@ module Hecks
91
101
  # Immutability by privilege: nothing updates or deletes journal
92
102
  # rows. The owner's implicit rights remain (Postgres has no way
93
103
  # to revoke them from the owner itself); a deployment's app
94
- # role connects as a NON-owner and gets exactly INSERT, per
104
+ # role connects as a non-owner and gets exactly INSERT, per
95
105
  # era, at mint time.
96
106
  #
97
- # GUARDED, same reasoning as the RLS ALTER TABLE calls just
107
+ # Guarded, same reasoning as the RLS ALTER TABLE calls just
98
108
  # below: REVOKE still writes pg_class.relacl (and takes the
99
109
  # matching lock) even when the resulting privileges are
100
110
  # unchanged, so an unconditional reissue on every ordinary
101
- # reboot raced two concurrent boots into
111
+ # reboot races two concurrent boots into
102
112
  # `PG::InternalError: tuple concurrently updated` — read
103
113
  # first, touch the catalog only on the boot that actually
104
114
  # needs to.
105
115
  #
106
116
  # `relacl IS NULL`, not `has_table_privilege('public', ...,
107
117
  # 'UPDATE')` — found live, not assumed: a brand-new table's
108
- # PUBLIC privilege is already "no UPDATE" by Postgres's own
118
+ # `PUBLIC` privilege is already "no UPDATE" by Postgres's own
109
119
  # default (nothing has ever been explicitly granted to
110
- # PUBLIC), so `has_table_privilege` answers false BOTH before
111
- # the REVOKE has ever run AND after it has — indistinguishable
112
- # by that check alone, which meant the very first boot's own
113
- # REVOKE never actually ran, `relacl` stayed NULL forever, and
114
- # "journal rows accept no UPDATE/DELETE from PUBLIC" was true
115
- # only by accident of Postgres's default, not by the explicit
116
- # privilege revocation this method exists to record.
120
+ # `PUBLIC`), so `has_table_privilege` answers false both before
121
+ # the REVOKE has ever run and after it has — indistinguishable
122
+ # by that check alone. Guarding on it means the very first
123
+ # boot's own REVOKE never actually runs, `relacl` stays NULL
124
+ # forever, and "journal rows accept no `UPDATE`/`DELETE` from
125
+ # `PUBLIC`" is true only by accident of Postgres's default, not
126
+ # by the explicit privilege revocation this method exists to
127
+ # record.
117
128
  # `relacl IS NULL` means "default ACL, nothing explicit yet" —
118
129
  # exactly the one-time signal needed, and (like the RLS flags
119
130
  # below) `pg_table_is_visible(oid)`, not a bare relname match,
@@ -122,45 +133,45 @@ module Hecks
122
133
  "SELECT relacl IS NULL FROM pg_class WHERE relname = $1 AND pg_table_is_visible(oid)", [journal]
123
134
  ).getvalue(0, 0)
124
135
  @db.exec("REVOKE UPDATE, DELETE ON #{quoted_journal} FROM PUBLIC") if relacl_null == "t"
125
- # RLS goes on AT PROVISIONING, never mid-life — enabling it
136
+ # RLS goes on at provisioning, never mid-life — enabling it
126
137
  # later would deny every role that has no policy yet, on
127
138
  # whatever the shape of the schema happened to be at that
128
139
  # moment.
129
140
  #
130
- # FORCE, not merely ENABLE: without it, the table OWNER is
141
+ # `FORCE`, not merely `ENABLE`: without it, the table owner is
131
142
  # exempt from every policy here, by Postgres default — which
132
143
  # would leave the schema writable forever to whoever holds
133
144
  # the owner's credentials, the one connection this whole
134
145
  # design cannot fence. Checked, not assumed: mint_era! never
135
146
  # inserts into the journal at all (only hecks_eras/
136
147
  # hecks_era_texts, neither RLS-protected), and merge_tail!'s
137
- # one journal INSERT targets the CURRENT era, which the fence
138
- # already admits for anyone with base privileges — so FORCE
148
+ # one journal INSERT targets the current era, which the fence
149
+ # already admits for anyone with base privileges — so `FORCE`
139
150
  # costs the owner nothing operations here actually need.
140
151
  #
141
- # This still exempts an actual Postgres SUPERUSER (or any
142
- # role granted BYPASSRLS) unconditionally — FORCE only
143
- # narrows what ENABLE already narrows for the owner
152
+ # This still exempts an actual Postgres superuser (or any
153
+ # role granted BYPASSRLS) unconditionally — `FORCE` only
154
+ # narrows what `ENABLE` already narrows for the owner
144
155
  # specifically, and superuser bypass sits above both. Running
145
156
  # migrations as a real superuser (self-hosted Postgres, most
146
157
  # commonly) leaves this gap open regardless; a managed
147
- # provider's admin account is typically NOT a superuser, and
148
- # is exactly what FORCE closes.
158
+ # provider's admin account is typically not a superuser, and
159
+ # is exactly what `FORCE` closes.
149
160
  #
150
- # GUARDED, not reissued unconditionally — measured, not
161
+ # Guarded, not reissued unconditionally — measured, not
151
162
  # assumed: `ALTER TABLE ... ENABLE/FORCE ROW LEVEL SECURITY`
152
- # takes AccessExclusiveLock EVEN WHEN THE SETTING IS ALREADY
153
- # CORRECT (Postgres does not skip the lock just because the
154
- # statement would be a no-op). ensure_base! runs on EVERY
163
+ # takes AccessExclusiveLock even when the setting is already
164
+ # correct (Postgres does not skip the lock just because the
165
+ # statement would be a no-op). ensure_base! runs on every
155
166
  # boot by the owning role, not only the first — so an
156
167
  # unconditional reissue here would mean every ordinary
157
168
  # reboot of the deployment's own identity re-freezes every
158
- # concurrent writer, on any era, for as long as that ALTER
159
- # TABLE has to wait its turn. Read the current state first;
169
+ # concurrent writer, on any era, for as long as that
170
+ # ALTER TABLE has to wait its turn. Read the current state first;
160
171
  # touch the catalog only on the boot that actually needs to.
161
- # pg_table_is_visible, NOT a bare relname match — a shared
172
+ # pg_table_is_visible, not a bare relname match — a shared
162
173
  # instance (storehouse) can hold a same-named journal table
163
- # per schema; catalog lookups here must resolve the SAME way
174
+ # per schema; catalog lookups here must resolve the same way
164
175
  # search_path resolves an unqualified SQL reference, or a
165
176
  # sibling domain's table satisfies a query meant for this
166
177
  # domain's own.
@@ -173,39 +184,48 @@ module Hecks
173
184
  install_transforms!
174
185
  end
175
186
 
176
- # A DOMAIN'S OWN IDENTITY CHANGED bridge its history under the
187
+ # Moves a renamed domain's journal, sequence, partitions and lineage rows
188
+ # from `formerly_known_as` to the current domain name, in one transaction.
189
+ #
190
+ # **A domain's own identity changed** — bridge its history under the
177
191
  # new name, before `provisioner?`/the CREATE TABLE IF NOT EXISTS
178
- # block below ever run. That ordering is load-bearing, not
192
+ # block in `ensure_base!` ever run. That ordering is load-bearing, not
179
193
  # tidiness: `provisioner?` and every statement in ensure_base!
180
- # test for the journal under `journal` — the NEW name — and a
194
+ # test for the journal under `journal` — the new name — and a
181
195
  # freshly-renamed domain looks, to those checks, exactly like a
182
- # domain that has never been provisioned at all. Left where it
183
- # was written, ensure_base! would happily CREATE TABLE IF NOT
184
- # EXISTS a brand-new, empty journal under the new name before
185
- # this method ever got a chance to run — and the ALTER TABLE ...
186
- # RENAME below would then fail, renaming onto a name that
187
- # already exists.
196
+ # domain that has never been provisioned at all. Called any later,
197
+ # ensure_base! would happily CREATE TABLE IF NOT EXISTS
198
+ # a brand-new, empty journal under the new name before
199
+ # this method ever got a chance to run — and the
200
+ # `ALTER TABLE ... RENAME` here would then fail, renaming onto a
201
+ # name that already exists.
188
202
  #
189
203
  # Three-way precheck, cheapest first:
190
204
  # 1. no hecks_eras table at all yet — a genuinely fresh
191
205
  # database; nothing to bridge, fall through to the
192
206
  # ordinary provisioning path.
193
- # 2. hecks_eras already has rows under the NEW name — this
207
+ # 2. hecks_eras already has rows under the new name — this
194
208
  # rename already ran (a prior boot, possibly this one on a
195
209
  # retry); idempotent no-op.
196
- # 3. hecks_eras has rows under the OLD name — run the
210
+ # 3. hecks_eras has rows under the old name — run the
197
211
  # migration.
198
212
  # Anything else (no rows under either name) falls through
199
213
  # harmlessly — `formerly_known_as` pointing at a name with no
200
214
  # held history is not an error, just inert.
201
- # One transactional migration, whose own comment above spells
202
- # out a three-way precheck and a FIXED lock-acquisition order
215
+ #
216
+ # One transactional migration, with the three-way precheck above
217
+ # and a fixed lock-acquisition order
203
218
  # (old before new, eras before ordinal) chosen specifically to
204
219
  # avoid a deadlock against a concurrent rename/mint/write.
205
220
  # Splitting this would separate the precheck from the locking
206
221
  # from the renames from the commit/rollback handling — each a
207
- # piece of ONE transaction that must stay in this exact order
222
+ # piece of one transaction that must stay in this exact order
208
223
  # or the deadlock-avoidance guarantee stops holding.
224
+ #
225
+ # @return [void]
226
+ # @raise [Runtime::WiringError] if the domain locks are not granted within 10s
227
+ # (`PG::LockNotAvailable`), or Postgres refuses any statement here, the
228
+ # prechecks included (`PG::Error`, message carried over); both roll back first
209
229
  # rubocop:disable-next Metrics/AbcSize
210
230
  # rubocop:disable-next Metrics/MethodLength
211
231
  def rename_domain!
@@ -237,18 +257,18 @@ module Hecks
237
257
  end
238
258
 
239
259
  @db.exec("ALTER TABLE #{quote(old_journal)} RENAME TO #{quote(journal)}")
240
- # An owned SERIAL/IDENTITY sequence moves automatically with
260
+ # An owned `SERIAL`/`IDENTITY` sequence moves automatically with
241
261
  # its table and errors if renamed explicitly — this one is a
242
- # plain CREATE SEQUENCE the journal's DEFAULT merely points
262
+ # plain `CREATE SEQUENCE` the journal's `DEFAULT` merely points
243
263
  # at (see ensure_base!), so it does need its own rename, and
244
264
  # the column default survives untouched: Postgres stores
245
265
  # nextval('...') as a regclass reference internally, not
246
266
  # literal text.
247
267
  @db.exec("ALTER SEQUENCE #{quote(old_sequence)} RENAME TO #{quote(sequence)}")
248
- # ALTER TABLE ... RENAME on the parent does NOT cascade to
268
+ # `ALTER TABLE ... RENAME` on the parent does not cascade to
249
269
  # child partition names — each one needs its own explicit
250
- # rename, sourced from the ordinals held under the OLD name,
251
- # captured above before the UPDATE below flips the column.
270
+ # rename, sourced from the ordinals held under the old name,
271
+ # captured above before the `UPDATE` below flips the column.
252
272
  ordinals.each do |ordinal|
253
273
  old_partition = "#{old_journal}_era_#{ordinal}"
254
274
  @db.exec("ALTER TABLE #{quote(old_partition)} RENAME TO #{quote(partition(ordinal))}")
@@ -260,7 +280,7 @@ module Hecks
260
280
  # Unlike its siblings, hecks_attestations is not created in
261
281
  # ensure_base! at all — only lazily, on first reattest! — so a
262
282
  # domain that never needed one must not be forced through an
263
- # UPDATE against a table that doesn't exist.
283
+ # `UPDATE` against a table that doesn't exist.
264
284
  if @db.exec_params(
265
285
  "SELECT to_regclass($1)", ["hecks_attestations"]
266
286
  )[0]["to_regclass"]
@@ -287,9 +307,76 @@ module Hecks
287
307
  raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{e.message.strip}"
288
308
  end
289
309
 
310
+ # Refuses to boot on a connection whose role Postgres exempts from row-level
311
+ # security — a superuser or a BYPASSRLS role — unless the caller opts in.
312
+ #
313
+ # **The fence has to be able to bite this connection, or nothing
314
+ # `ensure_base!` builds means anything**. The whole write-fence is
315
+ # row-level security (`FORCE ROW LEVEL SECURITY` above, plus
316
+ # advance_era!'s one INSERT policy), and Postgres exempts a
317
+ # superuser — or any role granted BYPASSRLS — from every policy
318
+ # on every table, unconditionally: `FORCE` only narrows the
319
+ # owner's exemption and has no lever against either of those.
320
+ # Found live, not reasoned about (BUG#24): the QA ledger's own
321
+ # `.world` named a bare database, so every session connected as
322
+ # the machine's default Postgres user — a superuser — and an
323
+ # old checkout wrote its own superseded era straight through
324
+ # two mints, 37 rows no newer head could read, and nothing
325
+ # warned. Asked of the catalog once, at boot, before anything
326
+ # is provisioned: the answer is a fact about the role, not about
327
+ # any table, so there is nothing to wait for and nothing to
328
+ # half-build first.
329
+ #
330
+ # Refuses by default — quiet divergence is the enemy — naming
331
+ # the role and both ways out. `allow_superuser` boots anyway,
332
+ # but says so on stderr on every boot, because the only guard
333
+ # left standing then is PostgresEra#append's own in-process
334
+ # superseded-era check (the belt to this suspender), and an
335
+ # operator reading the log deserves to know which one they are
336
+ # relying on. Presence-over-truthiness for the setting itself
337
+ # is `PostgresEra.setting`'s job (the caller's); here a truthy
338
+ # value opts in and anything else does not.
339
+ #
340
+ # @param allow_superuser [Boolean, Object, nil] any truthy value boots anyway
341
+ # with a warning on stderr; `false` or `nil` refuses
342
+ # @return [nil] when the role is fenced, or after the warning is written
343
+ # @raise [Runtime::WiringError] if the connection's role is a superuser or is
344
+ # granted BYPASSRLS, and `allow_superuser` is not truthy
345
+ # @raise [PG::Error] if the `pg_roles` lookup fails
346
+ def check_fence_applies!(allow_superuser: false)
347
+ row = @db.exec("SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user")[0]
348
+ exempt = []
349
+ exempt << "a superuser" if row["rolsuper"] == "t"
350
+ exempt << "granted BYPASSRLS" if row["rolbypassrls"] == "t"
351
+ return if exempt.empty?
352
+
353
+ role = row["rolname"].inspect
354
+ unless allow_superuser
355
+ raise Runtime::WiringError,
356
+ "cannot boot #{@domain}: PostgresEra's era write-fence is row-level security, and this " \
357
+ "connection's role #{role} is #{exempt.join(' and ')} — Postgres exempts it from every " \
358
+ "policy, FORCE included, so an old checkout connected this way keeps writing a superseded " \
359
+ "era and nothing refuses. Connect as an ordinary role instead (database " \
360
+ "\"postgres://<role>@<host>/<db>\" in the .world — a non-superuser OWNER still provisions " \
361
+ "and mints), or declare `allow_superuser true` in the same persisted_by block to boot with " \
362
+ "the fence void, on the record."
363
+ end
364
+
365
+ warn "[hecks] #{@domain}: booting PostgresEra as #{role}, #{exempt.join(' and ')}, under " \
366
+ "allow_superuser — the era write-fence is void for this connection; only this process's own " \
367
+ "superseded-era check (PostgresEra#append) stands between an old checkout and a superseded era"
368
+ end
369
+
370
+ # Tells whether this connection's role is the one that builds the schema:
371
+ # it owns the domain's journal, or no journal is visible yet.
372
+ #
290
373
  # Nothing provisioned yet — build it. Provisioned and owned —
291
- # keep it current. Provisioned by SOMEONE ELSE — this is an app
374
+ # keep it current. Provisioned by someone else — this is an app
292
375
  # role, and the owner has already done this work.
376
+ #
377
+ # @return [Boolean] true when no journal table is visible on the search path
378
+ # or `current_user` owns it; false when another role owns it
379
+ # @raise [PG::Error] if the catalog lookup fails
293
380
  def provisioner?
294
381
  rows = @db.exec_params(
295
382
  "SELECT pg_get_userbyid(relowner) = current_user AS owned FROM pg_class " \
@@ -299,7 +386,10 @@ module Hecks
299
386
  rows.ntuples.zero? || rows[0]["owned"] == "t"
300
387
  end
301
388
 
302
- # BUILD, THEN ATTACH never CREATE ... PARTITION OF. The two
389
+ # Creates and attaches one era's journal partition, unless it is already
390
+ # attached; finishes the attach for a table a crashed boot left detached.
391
+ #
392
+ # **Build, then ATTACH** — never CREATE ... PARTITION OF. The two
303
393
  # produce the same partition; only the lock differs, and that
304
394
  # difference is the whole availability story of a mint:
305
395
  #
@@ -307,17 +397,21 @@ module Hecks
307
397
  # CREATE, then ALTER ... ATTACH → ShareUpdateExclusiveLock
308
398
  #
309
399
  # AccessExclusive conflicts with every insert in the hierarchy —
310
- # routed through the parent OR addressed to an existing leaf —
311
- # so attaching the new era inside the mint transaction stopped
312
- # every writer for the WHOLE mint, tail materialization
400
+ # routed through the parent or addressed to an existing leaf —
401
+ # so attaching the new era that way inside the mint transaction
402
+ # stops every writer for the whole mint, tail materialization
313
403
  # included. ShareUpdateExclusive conflicts with neither, so the
314
404
  # old checkout keeps writing its own era straight through the
315
405
  # build and only pauses for the head swap at the end.
316
406
  #
317
- # That is what makes the fork real DURING a mint rather than
407
+ # That is what makes the fork real during a mint rather than
318
408
  # merely before and after one. Measured, and pinned by the spec
319
409
  # — which writes through a live mint rather than reading a lock
320
410
  # mode out of the catalog.
411
+ #
412
+ # @param era [Integer] ordinal of the era whose partition is ensured
413
+ # @return [void]
414
+ # @raise [PG::Error] if Postgres refuses the create or the attach
321
415
  def ensure_partition!(era)
322
416
  return if partition_attached?(era)
323
417
 
@@ -332,9 +426,17 @@ module Hecks
332
426
  SQL
333
427
  end
334
428
 
335
- # Attached, not merely present: a crash between the CREATE and
429
+ # Tells whether an era's partition is part of the journal, by asking
430
+ # `pg_inherits` rather than checking that the table exists.
431
+ #
432
+ # Attached, not merely present: a crash between the `CREATE` and
336
433
  # the ATTACH leaves a table that is not yet part of the journal,
337
434
  # and the next boot must finish the job rather than skip it.
435
+ #
436
+ # @param era [Integer] ordinal of the era whose partition is looked up
437
+ # @return [Boolean] true when the partition is attached to this domain's
438
+ # journal; false when it is missing or exists unattached
439
+ # @raise [PG::Error] if the catalog lookup fails
338
440
  def partition_attached?(era)
339
441
  @db.exec_params(
340
442
  "SELECT 1 FROM pg_inherits i " \