hecks 1.3.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 (355) 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 +5 -4
  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/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -2
@@ -13,17 +13,30 @@ module Hecks
13
13
  # instance really is a fresh store, with nothing shared across tests or
14
14
  # tenants.
15
15
  class Memory
16
- # TENANT-CAPABLE TRIVIALLY see Runtime::TenantCheck's own header
16
+ # Reports that two tenant boots of this adapter never share state.
17
+ #
18
+ # **Tenant-capable trivially** — see `Runtime::TenantCheck`'s own header
17
19
  # for the full reasoning. `@records` is a plain instance variable;
18
20
  # two `Runtime.boot` calls build two entirely separate Registry
19
21
  # objects and, through them, two entirely separate Memory
20
22
  # instances, so two tenant boots never share this adapter's state
21
23
  # by construction — nothing here needs to know "tenant" exists.
24
+ #
25
+ # @return [Boolean] always true
22
26
  def self.tenant_capable? = true
27
+
28
+ # Names the optional persistence capabilities `Ports::Persistence::AppendOnly` may rely on.
29
+ #
30
+ # @return [Array<Symbol>] `[:atomic_put]`
23
31
  def persistence_capabilities = [:atomic_put]
24
32
 
25
33
  attr_reader :aggregate, :events
26
34
 
35
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose records this store holds
36
+ # @param settings [Hash] world settings for the binding; accepted for the shared adapter
37
+ # constructor shape and ignored
38
+ # @param root [String, nil] project root directory; accepted for the shared adapter
39
+ # constructor shape and ignored
27
40
  def initialize(aggregate:, settings: {}, root: nil)
28
41
  @aggregate = aggregate
29
42
  @records = {}
@@ -32,39 +45,90 @@ module Hecks
32
45
  @outbox = []
33
46
  end
34
47
 
48
+ # Looks up the current record for one aggregate identity.
49
+ #
50
+ # @param id [String, Object] the aggregate identity, compared as `id.to_s`
51
+ # @return [Runtime::Instance, nil] the held record, or nil when no record has that id
35
52
  def find(id) = @records[id.to_s]
53
+
54
+ # Counts the records currently held, deleted ones excluded.
55
+ #
56
+ # @return [Integer] number of live records
36
57
  def count = @records.size
37
58
 
59
+ # Lists every held record, in insertion order unless an ordering attribute is given.
60
+ #
61
+ # @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
62
+ # by; nil leaves the records in insertion order
63
+ # @param direction [Symbol, String] `:asc` or `:desc`
64
+ # @return [Array<Runtime::Instance>] the held records, `[]` when the store is empty
65
+ # @raise [Runtime::WiringError] if `order_by` names no attribute of the aggregate
38
66
  def all(order_by: nil, direction: :asc)
39
67
  InMemoryOrdering.ordered(@records.values, aggregate: @aggregate, order_by: order_by, direction: direction)
40
68
  end
41
69
 
70
+ # Answers a declared query by filtering, ordering and paging the held records in Ruby.
71
+ #
72
+ # @param specification [QuerySpecification::Common::Options] the declared query
73
+ # @param args [Hash{Symbol => Object}] values for the specification's symbolic operands
74
+ # @param context [Hash] execution context; only `:registry` (a `Runtime::Registry` or
75
+ # nil) is read, for comparators that look up another aggregate
76
+ # @return [Array<Runtime::Instance>] the matching records, `[]` when none match
77
+ # @raise [Runtime::WiringError] if a where clause uses an operation no comparator handles
42
78
  def query(specification, args = {}, context: {})
43
79
  Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
44
80
  end
45
81
 
82
+ # Records one journal entry, holding a codec copy of its state.
83
+ #
84
+ # Through the state codec, like every durable adapter (PR A3): the
85
+ # journal holds `StateCodec.copy` — exactly what an encode-to-JSON
86
+ # then decode would hand back — never the caller's own live state
87
+ # objects, so an entry read back here has the same deep-symbol,
88
+ # plain-Hash shape a Heki/Sqlite/Postgres entry has.
89
+ #
90
+ # @param entry [Ports::Persistence::Entry] the save or delete to journal
91
+ # @return [Ports::Persistence::Entry] the caller's own `entry`, not the journalled copy
46
92
  def append(entry)
47
- @entries << entry
93
+ copied = Ports::Persistence::Entry.new(operation: entry.operation, id: entry.id,
94
+ state: copy(entry.state), mirrors: entry.mirrors)
95
+ @entries << copied
48
96
  entry
49
97
  end
50
98
 
99
+ # Applies one journal entry to the current-state Hash.
100
+ #
101
+ # @param entry [Ports::Persistence::Entry] the save or delete to materialize
102
+ # @return [Runtime::Instance, nil] the new record for a save; for a delete, the record
103
+ # removed, or nil when none was held
51
104
  def project(entry)
52
105
  if entry.save?
53
- @records[entry.id] = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state.dup)
106
+ @records[entry.id] = build_instance(entry)
54
107
  else
55
108
  @records.delete(entry.id)
56
109
  end
57
110
  end
58
111
 
112
+ # Journals and materializes an instance's current state in one call.
113
+ #
114
+ # @param instance [Runtime::Instance] the instance to store
115
+ # @return [Runtime::Instance] the stored record, a fresh instance over a copy of the state
59
116
  def save(instance)
60
- entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
117
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: copy(instance.state))
61
118
  append(entry)
62
119
  project(entry)
63
120
  end
64
121
 
122
+ # Stores an entry and reports whether it inserted, replaced or conflicted.
123
+ #
65
124
  # One in-memory critical section in the only thread touching this plain
66
125
  # Hash: classify and replace without a preliminary repository lookup.
67
126
  # Durable append and projection remain ordered exactly as ordinary save.
127
+ #
128
+ # @param entry [Ports::Persistence::Entry] the save to store
129
+ # @param insert_only [Boolean] when true, an existing record is left untouched
130
+ # @return [Symbol] `:inserted`, `:replaced`, or `:conflicted` when `insert_only` met an
131
+ # existing record and nothing was written
68
132
  def atomic_put(entry, insert_only: false)
69
133
  exists = @records.key?(entry.id.to_s)
70
134
  return :conflicted if insert_only && exists
@@ -75,29 +139,44 @@ module Hecks
75
139
  status
76
140
  end
77
141
 
142
+ # Journals a delete and removes the record, whether or not one is held.
143
+ #
144
+ # @param id [String, Object] the aggregate identity, journalled as `id.to_s`
145
+ # @return [Runtime::Instance, nil] the record removed, or nil when none was held
78
146
  def delete(id)
79
147
  entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
80
148
  append(entry)
81
149
  project(entry)
82
150
  end
83
151
 
152
+ # Appends an emitted event to the in-process event log.
153
+ #
154
+ # @param event [Runtime::Event] the emitted event
155
+ # @return [Array<Runtime::Event>] the live event log, including `event`
84
156
  def record_event(event) = @events << event
85
157
 
158
+ # Lists the journal in append order, for `AppendOnly#recover!` to replay.
159
+ #
160
+ # @return [Array<Ports::Persistence::Entry>] a copy of the journal, `[]` when nothing
161
+ # has been appended
86
162
  def entries = @entries.dup
87
163
 
164
+ # Empties the records, journal, event log and outbox, keeping the adapter itself.
165
+ #
88
166
  # Every other driven adapter (Postgres/PostgresEra/Sqlite/D1)
89
- # already implements this — `Ports::Persistence::AppendOnly#reset!`
167
+ # implements this too — `Ports::Persistence::AppendOnly#reset!`
90
168
  # forwards to it and only raises when the wrapped adapter doesn't
91
- # respond to `reset!` at all, which was always this adapter's own
92
- # gap, not a deliberate omission (nothing about "in memory" implies
93
- # "cannot be cleared"). Existing callers that fully re-`Hecks.boot`
169
+ # respond to `reset!` at all, and nothing about "in memory" implies
170
+ # "cannot be cleared". Existing callers that fully re-`Hecks.boot`
94
171
  # a domain per test case don't need this — they get a brand new
95
172
  # `Memory` instance, with brand new empty `@records`/`@events`/
96
173
  # `@entries`, for free. This is for the other case: a caller that
97
- # deliberately keeps ONE booted runtime across many cases (to skip
174
+ # deliberately keeps one booted runtime across many cases (to skip
98
175
  # `load_domain`'s own per-boot parse/verify cost) and wants each
99
176
  # case to start from the same clean slate `Hecks.boot` would have
100
177
  # given it, without paying for a fresh boot to get there.
178
+ #
179
+ # @return [Adapters::Memory] self, now empty
101
180
  def reset!
102
181
  @records = {}
103
182
  @events = []
@@ -106,13 +185,24 @@ module Hecks
106
185
  self
107
186
  end
108
187
 
188
+ # Runs the block directly, as the save + emit + outbox boundary other adapters commit.
189
+ #
109
190
  # No rollback here — a Hash has no transaction to join. Memory
110
191
  # implements `transaction` so `Interpreting#run_dispatch_order` has
111
192
  # one shape to call, and the outbox so a spec can watch rows move
112
193
  # pending → claimed → delivered without a database (the same
113
194
  # reason Memory records `events`). See `Runtime::Outbox`.
195
+ #
196
+ # @yield the writes to run together; an exception raised inside undoes nothing
197
+ # @return [Object] the block's own result
114
198
  def transaction = yield
115
199
 
200
+ # Holds new outbox rows, skipping any whose `delivery_id` is already held.
201
+ #
202
+ # @param rows [Array<Runtime::Outbox::Row>] pending rows to enqueue; each accepted row
203
+ # has its `id` assigned in place
204
+ # @return [Array<Runtime::Outbox::Row>] the rows actually enqueued, `[]` when every one
205
+ # was a duplicate
116
206
  def outbox_enqueue(rows)
117
207
  rows.filter_map do |row|
118
208
  next nil if @outbox.any? { |held| held.delivery_id == row.delivery_id }
@@ -123,6 +213,11 @@ module Hecks
123
213
  end
124
214
  end
125
215
 
216
+ # Marks a pending outbox row claimed and counts the delivery attempt.
217
+ #
218
+ # @param id [Integer] the row id `outbox_enqueue` assigned
219
+ # @return [Boolean] true when the row was pending and is now claimed; false when it is
220
+ # unknown or no longer pending
126
221
  def outbox_claim(id) # rubocop:disable Naming/PredicateMethod
127
222
  row = @outbox.find { |held| held.id == id }
128
223
  return false unless row&.pending?
@@ -132,6 +227,13 @@ module Hecks
132
227
  true
133
228
  end
134
229
 
230
+ # Records a delivery outcome on an outbox row, whatever status it held.
231
+ #
232
+ # @param id [Integer] the row id `outbox_enqueue` assigned
233
+ # @param status [String, Symbol] the new status, one of `Runtime::Outbox::STATUSES`;
234
+ # not validated here
235
+ # @param error [String, nil] the failure description, or nil to clear it
236
+ # @return [Boolean] true when the row exists and was updated; false when no row has `id`
135
237
  def outbox_settle(id, status:, error: nil) # rubocop:disable Naming/PredicateMethod
136
238
  row = @outbox.find { |held| held.id == id } or return false
137
239
  row.status = status.to_s
@@ -139,10 +241,104 @@ module Hecks
139
241
  true
140
242
  end
141
243
 
244
+ # Lists outbox rows in enqueue order, as copies a caller may mutate freely.
245
+ #
246
+ # @param status [String, Symbol, nil] only rows with this status; nil lists every row
247
+ # @return [Array<Runtime::Outbox::Row>] shallow copies of the matching rows, `[]` when
248
+ # none match
142
249
  def outbox_rows(status: nil)
143
250
  rows = status ? @outbox.select { |row| row.status == status.to_s } : @outbox
144
251
  rows.map(&:dup)
145
252
  end
253
+
254
+ private
255
+
256
+ # THE LANGUAGE'S OWN BOOTSTRAP SAVES ITS SELF-DESCRIPTION QUADRATICALLY
257
+ # OTHERWISE. `MetaValidator::Judge` dispatches every declaration in the
258
+ # self-hosted grammar into a fresh, private, never-durable `Memory`
259
+ # store (meta_validator.rb's own header: "each bluebook is judged in a
260
+ # fresh in-memory store") — and every nested-entity dispatch (a
261
+ # `ValueObject::Member`, then one `ValueObject::Member::Pair` per
262
+ # key/value pair) re-saves the WHOLE parent aggregate, because entities
263
+ # have no storage of their own (S17, ADR 0026). A table of N member
264
+ # rows costs O(N) dispatches, each PAYING TWICE for the aggregate's own
265
+ # size-N state: once in `StateCodec.copy` (`append`/`project`'s own
266
+ # encode-then-decode round trip) and again in `Instance#initialize`'s
267
+ # `hydrate_with_defaults`, which re-walks and re-validates every
268
+ # element of an entity list on EVERY save regardless of how many of
269
+ # them were already valid as of the previous one
270
+ # (`Value::EntityListCoercion#hydrate_entity_list` has no "already
271
+ # hydrated" shortcut for entity elements — only a value-object list
272
+ # element gets one). Both are O(N) per save, so a table of N rows
273
+ # costs O(N^2) total — and it is paid by every rspec worker and every
274
+ # `bin/*` subprocess that boots the language at all (found live: PR
275
+ # #738's 128-row `RefusalSiteArgument` table alone tripled this one
276
+ # aggregate's own save time, 6.7s -> 18.2s).
277
+ #
278
+ # `Runtime::Value.judge_bootstrapping?` (judge.rb's own `send_to`,
279
+ # wrapping every dispatch the judge makes) is already the flag that
280
+ # marks exactly this window and NOTHING else — "never for a REAL
281
+ # domain's own declared value objects... which Judge never dispatches
282
+ # commands against" (coercion.rb's own comment on the same flag). It
283
+ # is reused here rather than a new toggle for the same reason: one
284
+ # flag, one meaning, checked by two unrelated callers for two
285
+ # unrelated purposes (loosening a scalar-shape check there, skipping
286
+ # both round trips here) is simpler to reason about than two flags
287
+ # that would always be true or false together.
288
+ #
289
+ # WHY SKIPPING BOTH IS SAFE HERE, AND ONLY HERE: `entry.state` a save
290
+ # ever hands this adapter is always `instance.state.dup`
291
+ # (`AppendOnly#save`) — a shallow copy of an ALREADY-hydrated,
292
+ # ALREADY-validated live `Instance`'s own state, built by the very
293
+ # same `Value.for_attribute`/`hydrate_with_defaults` machinery
294
+ # `StateCodec.copy` and `Instance.new`'s default (`hydrate: true`)
295
+ # path would otherwise redo. Every VALUE inside it — a `Runtime::
296
+ # Value` (frozen through, see value.rb's own header) or a `list_of`
297
+ # attribute's own array (`Freezer.deep`d the moment it was built,
298
+ # instance.rb's own header on `Instance#dup`) — is already immutable,
299
+ # so a bare top-level `.dup` is exactly as safe as `Instance#dup`
300
+ # already trusts it to be everywhere else in this codebase; nothing
301
+ # below the top level is ever mutated in place. Re-deriving the same
302
+ # answer through the codec and through re-hydration is therefore
303
+ # pure, avoidable cost for this one caller — never a correctness
304
+ # requirement.
305
+ #
306
+ # `bootstrap_fast_path?` is the guard, and it is deliberately CHEAP —
307
+ # O(this aggregate's own declared attribute count), never O(N) — so
308
+ # it cannot reintroduce the very cost it exists to avoid: `StateCodec.
309
+ # decoded?` (the obvious-looking alternative) recurses into every
310
+ # `list_of` element to check IT, which is exactly the O(N) walk this
311
+ # whole change removes. A `Hash` with every top-level key already a
312
+ # `Symbol` is what `Instance#state` ALWAYS looks like (`Value.hydrate`
313
+ # refuses anything else, coercion.rb's own header), so it is checked
314
+ # here instead — true for the actual shape every real save has,
315
+ # false (falling back to the always-correct slow path) for anything
316
+ # that somehow doesn't.
317
+ #
318
+ # Verified empirically before landing, not just argued: instrumenting
319
+ # every bootstrapping-time save across a full `grammar_registry` boot
320
+ # and comparing this fast path's own state, journal entry, and stored
321
+ # `Instance` against the unmodified `StateCodec.copy` + `Instance.new
322
+ # (hydrate: true)` pipeline's found them equal (`==`) for every one of
323
+ # 6,617 saves, zero mismatches.
324
+ def bootstrap_fast_path?(state)
325
+ Runtime::Value.judge_bootstrapping? && state.is_a?(Hash) && state.keys.all?(Symbol)
326
+ end
327
+
328
+ def copy(state)
329
+ return state.dup if bootstrap_fast_path?(state)
330
+
331
+ Ports::Persistence::StateCodec.copy(@aggregate, state)
332
+ end
333
+
334
+ def build_instance(entry)
335
+ if bootstrap_fast_path?(entry.state)
336
+ Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state.dup, hydrate: false)
337
+ else
338
+ decoded = Ports::Persistence::StateCodec.copy(@aggregate, entry.state)
339
+ Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: decoded)
340
+ end
341
+ end
146
342
  end
147
343
  end
148
344
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Hecks
4
4
  module Adapters
5
- # A GENERIC STAND-IN for a real Checkout-Session-style adapter — no
5
+ # A generic stand-in for a real Checkout-Session-style adapter — no
6
6
  # network call, no real account, matching whatever real payment
7
7
  # adapter a project pairs it with by shape alone
8
8
  # (`create_session(event:, registration_id:, success_url:, cancel_url:)
@@ -5,12 +5,12 @@ module Hecks
5
5
  class Postgres
6
6
  # How a state crosses the column boundary: which fields persist, and
7
7
  # how each one encodes into (and decodes out of) its column. Same
8
- # SHAPE as Sqlite::Codec (one column per attribute, JSON for
8
+ # shape as Sqlite::Codec (one column per attribute, JSON for
9
9
  # nested/list) — not its mechanics. Sqlite's own columns carry no
10
10
  # type affinity worth trusting on the way back out (SQLite gives it
11
11
  # back typed anyway via the driver), but `pg` hands every column
12
- # back as TEXT unless a type map says otherwise, so a real `bigint`/
13
- # `double precision` column needs its OWN coercion on decode that
12
+ # back as text unless a type map says otherwise, so a real `bigint`/
13
+ # `double precision` column needs its own coercion on decode that
14
14
  # Sqlite never had to write.
15
15
  module Codec
16
16
  private
@@ -23,7 +23,7 @@ module Hecks
23
23
  fields << { name: lifecycle.field, attribute: nil, sql_type: "text" } if lifecycle && fields.none? do |field|
24
24
  field[:name] == lifecycle.field
25
25
  end
26
- # `projects` FIELDS (S12, ADR 0025) ARE A LOCAL COLUMN TOO — see
26
+ # `projects` fields (S12, ADR 0025) are a local column too — see
27
27
  # Sqlite::Codec#persisted_fields' own comment; identical reasoning,
28
28
  # `text` to match this file's own lowercase SQL type spelling.
29
29
  @aggregate.projected_fields.each do |field|
@@ -35,12 +35,12 @@ module Hecks
35
35
  end
36
36
 
37
37
  def encode(attr, value)
38
- # NEVER SET IS NOT EMPTY — same reasoning as Sqlite::Codec's own
38
+ # **Never set is not empty** — same reasoning as Sqlite::Codec's own
39
39
  # comment: a list attribute nothing has ever appended to has to
40
40
  # stay NULL to answer the same as Memory does, not become `[]`
41
41
  # invented by this adapter's own storage.
42
- return (value.nil? ? nil : JSON.generate(value)) if attr.list?
43
- return JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
42
+ return (value.nil? ? nil : state_json(value)) if attr.list?
43
+ return state_json(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
44
44
 
45
45
  value
46
46
  end
@@ -51,21 +51,37 @@ module Hecks
51
51
  encode(field[:attribute], value)
52
52
  end
53
53
 
54
+ # Every jsonb column's text goes through the state codec's `encode`
55
+ # (PR A3) — see Sqlite::Codec#state_json.
56
+ def state_json(value) = JSON.generate(Ports::Persistence::StateCodec.encode(@aggregate, value))
57
+
58
+ # Columns reassembled, then decoded once through the state codec
59
+ # (PR A3) — see Sqlite::Codec#decode, including why a NULL
60
+ # projected-only column reads back absent.
54
61
  def decode(row)
55
- persisted_fields.each_with_object({}) do |field, state|
62
+ state = persisted_fields.each_with_object({}) do |field, raw_state|
56
63
  attr = field[:attribute]
57
64
  unless attr
58
- state[field[:name]] = row[field[:name].to_s]
65
+ value = row[field[:name].to_s]
66
+ next if value.nil? && projected_only?(field)
67
+
68
+ raw_state[field[:name]] = value
59
69
  next
60
70
  end
61
71
  raw = row[attr.name.to_s]
62
- state[attr.name] =
72
+ raw_state[attr.name] =
63
73
  if attr.list? || value_object?(attr)
64
- raw ? JSON.parse(raw, symbolize_names: true) : nil
74
+ raw ? JSON.parse(raw) : nil
65
75
  else
66
76
  coerce_scalar(attr, raw)
67
77
  end
68
78
  end
79
+ Ports::Persistence::StateCodec.decode(@aggregate, state)
80
+ end
81
+
82
+ def projected_only?(field)
83
+ @aggregate.lifecycle&.field&.to_sym != field[:name].to_sym &&
84
+ @aggregate.projected_fields.any? { |projected| projected.name.to_sym == field[:name].to_sym }
69
85
  end
70
86
 
71
87
  # `pg` returns every column as a Ruby String by default (no type
@@ -3,7 +3,7 @@ require_relative "../../../runtime/outbox"
3
3
 
4
4
  module Hecks
5
5
  module Adapters
6
- # THE OUTBOX, POSTGRES-SHAPED — shared verbatim by `Postgres` and the
6
+ # **The outbox, Postgres-shaped** — shared verbatim by `Postgres` and the
7
7
  # era plugin's `PostgresEra`, the same way their `events` and
8
8
  # `hecks_saga_instances` DDL is copied between them: nothing here is
9
9
  # lineage-specific. Needs `@db` (a `PG::Connection`) and `table`
@@ -12,10 +12,19 @@ module Hecks
12
12
  # for the SQL idioms (`ON CONFLICT DO NOTHING` = idempotent enqueue,
13
13
  # `WHERE status = 'pending'` = the compare-and-set claim).
14
14
  module PostgresOutbox
15
- # RE-ENTRANT `Interpreting#run_dispatch_order` opens one
15
+ # Runs the block inside one Postgres transaction, joining an already-open one.
16
+ #
17
+ # **Re-entrant** — `Interpreting#run_dispatch_order` opens one
16
18
  # transaction around save+emit+outbox and the adapter's own
17
19
  # `append`/`atomic_put`/`delete` each open theirs; PG refuses
18
20
  # BEGIN inside BEGIN, so an inner call joins the open one.
21
+ #
22
+ # @yield the writes to commit together; an exception raised inside rolls the
23
+ # outermost transaction back
24
+ # @return [Object] the block's own result
25
+ # @raise [PG::ConnectionBad] if the connection died; a reconnect is attempted for the
26
+ # next caller before it is re-raised
27
+ # @raise [PG::Error] if a statement inside the block fails
19
28
  def transaction(&)
20
29
  return yield unless @db.transaction_status == PG::PQTRANS_IDLE
21
30
 
@@ -25,6 +34,13 @@ module Hecks
25
34
  raise
26
35
  end
27
36
 
37
+ # Inserts new outbox rows as pending, skipping any whose `delivery_id` already exists.
38
+ #
39
+ # @param rows [Array<Runtime::Outbox::Row>] rows to enqueue; each accepted row has its
40
+ # `id` and `status` assigned in place. `row.aggregate` is stored as given
41
+ # @return [Array<Runtime::Outbox::Row>] the rows actually inserted, `[]` when every one
42
+ # was a duplicate
43
+ # @raise [PG::Error] if an insert fails
28
44
  def outbox_enqueue(rows)
29
45
  rows.filter_map do |row|
30
46
  result = pg_exec_params(
@@ -40,6 +56,12 @@ module Hecks
40
56
  end
41
57
  end
42
58
 
59
+ # Claims a pending outbox row with a compare-and-set update, counting the attempt.
60
+ #
61
+ # @param id [Integer] the row id `outbox_enqueue` assigned
62
+ # @return [Boolean] true when the row was pending and is now claimed; false when it is
63
+ # unknown or another claimer got there first
64
+ # @raise [PG::Error] if the update fails
43
65
  def outbox_claim(id) # rubocop:disable Naming/PredicateMethod
44
66
  pg_exec_params(
45
67
  "UPDATE hecks_outbox SET status = 'claimed', attempts = attempts + 1, claimed_at = now() " \
@@ -48,6 +70,15 @@ module Hecks
48
70
  ).cmd_tuples == 1
49
71
  end
50
72
 
73
+ # Records a delivery outcome and its settle time on an outbox row, whatever status it
74
+ # held.
75
+ #
76
+ # @param id [Integer] the row id `outbox_enqueue` assigned
77
+ # @param status [String, Symbol] the new status, one of `Runtime::Outbox::STATUSES`;
78
+ # not validated here
79
+ # @param error [String, nil] the failure description, or nil to store NULL
80
+ # @return [Boolean] true when exactly one row was updated; false when no row has `id`
81
+ # @raise [PG::Error] if the update fails
51
82
  def outbox_settle(id, status:, error: nil) # rubocop:disable Naming/PredicateMethod
52
83
  pg_exec_params(
53
84
  "UPDATE hecks_outbox SET status = $2, error = $3, settled_at = now() WHERE id = $1",
@@ -55,6 +86,13 @@ module Hecks
55
86
  ).cmd_tuples == 1
56
87
  end
57
88
 
89
+ # Lists the outbox rows whose `aggregate` column equals this adapter's `table`, in
90
+ # enqueue order.
91
+ #
92
+ # @param status [String, Symbol, nil] only rows with this status; nil lists every row
93
+ # @return [Array<Runtime::Outbox::Row>] the matching rows, `event` parsed with Symbol
94
+ # keys; `[]` when none match
95
+ # @raise [PG::Error] if the statement fails
58
96
  def outbox_rows(status: nil)
59
97
  sql = "SELECT * FROM hecks_outbox WHERE aggregate = $1"
60
98
  binds = [table]
@@ -1,6 +1,6 @@
1
1
  module Hecks
2
2
  module Adapters
3
- # SELF-HEALING CONNECTION — shared verbatim by `Postgres` and the era
3
+ # **Self-healing connection** — shared verbatim by `Postgres` and the era
4
4
  # plugin's `PostgresEra`, the same way `PostgresOutbox` (outbox.rb) is:
5
5
  # nothing here is lineage-specific, and `PostgresOutbox`'s own
6
6
  # `@db.exec*` calls route through this module's `pg_exec`/
@@ -12,18 +12,25 @@ module Hecks
12
12
  # A backend killed out from under an adapter (a DBA's own
13
13
  # `pg_terminate_backend`, a load balancer's failover, a restart) —
14
14
  # chaos-tested against the plain `Postgres` adapter: `PG::ConnectionBad`
15
- # on the query that hit it, and PERMANENTLY on every query after,
15
+ # on the query that hit it, and permanently on every query after,
16
16
  # since nothing ever replaced `@db` with a live connection.
17
17
  # `pg_exec`/`pg_exec_params` are the two primitives every other method
18
18
  # in either class funnels through — wrapping them here, once,
19
- # self-heals `@db` for the NEXT caller. THE CURRENT CALL STILL
20
- # RAISES — reconnecting cannot tell a caller whether ITS OWN write
19
+ # self-heals `@db` for the next caller. The current call still
20
+ # raises — reconnecting cannot tell a caller whether its own write
21
21
  # reached the server before the connection died, so silently
22
22
  # retrying it here could silently double it; that ambiguity is
23
23
  # exactly why `Runtime::SagaInterpreter`'s own defect-retry exists
24
- # ONE LAYER UP, where a dispatch is retried as a whole (fresh
24
+ # one layer up, where a dispatch is retried as a whole (fresh
25
25
  # hydrate, fresh `given`s), not as a lone SQL statement.
26
26
  module PostgresReconnect
27
+ # Runs one parameterless statement, replacing a dead connection before re-raising.
28
+ #
29
+ # @param sql [String] the statement to run
30
+ # @return [PG::Result] the statement's result
31
+ # @raise [PG::ConnectionBad] if the connection died; `@db` is reconnected for the next
32
+ # caller, and this call is never retried
33
+ # @raise [PG::Error] if the server rejects the statement
27
34
  def pg_exec(sql)
28
35
  @db.exec(sql)
29
36
  rescue PG::ConnectionBad
@@ -31,6 +38,15 @@ module Hecks
31
38
  raise
32
39
  end
33
40
 
41
+ # Runs one statement with bind parameters, replacing a dead connection before
42
+ # re-raising.
43
+ #
44
+ # @param sql [String] the statement, with `$1`-style placeholders
45
+ # @param binds [Array<Object>] one value per placeholder, in order; nil binds NULL
46
+ # @return [PG::Result] the statement's result
47
+ # @raise [PG::ConnectionBad] if the connection died; `@db` is reconnected for the next
48
+ # caller, and this call is never retried
49
+ # @raise [PG::Error] if the server rejects the statement
34
50
  def pg_exec_params(sql, binds)
35
51
  @db.exec_params(sql, binds)
36
52
  rescue PG::ConnectionBad
@@ -40,11 +56,11 @@ module Hecks
40
56
 
41
57
  private
42
58
 
43
- # BEST-EFFORT — a reconnect attempt that itself fails (the server
59
+ # **Best-effort** — a reconnect attempt that itself fails (the server
44
60
  # is actually down, not just this one backend) leaves `@db`
45
61
  # unchanged; the `PG::ConnectionBad` already being re-raised by
46
62
  # `pg_exec`/`pg_exec_params` above still reaches the caller either
47
- # way, so swallowing a failed RECONNECT attempt here loses no
63
+ # way, so swallowing a failed reconnect attempt here loses no
48
64
  # information — it only avoids masking the original error with a
49
65
  # second one.
50
66
  def reconnect!