hecks 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -1,11 +1,13 @@
1
- require_relative "errors"
2
- require_relative "refusal_wording"
3
-
4
1
  module Hecks
5
2
  module Runtime
6
3
  # The invocation address is not part of a command's domain payload.
7
4
  # Aggregate commands carry one receiver identity; entity commands carry
8
5
  # the aggregate receiver followed by one identity for every entity hop.
6
+ #
7
+ # Reading a call's shape — what `to:` and `with:` mean, the BUG#7/#17/#18
8
+ # rules — lives in `Runtime::Invocation.from_call` now (invocation.rb).
9
+ # `envelope`/`payload` are kept as thin delegators for any caller still
10
+ # naming them; the dispatcher itself builds an Invocation instead.
9
11
  module Routing
10
12
  Envelope = Struct.new(:aggregate, :entities, keyword_init: true) do
11
13
  def initialize(aggregate:, entities: [])
@@ -16,94 +18,14 @@ module Hecks
16
18
 
17
19
  module_function
18
20
 
19
- def envelope(to, entity_depth: 0)
20
- return nil if to.nil?
21
-
22
- aggregate, entities = to.is_a?(Hash) ? parse_envelope_hash(to) : [to, []]
23
-
24
- raise TypeMismatch, "to: must name the receiving aggregate identity" if aggregate.nil? || aggregate.to_s.empty?
25
- if entities.size != entity_depth
26
- raise TypeMismatch,
27
- "to: for an entity command needs #{entity_depth} entity " \
28
- "#{entity_depth == 1 ? 'identity' : 'identities'} after the aggregate — got #{entities.size}"
29
- end
30
- raise TypeMismatch, "to: contains a blank entity identity" if entities.any? do |identity|
31
- identity.nil? || identity.to_s.empty?
32
- end
21
+ def envelope(to, entity_depth: 0) = Invocation.route(to, entity_depth: entity_depth)
33
22
 
34
- Envelope.new(aggregate: aggregate, entities: entities)
35
- end
36
-
37
- # The Hash-shaped half of `to:` — pulled out of `envelope` because
38
- # it is a self-contained parse (raises on an unrecognized key, then
39
- # returns the pair) with no dependency on anything `envelope` does
40
- # afterward; the validations that follow apply the same way
41
- # whichever branch produced `aggregate`/`entities`.
42
- def parse_envelope_hash(to)
43
- hash = to.transform_keys(&:to_sym)
44
- unknown = hash.keys - %i[aggregate entity entities]
45
- raise TypeMismatch, "to: does not recognize #{unknown.sort.join(', ')}" unless unknown.empty?
46
-
47
- [hash[:aggregate], entity_identities(hash)]
48
- end
49
- private_class_method :parse_envelope_hash
50
-
51
- # `with:` is deliberately strict. Compatibility-only calls still pass
52
- # loose keyword arguments through the old addressing gate, but a caller
53
- # choosing the explicit envelope cannot smuggle receiver identity back
54
- # into the payload.
23
+ # Loaded by invocation.rb (which requires this file), so `Invocation`
24
+ # is always defined by the time either method runs.
55
25
  def payload(command, with:, legacy:)
56
- if with && !legacy.empty?
57
- raise TypeMismatch,
58
- "dispatch takes command facts in with:, not both with: and loose keyword arguments"
59
- end
60
-
61
- return legacy unless with
62
- raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
63
-
64
- offered = with.transform_keys(&:to_sym)
65
- declared = command.attributes.map { |attribute| attribute.name.to_sym }
66
- refuse_unknown_facts!(command, offered, declared)
67
- refuse_absent_facts!(command, offered, declared)
68
- offered
69
- end
70
-
71
- # The two `with:` shape checks `payload` runs in sequence — pulled
72
- # out because each is a self-contained "compute a difference, raise
73
- # if non-empty" rule with no dependency on the other. Order stays
74
- # unknown-before-absent, exactly as inline: `payload` calls them in
75
- # that order, so a fact that is both unknown AND leaves something
76
- # else absent still raises UnknownArgument first, same as before.
77
- def refuse_unknown_facts!(command, offered, declared)
78
- unknown = (offered.keys - declared).sort
79
- return if unknown.empty?
80
-
81
- reading = declared.empty? ? "none" : declared.join(", ")
82
- raise UnknownArgument,
83
- RefusalWording.render("UnknownArgument", "unknown_args",
84
- command: command.hecks_name, unknown: unknown.join(", "),
85
- declared: reading)
86
- end
87
- private_class_method :refuse_unknown_facts!
88
-
89
- def refuse_absent_facts!(command, offered, declared)
90
- absent = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - offered.keys
91
- return if absent.empty?
92
-
93
- reading = declared.empty? ? "none" : declared.join(", ")
94
- raise AbsentArgument,
95
- RefusalWording.render("AbsentArgument", "absent_args",
96
- command: command.hecks_name, absent: absent.sort.join(", "),
97
- declared: reading)
98
- end
99
- private_class_method :refuse_absent_facts!
100
-
101
- def entity_identities(hash)
102
- raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
103
-
104
- hash.key?(:entities) ? Array(hash[:entities]) : Array(hash[:entity])
26
+ facts = Invocation.facts_for(command, with: with, legacy: legacy)
27
+ Invocation.new(verb: nil, target: nil, facts: facts).to_args
105
28
  end
106
- private_class_method :entity_identities
107
29
  end
108
30
  end
109
31
  end
@@ -3,27 +3,27 @@ require_relative "../value"
3
3
  module Hecks
4
4
  module Runtime
5
5
  class SagaInterpreter
6
- # How a saga decides WHICH CONVERSATION an event belongs to — three
6
+ # How a saga decides which conversation an event belongs to — three
7
7
  # tiers, each one a lesson.
8
8
  module Correlation
9
9
  private
10
10
 
11
- # A DOTTED PATH NAMES THE SCALAR FIELD, rather than asking a value object
11
+ # A dotted path names the scalar field, rather than asking a value object
12
12
  # to stand in for one. `correlates_by :end_to_end` would key a saga on
13
13
  # the whole ExternalTransfer::EndToEndReference — and what a non-scalar
14
- # correlation key even IS is representation-dependent (the object
14
+ # correlation key even is is representation-dependent (the object
15
15
  # itself? its serialised text?). `:"end_to_end.value"` reads the one
16
16
  # field with a single unambiguous rendering.
17
17
  def saga_correlation(process_manager, event)
18
18
  path = process_manager.correlates_by.to_s.split(".")
19
- # A LATER EVENT MAY ALREADY HOLD THE SCALAR. `reference.value` digs a
20
- # value object's field out of a FRESH declaration (TransferRequested's
21
- # `reference` IS a TransferReference) — but a downstream event this
19
+ # A later event may already hold the scalar. `reference.value` digs a
20
+ # value object's field out of a fresh declaration (TransferRequested's
21
+ # `reference` is a TransferReference) — but a downstream event this
22
22
  # same value was smuggled through as a passthrough argument
23
23
  # (AccountDebited's `reference:`, resolved by `dispatch_args` to the
24
24
  # bare correlation string) carries it as a scalar already, with
25
25
  # nothing left to dig. `"xfer-1".respond_to?(:[])` is true — String
26
- # has its OWN `[]` (substring indexing) — so checking for keyed
26
+ # has its own `[]` (substring indexing) — so checking for keyed
27
27
  # lookup explicitly, rather than "responds to `[]` at all", is what
28
28
  # stops the second segment from being read as a symbol index into a
29
29
  # string that has already arrived.
@@ -32,9 +32,9 @@ module Hecks
32
32
  end
33
33
  return value unless value.to_s.empty?
34
34
 
35
- # THE STAMP — `deliver_saga_dispatch` marks its own event before this
35
+ # **The stamp** — `deliver_saga_dispatch` marks its own event before this
36
36
  # saga's next step ever asks, for a leg whose command declares
37
- # NEITHER the correlation field itself nor the emitting aggregate's
37
+ # neither the correlation field itself nor the emitting aggregate's
38
38
  # own reference key (the two tiers above). command_interpreter/
39
39
  # argument_gate.rb names the old payload-only lookup "the weakest
40
40
  # part of the gate" : a correlation key arriving on a command only
@@ -48,17 +48,17 @@ module Hecks
48
48
  stamped = event.correlation && event.correlation[process_manager.correlation_head.to_s]
49
49
  return stamped unless stamped.nil? || stamped.to_s.empty?
50
50
 
51
- # A SELF-REFERENCING LEG carries the correlation forward under its
51
+ # A self-referencing leg carries the correlation forward under its
52
52
  # own emitting record's identity — `event.id`, not a field dug back
53
53
  # out of the payload. This used to read `event.payload[own_key]`
54
54
  # (`own_key` the aggregate's own reference-key convention, "wire",
55
- # "transfer"), which only ever held a value because LEGACY dispatch
55
+ # "transfer"), which only ever held a value because legacy dispatch
56
56
  # left the self-addressing key riding along in the payload
57
57
  # unfiltered. Routing separated from payload (`to:`/`with:`, the
58
58
  # facade's own `Handle#run` always uses it) closed exactly that
59
59
  # leak — correctly, since an addressing key is not a fact the
60
60
  # payload should carry — which left this tier reading an empty
61
- # Hash for any self-referencing leg with no OTHER declared
61
+ # Hash for any self-referencing leg with no other declared
62
62
  # attributes (`OnboardingCase.Clear`, `.Decline` — no `attribute`
63
63
  # lines at all): the saga silently stopped advancing, forever, for
64
64
  # exactly the leg this tier exists to correlate.
@@ -68,14 +68,14 @@ module Hecks
68
68
  # and unlike a payload dig it is populated by the record itself,
69
69
  # not by which dispatch convention the caller happened to use.
70
70
  #
71
- # GATED, still — a manually-dispatched command on a wholly
72
- # UNRELATED aggregate can share an event NAME this process_manager happens to
71
+ # Gated, still — a manually-dispatched command on a wholly
72
+ # unrelated aggregate can share an event name this process_manager happens to
73
73
  # handle (`Drawer.Take` also emits "Taken", the same name a
74
- # SAGA-DISPATCHED leg uses) with nothing this saga should read as
74
+ # saga-dispatched leg uses) with nothing this saga should read as
75
75
  # its own conversation. What makes a leg genuinely
76
76
  # self-referencing — the one fact worth trusting `event.id`
77
- # for — is that `correlates_by`'s own head field IS this event's
78
- # OWN aggregate's declared identity, not merely a same-shaped
77
+ # for — is that `correlates_by`'s own head field is this event's
78
+ # own aggregate's declared identity, not merely a same-shaped
79
79
  # name: `OnboardingCase.identity_heads` really does include
80
80
  # `:reference`, `correlates_by :"reference.value"`'s own head ;
81
81
  # `Drawer.identity_heads` is `[:number]`, nowhere close.
@@ -49,26 +49,26 @@ module Hecks
49
49
 
50
50
  private
51
51
 
52
- # THE CHECKPOINT WRITE, shared by every mutation site below —
53
- # holds `saga_mutex` across BOTH the in-memory Hash mutation and
52
+ # The checkpoint write, shared by every mutation site below —
53
+ # holds `saga_mutex` across both the in-memory Hash mutation and
54
54
  # the persistence write (§7), not just the Hash mutation alone:
55
- # two threads racing the SAME (process_manager, correlation) key
55
+ # two threads racing the same (process_manager, correlation) key
56
56
  # could otherwise interleave their writes out of order, silently
57
57
  # reordering a saga's own transition history — worse for the
58
58
  # adapters with no locking of their own (Heki) than for Postgres.
59
59
  # `deep_copy` guards against the exact shape of bug PR #175 itself
60
60
  # already found once (over-freezing a live, still-mutated Hash) —
61
- # never hand a persistence adapter the SAME object `advance_saga`/
61
+ # never hand a persistence adapter the same object `advance_saga`/
62
62
  # `unwind` go on to mutate in place; round-tripping through JSON
63
63
  # is also what guarantees the value is safe for every adapter that
64
64
  # itself calls `JSON.generate` on it.
65
65
  # `pending:` — see saga_pending_dispatch.rb. Injected into the
66
- # WRITTEN copy of memory only, never into `instance[:memory]`
66
+ # written copy of memory only, never into `instance[:memory]`
67
67
  # itself: every other reader of a live instance's memory
68
68
  # (`dispatch_args`'s "opening event memory" scope, the fuzzer's
69
69
  # own round-trip/shape checks, `saga_spec.rb`'s exact-equality
70
70
  # assertion against a fresh instance's seeded memory) sees exactly
71
- # what it always did. The marker exists ONLY in the persisted
71
+ # what it always did. The marker exists only in the persisted
72
72
  # blob, and only for as long as a dispatch cascade is genuinely
73
73
  # in flight for this instance.
74
74
  def checkpoint(process_manager, correlation, instance, domain, pending: nil)
@@ -82,8 +82,8 @@ module Hecks
82
82
  end
83
83
 
84
84
  # `deep_copy` is `JSON.parse(JSON.generate(hash), ...)`, which
85
- # only accepts an OBJECT at the top level — `completed_compensations`
86
- # is an ARRAY, so it gets its own wrap-and-unwrap rather than a
85
+ # only accepts an object at the top level — `completed_compensations`
86
+ # is an array, so it gets its own wrap-and-unwrap rather than a
87
87
  # second, parallel `deep_copy_array` reimplementing the same
88
88
  # round-trip. `|| []` — an instance from before this field existed
89
89
  # (or one that has never completed a compensable leg) rehydrates
@@ -105,15 +105,15 @@ module Hecks
105
105
  created = @registry.saga_mutex.synchronize do
106
106
  next false if @registry.saga_instances[process_manager.name].key?(correlation)
107
107
 
108
- # `.dup`, NOT THE SAME OBJECT — a fresh saga's own memory starts
109
- # as a COPY of the starting event's own payload, never the
108
+ # `.dup`, not the same object — a fresh saga's own memory starts
109
+ # as a copy of the starting event's own payload, never the
110
110
  # payload itself. A saga's own memory is meant to be written
111
111
  # into over its lifetime (remember-style, growing beyond what
112
112
  # the starting event carried) ; the payload it was seeded from
113
- # is a fact about something that ALREADY happened, logged and
113
+ # is a fact about something that already happened, logged and
114
114
  # emitted before the saga ever saw it. Sharing the one Hash
115
115
  # object between them means a write into the saga's own memory
116
- # is silently ALSO a write into an already-emitted event's own
116
+ # is silently also a write into an already-emitted event's own
117
117
  # payload — retroactively adding a field nothing announced.
118
118
  # `event.payload` is deep-frozen by `Event#emit!` by the time
119
119
  # this runs, so a naive in-place write here would raise
@@ -132,11 +132,11 @@ module Hecks
132
132
  instance: correlation, born: true, state: process_manager.states.first }
133
133
  end
134
134
 
135
- # THE MUTEX COVERS ONLY THE STATE-CHECK-AND-MUTATE-AND-CHECKPOINT
136
- # STEP, never the dispatch cascade that follows — `deliver_saga_
135
+ # The mutex covers only the state-check-and-mutate-and-checkpoint
136
+ # step, never the dispatch cascade that follows — `deliver_saga_
137
137
  # dispatch` calls `@door.reenter`, which can recursively re-enter
138
- # THIS SAME interpreter (a saga's own leg triggering another saga,
139
- # or itself again) on the SAME thread, and `Mutex` is not
138
+ # this same interpreter (a saga's own leg triggering another saga,
139
+ # or itself again) on the same thread, and `Mutex` is not
140
140
  # reentrant: holding it across that call would deadlock the
141
141
  # thread against itself the moment any real chain did that.
142
142
  def advance_saga(process_manager, event, domain)
@@ -156,7 +156,7 @@ module Hecks
156
156
  @registry.saga_log << record.merge(advanced: false, reason: "no conversation remembers #{correlation.inspect}")
157
157
  next false
158
158
  end
159
- # THE LEG IS CHOSEN BY (EVENT, CURRENT STATE) — C10.3. Read
159
+ # The leg is chosen by (event, current state) — C10.3. Read
160
160
  # under the mutex, against the state this instance holds right
161
161
  # now, so two legs on the same event from different states each
162
162
  # answer exactly when their own state is current.
@@ -189,7 +189,7 @@ module Hecks
189
189
  { on: event.name, from: from_state, to: to_state, dispatches: handler.dispatches.map(&:command_name) }
190
190
  end
191
191
 
192
- # THE SHARED TAIL of `advance_saga` and `unwind` — both are "guard,
192
+ # The shared tail of `advance_saga` and `unwind` — both are "guard,
193
193
  # mutate, checkpoint-with-pending" under the mutex (kept separate
194
194
  # per caller: `advance_saga`'s own guard also has to handle "no
195
195
  # instance at all", `unwind`'s doesn't), then this: log the real
@@ -197,11 +197,11 @@ module Hecks
197
197
  # pending marker once that cascade — however it ended — is done.
198
198
  def settle_transition(process_manager, event, handler, instance, correlation, domain, record, pre_state,
199
199
  drain_compensations: false)
200
- # `from:`/`to:` are the INSTANCE'S OWN real pre/post state — read
200
+ # `from:`/`to:` are the instance's own real pre/post state — read
201
201
  # back from `instance` itself, never re-derived from `handler.
202
202
  # from_state`/`handler.to_state` a second time. `Properties.saga_
203
203
  # advances_follow_declared_handlers` (fuzzing/properties.rb) builds
204
- # its OWN "declared edges" list from this SAME handler object (via
204
+ # its own "declared edges" list from this same handler object (via
205
205
  # `process_manager.handlers`), so a log entry that just echoed `handler.
206
206
  # from_state`/`handler.to_state` back could never disagree with
207
207
  # that list no matter what the runtime actually did — the entry
@@ -213,10 +213,10 @@ module Hecks
213
213
  # real mismatch instead of vanishing into a tautology.
214
214
  @registry.saga_log << record.merge(advanced: true, from: pre_state, to: instance[:state])
215
215
 
216
- # DERIVED COMPENSATION FIRST, NEWEST-FIRST — only for `unwind`'s
217
- # own call (`drain_compensations: true`): every leg THIS INSTANCE
216
+ # **Derived compensation first, newest-first** — only for `unwind`'s
217
+ # own call (`drain_compensations: true`): every leg this instance
218
218
  # actually completed that declared its own `compensates`, popped
219
- # and dispatched in reverse completion order, BEFORE any
219
+ # and dispatched in reverse completion order, before any
220
220
  # hand-written `on :refused` dispatches below — coexistence, not
221
221
  # replacement. Drained (not just read) as it fires: a saga's own
222
222
  # `on :refused` handler is guarded against re-entry by `unwind`'s
@@ -234,19 +234,19 @@ module Hecks
234
234
  deliver_saga_dispatch(process_manager, spec, event, instance, correlation, domain)
235
235
  end
236
236
 
237
- # THE CLEAR — guarded by the SAME identity check `end_saga`'s own
237
+ # **The clear** — guarded by the same identity check `end_saga`'s own
238
238
  # `.delete` return value implies: `deliver_saga_dispatch`'s
239
- # `@door.reenter` can synchronously trigger this SAME correlation's
239
+ # `@door.reenter` can synchronously trigger this same correlation's
240
240
  # `ends_on` event as a nested reaction (a leg's own dispatch is
241
241
  # what makes the saga's terminal event fire), which deletes this
242
242
  # row from the store before this line ever runs. Writing the
243
- # clear unconditionally would RESURRECT a legitimately-ended saga
243
+ # clear unconditionally would resurrect a legitimately-ended saga
244
244
  # — this diff's own first attempt did exactly that, caught by
245
245
  # `saga_durability_spec.rb`'s "deletes the checkpoint once a saga
246
246
  # genuinely ends" — so this only re-checkpoints when `instance`
247
- # is still THE SAME object `@saga_instances` holds for this
247
+ # is still the same object `@saga_instances` holds for this
248
248
  # correlation (`.equal?`, not `==`: a fresh saga reborn under the
249
- # same correlation between then and now is a DIFFERENT instance,
249
+ # same correlation between then and now is a different instance,
250
250
  # and writing this stale one's state onto that one's row would be
251
251
  # its own corruption). Under the mutex — dispatching is over by
252
252
  # now, so this is not the reentrancy hazard `advance_saga`'s own
@@ -264,9 +264,9 @@ module Hecks
264
264
  args = dispatch_args(process_manager, spec, event, instance, correlation)
265
265
  record = { process_manager: process_manager.name, instance: correlation, dispatch: spec.command_name }
266
266
 
267
- # THE RAW INPUTS `args` WAS RESOLVED FROM, captured alongside the
267
+ # The raw inputs `args` was resolved from, captured alongside the
268
268
  # result — never re-derived from history[:saga_instances] later
269
- # (that only ever holds the FINAL memory, after every step has
269
+ # (that only ever holds the final memory, after every step has
270
270
  # run; this dispatch's own memory, at the moment it actually
271
271
  # fired, is a different fact for a saga whose memory keeps
272
272
  # changing). `spec.with_spec.empty?` skipped: nothing declared
@@ -282,7 +282,7 @@ module Hecks
282
282
  end
283
283
 
284
284
  if @door.reaction_depth_reached?
285
- # THE CEILING IS NOT A DOMAIN DECISION EITHER — same reasoning as a
285
+ # The ceiling is not a domain decision either — same reasoning as a
286
286
  # crash, below — but unlike a crash there is nothing ambiguous
287
287
  # about it: the leg unambiguously did not run, so it unwinds
288
288
  # exactly like a refusal instead of stranding the instance for a
@@ -301,18 +301,18 @@ module Hecks
301
301
  attempt = 0
302
302
  compensation_recorded = false
303
303
  begin
304
- # RECORDED BEFORE DISPATCHING, not after `@door.reenter`
305
- # returns — `@door.reenter` can recursively RE-ENTER THIS SAME
306
- # saga interpreter (the event THIS dispatch emits triggers a
307
- # LATER handler, which can itself refuse and unwind) entirely
308
- # WITHIN this one call, before it ever returns here. Recording
309
- # "after reenter succeeds" would be too late for a NESTED
304
+ # Recorded before dispatching, not after `@door.reenter`
305
+ # returns — `@door.reenter` can recursively re-enter this same
306
+ # saga interpreter (the event this dispatch emits triggers a
307
+ # later handler, which can itself refuse and unwind) entirely
308
+ # within this one call, before it ever returns here. Recording
309
+ # "after reenter succeeds" would be too late for a nested
310
310
  # refusal to ever see this leg's own compensation — found
311
311
  # live: Settlement's own AccountDebited handler refuses
312
- # Account.Credit and unwinds from INSIDE Account.Debit's own
312
+ # Account.Credit and unwinds from inside Account.Debit's own
313
313
  # `reenter` call, so "delivered: true, then record" left the
314
314
  # ledger empty at the exact moment it was needed. Popped back
315
- # off in the rescues below if THIS leg's own attempt is the
315
+ # off in the rescues below if this leg's own attempt is the
316
316
  # one that failed — never left recorded for a refusal that
317
317
  # was never this leg's own to compensate.
318
318
  if spec.compensates && !compensation_recorded
@@ -336,7 +336,7 @@ module Hecks
336
336
  rescue *DOMAIN_REFUSALS => e
337
337
  unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
338
338
  # Same rule as the policy interpreter : a refusal by the target is
339
- # a recorded outcome, and the leg that raised it UNWINDS — see
339
+ # a recorded outcome, and the leg that raised it unwinds — see
340
340
  # `unwind`'s own comment for why the procedure runs its
341
341
  # compensation here rather than leaving the money (or whatever
342
342
  # else a leg moved) sitting out.
@@ -345,13 +345,13 @@ module Hecks
345
345
  rescue StandardError => e
346
346
  unrecord_compensation(instance, correlation, domain, process_manager) if compensation_recorded
347
347
  compensation_recorded = false
348
- # A DEFECT, not a refusal — see PolicyInterpreter#deliver's own
348
+ # A defect, not a refusal — see PolicyInterpreter#deliver's own
349
349
  # comment for the full reasoning: the same DOMAIN_REFUSALS split,
350
350
  # and the same "the triggering command already succeeded and
351
351
  # persisted by the time this runs" fact that makes catching it
352
352
  # here safe rather than reckless.
353
353
  #
354
- # UNLIKE a refusal, a crash is not a decision the domain made, so
354
+ # Unlike a refusal, a crash is not a decision the domain made, so
355
355
  # it does not unwind on the first failure — MAX_DEFECT_RETRIES
356
356
  # gives a transient failure (a DB timeout, a race, a cold start)
357
357
  # a chance to clear on its own, retrying the identical dispatch,
@@ -381,36 +381,36 @@ module Hecks
381
381
  end
382
382
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
383
383
 
384
- # THE ROLLBACK HALF of `deliver_saga_dispatch`'s own speculative
384
+ # The rollback half of `deliver_saga_dispatch`'s own speculative
385
385
  # pre-record (that method's own comment for why it has to be
386
- # speculative) — THIS leg's own attempt is the one that failed,
386
+ # speculative) — this leg's own attempt is the one that failed,
387
387
  # so whatever was just pushed for it was never actually earned.
388
388
  # `.pop`, not a search-and-delete: nothing else can have pushed
389
- # AFTER this leg's own entry without this leg's own `@door.
389
+ # after this leg's own entry without this leg's own `@door.
390
390
  # reenter` call having already returned (the recursive re-entry
391
- # this whole mechanism exists for only ever runs BETWEEN this
391
+ # this whole mechanism exists for only ever runs between this
392
392
  # push and this leg's own return, and a nested refusal that
393
393
  # consumed it already popped it itself — this rollback only ever
394
- # runs for THIS leg's own, still-present entry).
394
+ # runs for this leg's own, still-present entry).
395
395
  def unrecord_compensation(instance, correlation, domain, process_manager)
396
396
  instance[:completed_compensations].pop
397
397
  checkpoint(process_manager, correlation, instance, domain)
398
398
  end
399
399
 
400
- # A refused leg UNWINDS — the procedure runs the leg declared `on :refused`,
400
+ # A refused leg unwinds — the procedure runs the leg declared `on :refused`,
401
401
  # which is where the compensation lives. So does a leg that hit the
402
402
  # reaction-depth ceiling, and so does a leg that crashed and stayed
403
403
  # crashing through MAX_DEFECT_RETRIES — see `deliver_saga_dispatch`'s own
404
404
  # comments for why each of those is safe to route here.
405
405
  #
406
- # Until this existed a refusal was RECORDED and nothing else happened. The
406
+ # Until this existed a refusal was recorded and nothing else happened. The
407
407
  # wire's thousand was taken from the source, refused by the destination, and
408
408
  # sat nowhere until a human dispatched the reversal by hand ; banking's
409
409
  # settlement left a debit standing with no credit and no compensation at all.
410
410
  # Both bluebooks had written the compensating leg. Nothing armed it.
411
411
  #
412
- # A compensation that is itself refused does NOT unwind again, and needs no
413
- # flag to stop it: the state moves to the compensating leg's to_state BEFORE
412
+ # A compensation that is itself refused does not unwind again, and needs no
413
+ # flag to stop it: the state moves to the compensating leg's to_state before
414
414
  # its dispatches run, so a second refusal finds the instance no longer in
415
415
  # from_state and records that instead. The check is the guard.
416
416
  def unwind(process_manager, event, instance, correlation, domain)
@@ -442,7 +442,7 @@ module Hecks
442
442
 
443
443
  # See `settle_transition`'s own comment on `pre_state`/
444
444
  # `instance[:state]` — the real observed transition, not a
445
- # second read of the SAME handler object `Properties.saga_
445
+ # second read of the same handler object `Properties.saga_
446
446
  # advances_follow_declared_handlers` checks this log against.
447
447
  # `drain_compensations: true` — only `unwind`'s own call site
448
448
  # fires derived compensation; `advance_saga`'s own call never
@@ -451,16 +451,16 @@ module Hecks
451
451
  drain_compensations: true)
452
452
  end
453
453
 
454
- # A DERIVED COMPENSATION — `entry[:args]` is already resolved
454
+ # A derived compensation — `entry[:args]` is already resolved
455
455
  # (`record_completed_compensation`'s own comment for why), so this
456
456
  # skips `dispatch_args` entirely and goes straight to delivery,
457
- # through the SAME retry-on-defect path an ordinary forward leg
457
+ # through the same retry-on-defect path an ordinary forward leg
458
458
  # uses. Never re-enters `unwind` on its own failure — a
459
459
  # compensation that itself refuses is a real, pre-existing gap
460
460
  # this feature makes visible rather than closes (see this file's
461
461
  # own class-level notes); `compensation_failed: true` tags it
462
462
  # distinctly in the log instead of recording it identically to an
463
- # ordinary failed delivery, and every OTHER completed compensation
463
+ # ordinary failed delivery, and every other completed compensation
464
464
  # still queued still gets its own attempt.
465
465
  def deliver_derived_compensation(process_manager, entry, correlation, domain)
466
466
  record = { process_manager: process_manager.name, instance: correlation, dispatch: entry[:command_name] }
@@ -506,8 +506,45 @@ module Hecks
506
506
  )
507
507
  end
508
508
 
509
+ # BUG#6 — unconditionally the saga's own home domain, never inferred
510
+ # from `command_name`'s own shape. This used to guess: a leftover
511
+ # `::` after `Naming.command_ref`'s own rewrite was read as "already
512
+ # domain-qualified" and left alone. That heuristic cannot actually
513
+ # tell a genuinely cross-domain reference (`Banking::Account::
514
+ # Debit` -> one `::` survives) apart from a same-domain entity
515
+ # command reference (`Manifest::Slot::Fill` -> one `::` survives
516
+ # too, for an unrelated reason — entity nesting, not a domain
517
+ # qualifier) — both collapse to the identical "one `::` left" shape,
518
+ # and the string alone carries no further signal to split them
519
+ # (confirmed against `Naming.command_ref`'s own rewrite: it only
520
+ # ever strips the last `::`, so the count of what remains is blind
521
+ # to why it's there). Picking the cross-domain reading unconditionally
522
+ # left `qa/stress_domains/waybill`'s own `Packing` saga dispatching
523
+ # `Manifest::Slot::Fill` — an entity command in its own domain —
524
+ # unprefixed, so `Naming.split_verb` read "Manifest" as a domain
525
+ # name instead of this chapter's own aggregate, and the dispatch
526
+ # failed with `UnknownVerb`, silently recorded as an ordinary
527
+ # domain refusal rather than surfacing as the real bug it is.
528
+ #
529
+ # The fix mirrors `PolicyInterpreter#deliver`'s own mechanism,
530
+ # which never had this bug: a policy's cross-domain target is a
531
+ # separate, explicit field (`Policy#target_domain`, set only by the
532
+ # `across` keyword) — `deliver` unconditionally builds
533
+ # `"#{policy.target_domain || domain}::#{policy.trigger_command}"`,
534
+ # never asking whether `trigger_command` looks already-qualified.
535
+ # A saga's own `dispatch`/`compensates` has no such explicit field
536
+ # and no keyword to set one — and, confirmed against the entire
537
+ # corpus (banking's Onboarding/Settlement/ExternalSettlement,
538
+ # quality_control's BugCiWatch, and this domain's own Packing),
539
+ # no saga anywhere ever dispatches genuinely cross-domain: "every
540
+ # command a saga fires lands inside its own bluebook chapter"
541
+ # (`Projections::Diagrams#saga_diagram`'s own comment, written
542
+ # independently of this fix and still true). So the home domain is
543
+ # the only explicit context a saga dispatch ever has — this applies
544
+ # it the same way `deliver` applies its own default (no `across`)
545
+ # case, without inventing a keyword nothing in the corpus needs.
509
546
  def qualified(command_name, domain)
510
- command_name.include?("::") ? command_name : "#{domain}::#{command_name}"
547
+ "#{domain}::#{command_name}"
511
548
  end
512
549
 
513
550
  def end_saga(process_manager, event, domain)
@@ -1,13 +1,13 @@
1
1
  module Hecks
2
2
  module Runtime
3
- # THE ONE SHARED CONSTANT between `SagaInterpreter` (the writer) and
3
+ # The one shared constant between `SagaInterpreter` (the writer) and
4
4
  # `Registry::SagaPersistence#rehydrate_sagas!` (the reader) for a
5
5
  # scoped, minimal answer to the saga-durability review's item 8 (a
6
6
  # durable outbox): a marker that survives exactly the window a crash
7
7
  # in `advance_saga`/`unwind` can otherwise hide.
8
8
  #
9
- # THE PROBLEM THIS CLOSES — `checkpoint` persists a saga's new state
10
- # BEFORE the leg that justifies it (`handler.dispatches`) runs, and
9
+ # **The problem this closes** — `checkpoint` persists a saga's new state
10
+ # before the leg that justifies it (`handler.dispatches`) runs, and
11
11
  # deliberately so: the mutex it holds is not reentrant, and a
12
12
  # dispatch can re-enter this same interpreter. If the process dies
13
13
  # in that window, the store says the transition happened and there
@@ -15,33 +15,33 @@ module Hecks
15
15
  # domain never got asked), not a defect (nothing raised), just
16
16
  # silence indistinguishable from a leg that finished cleanly.
17
17
  #
18
- # THE FIX — `checkpoint` now writes this key into the SAME already-
18
+ # **The fix** — `checkpoint` now writes this key into the same already-
19
19
  # durable `memory` blob (no new column, no adapter/schema change:
20
20
  # `memory` is already an opaque, adapter-agnostic JSON blob every
21
21
  # `save_saga` implementation round-trips verbatim) whenever it
22
- # checkpoints a state a dispatch cascade hasn't run for YET, and
22
+ # checkpoints a state a dispatch cascade hasn't run for yet, and
23
23
  # clears it (a second checkpoint, `pending: nil`) once that cascade
24
24
  # — success, refusal-compensated, defect-compensated, or ceiling-
25
25
  # compensated — has actually run. A crash between those two writes
26
26
  # leaves the marker standing; `rehydrate_sagas!` strips it back out
27
- # of the LIVE instance's own `:memory` (so no dispatch/`given`/
27
+ # of the live instance's own `:memory` (so no dispatch/`given`/
28
28
  # fuzzer/doc consumer of a saga's memory ever sees this key — it
29
29
  # exists only in the persisted blob) and surfaces it loudly instead.
30
30
  #
31
- # WHAT THIS DELIBERATELY DOES NOT DO — auto-redrive the pending leg.
31
+ # **What this deliberately does not do** — auto-redrive the pending leg.
32
32
  # Redelivering a dispatch whose outcome is genuinely unknown is only
33
33
  # safe with idempotent delivery (the downstream command recognizing
34
34
  # and no-op'ing a duplicate), which hecks's command/event pipeline
35
35
  # has no mechanism for today. Blindly re-dispatching without that is
36
- # how a stalled transfer becomes a DOUBLE-CREDITED one — a strictly
36
+ # how a stalled transfer becomes a double-credited one — a strictly
37
37
  # worse defect than the stall it would replace. So this is real,
38
- # durable, crash-surviving VISIBILITY into exactly what a stalled
38
+ # durable, crash-surviving visibility into exactly what a stalled
39
39
  # saga was doing when the process died — the missing half of "no
40
40
  # reconciliation pass exists". The full pending → claimed →
41
41
  # delivered outbox now exists too (`Runtime::Outbox`, ADR 0053) and
42
- # is COMPLEMENTARY, not a replacement: the outbox row names the
43
- # EVENT owed to this saga (and redrives it if the saga never got to
44
- # claim it); this marker names the saga's own LEG mid-flight after
42
+ # is complementary, not a replacement: the outbox row names the
43
+ # event owed to this saga (and redrives it if the saga never got to
44
+ # claim it); this marker names the saga's own leg mid-flight after
45
45
  # the event was delivered. A crash can leave either standing.
46
46
  SAGA_PENDING_DISPATCH_KEY = :__hecks_saga_pending_dispatch__
47
47
  end