hecks 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -19,16 +19,16 @@ module Hecks
19
19
  @db.exec(
20
20
  "CREATE TABLE IF NOT EXISTS #{quoted_table} (id text PRIMARY KEY#{', ' unless columns.empty?}#{columns.join(', ')})"
21
21
  )
22
- # SELF-HEALING, SAME IDIOM AS `ensure_indexes!` BELOW
23
- # `CREATE TABLE IF NOT EXISTS` above does NOT retroactively add a
22
+ # **Self-healing, same idiom as `ensure_indexes!` below**
23
+ # `CREATE TABLE IF NOT EXISTS` above does not retroactively add a
24
24
  # column to an already-existing table (a committed database from
25
25
  # before optimistic-concurrency CAS existed), so this runs
26
26
  # unconditionally on every boot and no-ops once the column is
27
- # there. ADAPTER BOOKKEEPING ONLY — never listed in
27
+ # there. Adapter bookkeeping only — never listed in
28
28
  # `persisted_fields` (Codec), so it never appears in `decode`'s
29
29
  # domain-state hash or `Instance#to_h`.
30
30
  @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
31
+ # Same self-healing shape, for domain attributes themselves
32
32
  # `hecks_version` above only heals the adapter's own bookkeeping
33
33
  # column; a bluebook attribute added (or, via `translations/`,
34
34
  # renamed) after this table already exists is not bookkeeping,
@@ -47,7 +47,7 @@ module Hecks
47
47
  @db.exec("ALTER TABLE #{quoted_table} ADD COLUMN IF NOT EXISTS " \
48
48
  "#{quote_ident(field[:name])} #{field[:sql_type]}")
49
49
  end
50
- # RIGHT HERE, NOT A SEPARATE STEP IN `Postgres#initialize` —
50
+ # Right here, not a separate step in `Postgres#initialize` —
51
51
  # same idiom Sqlite::SchemaBuilder's own `create_aggregate_table!`
52
52
  # uses: index creation runs unconditionally, right after the
53
53
  # table it indexes exists, the same self-healing shape
@@ -118,11 +118,11 @@ module Hecks
118
118
  # Derived from every declared query's `where`/`order_by` fields —
119
119
  # no bluebook author opts in, matching every other self-healing
120
120
  # schema move this adapter makes. Walks the aggregate's own
121
- # queries AND each entity's own queries — the same enumeration
121
+ # queries and each entity's own queries — the same enumeration
122
122
  # `dsl/aggregate_builder.rb`'s own `query_surfaces` walks at
123
123
  # declaration-seal time — but an entity's own fields never
124
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
125
+ # rows live inside a list-typed attribute on the aggregate's own
126
126
  # table (`QueryInterpreter#entity_rows` resolves it that way, and
127
127
  # answers an entity query entirely in memory over `repository.all`
128
128
  # — it never reaches this adapter's own `query` method at all).
@@ -154,15 +154,15 @@ module Hecks
154
154
  # FROM jsonb_array_elements(...) ...)` (see `list_contains_clause`
155
155
  # below), a SQL shape neither a plain btree on the raw jsonb
156
156
  # column nor even a GIN jsonb index (`@>`, `?`) accelerates —
157
- # Postgres's own GIN jsonb operators match a DIFFERENT SQL shape
157
+ # Postgres's own GIN jsonb operators match a different SQL shape
158
158
  # than the `EXISTS` + `jsonb_array_elements` this codebase always
159
159
  # compiles a list `contains` to. An index here would be dead
160
160
  # weight, not free correctness, so none is attempted.
161
161
  def index_field!(field)
162
- # A HOP PATH ("owner/field" — `Bluebook::AggregateBuilder`'s own
162
+ # A hop path ("owner/field" — `Bluebook::AggregateBuilder`'s own
163
163
  # convention for a field reached by crossing a reference,
164
164
  # `aggregate_builder.rb`'s own `field.to_s.include?("/")`
165
- # checks) IS NEVER INDEXED HERE — same reasoning as `list_of`
165
+ # checks) is never indexed here — same reasoning as `list_of`
166
166
  # below: `query_expression`/`nested_expression` below have no
167
167
  # dialect for this shape at all (they'd compile the whole
168
168
  # "owner/field" string as one bare column/jsonb-path segment,
@@ -173,10 +173,10 @@ module Hecks
173
173
  # Skipping the index is always safe, the same way skipping a
174
174
  # `list_of` index is: an index is a perf optimization, not
175
175
  # correctness, so "never built" beats "built wrong." Whether a
176
- # hop-path field can be QUERIED at all against this adapter is
176
+ # hop-path field can be queried at all against this adapter is
177
177
  # a separate, still-open question this skip does not answer —
178
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
179
+ # docs/1.0-readiness.md item 2 for this gap's other two
180
180
  # independent failure points (Postgres querying, Rust codegen's
181
181
  # `OpenForSuspendedCustomers` — bin/rust_coverage's own
182
182
  # allowlist) — tracked together, not as three unrelated bugs.
@@ -186,7 +186,7 @@ module Hecks
186
186
  attribute = @aggregate.attribute(name)
187
187
  return if attribute&.list?
188
188
 
189
- # THE SAME EXPRESSION THE QUERY ITSELF COMPILES TO — calling
189
+ # The same expression the query itself compiles to — calling
190
190
  # `query_expression`/`plain_column`, the real dialect methods,
191
191
  # rather than a second, hand-rolled derivation of the same
192
192
  # path that could silently drift from it. An index whose
@@ -211,7 +211,7 @@ module Hecks
211
211
  @db.exec("CREATE INDEX IF NOT EXISTS #{quote_ident(name)} ON #{quoted_table} ((#{expression}))")
212
212
  end
213
213
 
214
- # Postgres identifiers cap at 63 bytes. Hashing TABLE + FIELD
214
+ # Postgres identifiers cap at 63 bytes. Hashing table + field
215
215
  # together keeps every generated index name well under that no
216
216
  # matter how long an aggregate or attribute name gets, and keeps
217
217
  # two distinct fields — on the same table or different ones —
@@ -23,19 +23,24 @@ module Hecks
23
23
  # actually needs to survive a live shape change. See
24
24
  # docs/implemented/postgres-era-adapter-split-plan.md for why the two are split.
25
25
  #
26
+ # ## What it is not
27
+ #
26
28
  # No `hecks_eras`, no lineage, no advisory-lock-per-write for era
27
29
  # tracking, no `lineage_capable?`/`era_check!` — this class simply
28
30
  # doesn't define those methods at all, and the capability idiom
29
31
  # elsewhere already treats their absence as "not lineage-capable".
30
32
  #
31
- # Storage shape (see postgres/schema_builder.rb for the DDL,
32
- # postgres/codec.rb for the encode/decode):
33
+ # ## Storage shape
34
+ #
35
+ # See postgres/schema_builder.rb for the DDL, postgres/codec.rb for the
36
+ # encode/decode.
37
+ #
33
38
  # - One real column per attribute, typed for a scalar
34
39
  # (`SQL_TYPES`, `text` default), `jsonb` for a nested (value-object)
35
- # or list-typed attribute — never JSON-in-TEXT the way `Sqlite` has
40
+ # or list-typed attribute — never JSON-in-text the way `Sqlite` has
36
41
  # to, since Postgres has a native jsonb type.
37
42
  # - `append` and `project` are two real Postgres statements — `save`/
38
- # `delete` wrap them in ONE transaction, same "the journal insert
43
+ # `delete` wrap them in one transaction, same "the journal insert
39
44
  # and the snapshot stay atomic" reasoning `PostgresEra#append`'s own
40
45
  # comment gives: a crash between the two must never leave a
41
46
  # half-written state. `append`/`project` stay plain, individually-
@@ -53,10 +58,24 @@ module Hecks
53
58
 
54
59
  attr_reader :aggregate
55
60
 
61
+ # Names the optional persistence capabilities `Ports::Persistence::AppendOnly` may rely on.
62
+ #
63
+ # @return [Array<Symbol>] `[:atomic_put, :optimistic_concurrency]`
56
64
  def persistence_capabilities = [:atomic_put, :optimistic_concurrency]
57
65
 
66
+ # Opens a connection to the database a world declares, scoped to its `schema` if any.
67
+ #
68
+ # @param name [String] the aggregate's name, used only in error messages
69
+ # @param settings [Hash{Symbol, String => Object}] world settings for the binding;
70
+ # `database` (a database name or a `postgres://` URL) is required and `schema` is
71
+ # optional, each read under a Symbol or a String key
72
+ # @return [PG::Connection] a live connection with `search_path` and
73
+ # `client_min_messages` already set
74
+ # @raise [Runtime::WiringError] if the settings declare no `database`, or Postgres
75
+ # refuses the connection or the `SET` statements
76
+ # @raise [LoadError] if the `pg` gem is not installed
58
77
  def self.connect_for(name, settings)
59
- # LAZY, ON PURPOSE — same reasoning as PostgresEra's own
78
+ # **Lazy, on purpose** — same reasoning as PostgresEra's own
60
79
  # connect_for: a domain that never wires Postgres should never
61
80
  # need the gem installed.
62
81
  require "pg"
@@ -75,7 +94,7 @@ module Hecks
75
94
  PG.connect(dbname: declared)
76
95
  end
77
96
 
78
- # SHARED-INSTANCE ISOLATION — same as PostgresEra's own: a
97
+ # **Shared-instance isolation** — same as PostgresEra's own: a
79
98
  # domain that declares `schema` is sharing its Postgres instance
80
99
  # with other domains, so every unqualified reference this
81
100
  # adapter constructs resolves through search_path. A domain with
@@ -83,8 +102,8 @@ module Hecks
83
102
  schema = settings.key?(:schema) ? settings[:schema] : settings["schema"]
84
103
  connection.exec("SET search_path TO #{connection.quote_ident(schema)}") if schema.to_s != ""
85
104
 
86
- # QUIET ON PURPOSE — same reasoning as PostgresEra's own: a
87
- # schema/table that already exists is the ORDINARY case on every
105
+ # **Quiet on purpose** — same reasoning as PostgresEra's own: a
106
+ # schema/table that already exists is the ordinary case on every
88
107
  # boot after the first, not news.
89
108
  connection.exec("SET client_min_messages = warning")
90
109
  connection
@@ -93,11 +112,22 @@ module Hecks
93
112
  "cannot bind Postgres at #{declared} for #{name}: #{e.message.strip}"
94
113
  end
95
114
 
115
+ # Connects and creates the aggregate, journal, event, saga and outbox tables if absent.
116
+ #
117
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose table this adapter owns
118
+ # @param settings [Hash{Symbol, String => Object}] world settings for the binding:
119
+ # `database` (required), `schema` and `domain` (optional; `domain` defaults to the
120
+ # aggregate's storage name and scopes saga rows)
121
+ # @param root [String, nil] project root directory; accepted for the shared adapter
122
+ # constructor shape and ignored
123
+ # @raise [Runtime::WiringError] if the settings declare no `database` or the connection
124
+ # is refused
125
+ # @raise [PG::Error] if creating a table or index fails
96
126
  def initialize(aggregate:, settings: {}, root: nil)
97
127
  @aggregate = aggregate
98
128
  @settings = settings
99
129
  @db = self.class.connect_for(aggregate.name, settings)
100
- # THE OPTIONAL saga-persistence capability's own scoping column
130
+ # The optional saga-persistence capability's own scoping column
101
131
  # (§2/§4) — falls back to the aggregate's own storage name for a
102
132
  # directly-instantiated adapter (specs), same fallback shape
103
133
  # Sqlite's own @domain already uses.
@@ -118,8 +148,18 @@ module Hecks
118
148
  create_outbox_table!
119
149
  end
120
150
 
151
+ # Names the aggregate's table; the journal table and outbox rows are keyed off it.
152
+ #
153
+ # @return [String] the aggregate's snake_case storage name, unquoted
121
154
  def table = @aggregate.storage_name
122
155
 
156
+ # Reads the current row for one aggregate identity, stamped with its stored version.
157
+ #
158
+ # @param id [String, Object] the aggregate identity, bound as `id.to_s`
159
+ # @return [Runtime::Instance, nil] the decoded record with `version` set, or nil when
160
+ # no row has that id
161
+ # @raise [PG::Error] if the statement fails; a `PG::ConnectionBad` also triggers a
162
+ # reconnect for the next caller
123
163
  def find(id)
124
164
  result = pg_exec_params("SELECT * FROM #{quoted_table} WHERE id = $1", [id.to_s])
125
165
  return nil if result.ntuples.zero?
@@ -127,9 +167,20 @@ module Hecks
127
167
  instance_from_row(result[0])
128
168
  end
129
169
 
130
- # order_by IS A RUNTIME VALUE see Sqlite#all's own reasoning;
170
+ # Lists every stored record, ordered by id unless an ordering attribute is given.
171
+ #
172
+ # order_by is a runtime value — see Sqlite#all's own reasoning;
131
173
  # whitelisted the identical way before it ever reaches
132
174
  # order_expression.
175
+ #
176
+ # @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
177
+ # by, with id as the tie-break; nil orders by id alone
178
+ # @param direction [Symbol, String] `:asc` or `:desc`, case-insensitive; anything else
179
+ # sorts ascending
180
+ # @return [Array<Runtime::Instance>] the decoded records with `version` set, `[]` when
181
+ # the table is empty
182
+ # @raise [Runtime::WiringError] if `order_by` names no attribute of the aggregate
183
+ # @raise [PG::Error] if the statement fails
133
184
  def all(order_by: nil, direction: :asc)
134
185
  order_sql = "ORDER BY id"
135
186
  if order_by
@@ -146,38 +197,59 @@ module Hecks
146
197
  pg_exec("SELECT * FROM #{quoted_table} #{order_sql}").map { |row| instance_from_row(row) }
147
198
  end
148
199
 
200
+ # Counts the rows in the aggregate's table, deleted records excluded.
201
+ #
202
+ # @return [Integer] number of current records
203
+ # @raise [PG::Error] if the statement fails
149
204
  def count = pg_exec("SELECT COUNT(*) FROM #{quoted_table}")[0]["count"].to_i
150
205
 
206
+ # Inserts one journal row, outside any transaction of its own.
207
+ #
208
+ # @param entry [Ports::Persistence::Entry] the save or delete to journal; `state` is
209
+ # encoded through the state codec and `mirrors` stored as JSON, or NULL when nil
210
+ # @return [Ports::Persistence::Entry] the same `entry`
211
+ # @raise [PG::Error] if the insert fails
151
212
  def append(entry)
152
213
  pg_exec_params(
153
214
  "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES ($1, $2, $3, $4)",
154
- # `mirrors` (unlike `state`) is a NULLABLE column — an absent
215
+ # `mirrors` (unlike `state`) is a nullable column — an absent
155
216
  # mirrors hash must bind a real SQL NULL, not the four-character
156
217
  # JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
157
218
  # check against it would never match. Same guard `sqlite.rb`/
158
219
  # `d1.rb`/`postgres_era.rb` already use for their own journal's
159
220
  # `mirrors` column.
160
- [entry.id, entry.operation, JSON.generate(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
221
+ [entry.id, entry.operation, state_json(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
161
222
  )
162
223
  entry
163
224
  end
164
225
 
226
+ # Upserts or deletes the aggregate's row for one journal entry, bumping its version.
227
+ #
165
228
  # `expected_version:` requests optimistic-concurrency CAS (see
166
229
  # `persistence_capabilities`/`Ports::Persistence::AppendOnly#save`).
167
- # `hecks_version` is ADAPTER BOOKKEEPING — never in `persisted_fields`
230
+ # `hecks_version` is adapter bookkeeping — never in `persisted_fields`
168
231
  # (Codec), so it never reaches `decode`'s domain-state hash. It goes
169
232
  # in the INSERT column list at `1` (a genuinely new row) and bumps by
170
233
  # one in the `ON CONFLICT DO UPDATE` branch; when `expected_version`
171
- # is given, that UPDATE branch additionally requires
234
+ # is given, that update branch additionally requires
172
235
  # `hecks_version = expected_version` to apply at all — Postgres's own
173
236
  # `INSERT ... ON CONFLICT DO UPDATE ... WHERE`, which gates only
174
- # whether the CONFLICT branch's update applies. A genuinely new row
237
+ # whether the conflict branch's update applies. A genuinely new row
175
238
  # never reaches that branch at all, so it always inserts regardless
176
- # of this WHERE. `RETURNING hecks_version` plus `ntuples.zero?` is
239
+ # of this where. `RETURNING hecks_version` plus `ntuples.zero?` is
177
240
  # how a real version mismatch is told apart from an ordinary write:
178
- # zero rows back means the conflict branch's WHERE excluded the row
241
+ # zero rows back means the conflict branch's where excluded the row
179
242
  # entirely — the version had already moved — so `nil` is returned
180
243
  # for the caller (`AppendOnly#save`) to treat as "stale, no-op".
244
+ #
245
+ # @param entry [Ports::Persistence::Entry] the save or delete to materialize
246
+ # @param expected_version [Integer, nil] the `hecks_version` the row must still hold for
247
+ # 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` no longer matched and nothing was written; for a delete, the
251
+ # `DELETE` statement's `PG::Result`
252
+ # @raise [PG::Error] if the statement fails
181
253
  # rubocop:disable Metrics/AbcSize -- the CAS/plain upsert split is one
182
254
  # protocol; splitting it would hide the version handshake.
183
255
  def project(entry, expected_version: nil)
@@ -206,31 +278,50 @@ module Hecks
206
278
  end
207
279
  # rubocop:enable Metrics/AbcSize
208
280
 
281
+ # Reads the whole journal back in append order, for `AppendOnly#recover!` to replay.
282
+ #
283
+ # @return [Array<Ports::Persistence::Entry>] every journalled entry, state decoded
284
+ # through the state codec and `mirrors` parsed with String keys (nil when none were
285
+ # stored); `[]` when nothing has been appended
286
+ # @raise [PG::Error] if the statement fails
287
+ # @raise [JSON::ParserError] if a stored `state` or `mirrors` value is not valid JSON
209
288
  def entries
210
289
  pg_exec("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
211
290
  state = JSON.parse(row["state"])
212
291
  Ports::Persistence::Entry.new(
213
292
  operation: row["operation"] || "save",
214
293
  id: row["aggregate_id"],
215
- state: state&.transform_keys(&:to_sym),
294
+ state: Ports::Persistence::StateCodec.decode(@aggregate, state),
216
295
  mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
217
296
  )
218
297
  end
219
298
  end
220
299
 
300
+ # Deletes every row of the aggregate's table and its journal; events, saga rows and
301
+ # outbox rows are left in place.
302
+ #
303
+ # @return [Adapters::Postgres] self
304
+ # @raise [PG::Error] if a statement fails
221
305
  def reset!
222
306
  pg_exec("DELETE FROM #{quoted_table}")
223
307
  pg_exec("DELETE FROM #{quoted_entry_table}")
224
308
  self
225
309
  end
226
310
 
227
- # ONE TRANSACTION, not the plain append-then-project two-step a
311
+ # Journals and upserts an instance's current state atomically.
312
+ #
313
+ # One transaction, not the plain append-then-project two-step a
228
314
  # file-based adapter needs a crash-recovery replay for (Heki) —
229
315
  # real Postgres ACID atomicity is sitting right there, so a crash
230
316
  # between the journal insert and the table upsert must not leave
231
317
  # the two disagreeing. `append`/`project` themselves stay plain,
232
318
  # transaction-free methods (see the class comment above) — the
233
319
  # transaction lives here, the one caller that runs both together.
320
+ #
321
+ # @param instance [Runtime::Instance] the instance to store
322
+ # @return [Runtime::Instance] a new instance over the saved state, `version` set to the
323
+ # row's new `hecks_version`
324
+ # @raise [PG::Error] if either statement fails; the transaction is rolled back
234
325
  def save(instance)
235
326
  entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
236
327
  transaction do
@@ -239,6 +330,14 @@ module Hecks
239
330
  end
240
331
  end
241
332
 
333
+ # Stores an entry under a per-identity advisory lock and reports whether it inserted,
334
+ # replaced or conflicted, so two concurrent creators of one id cannot both insert.
335
+ #
336
+ # @param entry [Ports::Persistence::Entry] the save to store
337
+ # @param insert_only [Boolean] when true, an existing row is left untouched
338
+ # @return [Symbol] `:inserted`, `:replaced`, or `:conflicted` when `insert_only` met an
339
+ # existing row and nothing was written
340
+ # @raise [PG::Error] if a statement fails; the transaction is rolled back
242
341
  def atomic_put(entry, insert_only: false)
243
342
  status = nil
244
343
  transaction do
@@ -262,6 +361,11 @@ module Hecks
262
361
  status
263
362
  end
264
363
 
364
+ # Journals a delete and removes the row atomically, whether or not a row exists.
365
+ #
366
+ # @param id [String, Object] the aggregate identity, journalled as `id.to_s`
367
+ # @return [Boolean] always true
368
+ # @raise [PG::Error] if either statement fails; the transaction is rolled back
265
369
  def delete(id)
266
370
  entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
267
371
  transaction do
@@ -271,6 +375,11 @@ module Hecks
271
375
  true
272
376
  end
273
377
 
378
+ # Inserts an emitted event into the shared `events` table.
379
+ #
380
+ # @param event [Runtime::Event] the emitted event; `payload` is stored as JSON
381
+ # @return [PG::Result] the insert's result; callers ignore it
382
+ # @raise [PG::Error] if the insert fails
274
383
  def record_event(event)
275
384
  pg_exec_params(
276
385
  "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
@@ -278,6 +387,12 @@ module Hecks
278
387
  )
279
388
  end
280
389
 
390
+ # Reads back every recorded event in insertion order — the whole `events` table, not
391
+ # only this aggregate's rows.
392
+ #
393
+ # @return [Array<Runtime::Event>] the stored events, `payload` parsed with Symbol keys
394
+ # and `occurred_at` as the String Postgres returns; `[]` when none are recorded
395
+ # @raise [PG::Error] if the statement fails
281
396
  def events
282
397
  pg_exec("SELECT * FROM events ORDER BY id").map do |row|
283
398
  Runtime::Event.new(
@@ -290,9 +405,22 @@ module Hecks
290
405
  end
291
406
  end
292
407
 
293
- # ── the OPTIONAL saga-persistence capability (§2) same DDL and
408
+ # Upserts one saga instance's checkpoint, keyed by domain, process manager and
409
+ # correlation.
410
+ #
411
+ # ── the optional saga-persistence capability (§2) — same DDL and
294
412
  # shape as PostgresEra's own (postgres_era.rb), not lineage-
295
413
  # specific, copied verbatim.
414
+ #
415
+ # @param process_manager [String, Symbol] the process manager's name
416
+ # @param correlation [String, Object] the instance's correlation value, stored as
417
+ # `correlation.to_s`
418
+ # @param state [String, Symbol] the saga's current state name
419
+ # @param memory [Hash] the saga's memory; must be JSON-serializable
420
+ # @param completed_compensations [Array] the ledger of completed compensable legs; must
421
+ # be JSON-serializable
422
+ # @return [PG::Result] the upsert's result; callers ignore it
423
+ # @raise [PG::Error] if the statement fails
296
424
  def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
297
425
  pg_exec_params(
298
426
  "INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
@@ -305,6 +433,13 @@ module Hecks
305
433
  )
306
434
  end
307
435
 
436
+ # Removes a finished saga instance's checkpoint; a missing row is not an error.
437
+ #
438
+ # @param process_manager [String, Symbol] the process manager's name
439
+ # @param correlation [String, Object] the instance's correlation value, matched as
440
+ # `correlation.to_s`
441
+ # @return [PG::Result] the delete's result; callers ignore it
442
+ # @raise [PG::Error] if the statement fails
308
443
  def delete_saga(process_manager:, correlation:)
309
444
  pg_exec_params(
310
445
  "DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
@@ -312,6 +447,18 @@ module Hecks
312
447
  )
313
448
  end
314
449
 
450
+ # Yields every checkpointed saga instance of this adapter's domain, for
451
+ # `Registry#rehydrate_sagas!` to restore at boot.
452
+ #
453
+ # @yieldparam process_manager [String] the process manager's name
454
+ # @yieldparam correlation [String] the instance's correlation value
455
+ # @yieldparam state [String] the saga's state name
456
+ # @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every depth
457
+ # @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
458
+ # when the column is NULL
459
+ # @return [Enumerator, PG::Result] an enumerator over the same five values when no block
460
+ # is given; otherwise the query result
461
+ # @raise [PG::Error] if the statement fails
315
462
  def each_saga
316
463
  return enum_for(:each_saga) unless block_given?
317
464
 
@@ -344,7 +491,7 @@ module Hecks
344
491
  "position(#{placeholder} in #{expression}) > 0"
345
492
  end
346
493
 
347
- # THE LIST COLUMN ITSELF IS THE JSONB ARRAY — no reaching into a
494
+ # The list column itself is the JSONB array — no reaching into a
348
495
  # shared blob a jsonb path has to walk into first (PostgresEra's
349
496
  # own version does, since every attribute there shares one `state`
350
497
  # column). Here, `column` names a real column of its own, already
@@ -357,17 +504,17 @@ module Hecks
357
504
 
358
505
  def plain_column(name) = quote_ident(name)
359
506
 
360
- # PostgresEra's own `jsonb_path` walks `[name, *path]` into ONE
361
- # shared `state` column — the attribute name is PART of the path
362
- # there. Here the attribute name IS THE COLUMN: the path into it
363
- # is whatever is LEFT after the column, never the column name
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
364
511
  # repeated inside its own path.
365
512
  def nested_expression(name, path, member)
366
513
  segments = path.empty? ? [(member || "value").to_s] : path
367
514
  jsonb_path(name, segments)
368
515
  end
369
516
 
370
- # Scalar, non-value-object attributes get a REAL typed column
517
+ # Scalar, non-value-object attributes get a real typed column
371
518
  # (bigint/double precision/text) — comparing and sorting one needs
372
519
  # no cast at all, unlike PostgresEra's shared jsonb `state` blob,
373
520
  # where even a top-level scalar only ever comes out of `#>>` as
@@ -428,7 +575,7 @@ module Hecks
428
575
  end
429
576
 
430
577
  # Same walk PostgresEra's own numeric_field? uses — decides
431
- # numericness at ANY depth from the declared shape itself, not a
578
+ # numericness at any depth from the declared shape itself, not a
432
579
  # runtime value.
433
580
  def numeric_field?(field)
434
581
  name, *path = field.to_s.split(".")
@@ -437,7 +584,7 @@ module Hecks
437
584
  end
438
585
  end
439
586
 
440
- # ARRAY[...] of individually-escaped literals — same escaping
587
+ # Array[...] of individually-escaped literals — same escaping
441
588
  # PostgresEra's own jsonb_path carries and the same reason: a
442
589
  # hand-rolled '{a,b,c}' array literal has no escaping at all, and
443
590
  # a segment is a field or value-object member name this method has
@@ -14,4 +14,9 @@ Hecks.adapter "PostgresEra" do
14
14
  field :database
15
15
  field :role
16
16
  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).
21
+ field :allow_superuser
17
22
  end
@@ -13,7 +13,7 @@ module Hecks
13
13
  # path; `forget`/`forget_all` exist for a caller that reloads an edited
14
14
  # file in-process (see their own comment).
15
15
  module Prism
16
- # NOT frozen — a real cache, keyed by file path and mutated by
16
+ # Not frozen — a real cache, keyed by file path and mutated by
17
17
  # #tree_for below (`TREES[file] ||= ...`) and #forget/#forget_all.
18
18
  # False positive for Style/MutableConstant.
19
19
  # rubocop:disable-next Style/MutableConstant
@@ -58,12 +58,12 @@ module Hecks
58
58
  TREES[file] ||= ::Prism.parse(File.read(file)).value
59
59
  end
60
60
 
61
- # `TREES` caches for the life of the PROCESS, keyed by path, with
61
+ # `TREES` caches for the life of the process, keyed by path, with
62
62
  # no staleness check — correct for every ordinary caller (a file
63
63
  # loads once per process: one `bin/ir` run, one rspec worker,
64
- # never edited out from under it), but WRONG for anything that
64
+ # never edited out from under it), but wrong for anything that
65
65
  # legitimately reloads an edited file in-process: a stale cached
66
- # tree reports a `given`/`ensures` block at its OLD line number,
66
+ # tree reports a `given`/`ensures` block at its old line number,
67
67
  # which no longer matches the freshly re-executed file's own
68
68
  # `block.source_location` — surfacing as "did not survive
69
69
  # extraction" on a perfectly valid file. Found for real building