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
@@ -2,67 +2,12 @@ module Hecks
2
2
  module Adapters
3
3
  class PostgresEra
4
4
  class Lineage
5
- # The one chunked, lock-free, resumable backfill loop — shared by
6
- # `backfill_head_snapshot!` (era 1's head-snapshot backfill) and
7
- # every field-cache table's own initial backfill. Governing
8
- # principle 1
9
- # (docs/implemented/postgres-era-adapter-split-plan.md): no
10
- # operation this plan touches may hold a lock across a scan whose
11
- # duration scales with table size — a single `INSERT ... SELECT`
12
- # over the whole journal (the one-shot form of
13
- # `backfill_head_snapshot!`) is exactly that, and so is a naive
14
- # "populate every cache row in one statement" field-cache backfill.
15
- #
16
- # ## The shape
17
- #
18
- # Read one bounded chunk (real rows, real ordinals) with
19
- # a plain SELECT — no lock held across it, so an ordinary reader or
20
- # writer is never blocked by a backfill in progress — then upsert
21
- # that chunk under the same transactionally-scoped advisory lock +
22
- # ordinal-guard idiom `append`'s own snapshot upsert already uses
23
- # (`WHERE ordinal < EXCLUDED.ordinal`), then persist a cursor
24
- # before moving to the next chunk. Repeat until a chunk reads back
25
- # short of a full page — that page was the last one.
26
- #
27
- # ## Resumable, not merely restartable
28
- #
29
- # A crash (or a second
30
- # concurrent boot) mid-backfill leaves the cursor exactly where the
31
- # last committed chunk left it — `hecks_backfill_progress` is
32
- # updated in the same transaction as the chunk's own upsert, so
33
- # cursor and data can never observably disagree (see
34
- # `run_chunk!`). The next attempt reads that cursor and continues;
35
- # it does not rescan what a prior attempt already committed.
36
- # Restartable would also be correct here (every upsert is
37
- # idempotent and ordinal-guarded — rerunning an already-done chunk
38
- # from id 1 changes nothing) but wastes real work on a large
39
- # table; resumability is what keeps a crash near the end of a
40
- # large backfill cheap to recover from instead of starting over.
41
- #
42
- # ## The lock key
43
- #
44
- # The lock key prefix is `hecks_field_cache:` — deliberately
45
- # disjoint from the three families already in use elsewhere in
46
- # this adapter (`hecks_ordinal:`, `hecks_eras:`,
47
- # `hecks_head_snapshot:` — see lineage.rb/head_compiler.rb/
48
- # mint_transaction.rb/tail_merge.rb) so a backfill chunk never
49
- # contends with a plain write, a mint, or a snapshot-table's own
50
- # first-creation lock. It is held for exactly one chunk's own
51
- # transaction, never across the whole backfill — two concurrent
52
- # backfillers of the same target simply take turns one chunk at a
53
- # time rather than racing to duplicate work; neither blocks an
54
- # unrelated reader or writer for even an instant.
5
+ # Chunked, lock-free, resumable backfill loop shared by the head-snapshot backfill and each
6
+ # field-cache table's initial backfill.
55
7
  module ResumableBackfill
56
8
  CHUNK_SIZE = 5_000
57
9
 
58
- # Creates `hecks_backfill_progress`, the table of per-target backfill cursors, if absent.
59
- #
60
- # Idempotent, unguarded — same idiom as every other DDL helper
61
- # in this file tree (`ensure_head_snapshot!` et al.): cheap,
62
- # runs on every boot, only ever does real work once.
63
- #
64
- # @return [void]
65
- # @raise [PG::Error] if Postgres refuses the DDL
10
+ # Creates `hecks_backfill_progress`, the per-target cursor table, if absent.
66
11
  def ensure_backfill_progress_table!
67
12
  @db.exec(<<~SQL)
68
13
  CREATE TABLE IF NOT EXISTS hecks_backfill_progress (
@@ -76,40 +21,11 @@ module Hecks
76
21
 
77
22
  # Fills `target` one committed chunk at a time, resuming from its stored cursor.
78
23
  #
79
- # Drives `target` (an already-created, currently-empty-or-
80
- # partially-filled table) through chunks until a source read
81
- # comes back short of `CHUNK_SIZE` rows. Two distinct callables,
82
- # not one — a head-snapshot row and a field-cache row carry
83
- # different columns (`state` jsonb vs. a single extracted
84
- # `value`), so there is no one generic "upsert this row" shape
85
- # to share; only the loop, the lock, and the cursor are generic.
86
- #
87
- # source_sql.call(cursor) — given the last-processed id (nil
88
- # before the first chunk), returns a SQL SELECT whose result
89
- # has an `id` column (text, ordered ascending) plus whatever
90
- # other columns `upsert` below needs. Must read `id >
91
- # cursor` (or unconditional when cursor is nil), `ORDER BY
92
- # id`, `LIMIT CHUNK_SIZE` — the caller owns the actual
93
- # column list/source tables; this method only owns the loop,
94
- # the lock, and the cursor.
95
- #
96
- # upsert.call(rows) — given the PG::Result of one chunk's
97
- # read, performs the actual guarded upsert into `target` and
98
- # returns nothing meaningful; runs inside the same
99
- # transaction/advisory-lock scope as the cursor update below,
100
- # so a crash between "wrote the chunk" and "advanced the
101
- # cursor" is impossible — they commit together or not at
102
- # all.
24
+ # `source_sql` takes the last id (nil before the first chunk) and returns a SELECT
25
+ # with an ascending text `id` column, `id > cursor`, `LIMIT CHUNK_SIZE`. `upsert`
26
+ # takes the chunk's PG::Result and runs in the cursor update's transaction.
103
27
  #
104
- # @param target [String] unquoted name of the table to fill; also the progress row's key
105
- # and the advisory-lock key
106
- # @param source_sql [#call] callable given the cursor (`String` id of the last row
107
- # processed, nil before the first chunk) that returns the chunk's SELECT as a `String`
108
- # @param upsert [#call] callable given one chunk's `PG::Result` that writes it into
109
- # `target`; its return value is ignored
110
- # @return [void]
111
- # @raise [PG::Error] if Postgres refuses the progress-table DDL, a source read, or a
112
- # cursor update; whatever `upsert` raises propagates too, rolling back that chunk
28
+ # @raise [PG::Error] on a failed read or cursor update; `upsert` errors roll back
113
29
  def chunked_backfill!(target, source_sql:, upsert:)
114
30
  ensure_backfill_progress_table!
115
31
  loop do
@@ -120,21 +36,16 @@ module Hecks
120
36
 
121
37
  private
122
38
 
123
- # One chunk, one transaction, one short-held lock. Re-reads
124
- # progress after acquiring the lock (not just before) — a second
125
- # concurrent booter may have already finished this exact chunk
126
- # (or the whole backfill) while this process was waiting for the
127
- # lock; without the re-read, it would redundantly reprocess a
128
- # chunk another process just committed. Harmless either way
129
- # (idempotent, ordinal-guarded) but the re-read is what keeps
130
- # two concurrent boots from both doing the full scan instead of
131
- # splitting it.
39
+ # One chunk, one transaction. Progress is re-read under the lock: a concurrent booter may
40
+ # have committed this chunk while we waited.
132
41
  def run_chunk!(target, source_sql:, upsert:)
133
42
  progress = backfill_progress(target)
134
43
  return true if progress[:completed]
135
44
 
136
45
  completed = false
137
46
  nested_transaction("hecks_backfill_chunk") do
47
+ # Prefix is disjoint from the ordinal/era/head-snapshot lock keys, so a chunk
48
+ # never blocks a write or a mint.
138
49
  @db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_field_cache:' || $1))", [target])
139
50
  progress = backfill_progress(target)
140
51
  if progress[:completed]
@@ -151,20 +62,14 @@ module Hecks
151
62
 
152
63
  upsert.call(rows)
153
64
  completed = rows.ntuples < CHUNK_SIZE
154
- # `PG::Result#[]` supports neither an out-of-range index nor
155
- # a negative one (unlike a plain Ruby Array) — same trap as
156
- # `backfill_progress` above, the explicit last-index form.
65
+ # `PG::Result#[]` rejects negative indexes, so index the last row explicitly.
157
66
  last_cursor = rows[rows.ntuples - 1]["id"]
158
67
  upsert_backfill_progress!(target, cursor: last_cursor, completed: completed)
159
68
  end
160
69
  completed
161
70
  end
162
71
 
163
- # `PG::Result#[]` raises IndexError on an out-of-range index —
164
- # unlike a plain Ruby Array, it does not return nil — so the
165
- # entirely ordinary case of "no progress row exists yet" (every
166
- # target's very first check) cannot be read via a bare `[0]`.
167
- # `ntuples.zero?` first, always.
72
+ # `PG::Result#[]` raises IndexError out of range, so check `ntuples.zero?` before `[0]`.
168
73
  def backfill_progress(target)
169
74
  result = @db.exec_params(
170
75
  "SELECT cursor, completed FROM hecks_backfill_progress WHERE target = $1", [target]
@@ -4,21 +4,12 @@ module Hecks
4
4
  module Adapters
5
5
  class PostgresEra
6
6
  class Lineage
7
- # The one deliberate, human-driven command that closes a fork:
8
- # `merge_tail!` re-enters an old era's post-cut writes into the
9
- # current era under named winners, and `diverged_count` observes how
10
- # far the two worlds have drifted at any time before that.
7
+ # `merge_tail!` closes a fork: an old era's post-cut writes re-enter under named winners;
8
+ # `diverged_count` measures the drift before that.
11
9
  module TailMerge
12
- # ── fork observability ─────────────────────────────────────────
13
-
14
10
  # Counts the journal rows an old era wrote after the next era's cut.
15
11
  #
16
- # Post-cut writes an old era made after the newer era was minted
17
- # — the divergence between the worlds, observable at any time.
18
- #
19
- # @param old_era [Integer] ordinal of the superseded era to measure
20
- # @return [Integer] number of `old_era` rows past the watermark of era `old_era + 1`;
21
- # 0 when the domain holds no such newer era
12
+ # @return [Integer] 0 when the domain holds no newer era
22
13
  # @raise [Runtime::WiringError] if a held era's text fails its integrity check
23
14
  def diverged_count(old_era)
24
15
  cut = eras.find { |era| era[:ordinal] == old_era + 1 }&.dig(:watermark)
@@ -29,41 +20,18 @@ module Hecks
29
20
  )[0]["count"].to_i
30
21
  end
31
22
 
32
- # ── tail-merge ─────────────────────────────────────────────────
33
- #
34
- # Folds every old era's post-cut writes into the current era's heads, under named winners.
23
+ # Folds every old era's post-cut writes into the current era's heads, under winners.
35
24
  #
36
- # The one deliberate command — it marks a business event (an app
37
- # retiring), never a shape change. One transaction: advance the
38
- # watermarks, rebuild the head so the tail interleaves by its
39
- # recorded global ordinal, append the declared winners, audit —
40
- # and roll the whole thing back on any refusal. Records touched
41
- # by both worlds since the cut refuse until each has an explicit
42
- # winner; resolution itself is append-only (the winner's state
43
- # re-enters as the newest row and wins structurally — originals
44
- # stay immutable).
25
+ # One transaction; any refusal rolls back. Records both worlds touched need a winner.
45
26
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
46
- # One Postgres transaction (`BEGIN`…`COMMIT`) with a manual `ROLLBACK`
47
- # at every early refusal, and a snapshot-before-mutation
48
- # invariant (`new_states` captured before the head rebuild lets
49
- # the tail interleave). Splitting the steps into separate
50
- # methods would force each to independently know how to roll
51
- # back the same shared transaction, and would separate the
52
- # snapshot from the mutation it must precede.
27
+ # One transaction with a manual `ROLLBACK` at each refusal, and `new_states` must be
28
+ # captured before the head rebuild; splitting the steps would scatter both.
53
29
  #
54
- # @param aggregates [Array<Bluebook::Aggregate>] the current bluebook's aggregates, each
55
- # of which gets its head rebuilt
56
- # @param edges [Array<Hash{Symbol => Bluebook::Translation}>] the full edge chain in mint
57
- # order, one `{ translation: }` Hash per step, as `LineageManager.edge_chain` builds it
58
- # @param winners [Hash{String => String}] aggregate id to winning side, `"old"` or
59
- # `"new"`; the winner's whole state is appended as the newest row
60
- # @param audit [#call, nil] callable run just before `COMMIT` that returns an
61
- # `Array<String>` of violations, empty to pass; nil skips the audit
62
- # @return [true] always, once the merge is committed; every refusal raises instead
63
- # @raise [Runtime::WiringError] if the domain stands at era 1, if an id touched by both
64
- # worlds since the cut has no entry in `winners`, if `audit` reports a violation, if
65
- # another mint or merge holds the domain lock for over 10s, if Postgres refuses any
66
- # statement, or if a held era's text fails its integrity check
30
+ # @param aggregates [Array<Bluebook::Aggregate>] the current bluebook's aggregates
31
+ # @param edges [Array<Hash>] the mint-order edge chain, from `LineageManager.edge_chain`
32
+ # @param winners [Hash{String => String}] aggregate id to `"old"` or `"new"`
33
+ # @param audit [#call, nil] returns violations (Array<String>); run before `COMMIT`
34
+ # @raise [Runtime::WiringError] at era 1, an unresolved conflict, audit or lock failure
67
35
  def merge_tail!(aggregates:, edges:, winners: {}, audit: nil)
68
36
  @db.exec("BEGIN")
69
37
  @db.exec("SET LOCAL lock_timeout = '10s'")
@@ -125,23 +93,14 @@ module Hecks
125
93
  end
126
94
  next unless state
127
95
 
128
- # Same two-step append does for a live write — journal
129
- # first, snapshot second — because this INSERT bypasses
130
- # PostgresEra#append entirely (it writes through Lineage
131
- # directly). Skipping the snapshot half here would mean a
132
- # merge winner lands in the journal but head_view — which
133
- # reads this era's live rows from the snapshot table, not
134
- # by re-scanning the journal — never shows it.
96
+ # Journal first, snapshot second, as a live write does: this INSERT bypasses
97
+ # PostgresEra#append, and head_view reads live rows from the snapshot table.
135
98
  ordinal = @db.exec_params(
136
99
  "INSERT INTO #{quoted_journal} (era, aggregate, aggregate_id, operation, state) " \
137
100
  "VALUES ($1, $2, $3, 'save', $4) RETURNING ordinal",
138
101
  [era, aggregate.storage_name, id, state]
139
102
  )[0]["ordinal"]
140
- # Always `'save'` — a merge winner is read from either
141
- # the matview's own `operation = 'save'` rows or the new
142
- # world's live head (`new_states`, captured from
143
- # `head_view`, which is save-only by construction), so
144
- # nothing reaching this INSERT is ever a delete.
103
+ # Always 'save': winners come from save-only sources, never a delete.
145
104
  @db.exec_params(
146
105
  "INSERT INTO #{quote(head_snapshot(aggregate.storage_name, era))} (id, ordinal, operation, state) " \
147
106
  "VALUES ($1, $2, 'save', $3) " \
@@ -182,26 +141,10 @@ module Hecks
182
141
  end
183
142
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
184
143
 
185
- # Ids touched by both worlds since the cut — the old world's
186
- # post-cut tail INTERSECTed with the new world's own writes.
144
+ # Ids written by both worlds since the cut, as `[storage_name, aggregate_id]` pairs.
187
145
  #
188
- # Known gap, not silently risked: this compares raw
189
- # `aggregate_id` values, with no notion of "these two different
190
- # ids are the same entity, rekeyed." If a domain's history
191
- # includes a rekey (see TranslationRekey) and is later
192
- # merged here, a record's pre-rekey and post-rekey rows will
193
- # never intersect — they just silently survive as two separate,
194
- # unrelated-looking heads (a duplicate, not corruption: nothing
195
- # here deletes or clobbers either side). Resolve any such
196
- # duplicate manually after a merge; teaching this intersect
197
- # about a rekey mapping is real, separate work, deliberately
198
- # out of scope for rekey's first pass.
199
- # @param aggregate [Bluebook::Aggregate] the aggregate to check for conflicting ids
200
- # @param edges [Hash] the fork's own era edges, as `names_by_era` reads them
201
- # @param era [Integer] the new world's own era
202
- # @param cut [Integer] the ordinal the fork happened at
203
- # @return [Array<Array(String, String)>] `[storage_name, aggregate_id]` pairs touched
204
- # by both worlds since the cut
146
+ # Compares raw ids: a rekeyed record's pre- and post-rekey rows never intersect and
147
+ # survive as two separate heads (a duplicate, not corruption); resolve by hand.
205
148
  def conflict_ids(aggregate, edges, era, cut)
206
149
  names = names_by_era(aggregate, edges)
207
150
  olds = (1...era).map { |ancestor| text_literal(names[:storage][ancestor - 1]) }.join(", ")
@@ -2,32 +2,14 @@ module Hecks
2
2
  module Adapters
3
3
  class PostgresEra
4
4
  class Lineage
5
- # Installs the shared, domain-independent `hecks_tr_*` Postgres
6
- # functions (extract/insert/rename/move/convert/drop over jsonb)
7
- # that a compiled translation rule set's SQL calls into — the SQL
8
- # compilation target kept equal to the Ruby reference transform by
9
- # the cross-execution equivalence spec, never a second source of
10
- # truth.
5
+ # Installs the shared `hecks_tr_*` jsonb functions that compiled translation SQL calls;
6
+ # kept equal to the Ruby reference transform by the equivalence spec.
11
7
  module TransformInstaller
12
- # Installs or replaces the six `hecks_tr_*` functions under a database-wide advisory lock.
8
+ # Installs or replaces the six `hecks_tr_*` functions under a database-wide lock.
13
9
  #
14
- # The jsonb rule transforms — installed once, idempotently. Kept
15
- # equal to the port's reference entry-JSON transform by the
16
- # cross-execution equivalence spec; the SQL here is a compilation
17
- # target, not a second source of truth.
18
- #
19
- # Locked, unlike every other statement `ensure_base!` runs — those
20
- # are all `CREATE ... IF NOT EXISTS`/`ADD COLUMN IF NOT EXISTS`,
21
- # which Postgres itself resolves safely under concurrent boots.
22
- # `CREATE OR REPLACE FUNCTION` is not: it always rewrites the
23
- # `pg_proc` row, so two sessions racing to (re)install the same
24
- # function — these six are shared/global, not per-domain, so any
25
- # two domains' concurrent first-boots can collide here — hit a
26
- # real `PG::InternalError: tuple concurrently updated`, not a
27
- # graceful no-op. `nested_transaction` is the same
28
- # already-open-transaction-safe wrapper `ensure_field_cache!`
29
- # uses for its own advisory lock; a fixed, domain-independent key
30
- # is correct since these functions have no domain of their own.
10
+ # Locked because `CREATE OR REPLACE FUNCTION` rewrites the `pg_proc` row, so concurrent
11
+ # boots of any two domains (the functions are global) can hit
12
+ # `tuple concurrently updated`.
31
13
  #
32
14
  # @return [void]
33
15
  # @raise [PG::Error] if Postgres refuses the lock or a function definition
@@ -38,21 +20,7 @@ module Hecks
38
20
  end
39
21
  end
40
22
 
41
- # Runs the six function definitions with no lock of its own; `install_transforms!` is
42
- # the locked entry point.
43
- #
44
- # Six independent `CREATE OR REPLACE FUNCTION` statements — each
45
- # self-contained SQL, no shared Ruby state, and (per this
46
- # module's own header comment) safe in any install order since
47
- # plpgsql bodies aren't resolved against each other until
48
- # called, not at create time. Split one-per-function below
49
- # purely so each has its own name and (where relevant) its own
50
- # comment to sit next to, not because the six have any
51
- # sequencing dependency on one another.
52
- #
53
- # @return [void]
54
- # @raise [PG::Error] if Postgres refuses a definition, including
55
- # `tuple concurrently updated` when two unlocked sessions race
23
+ # Runs the six definitions without a lock; `install_transforms!` is the locked entry.
56
24
  def install_transform_functions!
57
25
  install_hecks_tr_extract!
58
26
  install_hecks_tr_insert!
@@ -96,16 +64,8 @@ module Hecks
96
64
  SQL
97
65
  end
98
66
 
99
- # **Adversarial finding**: a destination whose top segment already
100
- # holds a value — most commonly a reference, a bare scalar id
101
- # — is refused rather than silently overwritten with an empty
102
- # object the moment a dotted destination needs to nest under it.
103
- # Overwriting is a drop that never declared itself, the one
104
- # thing this language exists to make explicit (see
105
- # hecks_tr_convert's own refusal below, the same shape). The
106
- # Ruby reference transform
107
- # (ports/persistence/plugins/era/lineage.rb's `insert`) raises
108
- # the identical wording.
67
+ # Refuses to nest under a non-object value (such as a reference id): overwriting it would
68
+ # be an undeclared drop. The Ruby reference transform's `insert` raises the same wording.
109
69
  def install_hecks_tr_insert!
110
70
  @db.exec(<<~SQL)
111
71
  CREATE OR REPLACE FUNCTION hecks_tr_insert(state jsonb, path text[], value jsonb, rule_label text) RETURNS jsonb
@@ -14,86 +14,8 @@ require_relative "../../../../../naming"
14
14
  module Hecks
15
15
  module Adapters
16
16
  class PostgresEra
17
- # The lineage topology inside one Postgres database: one journal
18
- # per domain, list-partitioned by era, with a single ordinal
19
- # sequence spanning partitions (total order across eras is
20
- # structural); a `hecks_eras` table holding each era's frozen
21
- # source text, its once-minted hash/label, and the watermark cut
22
- # into its ancestor; and, per aggregate, a head derived from the
23
- # journal — never a table anything rewrites.
24
- #
25
- # ## How a head is derived
26
- #
27
- # For era 1 the head is a plain view (latest save per id). From
28
- # era 2 on, the ancestor tail is a MATERIALIZED view whose
29
- # definition is the compiled, chained edge sequence — one CTE per
30
- # original edge, in mint order, never a flattened merged rule set
31
- # — with the watermark baked into the definition, so post-cut
32
- # old-era writes cannot leak into the new head even on `REFRESH`.
33
- # Live current-era writes overlay it through the head view.
34
- #
35
- # ## The current-era write fence
36
- #
37
- # Writing to a superseded schema drops the instant the new one
38
- # materializes: one shared row policy admits INSERTs to whichever
39
- # era was just established (era 1 at first hold, era N at mint),
40
- # and advancing it is part of the same transaction that builds the
41
- # new era's matview. There is no persisted per-role fork — any
42
- # granted role, app or the table's own owner
43
- # (`FORCE ROW LEVEL SECURITY` applies this to the owner too, not only
44
- # ordinary roles), writes the current era or nothing, from the moment that
45
- # transaction commits. A stale-era write during the narrow window
46
- # before that commit — RLS is checked once, when the statement
47
- # executes, never re-checked at commit, so a transaction that
48
- # inserted while the old era was still current can still land
49
- # after a concurrent mint has already moved the fence on — is
50
- # exactly what `diverged_count`/`merge_tail!` exist to reconcile; it
51
- # is the residual of an unavoidable race (ordinals are
52
- # sequence-assigned, not transactional — see below), not a
53
- # supported way to keep operating two schemas side by side. Only
54
- # an actual Postgres superuser (or a role granted BYPASSRLS)
55
- # sits above `FORCE` and keeps writing at will, forever.
56
- #
57
- # ## Ordinal order and commit order
58
- #
59
- # Lineage order is ordinal-assignment order: the ordinal comes from a
60
- # sequence, and a sequence's `nextval()` is never rolled back with its
61
- # transaction — accepted and documented rather than papered over.
62
- #
63
- # **One part of that is closed**. Left unserialized, two concurrent
64
- # plain writes can call `nextval()` in one order and COMMIT in the
65
- # other — nothing about a single autocommit INSERT statement stops a
66
- # slower one from finishing after a faster one that started later —
67
- # which makes "ordinal order" and "commit order" formally two different
68
- # total orders even with no mint anywhere near either write. So
69
- # `PostgresEra#append` holds
70
- # `pg_advisory_xact_lock(hashtext('hecks_ordinal:' || domain))` for the
71
- # length of its own transaction, a different key from `mint_era!` and
72
- # `merge_tail!`'s `hecks_eras:domain` — plain writes serialize against
73
- # each other only, never against a mint, and ordinal order equals
74
- # commit order for them.
75
- #
76
- # **The other part is deliberately open**. A stale-era write during the
77
- # narrow window before a mint's fence-move commits is the same race by
78
- # a different name — and closing it would mean a plain write
79
- # serializing against a mint, which is exactly the guarantee
80
- # `spec/adapters/driven/postgres_era/lineage_spec.rb`'s "an old
81
- # checkout keeps writing its own era through a mint" pins the absence
82
- # of. That race stays the residual `diverged_count`/`merge_tail!`
83
- # exist to reconcile, not something a plain write should ever block
84
- # for.
85
- #
86
- # ## File layout
87
- #
88
- # One concern per file under lineage/: provisioning (DDL and the
89
- # RLS posture), era_store (the hecks_eras rows and their integrity),
90
- # mint_transaction (the one transaction that makes an era real),
91
- # tail_merge (the one deliberate merge command), resumable_backfill
92
- # (the one chunked/lock-free/resumable scan loop, shared by
93
- # head_compiler's own backfill and field_cache's), head_compiler
94
- # (the chained-edge SQL a head derives through), field_cache (the
95
- # per-where-field read cache that lets a query skip the reduction
96
- # entirely), transform_installer (the hecks_tr_* jsonb helpers).
17
+ # The lineage topology inside one Postgres database: a journal per domain, partitioned by
18
+ # era, with each aggregate's head derived from it rather than stored.
97
19
  class Lineage
98
20
  include Provisioning
99
21
  include EraStore
@@ -106,16 +28,7 @@ module Hecks
106
28
 
107
29
  JOURNAL_COLUMNS = "ordinal, era, aggregate, aggregate_id, operation, state, mirrors".freeze
108
30
 
109
- # Postgres's own NAMEDATALEN limit: an identifier over 63 bytes is
110
- # silently truncated, never refused — so two different overlong
111
- # names that happen to share their first 63 bytes would collide
112
- # again, at a longer length, the exact same failure mode this
113
- # whole file exists to close for `storage_name` alone. Domain-
114
- # qualifying every name below (`qualified_name`, private) makes
115
- # that readily reachable: a long domain name stacked onto a long
116
- # aggregate name. A constant, not private — Ruby constants are
117
- # never actually scoped by `private`
118
- # (Lint/UselessConstantScoping), so this stays above it.
31
+ # Longer identifiers are silently truncated by Postgres, so `qualified_name` hashes them.
119
32
  POSTGRES_IDENTIFIER_LIMIT = 63
120
33
 
121
34
  attr_reader :db, :domain, :formerly_known_as
@@ -154,17 +67,7 @@ module Hecks
154
67
 
155
68
  # Names the view an aggregate's current state is read through.
156
69
  #
157
- # Domain-qualified, the same way `journal` is — see
158
- # `qualified_name`'s own comment and docs/decisions/0059. Unqualified,
159
- # two different domains bound to PostgresEra against the same
160
- # database, each declaring an aggregate whose own name snake_cases
161
- # to the same storage_name (found live: two unrelated "Note"
162
- # aggregates), derive the exact same
163
- # `note_head`/`note_head_snapshot_1` physical relations — every
164
- # boot of the second domain silently clobbers the first's
165
- # already-compiled head view, `ensure_first_head!`'s own
166
- # "belt-and-suspenders self-healing" being exactly the mechanism
167
- # that does it (postgres_era.rb's own comment there).
70
+ # Domain-qualified so two domains sharing an aggregate name do not clobber each other.
168
71
  #
169
72
  # @param storage_name [String] the aggregate's snake-cased storage name
170
73
  # @return [String] unquoted view name, at most `POSTGRES_IDENTIFIER_LIMIT` bytes
@@ -172,15 +75,7 @@ module Hecks
172
75
 
173
76
  # Names the snapshot table that backs one aggregate's head within one era.
174
77
  #
175
- # The transactionally-upserted read cache behind head_view — one row
176
- # per live id, keyed by id, carrying the ordinal it was last written
177
- # at. Scoped by era, not just storage_name — an aggregate that
178
- # isn't renamed across a mint keeps the same storage_name in both
179
- # eras, so storage_name alone would have era N+1 sharing one
180
- # physical table with era N: a freshly-minted era would inherit
181
- # every pre-mint (and, worse, pre-rekey/pre-translation) row
182
- # instead of starting empty. era-qualified naming is what
183
- # `partition`/`matview` already do for exactly this reason.
78
+ # Era-scoped so a new era starts empty, not with the previous era's rows.
184
79
  #
185
80
  # @param storage_name [String] the aggregate's snake-cased storage name
186
81
  # @param era [Integer] ordinal of the era the snapshot belongs to
@@ -197,19 +92,7 @@ module Hecks
197
92
 
198
93
  private
199
94
 
200
- # `@domain`, snake-cased and folded onto `suffix` — human-readable
201
- # in the ordinary case (every one of these names gets read
202
- # directly at a psql prompt during a live incident — see
203
- # docs/decisions/0059's own verification section), degrading to a
204
- # hashed, truncated form only once the readable form would
205
- # actually risk exceeding `POSTGRES_IDENTIFIER_LIMIT`. The same
206
- # trade `Runtime::StorageShape.mint_label` (a bare hash-prefix, no
207
- # attempt at readability at all — a mint label is never meant to
208
- # be legible on its own) and `FieldCache#field_cache` (fully
209
- # hashed, for the same reason) already make elsewhere in this
210
- # adapter — this one keeps more of the readable form than either,
211
- # since unlike a mint label or a field-cache table, these names
212
- # are the ones an operator reads and types by hand.
95
+ # Snake-cased domain folded onto `suffix`; hashed and truncated only past the length limit.
213
96
  def qualified_name(suffix)
214
97
  full = "#{Naming.snake(@domain)}_#{suffix}"
215
98
  return full if full.bytesize <= POSTGRES_IDENTIFIER_LIMIT