hecks 1.4.0 → 1.5.1

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 (275) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -13,7 +13,9 @@ module Hecks
13
13
  # outbound port operation) the external effects those reactions
14
14
  # cause. `future-features.md` item 8, built.
15
15
  #
16
- # **The shape**. One row per (event, consumer). A consumer is a named
16
+ # ## The shape
17
+ #
18
+ # One row per (event, consumer). A consumer is a named
17
19
  # policy or process manager that would react to the event — resolved
18
20
  # at enqueue time from the registry (`Fanout`), so the outbox records
19
21
  # who was owed what, not just that an event happened. Rows move
@@ -33,18 +35,22 @@ module Hecks
33
35
  # tracks delivery, not the domain's answer.
34
36
  # failed the consumer raised a defect (non-refusal error).
35
37
  #
36
- # **Delivery is inline by default** — the dispatcher drains the rows it
38
+ # ## Delivery is inline by default
39
+ #
40
+ # The dispatcher drains the rows it
37
41
  # just wrote, in the same call, in the order C10.2 fixes (per event
38
42
  # in `emits` order: that event's policy rows, then its saga rows —
39
43
  # the emitting domain's own policies before other domains'). Nothing about the
40
44
  # happy path is deferred or asynchronous; a caller still sees every
41
45
  # reaction settled when `dispatch` returns. What changes is the
42
- # crash window: a process that dies between commit and reaction
43
- # used to lose the reaction silently. Now the row survives, and
44
- # `Relay#redrive!` — run at boot by `Loader.run_boot_gates!` —
46
+ # crash window: without a durable row, a process that dies between
47
+ # commit and reaction loses the reaction silently. The row survives,
48
+ # and `Relay#redrive!` — run at boot by `Loader.run_boot_gates!` —
45
49
  # finds it.
46
50
  #
47
- # **What redrive does, and deliberately doesn't**. A `pending` row is
51
+ # ## What redrive does, and deliberately doesn't
52
+ #
53
+ # A `pending` row is
48
54
  # redriven: its consumer provably never started (claiming is the
49
55
  # first thing delivery does), so running it now is exactly-once by
50
56
  # construction. A `claimed` row is not auto-redriven: the consumer
@@ -57,7 +63,9 @@ module Hecks
57
63
  # unique per store, so a re-enqueue of the same fact to the same
58
64
  # consumer is a no-op rather than a second row.
59
65
  #
60
- # **Which adapters**. Memory (in-process rows — visible to specs,
66
+ # ## Which adapters
67
+ #
68
+ # Memory (in-process rows — visible to specs,
61
69
  # gone with the process, exactly like everything else Memory holds),
62
70
  # Sqlite and Postgres (a `hecks_outbox` table in the aggregate's own
63
71
  # database — the only way the enqueue can share the save's
@@ -71,13 +79,30 @@ module Hecks
71
79
 
72
80
  Row = Struct.new(:id, :delivery_id, :event_uid, :aggregate, :domain, :kind, :consumer, :event,
73
81
  :status, :attempts, :error, keyword_init: true) do
82
+ # Reports whether this row is still waiting to be claimed.
83
+ #
84
+ # @return [Boolean] true if this row's status is `"pending"`
74
85
  def pending? = status == "pending"
86
+
87
+ # Reports whether this row's consumer is currently running.
88
+ #
89
+ # @return [Boolean] true if this row's status is `"claimed"`
75
90
  def claimed? = status == "claimed"
91
+
92
+ # Reports whether this row's consumer ran to completion.
93
+ #
94
+ # @return [Boolean] true if this row's status is `"delivered"`
76
95
  def delivered? = status == "delivered"
96
+
97
+ # Reports whether this row's consumer raised a defect.
98
+ #
99
+ # @return [Boolean] true if this row's status is `"failed"`
77
100
  def failed? = status == "failed"
78
101
 
79
102
  # Wire-shaped — what an adapter persists. `event` is the event's
80
103
  # own `to_h` plus correlation; `Row.event_from` reverses it.
104
+ #
105
+ # @return [Hash{Symbol => Object}] this row's own fields, keyed by name
81
106
  def to_h
82
107
  { id: id, delivery_id: delivery_id, event_uid: event_uid, aggregate: aggregate, domain: domain,
83
108
  kind: kind, consumer: consumer, event: event, status: status, attempts: attempts, error: error }
@@ -89,6 +114,12 @@ module Hecks
89
114
 
90
115
  module_function
91
116
 
117
+ # Renders an event as the wire-shaped Hash an outbox row's own `event`
118
+ # field stores.
119
+ #
120
+ # @param event [Runtime::Event] the event to serialize
121
+ # @return [Hash{Symbol => Object}] `event`'s own `to_h`, with `correlation`
122
+ # merged in
92
123
  def serialize_event(event)
93
124
  event.to_h.merge(correlation: event.correlation)
94
125
  end
@@ -96,11 +127,21 @@ module Hecks
96
127
  # The emitting domain's own bluebook first, then the rest in load
97
128
  # order (C10.2) — the one policy ordering both `PolicyInterpreter#
98
129
  # policies_for` and `Fanout.policies` read.
130
+ #
131
+ # @param registry [Runtime::Registry] the booted registry whose loaded
132
+ # bluebooks are ordered
133
+ # @param domain [String, Symbol] the emitting domain, sorted first
134
+ # @return [Array<Bluebook::Chapter>] every loaded chapter, `domain`'s own first
99
135
  def bluebooks_home_first(registry, domain)
100
136
  home, others = registry.bluebooks.each_value.partition { |bluebook| bluebook.name == domain }
101
137
  home + others
102
138
  end
103
139
 
140
+ # Rebuilds a frozen event from an outbox row's own stored `event` field.
141
+ #
142
+ # @param hash [Hash{String, Symbol => Object}] the wire-shaped event Hash,
143
+ # as `serialize_event` built it (String or Symbol keys either way)
144
+ # @return [Runtime::Event] the rebuilt, frozen event
104
145
  def event_from(hash)
105
146
  hash = hash.transform_keys(&:to_sym)
106
147
  Event.new(
@@ -113,6 +154,12 @@ module Hecks
113
154
  ).emit!
114
155
  end
115
156
 
157
+ # Recursively symbolizes every Hash key reachable from `value`.
158
+ #
159
+ # @param value [Object] the value to symbolize; typically a Hash or Array,
160
+ # possibly nested
161
+ # @return [Object] `value` with every Hash key (at any depth) turned into a
162
+ # Symbol; a non-Hash, non-Array value passes through unchanged
116
163
  def deep_symbolize(value)
117
164
  case value
118
165
  when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
@@ -137,6 +184,12 @@ module Hecks
137
184
  # this consumer".
138
185
  # Row order is delivery order (C10.2): per event, in `emits`
139
186
  # order — that event's policy rows, then its saga rows.
187
+ #
188
+ # @param registry [Runtime::Registry] the booted registry every candidate
189
+ # consumer is resolved against
190
+ # @param events [Array<Runtime::Event>] the just-emitted events to build rows for
191
+ # @param domain [String, Symbol] the emitting domain
192
+ # @return [Array<Runtime::Outbox::Row>] one pending row per (event, consumer)
140
193
  def rows_for(registry, events, domain)
141
194
  uids = events.to_h { |event| [event, SecureRandom.uuid] }
142
195
  events.flat_map do |event|
@@ -144,6 +197,16 @@ module Hecks
144
197
  end
145
198
  end
146
199
 
200
+ # Builds one pending row per policy `event` triggers.
201
+ #
202
+ # @param registry [Runtime::Registry] the booted registry policies are
203
+ # resolved against
204
+ # @param event [Runtime::Event] the just-emitted event
205
+ # @param domain [String, Symbol] the emitting domain
206
+ # @param uid [String] this event's own enqueue-time UID, shared by every row
207
+ # built for it
208
+ # @return [Array<Runtime::Outbox::Row>] one pending row per matching policy,
209
+ # emitting domain's own bluebook first
147
210
  def policies(registry, event, domain, uid)
148
211
  emitting = Naming.demodulise(event.aggregate)
149
212
  Outbox.bluebooks_home_first(registry, domain).flat_map do |bluebook|
@@ -159,6 +222,18 @@ module Hecks
159
222
  end
160
223
  end
161
224
 
225
+ # Builds one pending row per process manager `event` advances, in `domain`
226
+ # only (a saga never reacts across domains).
227
+ #
228
+ # @param registry [Runtime::Registry] the booted registry process managers
229
+ # are resolved against
230
+ # @param event [Runtime::Event] the just-emitted event
231
+ # @param domain [String, Symbol] the domain whose declared process managers
232
+ # are checked
233
+ # @param uid [String] this event's own enqueue-time UID, shared by every row
234
+ # built for it
235
+ # @return [Array<Runtime::Outbox::Row>] one pending row per process manager
236
+ # `event` starts, ends, or advances
162
237
  def sagas(registry, event, domain, uid)
163
238
  bluebook = registry.bluebook(domain)
164
239
  return [] unless bluebook
@@ -170,6 +245,13 @@ module Hecks
170
245
  end
171
246
  end
172
247
 
248
+ # Reports whether `event` starts, ends, or advances `process_manager`.
249
+ #
250
+ # @param process_manager [Bluebook::ProcessManager] the declared process
251
+ # manager to check
252
+ # @param event [Runtime::Event] the just-emitted event
253
+ # @return [Boolean] true if `event` names `process_manager`'s own
254
+ # `starts_on`, `ends_on`, or a declared handler
173
255
  def listens?(process_manager, event)
174
256
  process_manager.starts_on == event.name || process_manager.ends_on == event.name ||
175
257
  !process_manager.handler_for(event.name).nil?
@@ -179,6 +261,14 @@ module Hecks
179
261
  # operation — the row is the durable record that an external
180
262
  # call was owed, claimed right before the adapter is asked and
181
263
  # settled right after. Everything else is a plain "reaction".
264
+ #
265
+ # @param registry [Runtime::Registry] the booted registry the trigger's own
266
+ # aggregate/port are resolved against
267
+ # @param policy [Bluebook::Policy] the policy whose trigger is classified
268
+ # @param home_domain [String, Symbol] the domain `policy` is declared in,
269
+ # used when `policy` declares no `target_domain`
270
+ # @return [String] `"effect"` when the trigger resolves to an outbound port
271
+ # operation, `"reaction"` otherwise
182
272
  def kind_for(registry, policy, home_domain)
183
273
  target = "#{policy.target_domain || home_domain}::#{policy.trigger_command}"
184
274
  parsed = Naming.split_verb(target)
@@ -204,11 +294,16 @@ module Hecks
204
294
  # by the Rust kernel (`spec/rust_conformance_spec.rb`); this is
205
295
  # an additive, Ruby-only log, the same rule `saga_dispatch_log`
206
296
  # and `policy_dispatch_log` already follow.
297
+ #
298
+ # @param registry [Runtime::Registry] the booted registry this relay drains
299
+ # rows for
207
300
  def initialize(registry)
208
301
  @registry = registry
209
302
  @log = []
210
303
  end
211
304
 
305
+ # Attaches the interpreters a delivered row's own consumer runs through.
306
+ #
212
307
  # A Dispatcher hands over the interpreters a consumer runs
213
308
  # through (`Dispatcher#initialize`). Until then this relay can
214
309
  # enqueue (that needs only the registry) but not deliver — and
@@ -216,20 +311,38 @@ module Hecks
216
311
  # to. The registry holds one relay for its lifetime; a second
217
312
  # dispatcher fronting the same registry re-attaches, which is
218
313
  # fine because both dispatchers share every log and store.
314
+ #
315
+ # @param policies [Runtime::PolicyInterpreter] the interpreter a `"policy:"`
316
+ # consumer reacts through
317
+ # @param sagas [Runtime::SagaInterpreter] the interpreter a `"saga:"` consumer
318
+ # advances through
319
+ # @return [Runtime::Outbox::Relay] self
219
320
  def attach(policies:, sagas:)
220
321
  @policies = policies
221
322
  @sagas = sagas
222
323
  self
223
324
  end
224
325
 
326
+ # Reports whether a dispatcher has attached its interpreters.
327
+ #
328
+ # @return [Boolean] true once `attach` has run
225
329
  def attached? = !@policies.nil?
226
330
 
331
+ # Writes one pending row per (event, consumer) `events` owes a reaction to.
332
+ #
227
333
  # Called inside the save transaction by `Interpreting` for the
228
334
  # command/entity paths, and outside one by `Dispatcher` for port
229
335
  # operations (which save nothing, so there is no transaction to
230
336
  # share). Returns the rows as stored (ids assigned), or nil when
231
337
  # the repository has no outbox — the dispatcher then reacts
232
338
  # directly, exactly as before.
339
+ #
340
+ # @param repository [Persistence::AppendOnly] the repository whose store the
341
+ # rows are enqueued into; a no-op unless it has an outbox
342
+ # @param events [Array<Runtime::Event>] the events this dispatch just emitted
343
+ # @param domain [String, Symbol] the emitting domain
344
+ # @return [Array<Runtime::Outbox::Row>, nil] the stored rows, `id` assigned;
345
+ # `nil` when `repository` has no outbox; `[]` when `events` is empty
233
346
  def enqueue(repository, events, domain)
234
347
  return nil unless repository.outbox?
235
348
  return [] if events.empty?
@@ -241,6 +354,15 @@ module Hecks
241
354
 
242
355
  # Drain the rows a dispatch just committed. `rows` nil means "no
243
356
  # outbox here" — react directly, the pre-outbox path.
357
+ #
358
+ # @param rows [Array<Runtime::Outbox::Row>, nil] the just-enqueued rows to
359
+ # deliver; nil to react directly instead (no outbox on this repository)
360
+ # @param events [Array<Runtime::Event>] the events this dispatch just emitted,
361
+ # read when `rows` is nil
362
+ # @param domain [String, Symbol] the emitting domain
363
+ # @param repository [Persistence::AppendOnly] the repository `rows` were
364
+ # enqueued into
365
+ # @return [void]
244
366
  def deliver(rows, events, domain, repository)
245
367
  if rows.nil?
246
368
  # Per event, in `emits` order — its policies, then its sagas
@@ -258,6 +380,11 @@ module Hecks
258
380
 
259
381
  # One row: claim, run its consumer, settle. A claim that fails
260
382
  # means another relay (or this one, re-entrantly) already has it.
383
+ #
384
+ # @param row [Runtime::Outbox::Row] the row to claim and deliver
385
+ # @param repository [Persistence::AppendOnly] the repository `row` is stored in
386
+ # @return [Boolean] true when this call claimed and delivered `row`; false
387
+ # when the claim failed, or the consumer raised a defect (recorded on `log`)
261
388
  def deliver_row(row, repository)
262
389
  return false unless repository.outbox_claim(row.id)
263
390
 
@@ -283,13 +410,24 @@ module Hecks
283
410
  end
284
411
 
285
412
  # Every row in every bound store, newest last. `status:` narrows.
413
+ #
414
+ # @param status [String, Symbol, nil] keep only rows with this status; nil
415
+ # for every row
416
+ # @return [Array<Runtime::Outbox::Row>] copies of the matching rows, across
417
+ # every store with an outbox
286
418
  def rows(status: nil)
287
419
  stores.flat_map { |repository| repository.outbox_rows(status: status) }
288
420
  end
289
421
 
422
+ # Runs the boot-time reconciliation over every bound store.
423
+ #
290
424
  # **Boot-time reconciliation**. Redrives `pending` rows (never
291
425
  # claimed — safe by construction); surfaces `claimed` rows and
292
426
  # redrives them only when told to (`claimed: true`).
427
+ #
428
+ # @param claimed [Boolean] whether to also redrive `claimed` rows (an
429
+ # explicit operator decision); false leaves them surfaced and untouched
430
+ # @return [Array<Runtime::Outbox::Row>] every row this call actually delivered
293
431
  def redrive!(claimed: false)
294
432
  redriven = []
295
433
  stores.each do |repository|
@@ -16,11 +16,18 @@ module Hecks
16
16
  class PolicyInterpreter
17
17
  attr_reader :registry
18
18
 
19
+ # @param registry [Runtime::Registry] the booted registry whose loaded
20
+ # bluebooks are scanned for candidate policies
21
+ # @param door [Runtime::Dispatcher] the dispatcher reactions re-enter through,
22
+ # and whose reaction-depth guard is checked before each delivery
19
23
  def initialize(registry, door:)
20
24
  @registry = registry
21
25
  @door = door
22
26
  end
23
27
 
28
+ # Fires every declared policy `event` triggers, recording each outcome on
29
+ # the registry's reaction log.
30
+ #
24
31
  # `deliver` returns `nil` for a policy whose `where` did not hold —
25
32
  # silently, the same as a policy `policies_for` never selected at all
26
33
  # (an `event_qualifier` miss carries no reaction_log entry either) —
@@ -32,6 +39,13 @@ module Hecks
32
39
  # way of running exactly the consumer a row names (`Runtime::
33
40
  # Outbox::Relay#run_consumer`) instead of every policy that
34
41
  # matches the event. Selection is otherwise identical.
42
+ #
43
+ # @param event [Runtime::Event] the just-emitted event to react to
44
+ # @param domain [String, Symbol] the domain `event`'s own aggregate belongs
45
+ # to, the emitting domain's policies fire first
46
+ # @param only [Array(Bluebook::Policy, String), nil] one `[policy, home_domain]`
47
+ # pair to run exactly, instead of scanning every loaded bluebook for candidates
48
+ # @return [void]
35
49
  def react(event, domain, only: nil)
36
50
  selected = only ? [only] : policies_for(event, domain)
37
51
  selected.each do |policy, home_domain|
@@ -195,14 +209,13 @@ module Hecks
195
209
  # The event's own identity is a fact too, not only its payload. A
196
210
  # for_each query commonly filters by the emitting record's own
197
211
  # identity (`OpenForCustomer`'s own `reference:`, scoping by the
198
- # very customer who was just suspended) — which used to arrive for
199
- # free because legacy dispatch left the self-addressing key riding
200
- # along in `event.payload` unfiltered. Routing separated from
201
- # payload (`to:`/`with:`, what the facade's own bang-methods always
202
- # use) correctly stopped carrying it there, which left this query
203
- # silently seeing neither the field it needs nor any error saying
204
- # why — an empty result read as "nothing to freeze" instead of "the
205
- # customer" the whole reaction exists to catch.
212
+ # very customer who was just suspended) — a fact routing keeps
213
+ # separate from payload (`to:`/`with:`, what the facade's own
214
+ # bang-methods always use), so `event.payload` alone does not carry
215
+ # it. Without this, the query would silently see neither the field
216
+ # it needs nor any error saying why — an empty result read as
217
+ # "nothing to freeze" instead of "the customer" the whole reaction
218
+ # exists to catch.
206
219
  #
207
220
  # Merged in only when the query declares an argument by that exact
208
221
  # name and the emitting aggregate's own identity is genuinely what
@@ -261,7 +274,7 @@ module Hecks
261
274
  label: "#{policy.name}'s trigger"
262
275
  )
263
276
 
264
- # **The raw inputs `args` was resolved from** — same additive,
277
+ # The raw inputs `args` was resolved from — same additive,
265
278
  # Ruby-only shape SagaInterpreter#deliver_saga_dispatch's own
266
279
  # saga_dispatch_log gets, for Properties.dispatch_binding_
267
280
  # fidelity's own independent re-derivation of Policy#with_spec's
@@ -25,6 +25,10 @@ module Hecks
25
25
  refuse_unknown_arguments refuse_absent_arguments normalize_args resolve_references resolve_route emit
26
26
  ].freeze
27
27
 
28
+ # @param registry [Runtime::Registry] the booted registry this interpreter dispatches
29
+ # against
30
+ # @param rules [Runtime::CommandRules] the shared command-rule checks (references) this
31
+ # interpreter's steps call
28
32
  def initialize(registry, rules:)
29
33
  @registry = registry
30
34
  @rules = rules
@@ -32,6 +36,22 @@ module Hecks
32
36
 
33
37
  # `invocation` — the `Runtime::Invocation` `Dispatcher` built;
34
38
  # `ctx.args` is its `to_args`, `ctx.route` its `target`.
39
+ #
40
+ # @param domain [String] the domain the aggregate belongs to
41
+ # @param aggregate [Bluebook::Aggregate] the aggregate the port operation belongs to
42
+ # @param operation [Bluebook::PortOperation] the port operation to dispatch
43
+ # @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for this call
44
+ # @return [Array<Runtime::Event>] the events recorded: the operation's own `emits` for
45
+ # an inbound operation, or a single `answers`/`refuses` event for an outbound one
46
+ # (an adapter failure is recorded as a `refuses` event, not raised)
47
+ # @raise [Runtime::UnknownArgument] if `invocation` offers an argument the operation
48
+ # does not declare
49
+ # @raise [Runtime::AbsentArgument] if `invocation` omits a non-optional declared
50
+ # argument
51
+ # @raise [Runtime::TypeMismatch] if an offered argument does not coerce to its declared
52
+ # type
53
+ # @raise [Runtime::NotFound] if an offered reference or the operation's own receiving
54
+ # record cannot be found
35
55
  def call(domain, aggregate, operation, invocation)
36
56
  ctx = Context.new(domain, aggregate, operation, invocation.to_args)
37
57
  ctx.invocation = invocation
@@ -22,10 +22,26 @@ module Hecks
22
22
  class QueryInterpreter
23
23
  attr_reader :registry
24
24
 
25
+ # @param registry [Runtime::Registry] the booted registry queries are answered
26
+ # against
25
27
  def initialize(registry)
26
28
  @registry = registry
27
29
  end
28
30
 
31
+ # Answers one declared aggregate or entity query, preferring a native adapter
32
+ # hook and falling back to interpreting the query over every loaded record.
33
+ #
34
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
35
+ # @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
36
+ # @param query_name [String] the query's declared name, or an entity query's
37
+ # dotted `"Entity.Query"` name
38
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
39
+ # @return [Array<Hash>] one frozen Hash per matching record, its state with
40
+ # `:id` merged in last
41
+ # @raise [Runtime::UnknownVerb] if `query_name` names no declared query
42
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
43
+ # declared type
44
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
29
45
  def call(domain, aggregate, query_name, args)
30
46
  return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
31
47
 
@@ -55,7 +71,7 @@ module Hecks
55
71
  # Instance#to_h's own comment: an aggregate free to declare its
56
72
  # own attribute literally named `id` has that attribute's own
57
73
  # wrapped value sitting in `record.state[:id]` already; merging
58
- # it over a `{id:}.merge(state)` used to let it silently
74
+ # it over a `{id:}.merge(state)` would let it silently
59
75
  # clobber the correct bare identity this row is supposed to
60
76
  # carry.
61
77
  # A query row is an answer, not a handle. Mutating one edits
@@ -81,6 +97,18 @@ module Hecks
81
97
  # adapters, but the fold itself — the empty candidate set, a
82
98
  # duplicate id, a dangling reference, a chain's inside-out
83
99
  # resolution order — would only ever be compared against itself.
100
+ #
101
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
102
+ # @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
103
+ # @param query_name [String] the query's declared name, or an entity query's
104
+ # dotted `"Entity.Query"` name
105
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
106
+ # @return [Array<Hash>] one Hash per matching record, its state with `:id`
107
+ # merged in last
108
+ # @raise [Runtime::UnknownVerb] if `query_name` names no declared query
109
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
110
+ # declared type
111
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
84
112
  def reference_call(domain, aggregate, query_name, args)
85
113
  return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
86
114
 
@@ -104,9 +132,9 @@ module Hecks
104
132
  ordered = ordered(matched, declared.order_by, declared.null_semantics)
105
133
  # **Offset first, then limit** — the order SQL means by `LIMIT n
106
134
  # OFFSET m`, and the order Ports::Query::InMemory#execute already
107
- # applies (see that file's own comment). This interpreter used to
108
- # never read declared.offset at all — offset silently vanished for
109
- # any query answered here, not just come out reversed.
135
+ # applies (see that file's own comment). Without reading
136
+ # `declared.offset`, offset would silently vanish for any query
137
+ # answered here, not just come out reversed.
110
138
  skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
111
139
  capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
112
140
 
@@ -249,12 +277,12 @@ module Hecks
249
277
  end
250
278
 
251
279
  # The comparator table itself lives in
252
- # QuerySpecification::Common::Comparison. This method and
253
- # Ports::Query::InMemory#holds? used to carry a copy each and the
254
- # two drifted — `none_in_state` reached only one of them, and
255
- # `comparable` disagreed about value objects with two numeric
256
- # members. What stays here is how a value is reached for this
257
- # path: the registry is instance state rather than an argument.
280
+ # QuerySpecification::Common::Comparison, shared with
281
+ # Ports::Query::InMemory#holds? rather than each carrying its own
282
+ # copy — two copies once drifted: `none_in_state` reached only one
283
+ # of them, and `comparable` disagreed about value objects with two
284
+ # numeric members. What stays here is how a value is reached for
285
+ # this path: the registry is instance state rather than an argument.
258
286
  def holds?(clause, held, args, record: nil, domain: nil)
259
287
  QuerySpecification::Common::Comparison.holds?(
260
288
  clause.op, comparable(held), comparable(resolve_query_value(clause.value, args)), registry: @registry
@@ -273,8 +301,8 @@ module Hecks
273
301
  # the same way a command argument's own is, so a `nil` offered for
274
302
  # a non-optional value-object-typed query attribute
275
303
  # (Governance::RoleAssignment.AssignmentsForActor's `actor_id`, say)
276
- # has to refuse — passing it through unchecked (as this used to)
277
- # let it through as a silent, unfiltered query instead, a real
304
+ # has to refuse — passing it through unchecked would let it through
305
+ # as a silent, unfiltered query instead, a real
278
306
  # Ruby/Rust divergence the fuzzer caught (QualityControl BUG#2).
279
307
  #
280
308
  # `checked_vo?` true is handled by `null_vo_argument!` directly,
@@ -6,10 +6,10 @@ require_relative "value"
6
6
  module Hecks
7
7
  module Runtime
8
8
  # Turns facts selected by a policy or process manager into the same
9
- # receiver/payload envelope an outside caller uses. Reaction declarations
10
- # historically selected both through one `with:` map, so this is the one
11
- # compatibility seam that separates receiver identities from facts after
12
- # resolving the declaration and before re-entering the dispatcher.
9
+ # receiver/payload envelope an outside caller uses. A reaction declares
10
+ # both receiver identities and facts through one `with:` map, so this is
11
+ # the one compatibility seam that separates them after resolving the
12
+ # declaration and before re-entering the dispatcher.
13
13
  module ReactionInvocation
14
14
  Target = Struct.new(:aggregate, :entities, :command, keyword_init: true)
15
15
  # :facts, not :values — Struct.new already defines #values (every
@@ -19,10 +19,17 @@ module Hecks
19
19
 
20
20
  module_function
21
21
 
22
- # The holding IR historically represented both an omitted projection and
23
- # an explicitly empty `with: {}` as the same empty array. Builders now
24
- # preserve declaration presence off-wire; reconstructed/legacy IR falls
25
- # back to the old non-empty reading.
22
+ # Reports whether a reaction declared an explicit `with:` projection.
23
+ #
24
+ # An omitted projection and an explicitly empty `with: {}` both hold
25
+ # as the same empty array on the holding IR. Builders preserve
26
+ # declaration presence off-wire, in `@projection_declared`;
27
+ # reconstructed/legacy IR without that ivar falls back to reading
28
+ # presence off whether `with_spec` is non-empty.
29
+ #
30
+ # @param declaration [Bluebook::Policy, Bluebook::DispatchSpec] the reacting
31
+ # declaration to check
32
+ # @return [Boolean] true if the declaration names an explicit `with:` projection
26
33
  def projection_declared?(declaration)
27
34
  if declaration.instance_variable_defined?(:@projection_declared)
28
35
  declaration.instance_variable_get(:@projection_declared)
@@ -31,11 +38,27 @@ module Hecks
31
38
  end
32
39
  end
33
40
 
41
+ # Resolves a declared `with:` projection against the scopes and bindings visible to it.
42
+ #
34
43
  # A reaction's source names resolve lexically, not globally. Policies
35
44
  # supply one event/row scope. Process managers supply current event then
36
45
  # opening-event memory, while correlation is an explicit binding ahead
37
46
  # of both. Missing names are refused here rather than materialized as nil
38
47
  # and accidentally presented as target command facts.
48
+ #
49
+ # @param with_spec [Hash{Symbol => Object}] each target fact name mapped to its source:
50
+ # a Symbol naming a fact visible in `bindings` or `scopes`, or any other value taken
51
+ # as a literal
52
+ # @param scopes [Array<Hecks::Runtime::ReactionInvocation::Scope, Array(String,
53
+ # Hash)>] the named fact scopes to resolve a Symbol source against, checked in order;
54
+ # a bare `[name, facts]` pair is wrapped into a `Scope`
55
+ # @param bindings [Hash] explicit bindings (such as a saga's correlation key), checked
56
+ # before any scope
57
+ # @param label [String] names this resolution in an `UnknownArgument` refusal
58
+ # @return [Hash{Symbol => Object}] `with_spec`'s keys mapped to their resolved,
59
+ # materialized values
60
+ # @raise [Runtime::UnknownArgument] if a Symbol source names a fact visible in no
61
+ # binding and no scope
39
62
  def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
40
63
  normalized_bindings = bindings.transform_keys(&:to_sym)
41
64
  normalized_scopes = scopes.map do |scope|
@@ -86,6 +109,28 @@ module Hecks
86
109
  # piece that is self-contained; what remains is the sequencing
87
110
  # itself, which further splitting would only relocate, not remove.
88
111
  # rubocop:disable-next Metrics/MethodLength, Metrics/PerceivedComplexity
112
+ #
113
+ # @param registry [Runtime::Registry] the booted registry to resolve `verb` against
114
+ # @param verb [String] the fully qualified target command verb
115
+ # @param projected [Hash] the facts to send, already resolved (e.g. by
116
+ # `resolve_mapping`) or, for a legacy reaction, the raw event/row payload
117
+ # @param explicit [Boolean] true when the reaction declared its own `with:` projection
118
+ # (`projection_declared?`); false forwards `projected` wholesale as legacy args
119
+ # @param passthrough [Array<String, Symbol>] extra fact names allowed to ride along
120
+ # unconsumed, beyond the receiver identity and declared command facts
121
+ # @param source_receiver [Hash{Symbol => Object}, nil] the triggering event's own
122
+ # `{aggregate:, identity:}`, offered as a same-aggregate receiver when nothing else
123
+ # supplies one; nil when there is no such event to inherit from
124
+ # @return [Hash{Symbol => Object}] `{to:, with:}` for an explicit projection targeting
125
+ # a non-creating command (`with:` only for a creating command); otherwise `projected`
126
+ # (with `to:` merged in when a receiver could be inherited)
127
+ # @raise [Runtime::UnknownVerb] if `verb` does not resolve to a declared command,
128
+ # entity command, or port operation (only when `explicit` is true; a legacy call
129
+ # resolving `verb` only to check inheritance swallows this and forwards unchanged)
130
+ # @raise [Runtime::TypeMismatch] if an explicit projection resolves no receiver
131
+ # identity for the target aggregate or one of its entities
132
+ # @raise [Runtime::UnknownArgument] if an explicit projection's facts include a name
133
+ # that is neither a consumed receiver identity nor a declared command fact
89
134
  def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
90
135
  args = projected.transform_keys(&:to_sym)
91
136
  unless explicit
@@ -16,16 +16,32 @@ module Hecks
16
16
  # model is simple enough for one; otherwise runs the whole join
17
17
  # in-process against loaded records.
18
18
  class ReadModelInterpreter
19
+ # @param registry [Runtime::Registry] the booted registry whose repositories
20
+ # this interpreter reads
19
21
  def initialize(registry) = @registry = registry
20
22
 
23
+ # Runs one declared read model and returns its projected rows.
24
+ #
25
+ # @param domain [String, Symbol] the domain the read model is declared in
26
+ # @param model [Bluebook::ReadModel] the read model to run
27
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
28
+ # @return [Array<Hash>] a one-element Array holding a Hash of head name to
29
+ # projected rows (or a single row, for a non-`:many` head)
30
+ # @raise [Runtime::TypeMismatch] if the reference argument is offered as a whole
31
+ # object rather than a plain identity, or a `median` field is not numeric
32
+ # @raise [Runtime::NotFound] if the reference argument names no record
33
+ # @raise [KeyError] if a rooted read model is asked without its reference argument
34
+ # @raise [ArgumentError] if `group_by` or `median` names a field its target
35
+ # aggregate does not declare
36
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
21
37
  def call(domain, model, args)
22
38
  project(domain, model, args)
23
39
  end
24
40
 
25
41
  private
26
42
 
27
- # **Root-first, then the SQLite escape hatch, then the join loop** —
28
- # each step's own comment names a real, previously-shipped bug the
43
+ # Root-first, then the SQLite escape hatch, then the join loop —
44
+ # each step's own comment names a real, already-shipped bug the
29
45
  # current order fixes (the reference/TenantScope refusal ordering
30
46
  # above, the root-first head processing below). Splitting this
31
47
  # into smaller methods would scatter that ordering across method
@@ -77,11 +93,11 @@ module Hecks
77
93
  # bluebook. `read_model_builder.rb`'s own `include` is
78
94
  # documented "Order-independent" (the `:many` flag is resolved
79
95
  # at build time, once `@reference_target` is known), but that
80
- # promise was never kept here: this loop used to run heads in
81
- # their literal declared order and match each "many" head
82
- # against whatever was already in `projected` — empty, the
83
- # very first time through, if a many-side head happened to be
84
- # declared before the root. A real, live bug (not a guess):
96
+ # promise is not kept without this: running heads in
97
+ # their literal declared order and matching each "many" head
98
+ # against whatever was already in `projected` would leave it
99
+ # empty, the very first time through, if a many-side head happened
100
+ # to be declared before the root. A real, live bug (not a guess):
85
101
  # `include Promotion` before `include Item` on a read model
86
102
  # whose root is Item silently returned an empty array for
87
103
  # Promotion — no error, just a wrong, too-small answer — while