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,22 +5,13 @@ require_relative "../../runtime/instance"
5
5
 
6
6
  module Hecks
7
7
  module Adapters
8
- # The default, no-external-dependency persistence/query adapter — a
9
- # plain in-process Hash of `Runtime::Instance` records per aggregate,
10
- # with an append-only `@entries`/`@events` log alongside it. Every
11
- # behaviors suite must resolve to this one (see
12
- # Behaviors::Expectations#guard_memory_only!) precisely because a fresh
13
- # instance really is a fresh store, with nothing shared across tests or
14
- # tenants.
8
+ # The default in-process adapter: a Hash of `Runtime::Instance` records
9
+ # per aggregate, plus an append-only `@entries`/`@events` log.
15
10
  class Memory
16
11
  # Reports that two tenant boots of this adapter never share state.
17
12
  #
18
- # **Tenant-capable trivially** — see `Runtime::TenantCheck`'s own header
19
- # for the full reasoning. `@records` is a plain instance variable;
20
- # two `Runtime.boot` calls build two entirely separate Registry
21
- # objects and, through them, two entirely separate Memory
22
- # instances, so two tenant boots never share this adapter's state
23
- # by construction — nothing here needs to know "tenant" exists.
13
+ # Trivially true: `@records` is a plain ivar, and each `Runtime.boot`
14
+ # builds a wholly separate Memory instance.
24
15
  #
25
16
  # @return [Boolean] always true
26
17
  def self.tenant_capable? = true
@@ -81,11 +72,10 @@ module Hecks
81
72
 
82
73
  # Records one journal entry, holding a codec copy of its state.
83
74
  #
84
- # Through the state codec, like every durable adapter (PR A3): the
85
- # journal holds `StateCodec.copy` — exactly what an encode-to-JSON
86
- # then decode would hand back — never the caller's own live state
87
- # objects, so an entry read back here has the same deep-symbol,
88
- # plain-Hash shape a Heki/Sqlite/Postgres entry has.
75
+ # Like every durable adapter, the journal holds `StateCodec.copy` —
76
+ # what an encode-to-JSON then decode would hand back — never the
77
+ # caller's own live state objects, so an entry read back here has
78
+ # the same deep-symbol, plain-Hash shape a Heki/Sqlite/Postgres entry has.
89
79
  #
90
80
  # @param entry [Ports::Persistence::Entry] the save or delete to journal
91
81
  # @return [Ports::Persistence::Entry] the caller's own `entry`, not the journalled copy
@@ -163,18 +153,8 @@ module Hecks
163
153
 
164
154
  # Empties the records, journal, event log and outbox, keeping the adapter itself.
165
155
  #
166
- # Every other driven adapter (Postgres/PostgresEra/Sqlite/D1)
167
- # implements this too — `Ports::Persistence::AppendOnly#reset!`
168
- # forwards to it and only raises when the wrapped adapter doesn't
169
- # respond to `reset!` at all, and nothing about "in memory" implies
170
- # "cannot be cleared". Existing callers that fully re-`Hecks.boot`
171
- # a domain per test case don't need this — they get a brand new
172
- # `Memory` instance, with brand new empty `@records`/`@events`/
173
- # `@entries`, for free. This is for the other case: a caller that
174
- # deliberately keeps one booted runtime across many cases (to skip
175
- # `load_domain`'s own per-boot parse/verify cost) and wants each
176
- # case to start from the same clean slate `Hecks.boot` would have
177
- # given it, without paying for a fresh boot to get there.
156
+ # Lets a caller that keeps one booted runtime across many cases reset
157
+ # to a clean slate without paying for a fresh boot.
178
158
  #
179
159
  # @return [Adapters::Memory] self, now empty
180
160
  def reset!
@@ -253,74 +233,13 @@ module Hecks
253
233
 
254
234
  private
255
235
 
256
- # The language's own bootstrap saves its self-description quadratically
257
- # otherwise. `MetaValidator::Judge` dispatches every declaration in the
258
- # self-hosted grammar into a fresh, private, never-durable `Memory`
259
- # store (meta_validator.rb's own header: "each bluebook is judged in a
260
- # fresh in-memory store") — and every nested-entity dispatch (a
261
- # `ValueObject::Member`, then one `ValueObject::Member::Pair` per
262
- # key/value pair) re-saves the whole parent aggregate, because entities
263
- # have no storage of their own (S17, ADR 0026). A table of N member
264
- # rows costs O(N) dispatches, each paying twice for the aggregate's own
265
- # size-N state: once in `StateCodec.copy` (`append`/`project`'s own
266
- # encode-then-decode round trip) and again in `Instance#initialize`'s
267
- # `hydrate_with_defaults`, which re-walks and re-validates every
268
- # element of an entity list on every save regardless of how many of
269
- # them were already valid as of the previous one
270
- # (`Value::EntityListCoercion#hydrate_entity_list` has no "already
271
- # hydrated" shortcut for entity elements — only a value-object list
272
- # element gets one). Both are O(N) per save, so a table of N rows
273
- # costs O(N^2) total — and it is paid by every rspec worker and every
274
- # `bin/*` subprocess that boots the language at all (found live: PR
275
- # #738's 128-row `RefusalSiteArgument` table alone tripled this one
276
- # aggregate's own save time, 6.7s -> 18.2s).
277
- #
278
- # `Runtime::Value.judge_bootstrapping?` (judge.rb's own `send_to`,
279
- # wrapping every dispatch the judge makes) is already the flag that
280
- # marks exactly this window and nothing else — "never for a real
281
- # domain's own declared value objects... which Judge never dispatches
282
- # commands against" (coercion.rb's own comment on the same flag). It
283
- # is reused here rather than a new toggle for the same reason: one
284
- # flag, one meaning, checked by two unrelated callers for two
285
- # unrelated purposes (loosening a scalar-shape check there, skipping
286
- # both round trips here) is simpler to reason about than two flags
287
- # that would always be true or false together.
288
- #
289
- # Why skipping both is safe here, and only here: `entry.state` a save
290
- # ever hands this adapter is always `instance.state.dup`
291
- # (`AppendOnly#save`) — a shallow copy of an already-hydrated,
292
- # already-validated live `Instance`'s own state, built by the very
293
- # same `Value.for_attribute`/`hydrate_with_defaults` machinery
294
- # `StateCodec.copy` and `Instance.new`'s default (`hydrate: true`)
295
- # path would otherwise redo. Every value inside it — a `Runtime::
296
- # Value` (frozen through, see value.rb's own header) or a `list_of`
297
- # attribute's own array (`Freezer.deep`d the moment it was built,
298
- # instance.rb's own header on `Instance#dup`) — is already immutable,
299
- # so a bare top-level `.dup` is exactly as safe as `Instance#dup`
300
- # already trusts it to be everywhere else in this codebase; nothing
301
- # below the top level is ever mutated in place. Re-deriving the same
302
- # answer through the codec and through re-hydration is therefore
303
- # pure, avoidable cost for this one caller — never a correctness
304
- # requirement.
305
- #
306
- # `bootstrap_fast_path?` is the guard, and it is deliberately cheap —
307
- # O(this aggregate's own declared attribute count), never O(N) — so
308
- # it cannot reintroduce the very cost it exists to avoid: `StateCodec.
309
- # decoded?` (the obvious-looking alternative) recurses into every
310
- # `list_of` element to check it, which is exactly the O(N) walk this
311
- # whole change removes. A `Hash` with every top-level key already a
312
- # `Symbol` is what `Instance#state` always looks like (`Value.hydrate`
313
- # refuses anything else, coercion.rb's own header), so it is checked
314
- # here instead — true for the actual shape every real save has,
315
- # false (falling back to the always-correct slow path) for anything
316
- # that somehow doesn't.
317
- #
318
- # Verified empirically before landing, not just argued: instrumenting
319
- # every bootstrapping-time save across a full `grammar_registry` boot
320
- # and comparing this fast path's own state, journal entry, and stored
321
- # `Instance` against the unmodified `StateCodec.copy` + `Instance.new
322
- # (hydrate: true)` pipeline's found them equal (`==`) for every one of
323
- # 6,617 saves, zero mismatches.
236
+ # Skips the codec copy and rehydration round trip while judging the
237
+ # self-hosted grammar (S17, ADR 0026), where re-validating already-valid
238
+ # entity state costs O(N) per save and O(N^2) across an N-row table.
239
+ # Safe only here: `entry.state` is already hydrated, validated, deep-
240
+ # frozen state from the same Value machinery the skipped paths rerun.
241
+ # The check itself must stay O(this aggregate's attribute count), not
242
+ # O(N), so it can't reintroduce the cost it exists to avoid.
324
243
  def bootstrap_fast_path?(state)
325
244
  Runtime::Value.judge_bootstrapping? && state.is_a?(Hash) && state.keys.all?(Symbol)
326
245
  end
@@ -2,20 +2,8 @@
2
2
 
3
3
  module Hecks
4
4
  module Adapters
5
- # A generic stand-in for a real Checkout-Session-style adapter — no
6
- # network call, no real account, matching whatever real payment
7
- # adapter a project pairs it with by shape alone
8
- # (`create_session(event:, registration_id:, success_url:, cancel_url:)
9
- # -> a URL string`), the same way `Memory` stands in for a real
10
- # persistence adapter without knowing anything about the domain bound
11
- # to it. Never reads `event`'s own fields — a real adapter needs the
12
- # price/name inside it to build a session a payer actually sees;
13
- # this only needs to look enough like one to swap in.
14
- #
15
- # The URL it returns carries the same `registration_id` a real
16
- # webhook's metadata would, so a caller building its own signed
17
- # webhook against it (a smoke test, CI) has something real to key
18
- # off of — the real shape production has, minus the network hop.
5
+ # A network-free stand-in for a Checkout-Session-style payment adapter, matching it by
6
+ # method shape alone. The returned URL carries `registration_id`, as webhook metadata would.
19
7
  class MockStripeAdapter
20
8
  # Accepts and discards the shared adapter constructor arguments; this adapter holds no
21
9
  # state of its own.
@@ -29,14 +17,10 @@ module Hecks
29
17
  # Builds a fake checkout URL carrying `registration_id`, standing in for a real
30
18
  # Checkout-Session call.
31
19
  #
32
- # @param event [Object] the record data a real adapter would read the price and name
33
- # from; adapter-defined shape (whatever the declaring bluebook's `checkout` port
34
- # operation supplies as `event`) and never read here
35
- # @param registration_id [String] the registration id to key a signed webhook off of;
36
- # embedded in the returned URL, never read otherwise
20
+ # @param event [Object] the record data a real adapter would read; never read here
21
+ # @param registration_id [String] embedded in the returned URL, never read otherwise
37
22
  # @param success_url [String] the base URL to redirect a successful payer to
38
- # @param cancel_url [String] accepted for the shared checkout-adapter call shape and
39
- # never read
23
+ # @param cancel_url [String] accepted for the shared call shape and never read
40
24
  # @return [String] `success_url` with `mock_checkout=1` and `mock_registration_id`
41
25
  # query parameters appended
42
26
  def create_session(event:, registration_id:, success_url:, cancel_url:)
@@ -3,15 +3,8 @@ require_relative "../../../runtime/value"
3
3
  module Hecks
4
4
  module Adapters
5
5
  class Postgres
6
- # How a state crosses the column boundary: which fields persist, and
7
- # how each one encodes into (and decodes out of) its column. Same
8
- # shape as Sqlite::Codec (one column per attribute, JSON for
9
- # nested/list) — not its mechanics. Sqlite's own columns carry no
10
- # type affinity worth trusting on the way back out (SQLite gives it
11
- # back typed anyway via the driver), but `pg` hands every column
12
- # back as text unless a type map says otherwise, so a real `bigint`/
13
- # `double precision` column needs its own coercion on decode that
14
- # Sqlite never had to write.
6
+ # Encodes and decodes a state to and from its columns, coercing `pg`'s text back to
7
+ # numbers.
15
8
  module Codec
16
9
  private
17
10
 
@@ -23,9 +16,7 @@ module Hecks
23
16
  fields << { name: lifecycle.field, attribute: nil, sql_type: "text" } if lifecycle && fields.none? do |field|
24
17
  field[:name] == lifecycle.field
25
18
  end
26
- # `projects` fields (S12, ADR 0025) are a local column too — see
27
- # Sqlite::Codec#persisted_fields' own comment; identical reasoning,
28
- # `text` to match this file's own lowercase SQL type spelling.
19
+ # `projects` fields are local columns too (ADR 0025).
29
20
  @aggregate.projected_fields.each do |field|
30
21
  next if fields.any? { |f| f[:name] == field.name }
31
22
 
@@ -35,10 +26,7 @@ module Hecks
35
26
  end
36
27
 
37
28
  def encode(attr, value)
38
- # **Never set is not empty** — same reasoning as Sqlite::Codec's own
39
- # comment: a list attribute nothing has ever appended to has to
40
- # stay NULL to answer the same as Memory does, not become `[]`
41
- # invented by this adapter's own storage.
29
+ # A never-set list stays NULL rather than becoming `[]`, matching Memory.
42
30
  return (value.nil? ? nil : state_json(value)) if attr.list?
43
31
  return state_json(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
44
32
 
@@ -51,13 +39,9 @@ module Hecks
51
39
  encode(field[:attribute], value)
52
40
  end
53
41
 
54
- # Every jsonb column's text goes through the state codec's `encode`
55
- # (PR A3) — see Sqlite::Codec#state_json.
56
42
  def state_json(value) = JSON.generate(Ports::Persistence::StateCodec.encode(@aggregate, value))
57
43
 
58
- # Columns reassembled, then decoded once through the state codec
59
- # (PR A3) — see Sqlite::Codec#decode, including why a NULL
60
- # projected-only column reads back absent.
44
+ # A NULL projected-only column reads back absent.
61
45
  def decode(row)
62
46
  state = persisted_fields.each_with_object({}) do |field, raw_state|
63
47
  attr = field[:attribute]
@@ -84,13 +68,7 @@ module Hecks
84
68
  @aggregate.projected_fields.any? { |projected| projected.name.to_sym == field[:name].to_sym }
85
69
  end
86
70
 
87
- # `pg` returns every column as a Ruby String by default (no type
88
- # map installed) — a `bigint`/`double precision` column has to be
89
- # coerced back on the way out, unlike Sqlite3's own driver, which
90
- # already types a row by the column's declared affinity. Mirrors
91
- # SQL_TYPES' own mapping: whatever gets a real numeric column here
92
- # gets converted back here, everything else (String, references,
93
- # enums, ...) passes through as the text it already is.
71
+ # `pg` returns every column as a String, so numeric columns are coerced back.
94
72
  def coerce_scalar(attr, raw)
95
73
  return nil if raw.nil?
96
74
 
@@ -3,21 +3,12 @@ require_relative "../../../runtime/outbox"
3
3
 
4
4
  module Hecks
5
5
  module Adapters
6
- # **The outbox, Postgres-shaped** — shared verbatim by `Postgres` and the
7
- # era plugin's `PostgresEra`, the same way their `events` and
8
- # `hecks_saga_instances` DDL is copied between them: nothing here is
9
- # lineage-specific. Needs `@db` (a `PG::Connection`) and `table`
10
- # (the aggregate's storage name) from the including class. See
11
- # `Runtime::Outbox` for what the four verbs mean, and Sqlite's copy
12
- # for the SQL idioms (`ON CONFLICT DO NOTHING` = idempotent enqueue,
13
- # `WHERE status = 'pending'` = the compare-and-set claim).
6
+ # Outbox storage shared by `Postgres` and `PostgresEra`; needs `@db` and `table`.
7
+ # See `Runtime::Outbox` for the four verbs.
14
8
  module PostgresOutbox
15
9
  # Runs the block inside one Postgres transaction, joining an already-open one.
16
10
  #
17
- # **Re-entrant** — `Interpreting#run_dispatch_order` opens one
18
- # transaction around save+emit+outbox and the adapter's own
19
- # `append`/`atomic_put`/`delete` each open theirs; PG refuses
20
- # BEGIN inside BEGIN, so an inner call joins the open one.
11
+ # Re-entrant: PG refuses BEGIN inside BEGIN, so an inner call joins the open one.
21
12
  #
22
13
  # @yield the writes to commit together; an exception raised inside rolls the
23
14
  # outermost transaction back
@@ -1,28 +1,7 @@
1
1
  module Hecks
2
2
  module Adapters
3
- # **Self-healing connection** — shared verbatim by `Postgres` and the era
4
- # plugin's `PostgresEra`, the same way `PostgresOutbox` (outbox.rb) is:
5
- # nothing here is lineage-specific, and `PostgresOutbox`'s own
6
- # `@db.exec*` calls route through this module's `pg_exec`/
7
- # `pg_exec_params` too, since both classes mix it in. Needs `@db` (a
8
- # `PG::Connection`), `@aggregate`, and `@settings` from the including
9
- # class — the same three `self.class.connect_for(@aggregate.name,
10
- # @settings)` already needs to build one in the first place.
11
- #
12
- # A backend killed out from under an adapter (a DBA's own
13
- # `pg_terminate_backend`, a load balancer's failover, a restart) —
14
- # chaos-tested against the plain `Postgres` adapter: `PG::ConnectionBad`
15
- # on the query that hit it, and permanently on every query after,
16
- # since nothing ever replaced `@db` with a live connection.
17
- # `pg_exec`/`pg_exec_params` are the two primitives every other method
18
- # in either class funnels through — wrapping them here, once,
19
- # self-heals `@db` for the next caller. The current call still
20
- # raises — reconnecting cannot tell a caller whether its own write
21
- # reached the server before the connection died, so silently
22
- # retrying it here could silently double it; that ambiguity is
23
- # exactly why `Runtime::SagaInterpreter`'s own defect-retry exists
24
- # one layer up, where a dispatch is retried as a whole (fresh
25
- # hydrate, fresh `given`s), not as a lone SQL statement.
3
+ # Replaces a dead `@db` after `PG::ConnectionBad`; shared by `Postgres` and `PostgresEra`.
4
+ # Needs `@db`, `@aggregate` and `@settings`.
26
5
  module PostgresReconnect
27
6
  # Runs one parameterless statement, replacing a dead connection before re-raising.
28
7
  #
@@ -56,13 +35,8 @@ module Hecks
56
35
 
57
36
  private
58
37
 
59
- # **Best-effort** — a reconnect attempt that itself fails (the server
60
- # is actually down, not just this one backend) leaves `@db`
61
- # unchanged; the `PG::ConnectionBad` already being re-raised by
62
- # `pg_exec`/`pg_exec_params` above still reaches the caller either
63
- # way, so swallowing a failed reconnect attempt here loses no
64
- # information — it only avoids masking the original error with a
65
- # second one.
38
+ # The failing call still raises and is never retried: it may have reached the server.
39
+ # A failed reconnect is swallowed so it does not mask the original error.
66
40
  def reconnect!
67
41
  @db = self.class.connect_for(@aggregate.name, @settings)
68
42
  rescue PG::Error
@@ -3,14 +3,8 @@ require "digest"
3
3
  module Hecks
4
4
  module Adapters
5
5
  class Postgres
6
- # The DDL: one table per aggregate, a real column per attribute (a
7
- # jsonb column per nested/list attribute), an append-only entry
8
- # table beside it, the shared events/saga tables — and the automatic
9
- # indexing this adapter carries that `PostgresEra` does not (see
10
- # docs/implemented/postgres-era-adapter-split-plan.md, governing principle 3:
11
- # every field a declared query filters or sorts on is safe to index
12
- # directly here, since there is no `head_view` reduction to route
13
- # around the way `PostgresEra`'s own cache-table story exists for).
6
+ # DDL for the aggregate, entry, event and saga tables, plus the automatic
7
+ # indexes derived from declared queries.
14
8
  module SchemaBuilder
15
9
  private
16
10
 
@@ -19,40 +13,15 @@ module Hecks
19
13
  @db.exec(
20
14
  "CREATE TABLE IF NOT EXISTS #{quoted_table} (id text PRIMARY KEY#{', ' unless columns.empty?}#{columns.join(', ')})"
21
15
  )
22
- # **Self-healing, same idiom as `ensure_indexes!` below** —
23
- # `CREATE TABLE IF NOT EXISTS` above does not retroactively add a
24
- # column to an already-existing table (a committed database from
25
- # before optimistic-concurrency CAS existed), so this runs
26
- # unconditionally on every boot and no-ops once the column is
27
- # there. Adapter bookkeeping only — never listed in
28
- # `persisted_fields` (Codec), so it never appears in `decode`'s
29
- # domain-state hash or `Instance#to_h`.
16
+ # CREATE TABLE IF NOT EXISTS never adds a column to an existing table, so
17
+ # the bookkeeping column is healed on every boot.
30
18
  @db.exec("ALTER TABLE #{quoted_table} ADD COLUMN IF NOT EXISTS hecks_version bigint NOT NULL DEFAULT 1")
31
- # Same self-healing shape, for domain attributes themselves —
32
- # `hecks_version` above only heals the adapter's own bookkeeping
33
- # column; a bluebook attribute added (or, via `translations/`,
34
- # renamed) after this table already exists is not bookkeeping,
35
- # but the identical gap applies: `CREATE TABLE IF NOT EXISTS`
36
- # is a no-op against the existing table, so without this a new
37
- # attribute boots clean and then dies `PG::UndefinedColumn` on
38
- # the first `project` — discovered chaos-testing a live rename
39
- # against this adapter (no era, no translation prompt; those
40
- # live in `PostgresEra` — this is the plain adapter's own,
41
- # simpler contract: "the table always has every column the
42
- # bluebook currently declares"). No `NOT NULL`, no `DEFAULT` —
43
- # existing rows get SQL NULL for a column they never had a
44
- # value for, exactly what a fresh row would get for an unset
45
- # optional attribute (`encode_field`'s own nil handling).
19
+ # Same healing for declared attributes: existing rows get NULL, as an unset
20
+ # optional attribute would.
46
21
  persisted_fields.each do |field|
47
22
  @db.exec("ALTER TABLE #{quoted_table} ADD COLUMN IF NOT EXISTS " \
48
23
  "#{quote_ident(field[:name])} #{field[:sql_type]}")
49
24
  end
50
- # Right here, not a separate step in `Postgres#initialize` —
51
- # same idiom Sqlite::SchemaBuilder's own `create_aggregate_table!`
52
- # uses: index creation runs unconditionally, right after the
53
- # table it indexes exists, the same self-healing shape
54
- # `ensure_head_snapshot!`/`ensure_first_head!` already use on
55
- # `PostgresEra`'s side.
56
25
  ensure_indexes!
57
26
  end
58
27
 
@@ -68,8 +37,6 @@ module Hecks
68
37
  SQL
69
38
  end
70
39
 
71
- # Same DDL as `PostgresEra`'s own — not lineage-specific, copied
72
- # verbatim (see docs/implemented/postgres-era-adapter-split-plan.md).
73
40
  def create_event_table!
74
41
  @db.exec(<<~SQL)
75
42
  CREATE TABLE IF NOT EXISTS events (
@@ -81,10 +48,27 @@ module Hecks
81
48
  occurred_at text
82
49
  )
83
50
  SQL
51
+ # Backs #events_for's per-record lookup (a `corrects` command's history read).
52
+ @db.exec(
53
+ "CREATE INDEX IF NOT EXISTS hecks_events_aggregate_id_idx ON events (aggregate, aggregate_id)"
54
+ )
55
+ end
56
+
57
+ # One row per aggregate table, naming the highest entry `sequence` this table has
58
+ # already had projected into it — shared across every aggregate, since it is keyed
59
+ # by table name rather than declared per aggregate.
60
+ def create_checkpoint_table!
61
+ @db.exec(<<~SQL)
62
+ CREATE TABLE IF NOT EXISTS hecks_checkpoints (
63
+ aggregate_table text PRIMARY KEY,
64
+ last_sequence bigint NOT NULL DEFAULT 0
65
+ )
66
+ SQL
67
+ # CREATE TABLE IF NOT EXISTS never adds a column to an existing table, so
68
+ # the bookkeeping column is healed on every boot, same as create_aggregate_table!.
69
+ @db.exec("ALTER TABLE hecks_checkpoints ADD COLUMN IF NOT EXISTS compacted_through bigint NOT NULL DEFAULT 0")
84
70
  end
85
71
 
86
- # Same DDL as `PostgresEra`'s own — not lineage-specific, copied
87
- # verbatim (see docs/implemented/postgres-era-adapter-split-plan.md).
88
72
  def create_saga_table!
89
73
  @db.exec(<<~SQL)
90
74
  CREATE TABLE IF NOT EXISTS hecks_saga_instances (
@@ -98,11 +82,7 @@ module Hecks
98
82
  PRIMARY KEY (domain, process_manager, correlation)
99
83
  )
100
84
  SQL
101
- # `CREATE TABLE IF NOT EXISTS` above is a no-op against a table
102
- # this same domain already created before this column existed
103
- # — the same idiom `rust/host/src/journal.rs`'s own
104
- # `sagas_backfilled` column addition already uses, for the
105
- # identical reason.
85
+ # Heals a table created before this column existed.
106
86
  @db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb " \
107
87
  "NOT NULL DEFAULT '[]'::jsonb")
108
88
  end
@@ -113,24 +93,8 @@ module Hecks
113
93
  SQL_TYPES.fetch(attr.type, "text")
114
94
  end
115
95
 
116
- # ── automatic indexing ───────────────────────────────────────
117
- #
118
- # Derived from every declared query's `where`/`order_by` fields —
119
- # no bluebook author opts in, matching every other self-healing
120
- # schema move this adapter makes. Walks the aggregate's own
121
- # queries and each entity's own queries — the same enumeration
122
- # `dsl/aggregate_builder.rb`'s own `query_surfaces` walks at
123
- # declaration-seal time — but an entity's own fields never
124
- # produce an index below: an entity has no table of its own, its
125
- # rows live inside a list-typed attribute on the aggregate's own
126
- # table (`QueryInterpreter#entity_rows` resolves it that way, and
127
- # answers an entity query entirely in memory over `repository.all`
128
- # — it never reaches this adapter's own `query` method at all).
129
- # Every list-typed attribute is excluded below regardless (see
130
- # `index_field!`), so an entity's own field would always resolve
131
- # to "skip" anyway. Walked explicitly rather than silently
132
- # dropped, so a reader can see entities were considered here, not
133
- # forgotten.
96
+ # Indexes every field a declared query filters or sorts on. Entity queries are
97
+ # skipped: entities have no table, and their queries run in memory.
134
98
  def ensure_indexes!
135
99
  query_surfaces.each do |owner, queries|
136
100
  next unless owner.equal?(@aggregate)
@@ -149,50 +113,18 @@ module Hecks
149
113
  index_field!(query.order_by.field) if query.order_by
150
114
  end
151
115
 
152
- # A `list_of` attribute is never indexed here — this adapter's
153
- # own `contains` on a list field compiles to `EXISTS (SELECT 1
154
- # FROM jsonb_array_elements(...) ...)` (see `list_contains_clause`
155
- # below), a SQL shape neither a plain btree on the raw jsonb
156
- # column nor even a GIN jsonb index (`@>`, `?`) accelerates —
157
- # Postgres's own GIN jsonb operators match a different SQL shape
158
- # than the `EXISTS` + `jsonb_array_elements` this codebase always
159
- # compiles a list `contains` to. An index here would be dead
160
- # weight, not free correctness, so none is attempted.
116
+ # A list attribute is never indexed: `contains` compiles to EXISTS over
117
+ # jsonb_array_elements, which neither a btree nor a GIN jsonb index accelerates.
161
118
  def index_field!(field)
162
- # A hop path ("owner/field" — `Bluebook::AggregateBuilder`'s own
163
- # convention for a field reached by crossing a reference,
164
- # `aggregate_builder.rb`'s own `field.to_s.include?("/")`
165
- # checks) is never indexed here — same reasoning as `list_of`
166
- # below: `query_expression`/`nested_expression` below have no
167
- # dialect for this shape at all (they'd compile the whole
168
- # "owner/field" string as one bare column/jsonb-path segment,
169
- # which is not a real column and errors PG::UndefinedColumn on
170
- # `CREATE INDEX`, discovered fuzzing a real Postgres boot of
171
- # `examples/banking` — Account's own `projects :customer_status,
172
- # from: :"customer.status"` compiles to exactly this shape).
173
- # Skipping the index is always safe, the same way skipping a
174
- # `list_of` index is: an index is a perf optimization, not
175
- # correctness, so "never built" beats "built wrong." Whether a
176
- # hop-path field can be queried at all against this adapter is
177
- # a separate, still-open question this skip does not answer —
178
- # see docs/future-features.md's fuzzer-adapter entry, and
179
- # docs/1.0-readiness.md item 2 for this gap's other two
180
- # independent failure points (Postgres querying, Rust codegen's
181
- # `OpenForSuspendedCustomers` — bin/rust_coverage's own
182
- # allowlist) — tracked together, not as three unrelated bugs.
119
+ # A hop path ("owner/field") has no column or jsonb path to index; it would
120
+ # fail CREATE INDEX with PG::UndefinedColumn.
183
121
  return if field.to_s.include?("/")
184
122
 
185
123
  name, *_path = field.to_s.split(".")
186
124
  attribute = @aggregate.attribute(name)
187
125
  return if attribute&.list?
188
126
 
189
- # The same expression the query itself compiles to — calling
190
- # `query_expression`/`plain_column`, the real dialect methods,
191
- # rather than a second, hand-rolled derivation of the same
192
- # path that could silently drift from it. An index whose
193
- # expression doesn't match the planner's own candidate
194
- # expression byte-for-byte is invisible to the planner,
195
- # created or not.
127
+ # Built from the query's own expression so the planner can match it byte for byte.
196
128
  expression = query_expression(field.to_s)
197
129
  if expression == plain_column(name)
198
130
  create_plain_index!(name)
@@ -211,13 +143,7 @@ module Hecks
211
143
  @db.exec("CREATE INDEX IF NOT EXISTS #{quote_ident(name)} ON #{quoted_table} ((#{expression}))")
212
144
  end
213
145
 
214
- # Postgres identifiers cap at 63 bytes. Hashing table + field
215
- # together keeps every generated index name well under that no
216
- # matter how long an aggregate or attribute name gets, and keeps
217
- # two distinct fields — on the same table or different ones —
218
- # from ever colliding, or truncating into the same name the way
219
- # a naive `"idx_#{table}_#{field}"` could once either got long
220
- # enough.
146
+ # Hashed so the name stays under Postgres's 63-byte identifier limit and never collides.
221
147
  def index_name(field)
222
148
  "hecks_idx_#{Digest::SHA256.hexdigest("#{table}:#{field}")[0, 40]}"
223
149
  end