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
@@ -15,38 +15,8 @@ require_relative "../../runtime/instance"
15
15
 
16
16
  module Hecks
17
17
  module Adapters
18
- # The plain Postgres store — flat, one table per aggregate, real
19
- # typed columns for scalars and jsonb for nested/list attributes,
20
- # exactly the shape `Sqlite` already made for its own table. Sibling
21
- # to `PostgresEra` (postgres_era.rb), which is the same database with
22
- # full lineage/era machinery on top — pick this one unless a domain
23
- # actually needs to survive a live shape change. See
24
- # docs/implemented/postgres-era-adapter-split-plan.md for why the two are split.
25
- #
26
- # ## What it is not
27
- #
28
- # No `hecks_eras`, no lineage, no advisory-lock-per-write for era
29
- # tracking, no `lineage_capable?`/`era_check!` — this class simply
30
- # doesn't define those methods at all, and the capability idiom
31
- # elsewhere already treats their absence as "not lineage-capable".
32
- #
33
- # ## Storage shape
34
- #
35
- # See postgres/schema_builder.rb for the DDL, postgres/codec.rb for the
36
- # encode/decode.
37
- #
38
- # - One real column per attribute, typed for a scalar
39
- # (`SQL_TYPES`, `text` default), `jsonb` for a nested (value-object)
40
- # or list-typed attribute — never JSON-in-text the way `Sqlite` has
41
- # to, since Postgres has a native jsonb type.
42
- # - `append` and `project` are two real Postgres statements — `save`/
43
- # `delete` wrap them in one transaction, same "the journal insert
44
- # and the snapshot stay atomic" reasoning `PostgresEra#append`'s own
45
- # comment gives: a crash between the two must never leave a
46
- # half-written state. `append`/`project` stay plain, individually-
47
- # callable methods (never wrapping their own transaction) so
48
- # `AppendOnly#recover!`'s replay — `project` alone, no `append` —
49
- # keeps working the same way it does on every other adapter.
18
+ # The plain Postgres store: one table per aggregate, real typed columns
19
+ # for scalars, jsonb for nested/list attributes. See `PostgresEra` for lineage/era support.
50
20
  class Postgres
51
21
  include SqlQueryBuilder
52
22
  include SchemaBuilder
@@ -94,11 +64,8 @@ module Hecks
94
64
  PG.connect(dbname: declared)
95
65
  end
96
66
 
97
- # **Shared-instance isolation** — same as PostgresEra's own: a
98
- # domain that declares `schema` is sharing its Postgres instance
99
- # with other domains, so every unqualified reference this
100
- # adapter constructs resolves through search_path. A domain with
101
- # no `schema` setting keeps Postgres's own default (public).
67
+ # A domain with `schema` set shares this Postgres instance, so every
68
+ # unqualified reference resolves through search_path.
102
69
  schema = settings.key?(:schema) ? settings[:schema] : settings["schema"]
103
70
  connection.exec("SET search_path TO #{connection.quote_ident(schema)}") if schema.to_s != ""
104
71
 
@@ -127,10 +94,8 @@ module Hecks
127
94
  @aggregate = aggregate
128
95
  @settings = settings
129
96
  @db = self.class.connect_for(aggregate.name, settings)
130
- # The optional saga-persistence capability's own scoping column
131
- # (§2/§4) — falls back to the aggregate's own storage name for a
132
- # directly-instantiated adapter (specs), same fallback shape
133
- # Sqlite's own @domain already uses.
97
+ # Scopes saga rows; falls back to the aggregate's storage name when
98
+ # settings gives no domain (e.g. a directly-instantiated adapter).
134
99
  @domain = (
135
100
  if settings.key?(:domain)
136
101
  settings[:domain]
@@ -146,6 +111,7 @@ module Hecks
146
111
  create_event_table!
147
112
  create_saga_table!
148
113
  create_outbox_table!
114
+ create_checkpoint_table!
149
115
  end
150
116
 
151
117
  # Names the aggregate's table; the journal table and outbox rows are keyed off it.
@@ -169,9 +135,7 @@ module Hecks
169
135
 
170
136
  # Lists every stored record, ordered by id unless an ordering attribute is given.
171
137
  #
172
- # order_by is a runtime value — see Sqlite#all's own reasoning;
173
- # whitelisted the identical way before it ever reaches
174
- # order_expression.
138
+ # order_by is a runtime value, whitelisted the same way Sqlite#all does.
175
139
  #
176
140
  # @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
177
141
  # by, with id as the tie-break; nil orders by id alone
@@ -205,54 +169,39 @@ module Hecks
205
169
 
206
170
  # Inserts one journal row, outside any transaction of its own.
207
171
  #
172
+ # Stamps the row's assigned `sequence` onto `entry` in place, so a `project` call right
173
+ # after (same object, same transaction) can advance the checkpoint with no extra query.
174
+ #
208
175
  # @param entry [Ports::Persistence::Entry] the save or delete to journal; `state` is
209
176
  # encoded through the state codec and `mirrors` stored as JSON, or NULL when nil
210
- # @return [Ports::Persistence::Entry] the same `entry`
177
+ # @return [Ports::Persistence::Entry] the same `entry`, `sequence` now set
211
178
  # @raise [PG::Error] if the insert fails
212
179
  def append(entry)
213
- pg_exec_params(
214
- "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES ($1, $2, $3, $4)",
215
- # `mirrors` (unlike `state`) is a nullable column — an absent
216
- # mirrors hash must bind a real SQL NULL, not the four-character
217
- # JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
218
- # check against it would never match. Same guard `sqlite.rb`/
219
- # `d1.rb`/`postgres_era.rb` already use for their own journal's
220
- # `mirrors` column.
180
+ result = pg_exec_params(
181
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES ($1, $2, $3, $4) " \
182
+ "RETURNING sequence",
183
+ # `mirrors` is nullable; an absent hash must bind SQL NULL, not the
184
+ # string "null", or a future IS NULL check would never match.
221
185
  [entry.id, entry.operation, state_json(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
222
186
  )
187
+ entry.sequence = result[0]["sequence"].to_i
223
188
  entry
224
189
  end
225
190
 
226
191
  # Upserts or deletes the aggregate's row for one journal entry, bumping its version.
227
192
  #
228
- # `expected_version:` requests optimistic-concurrency CAS (see
229
- # `persistence_capabilities`/`Ports::Persistence::AppendOnly#save`).
230
- # `hecks_version` is adapter bookkeeping — never in `persisted_fields`
231
- # (Codec), so it never reaches `decode`'s domain-state hash. It goes
232
- # in the INSERT column list at `1` (a genuinely new row) and bumps by
233
- # one in the `ON CONFLICT DO UPDATE` branch; when `expected_version`
234
- # is given, that update branch additionally requires
235
- # `hecks_version = expected_version` to apply at all — Postgres's own
236
- # `INSERT ... ON CONFLICT DO UPDATE ... WHERE`, which gates only
237
- # whether the conflict branch's update applies. A genuinely new row
238
- # never reaches that branch at all, so it always inserts regardless
239
- # of this where. `RETURNING hecks_version` plus `ntuples.zero?` is
240
- # how a real version mismatch is told apart from an ordinary write:
241
- # zero rows back means the conflict branch's where excluded the row
242
- # entirely — the version had already moved — so `nil` is returned
243
- # for the caller (`AppendOnly#save`) to treat as "stale, no-op".
193
+ # `expected_version` requests optimistic-concurrency CAS; nil back means it didn't match.
244
194
  #
245
195
  # @param entry [Ports::Persistence::Entry] the save or delete to materialize
246
196
  # @param expected_version [Integer, nil] the `hecks_version` the row must still hold for
247
197
  # an update to apply; nil writes unconditionally. Ignored for a delete
248
- # @return [Runtime::Instance, PG::Result, nil] for a save, a new instance over the
249
- # entry's state with `version` set to the stored `hecks_version`, or nil when
250
- # `expected_version` did not match and nothing was written; for a delete, the
251
- # `DELETE` statement's `PG::Result`
198
+ # @return [Runtime::Instance, PG::Result, nil] a new instance with `version` set, nil on
199
+ # CAS mismatch, or the `DELETE` statement's `PG::Result`
252
200
  # @raise [PG::Error] if the statement fails
253
201
  # rubocop:disable Metrics/AbcSize -- the CAS/plain upsert split is one
254
202
  # protocol; splitting it would hide the version handshake.
255
203
  def project(entry, expected_version: nil)
204
+ advance_checkpoint!(entry.sequence) if entry.sequence
256
205
  return pg_exec_params("DELETE FROM #{quoted_table} WHERE id = $1", [entry.id]) if entry.delete?
257
206
 
258
207
  instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
@@ -286,15 +235,66 @@ module Hecks
286
235
  # @raise [PG::Error] if the statement fails
287
236
  # @raise [JSON::ParserError] if a stored `state` or `mirrors` value is not valid JSON
288
237
  def entries
289
- pg_exec("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
290
- state = JSON.parse(row["state"])
291
- Ports::Persistence::Entry.new(
292
- operation: row["operation"] || "save",
293
- id: row["aggregate_id"],
294
- state: Ports::Persistence::StateCodec.decode(@aggregate, state),
295
- mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
296
- )
297
- end
238
+ entries_matching(
239
+ "SELECT aggregate_id, operation, state, mirrors, sequence FROM #{quoted_entry_table} ORDER BY sequence"
240
+ )
241
+ end
242
+
243
+ # Reads only the journal rows past a given `sequence`, for `AppendOnly#recover!` to replay
244
+ # after a checkpoint instead of the whole journal.
245
+ #
246
+ # @param sequence [Integer] the highest `sequence` already projected; rows at or below it
247
+ # are skipped
248
+ # @return [Array<Ports::Persistence::Entry>] the journalled entries past `sequence`, in
249
+ # append order, decoded exactly as `entries` decodes them; `[]` when none are newer
250
+ # @raise [PG::Error] if the statement fails
251
+ # @raise [JSON::ParserError] if a stored `state` or `mirrors` value is not valid JSON
252
+ def entries_since(sequence)
253
+ entries_matching(
254
+ "SELECT aggregate_id, operation, state, mirrors, sequence FROM #{quoted_entry_table} " \
255
+ "WHERE sequence > $1 ORDER BY sequence",
256
+ [sequence]
257
+ )
258
+ end
259
+
260
+ # Reads the highest journal `sequence` this table has already had projected into it.
261
+ #
262
+ # @return [Integer] `0` when the table has never been checkpointed (a fresh table, or one
263
+ # from before this bookkeeping existed) — `entries_since(0)` then reads the whole journal,
264
+ # matching `entries`' own full replay
265
+ # @raise [PG::Error] if the statement fails
266
+ def checkpoint
267
+ result = pg_exec_params("SELECT last_sequence FROM hecks_checkpoints WHERE aggregate_table = $1", [table])
268
+ result.ntuples.zero? ? 0 : result[0]["last_sequence"].to_i
269
+ end
270
+
271
+ # Reads the highest journal `sequence` compaction has already deleted from this table.
272
+ #
273
+ # @return [Integer] `0` when nothing has ever been compacted
274
+ # @raise [PG::Error] if the statement fails
275
+ def compacted_through
276
+ result = pg_exec_params("SELECT compacted_through FROM hecks_checkpoints WHERE aggregate_table = $1", [table])
277
+ result.ntuples.zero? ? 0 : result[0]["compacted_through"].to_i
278
+ end
279
+
280
+ # Deletes every journal row at or before `through` and records it, so a `:strict`
281
+ # projection catch-up can tell whether it still has the history it needs.
282
+ #
283
+ # Never moves `compacted_through` backwards, and never touches the aggregate's own
284
+ # table — only the journal, which a `:refresh` projection rebuild never needs.
285
+ #
286
+ # @param through [Integer] the highest `sequence` to delete
287
+ # @return [Integer] the number of journal rows deleted
288
+ # @raise [PG::Error] if a statement fails
289
+ def compact_entries!(through:)
290
+ removed = pg_exec_params("DELETE FROM #{quoted_entry_table} WHERE sequence <= $1", [through]).cmd_tuples
291
+ pg_exec_params(
292
+ "INSERT INTO hecks_checkpoints (aggregate_table, compacted_through) VALUES ($1, $2) " \
293
+ "ON CONFLICT (aggregate_table) DO UPDATE SET " \
294
+ "compacted_through = GREATEST(hecks_checkpoints.compacted_through, excluded.compacted_through)",
295
+ [table, through]
296
+ )
297
+ removed
298
298
  end
299
299
 
300
300
  # Deletes every row of the aggregate's table and its journal; events, saga rows and
@@ -310,13 +310,8 @@ module Hecks
310
310
 
311
311
  # Journals and upserts an instance's current state atomically.
312
312
  #
313
- # One transaction, not the plain append-then-project two-step a
314
- # file-based adapter needs a crash-recovery replay for (Heki) —
315
- # real Postgres ACID atomicity is sitting right there, so a crash
316
- # between the journal insert and the table upsert must not leave
317
- # the two disagreeing. `append`/`project` themselves stay plain,
318
- # transaction-free methods (see the class comment above) — the
319
- # transaction lives here, the one caller that runs both together.
313
+ # One transaction, so a crash between the journal insert and the row upsert
314
+ # can never leave the two disagreeing.
320
315
  #
321
316
  # @param instance [Runtime::Instance] the instance to store
322
317
  # @return [Runtime::Instance] a new instance over the saved state, `version` set to the
@@ -405,13 +400,32 @@ module Hecks
405
400
  end
406
401
  end
407
402
 
403
+ # Reads back one record's recorded events, oldest first — pushed down as a `WHERE`
404
+ # clause (`hecks_events_aggregate_id_idx`) instead of filtering `#events`'s whole-table
405
+ # read, since a `corrects` command's history lookup only ever needs this one record.
406
+ #
407
+ # @param aggregate [String] the `"domain::AggregateName"` key events are stored under
408
+ # @param id [String, Object] the record's identity, matched as `id.to_s`
409
+ # @return [Array<Runtime::Event>] the record's stored events; `[]` when it has none
410
+ # @raise [PG::Error] if the statement fails
411
+ def events_for(aggregate:, id:)
412
+ pg_exec_params(
413
+ "SELECT * FROM events WHERE aggregate = $1 AND aggregate_id = $2 ORDER BY id",
414
+ [aggregate, id.to_s]
415
+ ).map do |row|
416
+ Runtime::Event.new(
417
+ name: row["name"],
418
+ aggregate: row["aggregate"],
419
+ id: row["aggregate_id"],
420
+ payload: JSON.parse(row["payload"], symbolize_names: true),
421
+ occurred_at: row["occurred_at"]
422
+ )
423
+ end
424
+ end
425
+
408
426
  # Upserts one saga instance's checkpoint, keyed by domain, process manager and
409
427
  # correlation.
410
428
  #
411
- # ── the optional saga-persistence capability (§2) — same DDL and
412
- # shape as PostgresEra's own (postgres_era.rb), not lineage-
413
- # specific, copied verbatim.
414
- #
415
429
  # @param process_manager [String, Symbol] the process manager's name
416
430
  # @param correlation [String, Object] the instance's correlation value, stored as
417
431
  # `correlation.to_s`
@@ -475,8 +489,6 @@ module Hecks
475
489
 
476
490
  private
477
491
 
478
- # ── SqlQueryBuilder's dialect hooks ─────────────────────────────
479
-
480
492
  def select_list = "*"
481
493
  def from_relation = quoted_table
482
494
  def dialect_name = "Postgres"
@@ -491,11 +503,8 @@ module Hecks
491
503
  "position(#{placeholder} in #{expression}) > 0"
492
504
  end
493
505
 
494
- # The list column itself is the JSONB array — no reaching into a
495
- # shared blob a jsonb path has to walk into first (PostgresEra's
496
- # own version does, since every attribute there shares one `state`
497
- # column). Here, `column` names a real column of its own, already
498
- # jsonb, already the array.
506
+ # `column` is already a real jsonb column and already the array — no shared
507
+ # `state` blob to walk into first, unlike PostgresEra's own version.
499
508
  def list_contains_clause(column, member, placeholder)
500
509
  target = member.empty? ? "elem #>> '{}'" : "elem ->> #{text_literal(member)}"
501
510
  elements = "jsonb_array_elements(#{quote_ident(column)}) AS elem"
@@ -504,28 +513,15 @@ module Hecks
504
513
 
505
514
  def plain_column(name) = quote_ident(name)
506
515
 
507
- # PostgresEra's own `jsonb_path` walks `[name, *path]` into one
508
- # shared `state` column — the attribute name is part of the path
509
- # there. Here the attribute name is the column: the path into it
510
- # is whatever is left after the column, never the column name
511
- # repeated inside its own path.
516
+ # The attribute name is the column itself here, not part of the path (unlike
517
+ # PostgresEra's shared `state` blob) — the path is whatever follows the column.
512
518
  def nested_expression(name, path, member)
513
519
  segments = path.empty? ? [(member || "value").to_s] : path
514
520
  jsonb_path(name, segments)
515
521
  end
516
522
 
517
- # Scalar, non-value-object attributes get a real typed column
518
- # (bigint/double precision/text) — comparing and sorting one needs
519
- # no cast at all, unlike PostgresEra's shared jsonb `state` blob,
520
- # where even a top-level scalar only ever comes out of `#>>` as
521
- # text. A jsonb-extracted value (a value-object member reached
522
- # through `nested_expression`/`jsonb_path` above) still comes out
523
- # of `#>>` as text the exact same way PostgresEra's own does, and
524
- # still needs the same `::numeric` cast to compare/sort
525
- # numerically rather than lexicographically. `jsonb_extraction?`
526
- # tells the two apart by inspecting the expression `query_expression`
527
- # already built — never a second, hand-rolled field walk that
528
- # could disagree with the one the SQL actually uses.
523
+ # A jsonb-extracted value still comes out as text; cast it to compare/sort
524
+ # numerically instead of lexicographically.
529
525
  def comparable_expression(expression, value)
530
526
  value.is_a?(Numeric) && jsonb_extraction?(expression) ? "(#{expression})::numeric" : expression
531
527
  end
@@ -534,19 +530,14 @@ module Hecks
534
530
  pg_exec_params(sql, binds).map { |row| instance_from_row(row) }
535
531
  end
536
532
 
537
- # Stamps `.version` (adapter bookkeeping, never domain state — see
538
- # `Instance`'s own comment) from the row's `hecks_version` column on
539
- # every Instance this adapter builds from a real stored row, so a
540
- # later `save`'s optimistic-concurrency CAS has something to check
541
- # against.
533
+ # Stamps `.version` from `hecks_version` so a later CAS save has something
534
+ # to check against.
542
535
  def instance_from_row(row)
543
536
  instance = Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
544
537
  instance.version = row["hecks_version"].to_i
545
538
  instance
546
539
  end
547
540
 
548
- # ── the rest of the dialect ─────────────────────────────────────
549
-
550
541
  def quote_ident(name) = PG::Connection.quote_ident(name.to_s)
551
542
  def quoted_table = quote_ident(table)
552
543
  def entry_table = "#{table}_entries"
@@ -559,11 +550,8 @@ module Hecks
559
550
  jsonb_extraction?(expression) && numeric_field?(field) ? "(#{expression})::numeric" : expression
560
551
  end
561
552
 
562
- # Postgres defaults to NULLS LAST on ASC — same override
563
- # PostgresEra's own order_clause carries, so a declared query
564
- # answers identically no matter which adapter serves it (the
565
- # port's in-memory semantics, which Sqlite's own default happens
566
- # to match, put null rows FIRST ascending and LAST descending).
553
+ # Overrides Postgres's default NULLS LAST on ASC so every adapter answers
554
+ # a declared query's null ordering identically.
567
555
  def order_clause(order_by, policy)
568
556
  direction = order_by.direction.to_s.downcase == "desc" ? "DESC" : "ASC"
569
557
  nulls = case policy&.mode.to_s
@@ -584,15 +572,38 @@ module Hecks
584
572
  end
585
573
  end
586
574
 
587
- # Array[...] of individually-escaped literals — same escaping
588
- # PostgresEra's own jsonb_path carries and the same reason: a
589
- # hand-rolled '{a,b,c}' array literal has no escaping at all, and
590
- # a segment is a field or value-object member name this method has
591
- # no way to know is always schema-declared.
575
+ # Builds an escaped Array[...] literal rather than a hand-rolled '{a,b,c}'
576
+ # string, since a segment name isn't guaranteed schema-declared.
592
577
  def jsonb_path(column, segments)
593
578
  "#{quote_ident(column)} #>> ARRAY[#{segments.map { |segment| text_literal(segment) }.join(', ')}]::text[]"
594
579
  end
595
580
 
581
+ # Shared decode step for `entries`/`entries_since` — runs a query already selecting
582
+ # `aggregate_id, operation, state, mirrors, sequence` and builds one Entry per row.
583
+ def entries_matching(sql, binds = [])
584
+ pg_exec_params(sql, binds).map do |row|
585
+ state = JSON.parse(row["state"])
586
+ Ports::Persistence::Entry.new(
587
+ operation: row["operation"] || "save",
588
+ id: row["aggregate_id"],
589
+ state: Ports::Persistence::StateCodec.decode(@aggregate, state),
590
+ mirrors: row["mirrors"] && JSON.parse(row["mirrors"]),
591
+ sequence: row["sequence"].to_i
592
+ )
593
+ end
594
+ end
595
+
596
+ # Advances this table's checkpoint to `sequence`, never backwards — two overlapping
597
+ # `project` calls for out-of-order entries must not let an older sequence win.
598
+ def advance_checkpoint!(sequence)
599
+ pg_exec_params(
600
+ "INSERT INTO hecks_checkpoints (aggregate_table, last_sequence) VALUES ($1, $2) " \
601
+ "ON CONFLICT (aggregate_table) DO UPDATE SET " \
602
+ "last_sequence = GREATEST(hecks_checkpoints.last_sequence, EXCLUDED.last_sequence)",
603
+ [table, sequence]
604
+ )
605
+ end
606
+
596
607
  def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
597
608
  end
598
609
  end
@@ -1,22 +1,11 @@
1
- # ADR 0033 — the `PostgresEra` Ruby CLASS moved into the loadable era
2
- # persistence plugin (`lib/hecks/ports/persistence/plugins/era/`); this
3
- # declaration stays here, unmoved. `Adapters::Folder#load_library` globs
4
- # `.adapter` files by DIRECTORY, not by require path, on every boot,
5
- # regardless of which adapters a given domain actually binds — the same
6
- # reason every other adapter's own `.adapter` file lives beside this one.
7
- # Pure metadata (which port, which fields) — no era-specific code, and
8
- # harmless for a domain that never binds `persisted_by "PostgresEra"`.
9
- # `registry.adapter_class("PostgresEra")` is only ever resolved for a
10
- # domain that actually binds to it, at which point the plugin must
11
- # already be `require`d for the class to exist.
1
+ # Declares the PostgresEra adapter's port and fields; the class lives in the era plugin.
2
+ # This file is loaded on every boot, so it stays metadata only.
12
3
  Hecks.adapter "PostgresEra" do
13
4
  port "persistence"
14
5
  field :database
15
6
  field :role
16
7
  field :schema
17
- # `allow_superuser true` — the explicit, on-the-record opt-in to boot
18
- # over a connection whose role is a Postgres superuser or carries
19
- # BYPASSRLS, which the era write-fence (row-level security) cannot
20
- # bite. Refused by default; see `Lineage#check_fence_applies!` (BUG#24).
8
+ # Opt-in to boot over a superuser or BYPASSRLS role, which the era write fence cannot
9
+ # bite. Refused by default; see `Lineage#check_fence_applies!`.
21
10
  field :allow_superuser
22
11
  end
@@ -5,13 +5,8 @@ module Hecks
5
5
  module Adapters
6
6
  class NotExtractable < StandardError; end
7
7
 
8
- # Extracts a `given`/`ensures`/invariant block's own source text back out
9
- # of the `.rb` file it was defined in (via `block.source_location`),
10
- # parses it with the `prism` gem, and canonicalises it — how a rule's
11
- # predicate becomes readable, comparable text (era diffing, docs) rather
12
- # than an opaque compiled Proc. Per-process `TREES` cache keyed by file
13
- # path; `forget`/`forget_all` exist for a caller that reloads an edited
14
- # file in-process (see their own comment).
8
+ # Extracts a `given`/`ensures`/invariant block's own source text via
9
+ # `prism`, canonicalised into readable, comparable text (era diffing, docs).
15
10
  module Prism
16
11
  # Not frozen — a real cache, keyed by file path and mutated by
17
12
  # #tree_for below (`TREES[file] ||= ...`) and #forget/#forget_all.
@@ -67,32 +62,14 @@ module Hecks
67
62
  # @param file [String] the file path to parse and cache under
68
63
  # @return [Prism::ProgramNode] the parsed syntax tree's root node
69
64
  def tree_for(file)
70
- # ::Prism.parse_file(file) reads the file itself, at the C
71
- # extension level — bypassing Ruby's own File/IO layer entirely.
72
- # That's invisible to anything that virtualizes the filesystem at
73
- # the Ruby level instead of the OS level (e.g. tebako's memfs,
74
- # which presses a hecks-based app into a single executable —
75
- # see domain/README.md's "Deploying" section in a client site's domain for
76
- # why that matters). ::Prism.parse(File.read(file)) parses the
77
- # exact same bytes, just read through Ruby's File.read first,
78
- # which those tools do intercept.
65
+ # ::Prism.parse_file reads the file at the C-extension level, bypassing
66
+ # Ruby's File/IO layer — invisible to tools that virtualize the filesystem
67
+ # there (e.g. tebako's memfs). Read through File.read first so they intercept it.
79
68
  TREES[file] ||= ::Prism.parse(File.read(file)).value
80
69
  end
81
70
 
82
- # `TREES` caches for the life of the process, keyed by path, with
83
- # no staleness check — correct for every ordinary caller (a file
84
- # loads once per process: one `bin/ir` run, one rspec worker,
85
- # never edited out from under it), but wrong for anything that
86
- # legitimately reloads an edited file in-process: a stale cached
87
- # tree reports a `given`/`ensures` block at its old line number,
88
- # which does not match the freshly re-executed file's own
89
- # `block.source_location` — surfacing as "did not survive
90
- # extraction" on a perfectly valid file. Built for real building
91
- # `Hecks::Codemod` (lib/hecks/codemod.rb), which needs exactly this
92
- # invalidation rather than reaching into `TREES.clear` directly — a
93
- # private implementation detail poked from outside. `forget`/
94
- # `forget_all` are the real API so nothing else that reloads an
95
- # edited file in-process has to know `TREES` exists at all.
71
+ # Drops one file from the process-wide parse cache, for a caller (`Hecks::Codemod`)
72
+ # that reloads an edited file in-process and needs the stale tree invalidated.
96
73
  #
97
74
  # @param file [String] the file path to drop from the cache
98
75
  # @return [Prism::ProgramNode, nil] the cached tree that was removed, or nil if