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
@@ -6,44 +6,44 @@ require_relative "../runtime/saga_interpreter"
6
6
 
7
7
  module Hecks
8
8
  module Fuzzing
9
- # A SECOND COMPARISON AXIS — every OTHER check this practice runs is
10
- # DIFFERENTIAL: Ruby vs the compiled Rust conformance binary, on the
9
+ # **A second comparison axis** — every other check this practice runs is
10
+ # differential: Ruby vs the compiled Rust conformance binary, on the
11
11
  # same generated sequence (`bin/qa_sweep`'s own `diff_ruby_vs_rust`,
12
12
  # `Properties.check`). Differential comparison structurally cannot
13
- # catch a bug where both engines are wrong the SAME way, or where a
13
+ # catch a bug where both engines are wrong the same way, or where a
14
14
  # single engine is self-inconsistent with nothing to differentially
15
- # compare it against. This module asks a DIFFERENT question of ONE
16
- # engine at a time: does it agree with ITSELF?
15
+ # compare it against. This module asks a different question of one
16
+ # engine at a time: does it agree with itself?
17
17
  #
18
- # THREE CHECKS, run on the SAME already-generated sequence and its
18
+ # Three checks, run on the same already-generated sequence and its
19
19
  # resulting live state — no second fuzzing pass, no re-dispatch
20
20
  # through the command layer:
21
21
  #
22
22
  # 1. `check_rehydration` — does reloading an aggregate from its
23
23
  # own durable journal reproduce exactly the state a live dispatch
24
24
  # already produced?
25
- # 2. `check_idempotency` — does replaying that SAME journal a
25
+ # 2. `check_idempotency` — does replaying that same journal a
26
26
  # second time change anything? (a variant of #1, but a different
27
27
  # failure mode: no leaked state between applications, not just
28
28
  # "cold load works once.")
29
29
  # 3. `check_value_object_round_trip` — does every value object the
30
30
  # sequence actually built survive `to_json` then rebuilt back?
31
31
  #
32
- # THE REHYDRATION PATH, FOUND, NOT GUESSED. hecks is not event-sourced
32
+ # **The rehydration path, found, not guessed**. hecks is not event-sourced
33
33
  # at the aggregate level — there is no `AccountOpened`-shaped log a
34
- # `CommandInterpreter` folds to rebuild state. What there IS, real and
34
+ # `CommandInterpreter` folds to rebuild state. What there is, real and
35
35
  # already shipping, is `Ports::Persistence::AppendOnly` (lib/hecks/
36
36
  # ports/persistence/append_only.rb): every adapter accepts the same
37
- # `Entry` stream (`operation`, `id`, the FULL state after that
37
+ # `Entry` stream (`operation`, `id`, the full state after that
38
38
  # command — not a delta) and answers `#entries`; `#recover!` — "an
39
39
  # append is durable before a projection is attempted; replaying the
40
40
  # log restores a snapshot/table after a crash in that small window" —
41
- # is called on EVERY repository this runtime ever builds
42
- # (`RepositoryFactory.build`'s own `recover: true` default). That IS
41
+ # is called on every repository this runtime ever builds
42
+ # (`RepositoryFactory.build`'s own `recover: true` default). That is
43
43
  # the production cold-rehydration path. Reusing `#recover!` directly
44
- # against the LIVE adapter would prove nothing, though: `Fuzzing::
44
+ # against the live adapter would prove nothing, though: `Fuzzing::
45
45
  # Replay` runs against `Adapters::Memory` (`IsolatedBoot`'s own
46
- # default), and Memory's own `Entry#state` is a SHALLOW `instance.
46
+ # default), and Memory's own `Entry#state` is a shallow `instance.
47
47
  # state.dup` — the exact same `Runtime::Value` objects a command
48
48
  # produced ride along unchanged, so folding them straight back through
49
49
  # Memory's own `#project` is a tautology that can never fail (`Value#
@@ -51,16 +51,16 @@ module Hecks
51
51
  # branch passes an already-typed value straight through, no
52
52
  # rebuilding at all).
53
53
  #
54
- # So `cold_read`, below, feeds the SAME entries through `Adapters::
55
- # Heki` instead — a REAL, already-shipped, disk-backed adapter
54
+ # So `cold_read`, below, feeds the same entries through `Adapters::
55
+ # Heki` instead — a real, already-shipped, disk-backed adapter
56
56
  # (examples/banking's own `persisted_by("Heki")`), in a throwaway
57
57
  # directory. Writing forces every value through `JSON.generate`
58
58
  # (Heki's own journal line, its own compressed snapshot); reading
59
- # back through a FRESH `Heki` instance (unmemoized `@store`) forces
59
+ # back through a fresh `Heki` instance (unmemoized `@store`) forces
60
60
  # `read_snapshot`/`replay_journal` — real `JSON.parse`, real
61
61
  # `Zlib::Inflate`, real bytes off a real filesystem — which is what
62
62
  # makes `Instance.hydrate_with_defaults` → `Value.hydrate` →
63
- # `Value.for_attribute` actually REBUILD every value object from raw
63
+ # `Value.for_attribute` actually rebuild every value object from raw
64
64
  # data via `Value.build`, the same coercion/validation path a real
65
65
  # restart takes, rather than pass the live object through unchanged.
66
66
  # This is the exact mechanism `AppendOnly#recover!` names in its own
@@ -70,12 +70,20 @@ module Hecks
70
70
  module SelfConsistency
71
71
  module_function
72
72
 
73
- # THE WHOLE PASS called once, with the runtime STILL LIVE (inside
73
+ # Every Heki this pass opens is guarded like a runtime repository
74
+ # (Phase 2, Track A, PR A4). These adapters are built directly, not
75
+ # through `RepositoryFactory.build`, so without this a cold read here
76
+ # could hand `Runtime::Instance` undecoded state and the codec's
77
+ # guarantee would have a bypass in exactly the check that exists to
78
+ # compare stored state against live state.
79
+ def guarded_heki(**) = Ports::Persistence::CodecBoundary.guard!(Adapters::Heki.new(**))
80
+
81
+ # The whole pass — called once, with the runtime still live (inside
74
82
  # `Replay.call`'s own `IsolatedBoot.call` block, before the tmp
75
83
  # directory and its adapters go out of scope) and the `history`
76
84
  # `Replay.call` is about to return. Nothing here boots a second
77
85
  # runtime or dispatches a single command; every check below reads
78
- # data this ONE replay already produced. Just the three checks
86
+ # data this one replay already produced. Just the three checks
79
87
  # below, run and collected — kept as three independently callable
80
88
  # methods (not fused into one shared fold) so a spec proving one
81
89
  # check can fire never has to reason about the other two.
@@ -86,12 +94,12 @@ module Hecks
86
94
  saga_redelivery_idempotency: check_saga_idempotency(runtime, history) }
87
95
  end
88
96
 
89
- # CHECK 1 — REHYDRATE-FROM-JOURNAL == LIVE STATE.
97
+ # Check 1 — rehydrate-from-journal == live state.
90
98
  def check_rehydration(runtime)
91
99
  each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
92
100
  live = snapshot(repository)
93
101
  Dir.mktmpdir("hecks-self-consistency") do |tmp|
94
- writer = Adapters::Heki.new(aggregate: aggregate, root: tmp)
102
+ writer = guarded_heki(aggregate: aggregate, root: tmp)
95
103
  rehydrated = fold!(writer, tmp, aggregate, entries)
96
104
  next if rehydrated == live
97
105
 
@@ -101,8 +109,8 @@ module Hecks
101
109
  end
102
110
  end
103
111
 
104
- # CHECK 2 — REPLAY IDEMPOTENCY: folding the SAME entries into the
105
- # SAME durable store a second time must change nothing. A variant
112
+ # Check 2 — replay idempotency: folding the same entries into the
113
+ # same durable store a second time must change nothing. A variant
106
114
  # of check 1, worth stating separately — this catches a
107
115
  # replay-specific bug (leaked state between applications, a
108
116
  # double-applied effect) that a single, one-shot cold read could
@@ -110,7 +118,7 @@ module Hecks
110
118
  def check_idempotency(runtime)
111
119
  each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
112
120
  Dir.mktmpdir("hecks-self-consistency") do |tmp|
113
- writer = Adapters::Heki.new(aggregate: aggregate, root: tmp)
121
+ writer = guarded_heki(aggregate: aggregate, root: tmp)
114
122
  once = fold!(writer, tmp, aggregate, entries)
115
123
  twice = fold!(writer, tmp, aggregate, entries)
116
124
  next if once == twice
@@ -121,9 +129,9 @@ module Hecks
121
129
  end
122
130
  end
123
131
 
124
- # CHECK 3 — every `Runtime::Value` the sequence actually built
132
+ # Check 3 — every `Runtime::Value` the sequence actually built
125
133
  # (walked out of the replay's own `instances`/`events`/`queries`,
126
- # never a hand-picked example), round-tripped through the REAL
134
+ # never a hand-picked example), round-tripped through the real
127
135
  # serialize/deserialize pair: `Value#to_json` (JSON.generate(to_h),
128
136
  # value.rb) out, `Value.build` (value/coercion.rb — the same
129
137
  # constructor a command argument's own raw JSON goes through) back
@@ -132,16 +140,16 @@ module Hecks
132
140
  # `from_json` per closed set/value object); `Value.build` is the
133
141
  # actual Ruby door a raw, untyped Hash becomes a validated,
134
142
  # admitted, invariant-checked value object through.
135
- # `aggregate:` THREADED ALONGSIDE EVERY VALUE FOUND, NOT DROPPED
143
+ # `aggregate:` threaded alongside every value found, not dropped
136
144
  # `Value.build(value_object, fields, aggregate)`'s third argument is
137
- # what lets `normalize_composite_fields` resolve a NESTED composite
145
+ # what lets `normalize_composite_fields` resolve a nested composite
138
146
  # field's own type by name (`value_object_for(aggregate, type)`).
139
147
  # Building with `aggregate: nil` (this method's first version, live-
140
148
  # tested against `examples/pizzas` while this was being written)
141
149
  # silently skips that step entirely — `Pizza`'s own `price_cents`/
142
- # `size` fields round-tripped back as bare, STRING-keyed Hashes
150
+ # `size` fields round-tripped back as bare, string-keyed Hashes
143
151
  # instead of rebuilt `Money`/`PizzaSize` value objects, a false
144
- # POSITIVE this check would have reported as a real bug on every
152
+ # positive this check would have reported as a real bug on every
145
153
  # single sweep. Resolved from `history[:instances]`' own key
146
154
  # (`"Domain::Aggregate#id"`, `Replay#snapshot_instances`) and
147
155
  # `history[:events]`' own `event[:aggregate]` (`"Domain::Aggregate"`,
@@ -150,7 +158,7 @@ module Hecks
150
158
  # off `history` rather than a second lookup. `history[:queries]`'
151
159
  # own rows have no single owning aggregate reliably named on the
152
160
  # entry itself (a cross-aggregate read model, a `for_each` target),
153
- # so they are left OUT of this walk rather than risk the same false
161
+ # so they are left out of this walk rather than risk the same false
154
162
  # positive `nil` already produced once — `instances` and `events`
155
163
  # alone already reach every value object a generated sequence
156
164
  # actually persisted or announced.
@@ -186,12 +194,12 @@ module Hecks
186
194
  end
187
195
  end
188
196
 
189
- # CHECK 4 — SAGA COLD-REHYDRATION (ANGLE-10). Checks 1/2 above cold-
190
- # read an AGGREGATE's own journal through Heki; nothing in this file
191
- # ever exercised the OTHER durable store `SagaInterpreter#checkpoint`
197
+ # Check 4 — saga cold-rehydration (angle-10). Checks 1/2 above cold-
198
+ # read an aggregate's own journal through Heki; nothing in this file
199
+ # ever exercised the other durable store `SagaInterpreter#checkpoint`
192
200
  # writes through — `Ports::Persistence::NullSagaStore`'s own header
193
201
  # calls Heki's `SagaStore` (`adapters/driven/heki/saga_store.rb`) the
194
- # OPTIONAL saga-persistence capability, and it is real and already
202
+ # optional saga-persistence capability, and it is real and already
195
203
  # shipping, just never fuzzed: `Registry#rehydrate_sagas!` — the
196
204
  # production "process just restarted" path — folds exactly what
197
205
  # `each_saga` yields back into `@saga_instances`, and until now
@@ -201,26 +209,26 @@ module Hecks
201
209
  # rather than folding it into checks 1/2's own aggregate walk.
202
210
  #
203
211
  # `history[:saga_instances]` (`replay.rb`'s own `saga_instances`
204
- # local, built once at the very end of a replay) is the SAME
212
+ # local, built once at the very end of a replay) is the same
205
213
  # materialized `{pm_name => {correlation => {state:, memory:}}}`
206
214
  # shape `SagaInterpreter#checkpoint` itself hands a real adapter —
207
215
  # read from `history`, not re-derived from the (by-now-live, already
208
216
  # mutated by whatever `check_saga_idempotency` ran first, see that
209
217
  # method's own header) `runtime.registry.saga_instances`. Written
210
- # through a REAL `Adapters::Heki` (a throwaway tmpdir, one per
218
+ # through a real `Adapters::Heki` (a throwaway tmpdir, one per
211
219
  # process manager so two process managers with correlations that
212
220
  # happen to collide as strings never share a store), read back
213
- # through a FRESH instance (unmemoized `@store`/`@saga_store`, same
221
+ # through a fresh instance (unmemoized `@store`/`@saga_store`, same
214
222
  # reason `fold!` above uses one) — forcing the identical
215
223
  # `JSON.generate`/`JSON.parse` boundary a real crash-then-restart
216
224
  # takes, not a live-object pass-through.
217
225
  #
218
- # ONE FINDING PER (domain, process manager) — every correlation this
226
+ # One finding per (domain, process manager) — every correlation this
219
227
  # process manager's own `history[:saga_instances]` entry holds,
220
228
  # compared as a whole Hash — the same aggregate-granularity (not
221
229
  # per-record) `check_rehydration` already reports at.
222
230
  #
223
- # `completed_compensations` is DELIBERATELY OUT OF SCOPE — `history[
231
+ # `completed_compensations` is deliberately out of scope — `history[
224
232
  # :saga_instances]` never captures it (`replay.rb`'s own comment:
225
233
  # only `state`/`memory` are threaded through, since a saga's
226
234
  # in-flight compensation ledger is a fact about a leg still running,
@@ -237,7 +245,7 @@ module Hecks
237
245
  next unless anchor
238
246
 
239
247
  Dir.mktmpdir("hecks-self-consistency-saga") do |tmp|
240
- writer = Adapters::Heki.new(aggregate: anchor, root: tmp, settings: { domain: domain_name })
248
+ writer = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
241
249
  persisted.each do |correlation, saga|
242
250
  writer.save_saga(process_manager: process_manager.name, correlation: correlation.to_s,
243
251
  state: saga[:state], memory: saga[:memory], completed_compensations: [])
@@ -253,21 +261,21 @@ module Hecks
253
261
  end
254
262
  end
255
263
 
256
- # CHECK 5 — REDELIVERY IDEMPOTENCY OF THE CHECKPOINT-THEN-LOAD PATH.
264
+ # Check 5 — redelivery idempotency of the checkpoint-then-load path.
257
265
  # `check_saga_rehydration` above proves cold-reading a checkpoint
258
- # reproduces the same DATA; this proves the OTHER half of a real
266
+ # reproduces the same data; this proves the other half of a real
259
267
  # crash/restart — a message an at-least-once delivery mechanism (an
260
268
  # outbox redrive, a queue redelivery) hands the rehydrated saga a
261
- # SECOND time — does not silently re-advance it. There is no flag
269
+ # second time — does not silently re-advance it. There is no flag
262
270
  # for this in production (`SagaInterpreter#unwind`'s own comment:
263
271
  # "the check is the guard") — the (event, current state) lookup
264
- # `handler_for` performs is the ENTIRE mechanism, and it has never
272
+ # `handler_for` performs is the entire mechanism, and it has never
265
273
  # been exercised against a state this practice loaded from cold
266
274
  # storage rather than one still sitting in a live process's memory.
267
275
  #
268
- # ONE (PROCESS MANAGER, CORRELATION) TESTED, using a fresh
276
+ # One (process manager, correlation) tested, using a fresh
269
277
  # `Runtime::SagaInterpreter` sharing `runtime`'s own `registry` and
270
- # `door: runtime` — the identical two objects the DISPATCHER'S own
278
+ # `door: runtime` — the identical two objects the dispatcher's own
271
279
  # `@sagas` was built from (`dispatcher.rb`'s own `SagaInterpreter.
272
280
  # new(registry, door: self)`) — not a hand-rolled re-implementation
273
281
  # of `advance_saga`'s own state-guard. `only: process_manager` scopes
@@ -275,13 +283,13 @@ module Hecks
275
283
  # keyword the outbox relay already uses to run one consumer alone
276
284
  # (`Runtime::Outbox::Relay#run_consumer`).
277
285
  #
278
- # WHICH EVENT TO REDELIVER — `runtime.registry.saga_log`'s own last
286
+ # Which event to redeliver — `runtime.registry.saga_log`'s own last
279
287
  # `advanced: true` row for this (process manager, correlation) names
280
- # the event BY NAME ONLY; the REAL `Runtime::Event` object (payload,
288
+ # the event by name only; the real `Runtime::Event` object (payload,
281
289
  # aggregate, id, `correlation` — everything `saga_correlation`/
282
290
  # `dispatch_args` actually read) lives in `runtime.events`, still
283
- # live for exactly this reason (this file's own header: "runtime IS
284
- # STILL LIVE HERE"). Matched back by NAME plus `saga_correlation`
291
+ # live for exactly this reason (this file's own header: "runtime is
292
+ # still live here"). Matched back by name plus `saga_correlation`
285
293
  # itself (`Runtime::SagaInterpreter::Correlation`, `private`) —
286
294
  # reused via `send` rather than reproduced, because reproducing its
287
295
  # three-tier fallback (a dotted payload field, a stamped passthrough,
@@ -291,25 +299,25 @@ module Hecks
291
299
  # its own `saga_log` row's `on:` is the synthetic `REFUSED` trigger
292
300
  # name, never a real domain event, so there is nothing to redeliver.
293
301
  #
294
- # SIMULATING "JUST RESTARTED" — the live registry's own in-memory
295
- # `saga_instances[pm][correlation]` slot is overwritten, IN PLACE,
296
- # with whatever a cold Heki read of the SAME checkpoint answers
302
+ # Simulating "just restarted" — the live registry's own in-memory
303
+ # `saga_instances[pm][correlation]` slot is overwritten, in place,
304
+ # with whatever a cold Heki read of the same checkpoint answers
297
305
  # (exactly what `Registry#rehydrate_sagas!` does for real on every
298
306
  # boot), the redelivery is driven through the real interpreter, and
299
307
  # the slot is put back — `ensure`d — once this correlation's own
300
- # check is done. Safe ONLY because `check`/`Replay.call` run this,
308
+ # check is done. Safe only because `check`/`Replay.call` run this,
301
309
  # synchronously, single-threaded, as the very last thing before
302
310
  # `runtime` and its whole tmp directory go out of scope for good;
303
- # nothing downstream of this method ever reads the LIVE registry
311
+ # nothing downstream of this method ever reads the live registry
304
312
  # again (`check_saga_rehydration`, `check_rehydration`, `check_
305
313
  # idempotency`, `check_value_object_round_trip` all read `history`'s
306
314
  # own frozen snapshot instead, never `runtime.registry` — so calling
307
315
  # order relative to this method's own mutation doesn't matter).
308
316
  #
309
- # THE ASSERTION IS ABOUT `state`/`memory`, NOT "did a dispatch fire"
310
- # — a leg whose own `from:`/`to:` are the SAME state (every existing
317
+ # The assertion is about `state`/`memory`, not "did a dispatch fire"
318
+ # — a leg whose own `from:`/`to:` are the same state (every existing
311
319
  # saga's own starts_on self-transition, `waybill.bluebook`'s own leg
312
- # 1/2) is EXPECTED to re-run on redelivery with no visible state
320
+ # 1/2) is expected to re-run on redelivery with no visible state
313
321
  # change at all; that is a property of the declared handler graph,
314
322
  # not a rehydration defect, and asserting against it here would
315
323
  # manufacture a false positive on every saga this corpus has. A
@@ -340,18 +348,18 @@ module Hecks
340
348
 
341
349
  # ── Rust-side self-consistency ───────────────────────────────────
342
350
  #
343
- # THE COMPILED BINARY'S OWN REHYDRATION DOOR, ALREADY SHIPPING
344
- # `kernel/cli.rs`'s `run` accepts an OPTIONAL top-level `"seed"` key
345
- # ("the exact 'Domain::Aggregate#id' -> state shape THIS run's own
346
- # 'instances' output already produces... lets a HOST seed prior
351
+ # The compiled binary's own rehydration door, already shipping
352
+ # `kernel/cli.rs`'s `run` accepts an optional top-level `"seed"` key
353
+ # ("the exact 'Domain::Aggregate#id' -> state shape this run's own
354
+ # 'instances' output already produces... lets a host seed prior
347
355
  # state back in instead of replaying `steps` from scratch every
348
356
  # invocation"). `Store::from_seed`/`Store::instances` are that
349
357
  # mechanism's own two halves — `rust/host` (docs/implemented/
350
358
  # decisions/0012) depends on them being true inverses for real,
351
- # today. `rust_seed_round_trip` exercises exactly that: a SECOND,
352
- # independent invocation of the SAME binary, `"steps": []` (nothing
359
+ # today. `rust_seed_round_trip` exercises exactly that: a second,
360
+ # independent invocation of the same binary, `"steps": []` (nothing
353
361
  # new dispatched — this is rehydration, not re-dispatch), seeded
354
- # with whatever `"instances"` a PRIOR invocation already produced.
362
+ # with whatever `"instances"` a prior invocation already produced.
355
363
  # `differ` (unused, kept in the signature — see below) is a
356
364
  # `RustConformanceHelpers`-including instance (`bin/qa_sweep`'s own
357
365
  # `Differ`), the same argument `check_rust_rehydration`/`check_
@@ -359,25 +367,25 @@ module Hecks
359
367
  # rather than dropped from all three signatures at once so a
360
368
  # future differential-style reduction has a door already open,
361
369
  # without this module ever `require`ing `spec/support/` itself.
362
- # NEITHER `strip_emitted_flags!` NOR ANY OTHER DIFFERENTIAL-ONLY
363
- # REDUCTION RUNS HERE — that reduction exists so a Rust-only
370
+ # Neither `strip_emitted_flags!` nor any other differential-only
371
+ # reduction runs here — that reduction exists so a Rust-only
364
372
  # bookkeeping field (`emitted_<event>`, docs/decisions/0049) never
365
373
  # counts against Ruby, which has no equivalent field to agree with
366
374
  # at all (`RustConformanceHelpers#strip_emitted_flags!`'s own
367
375
  # comment). This check has no Ruby side to spare — it is asking the
368
- # Rust binary whether it agrees with ITSELF, so `emitted_*` fields
376
+ # Rust binary whether it agrees with itself, so `emitted_*` fields
369
377
  # are exactly as real a fact to compare as any other. Stripping them
370
378
  # here (an earlier version of this method did) silently deleted
371
- # them from the RETURNED seed-round-trip result while leaving them
372
- # present on the ORIGINAL `seed_instances` a caller passes in — an
373
- # asymmetric comparison that reported EVERY record carrying one as
379
+ # them from the returned seed-round-trip result while leaving them
380
+ # present on the original `seed_instances` a caller passes in — an
381
+ # asymmetric comparison that reported every record carrying one as
374
382
  # a rehydration divergence, unconditionally, on every domain that
375
383
  # has one at all. Found live against `examples/banking`
376
384
  # (`Banking::Account`'s own `corrects` reaction) while this
377
385
  # integration was being written, by comparing this method's own
378
- # answer against the compiled binary's RAW stdout for the identical
386
+ # answer against the compiled binary's raw stdout for the identical
379
387
  # seed call: the raw round trip preserved `emitted_fee_applied`
380
- # correctly; only THIS method's own stripping dropped it. `spec/
388
+ # correctly; only this method's own stripping dropped it. `spec/
381
389
  # self_consistency_rust_spec.rb`'s own "banking" example pins the
382
390
  # regression against a real domain going forward.
383
391
  def rust_seed_round_trip(binary, _differ, seed_instances)
@@ -391,7 +399,7 @@ module Hecks
391
399
  parsed["instances"]
392
400
  end
393
401
 
394
- # CHECK 1, RUST SIDE — seeding a fresh invocation with a PRIOR
402
+ # Check 1, Rust side — seeding a fresh invocation with a prior
395
403
  # invocation's own live `"instances"` must reproduce that same
396
404
  # state, unchanged. `live_instances` is `rust_output["instances"]`
397
405
  # — the exact same value `bin/qa_sweep`'s own differential compare
@@ -403,8 +411,8 @@ module Hecks
403
411
  [{ field: "rust_rehydration", live: live_instances, rehydrated: rehydrated }]
404
412
  end
405
413
 
406
- # CHECK 2, RUST SIDE — seeding with what a first seed round trip
407
- # already produced, a SECOND time, must not drift any further. The
414
+ # Check 2, Rust side — seeding with what a first seed round trip
415
+ # already produced, a second time, must not drift any further. The
408
416
  # same "replay it again, byte for byte" claim `check_idempotency`
409
417
  # proves for Ruby, aimed at the one rehydration door this compiled
410
418
  # binary actually has.
@@ -418,7 +426,7 @@ module Hecks
418
426
 
419
427
  # ── shared plumbing ─────────────────────────────────────────────
420
428
 
421
- # EVERY [domain, aggregate] PAIR THIS SEQUENCE ACTUALLY WROTE TO
429
+ # Every [domain, aggregate] pair this sequence actually wrote to
422
430
  # an aggregate with an empty `#entries` never had anything dispatch
423
431
  # against it this run, so there is nothing to rehydrate and no
424
432
  # finding a "clean, nothing touched" report would mean anything
@@ -442,12 +450,12 @@ module Hecks
442
450
  repository.all.to_h { |record| [record.id.to_s, Runtime::Value.materialize(record.state)] }
443
451
  end
444
452
 
445
- # EVERY [domain, process manager] PAIR ANY LOADED BLUEBOOK DECLARES
453
+ # Every [domain, process manager] pair any loaded bluebook declares
446
454
  # regardless of whether this replay's own `history[:saga_instances]`
447
455
  # ever touched it (mirrors `each_touched_repository`'s own walk one
448
456
  # level up; the "did anything actually persist" filter lives in each
449
457
  # check's own caller, same as that method's `entries.empty?` guard).
450
- # An empty return here IS the "domain declares no process manager"
458
+ # An empty return here is the "domain declares no process manager"
451
459
  # skip `check_saga_rehydration`/`check_saga_idempotency` both need —
452
460
  # `filter_map`/`flat_map` over an empty Array already answers `[]`,
453
461
  # identical to "ran and found nothing," which is deliberate: neither
@@ -461,7 +469,7 @@ module Hecks
461
469
  found
462
470
  end
463
471
 
464
- # THE LIVE-SIDE GROUND TRUTH, key-shape-normalized (see `deep_
472
+ # The live-side ground truth, key-shape-normalized (see `deep_
465
473
  # stringify_keys`'s own comment) so it compares fairly against a
466
474
  # real Heki round trip's own shallow-symbolize convention.
467
475
  def normalize_saga_rows(persisted)
@@ -470,38 +478,38 @@ module Hecks
470
478
  end
471
479
  end
472
480
 
473
- # A FRESH `Adapters::Heki` AT THE SAME `tmp`/`domain` — unmemoized
481
+ # A fresh `Adapters::Heki` at the same `tmp`/`domain` — unmemoized
474
482
  # `@store`/`@saga_store`, so `#each_saga` is forced back through
475
483
  # `read_snapshot`/`replay_journal`, real bytes off real disk, not
476
484
  # whatever the writer that just wrote them still holds in its own
477
485
  # process memory (the same reason `fold!`, above, opens a second
478
486
  # `Adapters::Heki` instance rather than reading its own writer back).
479
487
  def cold_read_saga_rows(anchor, tmp, domain_name)
480
- reader = Adapters::Heki.new(aggregate: anchor, root: tmp, settings: { domain: domain_name })
488
+ reader = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
481
489
  reader.each_saga.with_object({}) do |(_pm, correlation, state, memory, _completed), rows|
482
490
  rows[correlation] = { state: state, memory: deep_stringify_keys(memory) }
483
491
  end
484
492
  end
485
493
 
486
- # `SagaStore#each_saga` ONLY EVER SYMBOLIZES `memory`'S OWN TOP-LEVEL
487
- # KEYS (`heki/saga_store.rb`'s own `each_saga`, one level deep) —
494
+ # `SagaStore#each_saga` only ever symbolizes `memory`'s own top-level
495
+ # keys (`heki/saga_store.rb`'s own `each_saga`, one level deep) —
488
496
  # `Registry::SagaPersistence#warn_stalled_saga` already documents
489
497
  # this exact asymmetry for the one reserved key production code
490
- # cares about (`SAGA_PENDING_DISPATCH_KEY`). A NESTED composite
498
+ # cares about (`SAGA_PENDING_DISPATCH_KEY`). A nested composite
491
499
  # memory field (any saga whose starting event carries a value
492
500
  # object, which is most of them — `waybill.bluebook`'s own
493
- # `ConsignmentRequested` alone has three) comes back with STRING
494
- # keys at every level BELOW the top, while `history[:saga_instances]`
495
- # 's own `Runtime::Value.materialize` call produces SYMBOL keys
501
+ # `ConsignmentRequested` alone has three) comes back with string
502
+ # keys at every level below the top, while `history[:saga_instances]`
503
+ # 's own `Runtime::Value.materialize` call produces symbol keys
496
504
  # throughout. That asymmetry is Heki's own documented, accepted
497
505
  # storage convention — an "opaque, adapter-agnostic JSON blob"
498
506
  # (`SagaInterpreter#checkpoint`'s own comment), never a typed
499
- # rebuild the way an AGGREGATE's own composite fields get on cold
507
+ # rebuild the way an aggregate's own composite fields get on cold
500
508
  # read (this file's own header: there is no VO schema to rebuild
501
509
  # against for a saga's memory blob at all) — not a rehydration
502
510
  # defect this check exists to find. Recursively re-stringifying
503
- # BOTH sides before comparing is what tells that KNOWN, accepted
504
- # shape difference apart from an ACTUAL data-loss bug (a dropped
511
+ # both sides before comparing is what tells that known, accepted
512
+ # shape difference apart from an actual data-loss bug (a dropped
505
513
  # key, a changed value, a missing field) — exactly the kind (b)'s
506
514
  # own seeded fixture in `spec/fuzzing/self_consistency_saga_spec.rb`
507
515
  # proves this still catches.
@@ -513,7 +521,7 @@ module Hecks
513
521
  end
514
522
  end
515
523
 
516
- # THE REAL, ALREADY-ANNOUNCED EVENT this correlation's CURRENT
524
+ # The real, already-announced event this correlation's current
517
525
  # checkpoint came from — walked back out of `runtime.registry.
518
526
  # saga_log`'s own `advanced: true` rows (newest first), skipping the
519
527
  # synthetic `REFUSED` trigger (`Runtime::SagaInterpreter::REFUSED`
@@ -530,7 +538,7 @@ module Hecks
530
538
  # payload field, a stamped passthrough, a self-identifying
531
539
  # `event.id`) here rather than reusing it would be exactly the
532
540
  # "hand-rolled approximation" this check exists to avoid; `send` on
533
- # an interpreter sharing this SAME `runtime`'s own registry is the
541
+ # an interpreter sharing this same `runtime`'s own registry is the
534
542
  # real thing, not a copy of it.
535
543
  def last_advancing_event(runtime, interpreter, process_manager, correlation)
536
544
  entry = runtime.registry.saga_log.reverse_each.find do |row|
@@ -544,20 +552,24 @@ module Hecks
544
552
  end
545
553
  end
546
554
 
547
- # ONE (process manager, correlation)'s OWN redelivery check — pulled
555
+ # One (process manager, correlation)'s own redelivery check — pulled
548
556
  # out of `check_saga_idempotency` itself so that method's own
549
557
  # `flat_map`/`filter_map` walk stays readable; every local this
550
558
  # shares with its caller (`interpreter`, `anchor`) is passed in
551
- # rather than re-derived.
559
+ # rather than re-derived. The `saga_log` mark/restore pair (BUG#39
560
+ # fix) belongs right where it guards `interpreter.advance`, not in
561
+ # a helper a reader would have to jump to just to see what is and
562
+ # isn't being restored around that one call.
552
563
  def check_one_saga_redelivery(runtime, interpreter, domain_name, process_manager, anchor,
553
564
  correlation, saga, redelivery)
565
+ # rubocop:disable-next Metrics/BlockLength
554
566
  Dir.mktmpdir("hecks-self-consistency-saga") do |tmp|
555
- writer = Adapters::Heki.new(aggregate: anchor, root: tmp, settings: { domain: domain_name })
567
+ writer = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
556
568
  writer.save_saga(process_manager: process_manager.name, correlation: correlation.to_s,
557
569
  state: saga[:state], memory: saga[:memory], completed_compensations: [])
558
570
 
559
- rehydrated = Adapters::Heki.new(aggregate: anchor, root: tmp, settings: { domain: domain_name })
560
- .each_saga.find { |_pm, corr, *| corr == correlation.to_s }
571
+ rehydrated = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
572
+ .each_saga.find { |_pm, corr, *| corr == correlation.to_s }
561
573
  next unless rehydrated
562
574
 
563
575
  _pm, _corr, state, memory, compensations = rehydrated
@@ -565,6 +577,17 @@ module Hecks
565
577
 
566
578
  saga_instances = runtime.registry.saga_instances[process_manager.name]
567
579
  original = saga_instances[correlation]
580
+
581
+ # `saga_log` is `runtime.sagas`/`history[:sagas]` itself, by
582
+ # reference (`Dispatcher#sagas` — `@registry.saga_log`).
583
+ # `interpreter.advance` below is a real `advance_saga` dispatch,
584
+ # which appends its own row to this same array unconditionally —
585
+ # a true fact about this probe, but an unrestored append leaks
586
+ # into the primary trace (BUG#39). Marked here, sliced back off
587
+ # in `ensure`, same restore-what-I-mutated idiom as
588
+ # `saga_instances[correlation]` below.
589
+ saga_log = runtime.registry.saga_log
590
+ saga_log_mark = saga_log.size
568
591
  begin
569
592
  saga_instances[correlation] = { state: state, memory: memory, completed_compensations: compensations || [] }
570
593
  interpreter.advance(redelivery, domain_name, only: process_manager)
@@ -576,6 +599,8 @@ module Hecks
576
599
  { field: "saga_redelivery_idempotency", domain: domain_name, process_manager: process_manager.name,
577
600
  correlation: correlation, on: redelivery.name, before: before, after: after_shape }
578
601
  ensure
602
+ saga_log.slice!(saga_log_mark..) if saga_log.size > saga_log_mark
603
+
579
604
  if original
580
605
  saga_instances[correlation] = original
581
606
  else
@@ -585,13 +610,13 @@ module Hecks
585
610
  end
586
611
  end
587
612
 
588
- # ONE FOLD OF `entries` INTO `writer` (a real, already-open `Heki`
589
- # adapter at `tmp`), THEN A COLD READ BACK through a BRAND NEW `Heki`
590
- # instance at the SAME path — `@store` on a fresh instance starts
613
+ # One fold of `entries` into `writer` (a real, already-open `Heki`
614
+ # adapter at `tmp`), then a cold read back through a brand new `Heki`
615
+ # instance at the same path — `@store` on a fresh instance starts
591
616
  # unmemoized, so `#all` below is forced through `#read` →
592
617
  # `#read_snapshot`/`#replay_journal`, real disk bytes, not whatever
593
618
  # `writer` still holds cached in its own process memory. Called
594
- # TWICE in a row against the SAME `writer` (see `check`/
619
+ # twice in a row against the same `writer` (see `check`/
595
620
  # `check_idempotency` above) is exactly "replay the same journal a
596
621
  # second time" — `writer` already holds everything the first fold
597
622
  # wrote, so a second fold re-applies the identical operations on
@@ -602,29 +627,33 @@ module Hecks
602
627
  writer.project(entry)
603
628
  end
604
629
 
605
- Adapters::Heki.new(aggregate: aggregate, root: tmp).all
606
- .to_h { |record| [record.id.to_s, Runtime::Value.materialize(record.state)] }
630
+ guarded_heki(aggregate: aggregate, root: tmp).all
631
+ .to_h do |record|
632
+ [
633
+ record.id.to_s, Runtime::Value.materialize(record.state)
634
+ ]
635
+ end
607
636
  end
608
637
 
609
- # RECURSES THROUGH A `Value`'S OWN FIELDS VIA `#[]`, NOT `#to_h` —
638
+ # Recurses through a `Value`'s own fields via `#[]`, not `#to_h` —
610
639
  # `#to_h` already materializes every field (`Value.materialize`),
611
640
  # which would hide a nested `Value` from this walk before it ever
612
- # got here. In practice a value object's OWN composite fields
641
+ # got here. In practice a value object's own composite fields
613
642
  # (`Coercion#normalize_composite_fields`) are validated but stored
614
643
  # as plain, already-materialized Hashes, not re-wrapped `Value`
615
644
  # instances — confirmed live, not assumed — so this recursion finds
616
- # nothing further past the field it started from FOR TODAY'S
645
+ # nothing further past the field it started from for today's
617
646
  # coercion pipeline specifically. Kept anyway, not dead code: the
618
647
  # generic `Hash`/`Array` branches below reach the exact same nested
619
648
  # data through `node[attribute.name]` regardless, and a future
620
- # change that DOES start wrapping composite fields as real `Value`
649
+ # change that does start wrapping composite fields as real `Value`
621
650
  # instances would be walked correctly here with no change needed.
622
- # `seen` is a `compare_by_identity` Hash: the same INSTANCE can legitimately
651
+ # `seen` is a `compare_by_identity` Hash: the same instance can legitimately
623
652
  # appear more than once (an aggregate's live state and an event
624
653
  # payload both reference the exact same frozen object), and
625
654
  # checking it twice would just waste time, never change the
626
655
  # answer — identity, not `Value#==`, is the right notion of
627
- # "already found" here (two DIFFERENT value objects that happen to
656
+ # "already found" here (two different value objects that happen to
628
657
  # hold equal fields are still two separate round trips to prove).
629
658
  def walk_value_objects(node, found, seen, aggregate)
630
659
  case node