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,47 +5,24 @@ module Hecks
5
5
  module Adapters
6
6
  class PostgresEra
7
7
  class Lineage
8
- # `mint_era!` itself: one transaction that writes the new era row,
9
- # attaches its partition, recompiles every aggregate's head, grants
10
- # an app role, and — last, right before commit — advances the
11
- # current-era RLS fence (`advance_era!`) that drops write access to
12
- # the old schema the instant it commits.
8
+ # `mint_era!`: writes a new era in one transaction and advances the current-era
9
+ # RLS fence (`advance_era!`) just before commit.
13
10
  module MintTransaction
14
- # ── the mint transaction ───────────────────────────────────────
15
- #
16
- # Makes a new era real, or reports that a concurrent minter already did.
17
- #
18
- # One transaction: the new era row (held text + minted name +
19
- # cut watermark), the new partition, and every aggregate's
20
- # recompiled matview + head view. The advisory lock is the
21
- # writer fence — two concurrent minters serialize, and the
22
- # second finds the era already held. Populating the matview
23
- # inside the transaction means a convert meeting an unmapped
24
- # value refuses the whole mint, loudly, before anything boots.
11
+ # Makes a new era real in one locked transaction, or reports a concurrent minter won.
25
12
  #
26
13
  # @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`
14
+ # @param hash [String] the era's shape hash, SHA-256 hex
15
+ # @param label [String] the era's short label, a prefix of `hash`
29
16
  # @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
17
+ # @param aggregates [Array<Bluebook::Aggregate>] aggregates whose heads are recompiled
18
+ # @param edges [Array<Hash>] the full edge chain in mint order, one per step
19
+ # @param role [String, nil] app role to grant privileges to; nil grants nothing
20
+ # @param projection [Hash{String => Object}, nil] storage-shape projection, or nil
21
+ # @return [Boolean] true once committed; false, rolled back, if `ordinal` is held
22
+ # @raise [Runtime::WiringError] on a lock wait over 10s or a Postgres error
43
23
  def mint_era!(ordinal:, hash:, label:, held_text:, aggregates:, edges:, role: nil, projection: nil)
44
24
  @db.exec("BEGIN")
45
- # A concurrent minter blocks briefly, then refuses with a name
46
- # rather than hanging: mint is non-interactive (the human
47
- # decision already happened in the audit tool), so the lock is
48
- # only ever held for the transaction itself.
25
+ # Mint is non-interactive, so a concurrent minter fails fast with a name, not a hang.
49
26
  @db.exec("SET LOCAL lock_timeout = '10s'")
50
27
  @db.exec("SELECT pg_advisory_xact_lock(hashtext('hecks_eras:' || #{text_literal(@domain)}))")
51
28
  if eras.any? { |era| era[:ordinal] == ordinal }
@@ -63,35 +40,16 @@ module Hecks
63
40
  )
64
41
  archive_text!(ordinal, held_text)
65
42
  ensure_partition!(ordinal)
66
- # After compile_head!, not before — this era's own snapshot
67
- # table (a possibly-renamed name, per aggregate) is what
68
- # grant_role! grants on, and compile_head! is what creates it.
69
- # Granting first would GRANT on a relation that does not
70
- # exist yet for any aggregate renamed in this very edge.
43
+ # grant_role! must follow compile_head!, which creates the per-era snapshot
44
+ # tables that it grants on.
71
45
  aggregates.each { |aggregate| compile_head!(aggregate, ordinal, label, edges) }
72
46
  grant_role!(role, aggregates: aggregates, era: ordinal) if role
73
- # **Last, right before `COMMIT`** — not merely unconditional. Once
74
- # acquired, a lock is held until the transaction ends, not
75
- # just for the statement that took it — so advance_era!'s
76
- # DROP POLICY/CREATE POLICY (AccessExclusiveLock, same family
77
- # as ALTER TABLE, and unavoidably so: Postgres has no lighter
78
- # form for changing a policy, unlike the partition attach
79
- # below) blocks every concurrent writer for as long as it sits
80
- # before the expensive step. Ordered here, that block is the
81
- # width of a few catalog statements plus the commit itself,
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.
47
+ # Must stay last, right before `COMMIT`: DROP/CREATE POLICY takes an
48
+ # AccessExclusiveLock held to commit, so placing it before compile_head!
49
+ # would block every writer for the whole matview build.
87
50
  #
88
- # Unconditional regardless of position — this is the line that
89
- # drops writing to the old schema. It does not wait for a role
90
- # to be configured on this boot, because the cutoff is a fact
91
- # about the era, not about who happened to mint it: an old
92
- # checkout's role, granted by some earlier boot this one knows
93
- # nothing about, must lose write access the instant this
94
- # transaction commits.
51
+ # Unconditional, even with no role configured: the cutoff is a fact about the
52
+ # era, and a role granted by an earlier boot must lose old-era writes on commit.
95
53
  advance_era!(ordinal)
96
54
  @db.exec("COMMIT")
97
55
  true
@@ -115,41 +73,16 @@ module Hecks
115
73
 
116
74
  # Grants an app role what it needs to append to the journal and read and write heads.
117
75
  #
118
- # Base privileges for a deployment's app role — a non-owner,
119
- # which may append and read once the shared era fence below
120
- # admits it, and owns nothing. Idempotent, and unconcerned with
121
- # which era is current: that is advance_era!'s job, not this
122
- # one's, so a role can be onboarded at any time without
123
- # disturbing who may write what right now.
124
- #
125
- # Owner-only work, so a non-owner app boot skips it: the
126
- # provisioner has already granted that role, and re-affirming is
127
- # idempotent anyway.
128
- #
129
- # `aggregates:`/`era:` cover the read-cache side of the same
130
- # story: unlike the journal (immutable, owner-provisioned once),
131
- # each aggregate's head_snapshot table is a table an app role
132
- # must itself `INSERT`/`UPDATE`/`DELETE` into — PostgresEra#append writes
133
- # it directly, not through a view — so it needs real DML grants,
134
- # not just the SELECT a derived read surface would need. `era:`
135
- # is the ordinal this role is about to write under (the one
136
- # hold_first!/mint_era! just made current, or the superseded one
137
- # a stale checkout still speaks) — head_snapshot is era-scoped,
138
- # so granting on the wrong era's table would grant on a table
139
- # this role will never touch. Every caller passes its own
140
- # bluebook's aggregates and resolved era — including a
141
- # held-but-superseded checkout, since a role connecting to it
142
- # for the first time (a new instance of an old checkout) has no
143
- # privileges yet either.
76
+ # Idempotent and owner-only, so a non-owner boot skips it. Head snapshot tables are
77
+ # era-scoped, so `era:` names the ordinal the role is about to write under.
144
78
  #
145
79
  # @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
80
+ # @param aggregates [Array<Bluebook::Aggregate>] aggregates whose head tables and views
81
+ # are granted on; ignored when `era` is nil
82
+ # @param era [Integer, nil] era whose snapshot tables to grant on; nil grants only the
83
+ # journal and sequence privileges
150
84
  # @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
85
+ # @raise [PG::Error] if Postgres refuses a grant
153
86
  def grant_role!(role, aggregates: [], era: nil)
154
87
  return unless provisioner?
155
88
 
@@ -170,41 +103,15 @@ module Hecks
170
103
 
171
104
  # Replaces the journal's row policies: one era accepts INSERTs, every row stays readable.
172
105
  #
173
- # The current-era fence — one policy, shared by every granted
174
- # role, not one per role. Advancing it is what drops writing to
175
- # the old schema the instant the new one materializes: the
176
- # moment this commits, no role — old or new, whether or not it
177
- # did anything to earn this mint — may insert anything but the
178
- # era named here. There is no persisted fork: a checkout that
179
- # keeps a role fenced to a stale era does not exist in this
180
- # design, because there is no such thing as a role fenced to an
181
- # era at all — only the one era everyone currently shares.
182
- #
183
- # A row policy, not a partition grant. Postgres checks INSERT
184
- # privilege on the partitioned parent for a routed insert and
185
- # never consults the partition's own grants, so a per-partition
186
- # GRANT/REVOKE is inert in both directions: grant only on the
187
- # partition and nobody can write at all; grant on the parent and
188
- # they may write into every era, ancestors included. Measured,
189
- # not reasoned about — see the spec, which writes through the
190
- # fence rather than asserting the catalog.
191
- #
192
- # The table owner bypasses RLS by default, and that is load
193
- # bearing: mint and merge run as the owner and must be able to
194
- # write any era (the merge re-enters a winner's state into the
195
- # current era, and compile_head! reads every ancestor).
106
+ # One policy shared by every role; call only with the new current ordinal, since a
107
+ # superseded one would roll the fence back. The table owner bypasses RLS.
196
108
  #
197
- # Call only with the new current ordinal — from hold_first! (era
198
- # 1) or mint_era! (era N). Calling this with a superseded
199
- # ordinal — from a boot that merely recognizes an old checkout —
200
- # would roll the fence backward and silently reopen the old
201
- # schema for everyone. That path grants a role's privileges
202
- # (grant_role!) and stops there on purpose.
109
+ # Row policy rather than partition grants: Postgres checks INSERT privilege on the
110
+ # partitioned parent and ignores the partition's own grants.
203
111
  #
204
112
  # @param ordinal [Integer] ordinal of the era that becomes the only writable one
205
113
  # @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
114
+ # @raise [PG::Error] if Postgres refuses the policy change, such as for a non-owner
208
115
  def advance_era!(ordinal)
209
116
  @db.exec("DROP POLICY IF EXISTS hecks_current_era ON #{quoted_journal}")
210
117
  @db.exec(
@@ -2,36 +2,13 @@ module Hecks
2
2
  module Adapters
3
3
  class PostgresEra
4
4
  class Lineage
5
- # Owner-only schema bootstrap: creates the lineage tables
6
- # (`hecks_eras`, `hecks_era_texts`, `hecks_approvals`, the
7
- # partitioned journal), sets up its RLS fence, bridges a renamed
8
- # domain's history (`rename_domain!`), and attaches each era's
9
- # journal partition (`ensure_partition!`) — the build-then-ATTACH
10
- # dance that keeps a mint from stopping every writer.
5
+ # Owner-only schema bootstrap: lineage tables, the RLS fence, the domain rename
6
+ # bridge, and per-era journal partitions.
11
7
  module Provisioning
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.
8
+ # Builds or tops up this domain's lineage schema when the connected role owns the
9
+ # journal (or none exists yet); any other role only attempts the rename bridge.
16
10
  #
17
- # Provisioning is the owner's job, and a deployment's app role is
18
- # deliberately not the owner — it may append and read, and it
19
- # owns nothing. By the time such a role connects, the base is
20
- # already built, so its boot verifies rather than builds.
21
- #
22
- # Without this guard the per-era fence below is unreachable: the
23
- # ALTER TABLE and REVOKE here are owner-only, so the very role
24
- # grant_era! exists to constrain could never finish booting
25
- # ("must be owner of table hecks_eras"). A fence nothing can
26
- # reach is not a fence.
27
- # One bootstrap sequence of DDL, order-dependent throughout —
28
- # every guarded ALTER (RLS, REVOKE) is guarded and placed
29
- # exactly where it is for measured, documented reasons (see the
30
- # inline comments on each: catalog-lock avoidance, RLS timing,
31
- # `FORCE` semantics). Splitting this into smaller methods would
32
- # only hide that single ordered sequence behind several call
33
- # sites, with nothing gained — each statement already reads as
34
- # one step, and the length here is DDL, not branching logic.
11
+ # ALTER TABLE and REVOKE are owner-only, so a non-owner app role must skip them to boot.
35
12
  #
36
13
  # @return [void]
37
14
  # @raise [Runtime::WiringError] if the `formerly_known_as` rename cannot take
@@ -55,12 +32,10 @@ module Hecks
55
32
  SQL
56
33
  @db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_digest text")
57
34
  @db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_projection jsonb")
58
- # which canonical-form version minted this name — see
59
- # Runtime::StorageShape::FORM_VERSION; rows minted before the
60
- # column carry NULL, read as an implicit 1
35
+ # Canonical-form version that minted the name (Runtime::StorageShape::FORM_VERSION);
36
+ # NULL reads as 1.
61
37
  @db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS canon_form int")
62
- # every frozen text version, archived where an edit cannot
63
- # reach it — the recovery the hard reattest refusal points at
38
+ # Every frozen text version, archived where an edit cannot reach it.
64
39
  @db.exec(<<~SQL)
65
40
  CREATE TABLE IF NOT EXISTS hecks_era_texts (
66
41
  domain text NOT NULL,
@@ -82,10 +57,8 @@ module Hecks
82
57
  )
83
58
  SQL
84
59
  @db.exec("CREATE SEQUENCE IF NOT EXISTS #{quote(sequence)}")
85
- # `GENERATED ALWAYS AS IDENTITY` is the intent, but identity
86
- # columns on partitioned tables need Postgres 17 — an owned
87
- # sequence default is the same spanning ordinal on any
88
- # supported server.
60
+ # An owned sequence default, not generated always as identity, which
61
+ # partitioned tables only support from Postgres 17.
89
62
  @db.exec(<<~SQL)
90
63
  CREATE TABLE IF NOT EXISTS #{quoted_journal} (
91
64
  ordinal bigint NOT NULL DEFAULT nextval('#{sequence}'),
@@ -98,83 +71,27 @@ module Hecks
98
71
  ) PARTITION BY LIST (era)
99
72
  SQL
100
73
  ensure_partition!(1)
101
- # Immutability by privilege: nothing updates or deletes journal
102
- # rows. The owner's implicit rights remain (Postgres has no way
103
- # to revoke them from the owner itself); a deployment's app
104
- # role connects as a non-owner and gets exactly INSERT, per
105
- # era, at mint time.
106
- #
107
- # Guarded, same reasoning as the RLS ALTER TABLE calls just
108
- # below: REVOKE still writes pg_class.relacl (and takes the
109
- # matching lock) even when the resulting privileges are
110
- # unchanged, so an unconditional reissue on every ordinary
111
- # reboot races two concurrent boots into
112
- # `PG::InternalError: tuple concurrently updated` — read
113
- # first, touch the catalog only on the boot that actually
114
- # needs to.
74
+ # Journal rows are immutable by privilege: nothing updates or deletes them.
75
+ # The REVOKE is guarded because it rewrites pg_class.relacl and takes a lock
76
+ # even when nothing changes, so concurrent reboots would race into
77
+ # `tuple concurrently updated`.
115
78
  #
116
- # `relacl IS NULL`, not `has_table_privilege('public', ...,
117
- # 'UPDATE')` — found live, not assumed: a brand-new table's
118
- # `PUBLIC` privilege is already "no UPDATE" by Postgres's own
119
- # default (nothing has ever been explicitly granted to
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.
128
- # `relacl IS NULL` means "default ACL, nothing explicit yet" —
129
- # exactly the one-time signal needed, and (like the RLS flags
130
- # below) `pg_table_is_visible(oid)`, not a bare relname match,
131
- # for the same shared-instance/storehouse reason.
79
+ # The guard is `relacl IS NULL`, not has_table_privilege('public', ..., 'UPDATE'):
80
+ # a new table already answers "no UPDATE", so that check would skip the first
81
+ # REVOKE and leave relacl NULL forever. pg_table_is_visible(oid), not a bare
82
+ # relname match, so a sibling schema's same-named table is never mistaken for ours.
132
83
  relacl_null = @db.exec_params(
133
84
  "SELECT relacl IS NULL FROM pg_class WHERE relname = $1 AND pg_table_is_visible(oid)", [journal]
134
85
  ).getvalue(0, 0)
135
86
  @db.exec("REVOKE UPDATE, DELETE ON #{quoted_journal} FROM PUBLIC") if relacl_null == "t"
136
- # RLS goes on at provisioning, never mid-life — enabling it
137
- # later would deny every role that has no policy yet, on
138
- # whatever the shape of the schema happened to be at that
139
- # moment.
140
- #
141
- # `FORCE`, not merely `ENABLE`: without it, the table owner is
142
- # exempt from every policy here, by Postgres default — which
143
- # would leave the schema writable forever to whoever holds
144
- # the owner's credentials, the one connection this whole
145
- # design cannot fence. Checked, not assumed: mint_era! never
146
- # inserts into the journal at all (only hecks_eras/
147
- # hecks_era_texts, neither RLS-protected), and merge_tail!'s
148
- # one journal INSERT targets the current era, which the fence
149
- # already admits for anyone with base privileges — so `FORCE`
150
- # costs the owner nothing operations here actually need.
87
+ # RLS goes on at provisioning, never mid-life: enabling it later would deny
88
+ # every role that has no policy yet.
151
89
  #
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
155
- # specifically, and superuser bypass sits above both. Running
156
- # migrations as a real superuser (self-hosted Postgres, most
157
- # commonly) leaves this gap open regardless; a managed
158
- # provider's admin account is typically not a superuser, and
159
- # is exactly what `FORCE` closes.
90
+ # FORCE, not just enable, or the table owner is exempt from every policy. A
91
+ # superuser or BYPASSRLS role stays exempt regardless (see check_fence_applies!).
160
92
  #
161
- # Guarded, not reissued unconditionally — measured, not
162
- # assumed: `ALTER TABLE ... ENABLE/FORCE ROW LEVEL SECURITY`
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
166
- # boot by the owning role, not only the first — so an
167
- # unconditional reissue here would mean every ordinary
168
- # reboot of the deployment's own identity re-freezes every
169
- # concurrent writer, on any era, for as long as that
170
- # ALTER TABLE has to wait its turn. Read the current state first;
171
- # touch the catalog only on the boot that actually needs to.
172
- # pg_table_is_visible, not a bare relname match — a shared
173
- # instance (storehouse) can hold a same-named journal table
174
- # per schema; catalog lookups here must resolve the same way
175
- # search_path resolves an unqualified SQL reference, or a
176
- # sibling domain's table satisfies a query meant for this
177
- # domain's own.
93
+ # Guarded: enable/FORCE takes an AccessExclusiveLock even as a no-op, and this
94
+ # runs on every boot, so an unconditional reissue would freeze concurrent writers.
178
95
  current = @db.exec_params(
179
96
  "SELECT relrowsecurity, relforcerowsecurity FROM pg_class " \
180
97
  "WHERE relname = $1 AND pg_table_is_visible(oid)", [journal]
@@ -184,48 +101,16 @@ module Hecks
184
101
  install_transforms!
185
102
  end
186
103
 
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
191
- # new name, before `provisioner?`/the CREATE TABLE IF NOT EXISTS
192
- # block in `ensure_base!` ever run. That ordering is load-bearing, not
193
- # tidiness: `provisioner?` and every statement in ensure_base!
194
- # test for the journal under `journal` — the new name — and a
195
- # freshly-renamed domain looks, to those checks, exactly like a
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.
202
- #
203
- # Three-way precheck, cheapest first:
204
- # 1. no hecks_eras table at all yet — a genuinely fresh
205
- # database; nothing to bridge, fall through to the
206
- # ordinary provisioning path.
207
- # 2. hecks_eras already has rows under the new name — this
208
- # rename already ran (a prior boot, possibly this one on a
209
- # retry); idempotent no-op.
210
- # 3. hecks_eras has rows under the old name — run the
211
- # migration.
212
- # Anything else (no rows under either name) falls through
213
- # harmlessly — `formerly_known_as` pointing at a name with no
214
- # held history is not an error, just inert.
104
+ # Moves a domain's journal, sequence, partitions and lineage rows from
105
+ # `formerly_known_as` to the current name, in one transaction.
215
106
  #
216
- # One transactional migration, with the three-way precheck above
217
- # and a fixed lock-acquisition order
218
- # (old before new, eras before ordinal) chosen specifically to
219
- # avoid a deadlock against a concurrent rename/mint/write.
220
- # Splitting this would separate the precheck from the locking
221
- # from the renames from the commit/rollback handling — each a
222
- # piece of one transaction that must stay in this exact order
223
- # or the deadlock-avoidance guarantee stops holding.
107
+ # Runs before `provisioner?` and the CREATE TABLE calls in `ensure_base!`; a no-op when
108
+ # `hecks_eras` is missing, the new name has rows, or the former name has none.
109
+ # Locks go in a fixed order (old before new, eras before ordinal) so nothing deadlocks.
224
110
  #
225
111
  # @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
112
+ # @raise [Runtime::WiringError] if the locks are not granted within 10s or Postgres
113
+ # refuses a statement (prechecks included); both roll back first
229
114
  # rubocop:disable-next Metrics/AbcSize
230
115
  # rubocop:disable-next Metrics/MethodLength
231
116
  def rename_domain!
@@ -245,10 +130,6 @@ module Hecks
245
130
 
246
131
  @db.exec("BEGIN")
247
132
  @db.exec("SET LOCAL lock_timeout = '10s'")
248
- # Fixed order — old before new, the `eras:` family before the
249
- # `ordinal:` family — so two rename attempts (or a rename
250
- # racing a mint/plain-write on either name) can only ever
251
- # queue behind one another, never deadlock.
252
133
  [
253
134
  "hecks_eras:#{@formerly_known_as}", "hecks_eras:#{@domain}",
254
135
  "hecks_ordinal:#{@formerly_known_as}", "hecks_ordinal:#{@domain}"
@@ -257,18 +138,10 @@ module Hecks
257
138
  end
258
139
 
259
140
  @db.exec("ALTER TABLE #{quote(old_journal)} RENAME TO #{quote(journal)}")
260
- # An owned `SERIAL`/`IDENTITY` sequence moves automatically with
261
- # its table and errors if renamed explicitly — this one is a
262
- # plain `CREATE SEQUENCE` the journal's `DEFAULT` merely points
263
- # at (see ensure_base!), so it does need its own rename, and
264
- # the column default survives untouched: Postgres stores
265
- # nextval('...') as a regclass reference internally, not
266
- # literal text.
141
+ # The sequence is a plain create sequence, not owned by the column, so it
142
+ # does not move with the table and needs its own rename.
267
143
  @db.exec("ALTER SEQUENCE #{quote(old_sequence)} RENAME TO #{quote(sequence)}")
268
- # `ALTER TABLE ... RENAME` on the parent does not cascade to
269
- # child partition names — each one needs its own explicit
270
- # rename, sourced from the ordinals held under the old name,
271
- # captured above before the `UPDATE` below flips the column.
144
+ # Renaming the parent does not rename its partitions.
272
145
  ordinals.each do |ordinal|
273
146
  old_partition = "#{old_journal}_era_#{ordinal}"
274
147
  @db.exec("ALTER TABLE #{quote(old_partition)} RENAME TO #{quote(partition(ordinal))}")
@@ -277,10 +150,7 @@ module Hecks
277
150
  %w[hecks_eras hecks_era_texts hecks_approvals].each do |table|
278
151
  @db.exec_params("UPDATE #{table} SET domain = $1 WHERE domain = $2", [@domain, @formerly_known_as])
279
152
  end
280
- # Unlike its siblings, hecks_attestations is not created in
281
- # ensure_base! at all — only lazily, on first reattest! — so a
282
- # domain that never needed one must not be forced through an
283
- # `UPDATE` against a table that doesn't exist.
153
+ # hecks_attestations is created lazily on first reattest!, so it may not exist.
284
154
  if @db.exec_params(
285
155
  "SELECT to_regclass($1)", ["hecks_attestations"]
286
156
  )[0]["to_regclass"]
@@ -307,35 +177,11 @@ module Hecks
307
177
  raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{e.message.strip}"
308
178
  end
309
179
 
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.
180
+ # Refuses to boot on a connection whose role Postgres exempts from row-level security
181
+ # (a superuser or BYPASSRLS role) unless the caller opts in.
312
182
  #
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.
183
+ # Those roles bypass the write-fence, so a stale checkout could keep writing a
184
+ # superseded era unnoticed. With `allow_superuser` it boots and warns on stderr.
339
185
  #
340
186
  # @param allow_superuser [Boolean, Object, nil] any truthy value boots anyway
341
187
  # with a warning on stderr; `false` or `nil` refuses
@@ -370,10 +216,6 @@ module Hecks
370
216
  # Tells whether this connection's role is the one that builds the schema:
371
217
  # it owns the domain's journal, or no journal is visible yet.
372
218
  #
373
- # Nothing provisioned yet — build it. Provisioned and owned —
374
- # keep it current. Provisioned by someone else — this is an app
375
- # role, and the owner has already done this work.
376
- #
377
219
  # @return [Boolean] true when no journal table is visible on the search path
378
220
  # or `current_user` owns it; false when another role owns it
379
221
  # @raise [PG::Error] if the catalog lookup fails
@@ -389,25 +231,10 @@ module Hecks
389
231
  # Creates and attaches one era's journal partition, unless it is already
390
232
  # attached; finishes the attach for a table a crashed boot left detached.
391
233
  #
392
- # **Build, then ATTACH** — never CREATE ... PARTITION OF. The two
393
- # produce the same partition; only the lock differs, and that
394
- # difference is the whole availability story of a mint:
395
- #
396
- # CREATE TABLE ... PARTITION OF → AccessExclusiveLock (parent)
397
- # CREATE, then ALTER ... ATTACH → ShareUpdateExclusiveLock
398
- #
399
- # AccessExclusive conflicts with every insert in the hierarchy —
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
403
- # included. ShareUpdateExclusive conflicts with neither, so the
404
- # old checkout keeps writing its own era straight through the
405
- # build and only pauses for the head swap at the end.
406
- #
407
- # That is what makes the fork real during a mint rather than
408
- # merely before and after one. Measured, and pinned by the spec
409
- # — which writes through a live mint rather than reading a lock
410
- # mode out of the catalog.
234
+ # Build then ATTACH, never CREATE ... PARTITION OF: the latter takes an
235
+ # AccessExclusiveLock on the parent and stops every writer for the whole mint,
236
+ # while ATTACH takes only ShareUpdateExclusiveLock, so a running checkout keeps
237
+ # writing through the build.
411
238
  #
412
239
  # @param era [Integer] ordinal of the era whose partition is ensured
413
240
  # @return [void]
@@ -429,9 +256,8 @@ module Hecks
429
256
  # Tells whether an era's partition is part of the journal, by asking
430
257
  # `pg_inherits` rather than checking that the table exists.
431
258
  #
432
- # Attached, not merely present: a crash between the `CREATE` and
433
- # the ATTACH leaves a table that is not yet part of the journal,
434
- # and the next boot must finish the job rather than skip it.
259
+ # A table left by a crash between CREATE and ATTACH exists but is not attached,
260
+ # and the next boot must finish the job.
435
261
  #
436
262
  # @param era [Integer] ordinal of the era whose partition is looked up
437
263
  # @return [Boolean] true when the partition is attached to this domain's