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
@@ -1,7 +1,8 @@
1
1
  require_relative "errors"
2
+ require_relative "../deprecation"
2
3
  require_relative "refusal_wording"
3
4
  require_relative "caller"
4
- require_relative "routing"
5
+ require_relative "invocation"
5
6
  require_relative "command_rules"
6
7
  require_relative "command_interpreter"
7
8
  require_relative "entity_interpreter"
@@ -25,6 +26,8 @@ module Hecks
25
26
  MAX_REACTION_DEPTH = 5
26
27
 
27
28
  Result = Struct.new(:verb, :instance, :events, :execution_plan, :persistence_outcome, keyword_init: true) do
29
+ # Reads the identity of the record the dispatch settled on.
30
+ #
28
31
  # `instance` is nil for a port operation dispatched by verb (below)
29
32
  # — nothing was hydrated or saved, the same reason
30
33
  # `PortOperationInterpreter#emit`'s own comment gives for sourcing
@@ -33,7 +36,15 @@ module Hecks
33
36
  # asks this Result for `.id`/`.state` made a category error the
34
37
  # domain itself already told it about (there is no record here),
35
38
  # not a crash-worthy one.
39
+ #
40
+ # @return [String, nil] the record's identity; nil for a port operation, which
41
+ # hydrates no record
36
42
  def id = instance&.id
43
+
44
+ # Reads the settled record's attributes as one Hash, for the same nil-safe reason as `id`.
45
+ #
46
+ # @return [Hash{Symbol => Object}, nil] the record's state with `:id` merged in
47
+ # last; nil for a port operation, which hydrates no record
37
48
  def state = instance&.to_h
38
49
 
39
50
  def to_s
@@ -46,6 +57,8 @@ module Hecks
46
57
 
47
58
  attr_reader :registry
48
59
 
60
+ # @param registry [Runtime::Registry] the booted registry every interpreter reads; its
61
+ # outbox is attached to this dispatcher's policy and saga interpreters
49
62
  def initialize(registry)
50
63
  @registry = registry
51
64
  rules = CommandRules.new(registry)
@@ -56,7 +69,7 @@ module Hecks
56
69
  @read_models = ReadModelInterpreter.new(registry)
57
70
  @policies = PolicyInterpreter.new(registry, door: self)
58
71
  @sagas = SagaInterpreter.new(registry, door: self)
59
- # THE RELAY IS THE REGISTRY'S, NOT THIS DISPATCHER'S — the
72
+ # The relay is the registry's, not this dispatcher's — the
60
73
  # interpreters enqueue through `@registry.outbox` from inside
61
74
  # the save transaction, and this dispatcher drains through the
62
75
  # same object, so there is exactly one relay per registry no
@@ -65,20 +78,128 @@ module Hecks
65
78
  @registry.outbox.attach(policies: @policies, sagas: @sagas)
66
79
  end
67
80
 
81
+ # Exposes the registry's outbox relay, the object this dispatcher drains reactions through.
82
+ #
68
83
  # `runtime.outbox.rows`, `.rows(status: "claimed")`, `.redrive!`,
69
84
  # `.log` — see `Runtime::Outbox`.
85
+ #
86
+ # @return [Runtime::Outbox::Relay] the registry's one relay, shared by every dispatcher
87
+ # fronting that registry
70
88
  def outbox = @registry.outbox
71
89
 
90
+ # Exposes every event emitted through this registry since boot or the last reset.
91
+ #
92
+ # @return [Array<Runtime::Event>] the registry's live event log, oldest first
72
93
  def events = @registry.event_log
73
94
 
95
+ # Exposes one record per policy reaction that was delivered, refused or left undelivered.
96
+ #
97
+ # @return [Array<Hash{Symbol => Object}>] the registry's live reaction log, oldest first
74
98
  def reactions = @registry.reaction_log
75
99
 
100
+ # Exposes one record per process-manager step: a start, an advance, a delivery, a refusal.
101
+ #
102
+ # @return [Array<Hash{Symbol => Object}>] the registry's live saga log, oldest first
76
103
  def sagas = @registry.saga_log
104
+
105
+ # Exposes the raw inputs each saga dispatch bound its arguments from, a Ruby-only log.
106
+ #
107
+ # @return [Array<Hash{Symbol => Object}>] the registry's live saga dispatch log, oldest first
77
108
  def saga_dispatches = @registry.saga_dispatch_log
109
+
110
+ # Exposes the raw inputs each policy trigger bound its arguments from, a Ruby-only log.
111
+ #
112
+ # @return [Array<Hash{Symbol => Object}>] the registry's live policy dispatch log, with
113
+ # keys `:policy`, `:on`, `:payload`, `:with_spec` and `:args`
78
114
  def policy_dispatches = @registry.policy_dispatch_log
115
+
116
+ # Lists every verb the loaded bluebooks declare.
117
+ #
118
+ # @return [Array<String>] the verbs of every loaded bluebook, sorted
79
119
  def verbs = @registry.verbs
80
120
 
121
+ # Runs one command, entity command or port operation, then every policy and saga reaction
122
+ # its events are owed.
123
+ #
124
+ # Loose keyword facts are deprecated (roadmap I3) — `dispatch(verb,
125
+ # amount: 5)` still works, and warns once per call site; pass
126
+ # `with: { amount: 5 }` (and the receiver in `to:`) instead. The
127
+ # keyword door closes in `LEGACY_ARGS_REMOVAL`. `bin/codemod_legacy_
128
+ # dispatch_args` rewrites existing callers.
129
+ #
130
+ # @param verb [String] the fully qualified verb: `"Domain::Aggregate.Command"`,
131
+ # `"Domain::Aggregate.Entity.Command"` or `"Domain::Aggregate.Port.Operation"`
132
+ # @param to [String, Hash, nil] the receiver: an aggregate identity, or an entity route
133
+ # Hash with `:aggregate` and one of `:entity`/`:entities`; nil when the facts carry
134
+ # the identity themselves
135
+ # @param with [Hash, nil] the command's facts, keyed by argument name (String or Symbol);
136
+ # may not be combined with loose keyword facts
137
+ # @param saga_correlation [Hash, nil] correlation head => value, stamped on every
138
+ # emitted event when a saga leg causes this dispatch; nil otherwise
139
+ # @param legacy_args [Hash{Symbol => Object}] deprecated loose keyword facts
140
+ # @return [Runtime::Dispatcher::Result] the verb, settled instance (nil for a port
141
+ # operation), emitted events, execution plan and persistence outcome
142
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
143
+ # aggregate, command, entity or port operation that is not declared
144
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
145
+ # the call (`GivenNotMet`, `TypeMismatch`, `Unauthorized`, `NotFound`, …)
146
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
147
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
148
+ # @raise [Deprecation::Error] if loose keyword facts are given where
149
+ # `Deprecation.raise_on!(:legacy_dispatch_args)` applies
81
150
  def dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args)
151
+ Dispatcher.deprecate_loose_facts(legacy_args)
152
+ dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, legacy_args: legacy_args)
153
+ end
154
+
155
+ LEGACY_ARGS_REMOVAL = "1.5.0".freeze
156
+ LEGACY_ARGS_WARNING =
157
+ "passing command facts to dispatch as loose keyword arguments is deprecated and will be removed in " \
158
+ "hecks #{LEGACY_ARGS_REMOVAL} — pass them as `with: { ... }`, with the receiver identity in `to:` " \
159
+ "(bin/codemod_legacy_dispatch_args rewrites existing callers)".freeze
160
+
161
+ # Warns, once per call site, that a dispatch passed its facts as loose keyword arguments.
162
+ #
163
+ # @param legacy_args [Hash] the loose keyword facts a dispatch received; empty means
164
+ # nothing deprecated was used, and nothing is warned
165
+ # @return [nil] always, whether or not a warning was issued
166
+ # @raise [Deprecation::Error] if `legacy_args` is not empty where
167
+ # `Deprecation.raise_on!(:legacy_dispatch_args)` applies
168
+ def self.deprecate_loose_facts(legacy_args)
169
+ Deprecation.call(:legacy_dispatch_args, LEGACY_ARGS_WARNING) unless legacy_args.empty?
170
+ end
171
+
172
+ # Dispatches a verb whose receiver and facts arrive together in one flat Hash.
173
+ #
174
+ # **The flat-facts wire form** — one Hash, not keywords, and not
175
+ # deprecated: the shape `spec/corpus/*.json` steps, the Rust kernel's
176
+ # `cli.rs` contract, a reaction without a `with:` projection, and the
177
+ # self-hosted meta-domain all carry. Routes exactly as
178
+ # `dispatch(verb, **args)` does: a Symbol `:to`, `:with` or
179
+ # `:saga_correlation` key is lifted out as that keyword, everything
180
+ # else is a fact (so a String "to" key stays a fact).
181
+ # Framework code that replays data calls this; application code
182
+ # calls `dispatch(verb, to:, with:)`.
183
+ #
184
+ # @param verb [String] the fully qualified verb, in any shape `dispatch` accepts
185
+ # @param args [Hash] the facts, plus optional Symbol keys `:to`, `:with` and
186
+ # `:saga_correlation`, read as `dispatch`'s keywords of the same names; not mutated
187
+ # @return [Runtime::Dispatcher::Result] the same result `dispatch` returns
188
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
189
+ # aggregate, command, entity or port operation that is not declared
190
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
191
+ # the call
192
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
193
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
194
+ def dispatch_flat(verb, args = {})
195
+ facts = args.dup
196
+ to = facts.delete(:to)
197
+ with = facts.delete(:with)
198
+ saga_correlation = facts.delete(:saga_correlation)
199
+ dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, legacy_args: facts)
200
+ end
201
+
202
+ def dispatch_invocation(verb, to:, with:, saga_correlation:, legacy_args:)
82
203
  domain, aggregate_name, command_name = parse(verb)
83
204
  aggregate = resolve_aggregate(domain, aggregate_name, verb)
84
205
 
@@ -86,7 +207,7 @@ module Hecks
86
207
  if command_name.include?(".")
87
208
  head, sub = command_name.split(".", 2)
88
209
  port = aggregate.port(head)
89
- # A PORT OPERATION, reached by the SAME verb shape an entity
210
+ # A port operation, reached by the same verb shape an entity
90
211
  # command already uses ("Domain::Aggregate.Head.Rest") — ports
91
212
  # are checked first, so an aggregate that ever declared both a
92
213
  # port and an entity of the same name would resolve to the
@@ -99,34 +220,35 @@ module Hecks
99
220
  # path.
100
221
  if port
101
222
  operation = port.operation(sub) ||
102
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "port_no_operation",
103
- port: head, operation: sub.inspect))
104
- route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
105
- [nil, @port_ops.call(domain, aggregate, operation, args, route: route), nil, nil, :enqueue]
223
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "port_no_operation",
224
+ port: head, operation: sub))
225
+ invocation = Invocation.from_call(verb, to: to, with: with, legacy: legacy_args,
226
+ receiver: :port, aggregate: aggregate) { operation }
227
+ [nil, @port_ops.call(domain, aggregate, operation, invocation), nil, nil, :enqueue]
106
228
  else
107
- entity_depth = command_name.split(".").size - 1
108
- route = Routing.envelope(to, entity_depth: entity_depth)
109
- @entities.call(domain, aggregate, command_name, legacy_args, route: route, with: with)
229
+ resolution = nil
230
+ invocation = Invocation.from_call(verb, to: to, with: with, legacy: legacy_args,
231
+ receiver: :entity, entity_depth: command_name.count(".")) do
232
+ (resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
233
+ end
234
+ @entities.call(domain, aggregate, resolution, invocation)
110
235
  end
111
236
  else
112
- command = aggregate.command(command_name) ||
113
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command",
114
- aggregate: aggregate_name, command: command_name.inspect))
115
- args = Routing.payload(command, with: with, legacy: legacy_args)
116
- route = Routing.envelope(to)
117
- @commands.call(domain, aggregate, command, args, saga_correlation, route: route)
237
+ command = command_of(aggregate, aggregate_name, command_name)
238
+ invocation = Invocation.from_call(verb, to: to, with: with, legacy: legacy_args) { command }
239
+ @commands.call(domain, aggregate, command, invocation, saga_correlation)
118
240
  end
119
241
 
120
- # Correlation is SET AT CONSTRUCTION now, not merged on here —
121
- # it is part of the transaction, known from this method's own
122
- # argument before a single event exists. It used to be stamped
123
- # onto already-emitted events, which is what kept an event
124
- # mutable after it had happened.
242
+ # Correlation is set when each event is constructed, not merged on
243
+ # here — it is part of the transaction, known from this method's
244
+ # own argument before a single event exists. Stamping it onto
245
+ # already-emitted events would keep an event mutable after it had
246
+ # happened.
125
247
  #
126
- # The ordering this note used to guard still holds, and more
127
- # simply: `SagaInterpreter#advance` runs on THIS domain's
128
- # `announced` events within this very call, and finds the
129
- # correlation already there because it was never absent.
248
+ # The ordering that depends on it holds for the same reason:
249
+ # `SagaInterpreter#advance` runs on this domain's `announced`
250
+ # events within this very call, and finds the correlation already
251
+ # there because it was never absent.
130
252
 
131
253
  react(announced, domain, aggregate, outbox_rows)
132
254
 
@@ -134,7 +256,12 @@ module Hecks
134
256
  execution_plan: execution_plan, persistence_outcome: persistence_outcome)
135
257
  end
136
258
 
137
- # EVERYTHING OWED BECAUSE `announced` COMMITTEDpolicies first,
259
+ # **The one body both doors run** `dispatch` (keywords) and
260
+ # `dispatch_flat` (one Hash) differ only in how the call's parts are
261
+ # spelled, never in what happens next.
262
+ private :dispatch_invocation
263
+
264
+ # **Everything owed because `announced` committed** — policies first,
138
265
  # then sagas, the order this method always ran them in. The
139
266
  # command/entity interpreters hand back the outbox rows they
140
267
  # enqueued inside the save transaction (`Interpreting#
@@ -152,9 +279,11 @@ module Hecks
152
279
  end
153
280
  private :react
154
281
 
155
- # "IF THIS WERE DISPATCHED RIGHT NOW, WOULD IT SUCCEED" the same
282
+ # Answers whether a command would succeed right now, without saving, emitting or reacting.
283
+ #
284
+ # "If this were dispatched right now, would it succeed" — the same
156
285
  # pipeline #dispatch itself runs (arguments coerced, givens checked,
157
- # mutations applied IN MEMORY, ensures checked against the settled
286
+ # mutations applied in memory, ensures checked against the settled
158
287
  # result), except `step_save`/`step_emit` never run, and neither do
159
288
  # policies or sagas afterward: nothing here is committed, so nothing
160
289
  # should react to it. Built for exactly the shape a whole-board
@@ -164,15 +293,26 @@ module Hecks
164
293
  # own move purely to trigger the check, which then had to avoid
165
294
  # interfering with the very position being tested).
166
295
  #
167
- # RAISES THE SAME REFUSALS #dispatch does — a DomainRefusal
296
+ # Raises the same refusals #dispatch does — a DomainRefusal
168
297
  # subclass propagates normally, so a caller checking "would this be
169
298
  # legal" writes the identical rescue clause a real dispatch already
170
299
  # needs; this returns `true` only when nothing was refused.
171
300
  #
172
- # NEVER A PORT VERB — `PortOperationInterpreter`'s own side effects
301
+ # **Never a port verb** — `PortOperationInterpreter`'s own side effects
173
302
  # (an external gateway call, say) have no meaningful in-memory-only
174
303
  # form, so this refuses one outright rather than silently running
175
304
  # it for real, which "dry" would otherwise quietly lie about.
305
+ #
306
+ # @param verb [String] the fully qualified aggregate or entity command verb
307
+ # @param args [Hash{Symbol => Object}] the command's facts as flat keywords; a key named
308
+ # `to` or `with` is an ordinary fact here, never routing (BUG#131)
309
+ # @return [true] whenever nothing refused; a refusal is raised, never returned as false
310
+ # @raise [Runtime::WiringError] if the verb names a port operation, or the aggregate's
311
+ # repository cannot be resolved
312
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
313
+ # aggregate, command or entity that is not declared
314
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` that the real dispatch
315
+ # would raise
176
316
  def dry_run?(verb, **args)
177
317
  domain, aggregate_name, command_name = parse(verb)
178
318
  aggregate = resolve_aggregate(domain, aggregate_name, verb)
@@ -185,18 +325,25 @@ module Hecks
185
325
  "only for aggregate and entity commands"
186
326
  end
187
327
 
188
- @entities.call(domain, aggregate, command_name, args, dry_run: true)
328
+ # `to:`/`with:` are not keywords of this method — a key named
329
+ # either is an ordinary fact here (BUG#131), so both go in as nil.
330
+ resolution = nil
331
+ invocation = Invocation.from_call(verb, to: nil, with: nil, legacy: args, receiver: :entity) do
332
+ (resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
333
+ end
334
+ @entities.call(domain, aggregate, resolution, invocation, dry_run: true)
189
335
  else
190
- command = aggregate.command(command_name) ||
191
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command",
192
- aggregate: aggregate_name, command: command_name.inspect))
193
- @commands.call(domain, aggregate, command, args, dry_run: true)
336
+ command = command_of(aggregate, aggregate_name, command_name)
337
+ invocation = Invocation.from_call(verb, to: nil, with: nil, legacy: args) { command }
338
+ @commands.call(domain, aggregate, command, invocation, dry_run: true)
194
339
  end
195
340
 
196
341
  true
197
342
  end
198
343
 
199
- # THE DOOR AN ADAPTER OUTSIDE THE BLUEBOOK CALLS THROUGH never the
344
+ # Runs one port operation named by its parts, then the reactions its events are owed.
345
+ #
346
+ # The door an adapter outside the bluebook calls through — never the
200
347
  # domain itself. `port_name`/`operation_name` are separate arguments
201
348
  # rather than one packed verb string on purpose: there is no established
202
349
  # wire spelling for "domain, aggregate, port, operation" yet, and
@@ -205,66 +352,69 @@ module Hecks
205
352
  # No adapter-to-port binding lookup happens here — that is
206
353
  # `Hecks.adapter`'s existing job (unchanged by this), and wiring "which
207
354
  # adapter may call this port" through is the next piece, not this one.
355
+ #
356
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
357
+ # @param aggregate_name [String, Symbol] name of the aggregate that declares the port
358
+ # @param port_name [String] name of the port, as the aggregate declares it
359
+ # @param operation_name [String] name of the operation on that port
360
+ # @param to [String, Hash, nil] the receiving aggregate's identity; when nil it is read
361
+ # from the facts, by the operation's reference or identity attribute
362
+ # @param with [Hash, nil] the operation's facts, keyed by argument name
363
+ # @param legacy_args [Hash{Symbol => Object}] deprecated loose keyword facts
364
+ # @return [Array<Runtime::Event>] the events the operation announced: one per declared
365
+ # `emits` for an inbound operation, the one answering or refusing event for an
366
+ # outbound one
367
+ # @raise [Runtime::UnknownVerb] if the domain, aggregate, port or operation is not declared
368
+ # @raise [Runtime::TypeMismatch] if no receiving identity can be found, or `to:`/`with:`
369
+ # is malformed
370
+ # @raise [Runtime::NotFound] if the receiving aggregate record does not exist
371
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
372
+ # @raise [Deprecation::Error] if loose keyword facts are given where
373
+ # `Deprecation.raise_on!(:legacy_dispatch_args)` applies
208
374
  def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args)
375
+ Dispatcher.deprecate_loose_facts(legacy_args)
209
376
  aggregate = resolve_aggregate(domain, aggregate_name, "#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}")
210
377
  port = aggregate.port(port_name) ||
211
378
  raise(UnknownVerb, "#{aggregate_name} has no port #{port_name.inspect}")
212
379
  operation = port.operation(operation_name) ||
213
380
  raise(UnknownVerb, "#{port_name} has no operation #{operation_name.inspect}")
214
381
 
215
- route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
216
- announced = @port_ops.call(domain, aggregate, operation, args, route: route)
382
+ invocation = Invocation.from_call("#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}",
383
+ to: to, with: with, legacy: legacy_args,
384
+ receiver: :port, aggregate: aggregate) { operation }
385
+ announced = @port_ops.call(domain, aggregate, operation, invocation)
217
386
 
218
387
  react(announced, domain, aggregate, :enqueue)
219
388
 
220
389
  announced
221
390
  end
222
391
 
223
- def port_invocation(aggregate, operation, to:, with:, legacy:)
224
- legacy = legacy.dup
225
- identity = operation.identity_attribute(aggregate.hecks_name)
226
- if to.nil? && identity && legacy.key?(identity.name)
227
- to = legacy.delete(identity.name)
228
- elsif to.nil? && operation.to == aggregate.hecks_name
229
- # `to:`-DECLARED OPERATIONS carry no Reference-typed attribute at
230
- # all (PortOperationBuilder#initialize's own comment on why
231
- # genuine routing metadata, not an attribute), so `identity`
232
- # above is always nil for these; this is the second, purely
233
- # additive lookup they need instead. The routing value sits in
234
- # a PLAIN external-fact attribute, named for the owning
235
- # aggregate's own identified_by field the domain author's job
236
- # to match, same discipline reference_to's own `as:` always
237
- # required. Composite identity (more than one identified_by
238
- # component) isn't attempted here `.first` only, no domain in
239
- # the real corpus has needed more for a port operation yet.
240
- #
241
- # READ, NOT deleted — unlike the Reference-attribute branch
242
- # above, this is a genuine declared operation attribute (Rust's
243
- # own comment: "declare only external facts with attribute"),
244
- # not synthetic routing-only state; the operation's own
245
- # attributes still expect to find it in the payload a few steps
246
- # later (refuse_absent_arguments), and a real, live
247
- # AbsentArgument confirmed this the hard way before `[]`
248
- # replaced `delete`.
249
- identity_name = Array(aggregate.identified_by).first
250
- to = legacy[identity_name] if identity_name && legacy.key?(identity_name)
251
- end
252
-
253
- route = Routing.envelope(to)
254
- raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless route
255
-
256
- [route, Routing.payload(operation, with: with, legacy: legacy)]
257
- end
258
- private :port_invocation
259
-
392
+ # Answers a declared query: an aggregate query, an entity query, or a read model.
393
+ #
394
+ # The verb's shape picks the interpreter. `"Domain.ReadModel"` — no `::` before the
395
+ # dot — is a read model; `"Domain::Aggregate.Query"` is an aggregate query, and
396
+ # `"Domain::Aggregate.Entity.Query"` an entity query.
397
+ #
398
+ # @param verb [String, Symbol] the query's verb, in one of the three shapes above
399
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
400
+ # @return [Array<Hash>] for an aggregate query, one deep-frozen Hash per matching record,
401
+ # its state with `:id` merged in last; for an entity query, one Hash per matching
402
+ # element with the parent's reference key merged in first; for a read model, a
403
+ # one-element Array holding a Hash of head name to projected rows
404
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
405
+ # aggregate, entity, query or read model that is not declared
406
+ # @raise [Runtime::NotFound] if a read model's root reference names no record
407
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
408
+ # @raise [KeyError] if a rooted read model is asked without its reference argument
409
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
260
410
  def query(verb, **args)
261
411
  domain, query_name = verb.to_s.split(".", 2)
262
412
  if query_name && !domain.include?("::")
263
413
  bluebook = @registry.bluebook(domain) ||
264
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
414
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_domain", domain: domain, verb: verb))
265
415
  model = bluebook.read_model(query_name) ||
266
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_read_model",
267
- domain: domain, query: query_name.inspect))
416
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_read_model",
417
+ domain: domain, query: query_name))
268
418
  return @read_models.call(domain, model, args)
269
419
  end
270
420
 
@@ -274,10 +424,22 @@ module Hecks
274
424
  @queries.call(domain, aggregate, query_name, args)
275
425
  end
276
426
 
277
- # The same ask, answered by the reference interpreter alone — never
278
- # the bound adapter's native hook. Read models have no reference
279
- # twin, so only the aggregate-query form answers here; the fuzzer's
280
- # query oracle diffs this against #query's answer.
427
+ # Answers an aggregate or entity query through the reference interpreter alone.
428
+ #
429
+ # The same ask as `#query`, never answered by the bound adapter's
430
+ # native hook. Read models have no reference twin, so only the
431
+ # `"Domain::Aggregate.Query"` forms answer here; the fuzzer's
432
+ # query oracle diffs this against `#query`'s answer.
433
+ #
434
+ # @param verb [String] the fully qualified query verb, `"Domain::Aggregate.Query"` or
435
+ # `"Domain::Aggregate.Entity.Query"`
436
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
437
+ # @return [Array<Hash>] one Hash per matching record, its state with `:id` merged in
438
+ # last; for an entity query, one Hash per matching element
439
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
440
+ # aggregate, entity or query that is not declared
441
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
442
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
281
443
  def reference_query(verb, **args)
282
444
  domain, aggregate_name, query_name = parse(verb)
283
445
  aggregate = resolve_aggregate(domain, aggregate_name, verb)
@@ -285,7 +447,10 @@ module Hecks
285
447
  @queries.reference_call(domain, aggregate, query_name, args)
286
448
  end
287
449
 
288
- # A reaction is the SYSTEM acting, not the caller who happened to be
450
+ # Dispatches a reaction's command one level deeper in the cascade, as the system rather
451
+ # than as the triggering caller.
452
+ #
453
+ # A reaction is the system acting, not the caller who happened to be
289
454
  # on the stack when the triggering command ran — the ambient caller
290
455
  # is cleared for the reaction's own dispatch, so a triggering
291
456
  # caller's role can neither satisfy nor block a reaction command it
@@ -296,39 +461,67 @@ module Hecks
296
461
  # dispatching through it (a Puma worker pool, say), so a plain ivar
297
462
  # here is exactly the known Puma-concurrency bug class: two
298
463
  # concurrent top-level dispatches on different threads would
299
- # increment/decrement the SAME counter, letting one thread's nested
464
+ # increment/decrement the same counter, letting one thread's nested
300
465
  # reaction depth leak into another thread's unrelated dispatch. A
301
466
  # `Mutex` is not the answer either — a reaction cascade re-enters
302
- # `reenter` on the SAME thread (see `SagaInterpreter#advance_saga`'s
467
+ # `reenter` on the same thread (see `SagaInterpreter#advance_saga`'s
303
468
  # own comment on why a non-reentrant `Mutex` can't guard this).
304
469
  # `Thread.current`-backed, saved/restored around the call with a
305
470
  # plain local + `ensure`, is the same idiom `Runtime::Caller`
306
471
  # (`caller.rb`) already established for exactly this shape of
307
472
  # per-thread ambient state.
473
+ #
474
+ # The depth is not checked here: a reacting interpreter asks
475
+ # `reaction_depth_reached?` first and records an undelivered reaction
476
+ # instead of calling this.
477
+ #
478
+ # @param verb [String] the fully qualified verb of the reaction's target command
479
+ # @param saga_correlation [Hash{String => Object}, nil] correlation head => value when a
480
+ # saga leg dispatches; nil for a policy
481
+ # @param args [Hash{Symbol => Object}] the flat facts, read exactly as `dispatch_flat`
482
+ # reads them, so `:to` and `:with` keys route rather than count as facts
483
+ # @return [Runtime::Dispatcher::Result] the result of the nested dispatch
484
+ # @raise [Runtime::UnknownVerb] if the verb names nothing declared
485
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the target
486
+ # refuses; the policy and saga interpreters rescue these as recorded outcomes
308
487
  def reenter(verb, saga_correlation: nil, **args)
309
488
  depth = Thread.current[:hecks_reaction_depth].to_i
310
489
  Thread.current[:hecks_reaction_depth] = depth + 1
311
- Caller.without { dispatch(verb, saga_correlation: saga_correlation, **args) }
490
+ Caller.without { dispatch_flat(verb, args.merge(saga_correlation: saga_correlation)) }
312
491
  ensure
313
492
  Thread.current[:hecks_reaction_depth] = depth
314
493
  end
315
494
 
495
+ # Answers whether the calling thread's reaction cascade is as deep as it may go.
496
+ #
497
+ # @return [Boolean] true once this thread's nested `reenter` calls number
498
+ # `MAX_REACTION_DEPTH` or more; other threads' cascades are not counted
316
499
  def reaction_depth_reached? = Thread.current[:hecks_reaction_depth].to_i >= MAX_REACTION_DEPTH
500
+
501
+ # Reads the cascade limit, for the reason an interpreter records when it declines to react.
502
+ #
503
+ # @return [Integer] `MAX_REACTION_DEPTH`, the number of nested reactions allowed
317
504
  def max_reaction_depth = MAX_REACTION_DEPTH
318
505
 
319
506
  private
320
507
 
321
508
  def parse(verb)
322
509
  Naming.split_verb(verb) ||
323
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "not_fully_qualified", verb: verb.inspect))
510
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "not_fully_qualified", verb: verb))
511
+ end
512
+
513
+ def command_of(aggregate, aggregate_name, command_name)
514
+ aggregate.command(command_name) ||
515
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "aggregate_no_command",
516
+ aggregate: aggregate_name, command: command_name))
324
517
  end
325
518
 
326
519
  def resolve_aggregate(domain, aggregate_name, verb)
327
520
  bluebook = @registry.bluebook(domain) ||
328
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
521
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_domain", domain: domain, verb: verb))
329
522
  bluebook.aggregate(aggregate_name) ||
330
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_aggregate",
331
- domain: domain, aggregate: aggregate_name.inspect))
523
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_aggregate",
524
+ domain: domain, aggregate: aggregate_name))
332
525
  end
333
526
  end
334
527
  end