hecks 1.4.0 → 1.5.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 (267) 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.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
@@ -10,9 +10,19 @@ module Hecks
10
10
  # loading, and the smaller checks are also called piecemeal by the
11
11
  # repository factory.
12
12
  module Verification
13
+ # Runs the whole wiring gate against this registry's loaded bluebooks,
14
+ # hexagons, ports and adapters.
15
+ #
16
+ # @return [Runtime::Registry] self
17
+ # @raise [Runtime::WiringError] if a bind names an undeclared aggregate, an
18
+ # adapter cannot satisfy its port's verb or declared `answers`, a world
19
+ # setting names a field its adapter does not declare, the default adapter
20
+ # is unusable, or a command declares a role with no authorization provider
21
+ # attached
13
22
  def verify!
14
23
  verify_default_adapter!
15
24
  verify_singleton_port_answers!
25
+ refuse_cross_package_bluebook_merge!
16
26
 
17
27
  @hecksagons.each_value do |hexagon|
18
28
  refuse_ungoverned_roles!(hexagon)
@@ -50,6 +60,13 @@ module Hecks
50
60
  self
51
61
  end
52
62
 
63
+ # Checks that the framework-wide default persistence adapter (used by any
64
+ # aggregate left out of an explicit bind list) is itself wired correctly.
65
+ #
66
+ # @return [Runtime::Registry] self
67
+ # @raise [Runtime::WiringError] if the default adapter cannot satisfy the
68
+ # persistence port's verb, is missing a declared `answers` method, or has
69
+ # no Ruby implementation
53
70
  def verify_default_adapter!
54
71
  name = Ports::Persistence::DEFAULT_ADAPTER
55
72
 
@@ -68,6 +85,14 @@ module Hecks
68
85
  "aggregate with no bind could not be given one: #{e.message}"
69
86
  end
70
87
 
88
+ # Checks that `bind`'s adapter implements the port it names and satisfies
89
+ # the verb the bind declares.
90
+ #
91
+ # @param bind [Bluebook::Bind] the bind to check
92
+ # @return [void]
93
+ # @raise [Runtime::WiringError] if `bind`'s adapter is unknown, declares an
94
+ # unknown port, is missing a declared `answers` method, or cannot satisfy
95
+ # `bind`'s own verb
71
96
  def check_verb(bind)
72
97
  port = port_for(bind)
73
98
  check_answers(port, bind.adapter)
@@ -85,6 +110,13 @@ module Hecks
85
110
  # actually call. `answers` is optional per port (an empty list is
86
111
  # today's pre-existing behavior, unchecked), so this only ever
87
112
  # tightens a port that opted in.
113
+ #
114
+ # @param port [Bluebook::Port] the port whose declared `answers` methods
115
+ # `adapter_name` must respond to
116
+ # @param adapter_name [String] the adapter's declared name to check
117
+ # @return [void]
118
+ # @raise [Runtime::WiringError] if `adapter_name` has no Ruby implementation,
119
+ # or its implementation does not respond to one of `port.answers`
88
120
  def check_answers(port, adapter_name)
89
121
  answers = Array(port.answers)
90
122
  return if answers.empty?
@@ -114,6 +146,12 @@ module Hecks
114
146
  # exactly one adapter, wired, missing a method `answers` names.
115
147
  PER_AGGREGATE_PORTS = %w[persistence projection loading].freeze
116
148
 
149
+ # Checks every singleton port (not per-aggregate-bound) with exactly one
150
+ # wired adapter against its own declared `answers` methods.
151
+ #
152
+ # @return [Runtime::Registry] self
153
+ # @raise [Runtime::WiringError] if a singleton port's one wired adapter is
154
+ # missing one of its declared `answers` methods
117
155
  def verify_singleton_port_answers!
118
156
  @ports.each_value do |port|
119
157
  next if PER_AGGREGATE_PORTS.include?(port.name)
@@ -127,6 +165,16 @@ module Hecks
127
165
  self
128
166
  end
129
167
 
168
+ # Checks that every setting `settings` declares (besides `:adapter`) is a
169
+ # field `bind`'s adapter actually admits.
170
+ #
171
+ # @param bind [Bluebook::Bind] the bind naming the adapter to check against;
172
+ # a no-op if its adapter is unknown
173
+ # @param settings [Hash{Symbol => Object}] the world's declared settings for
174
+ # this bind
175
+ # @return [void]
176
+ # @raise [Runtime::WiringError] if `settings` declares a field `bind`'s
177
+ # adapter does not declare
130
178
  def check_settings(bind, settings)
131
179
  adapter = @adapters[bind.adapter]
132
180
  return unless adapter
@@ -141,6 +189,12 @@ module Hecks
141
189
  "Add the field to the adapter, or remove it from the world."
142
190
  end
143
191
 
192
+ # Finds the port `bind`'s adapter declares.
193
+ #
194
+ # @param bind [Bluebook::Bind] the bind naming the adapter to look up
195
+ # @return [Bluebook::Port] the port `bind`'s adapter declares
196
+ # @raise [Runtime::WiringError] if `bind` names an unknown adapter, or one
197
+ # declaring an unknown port
144
198
  def port_for(bind)
145
199
  adapter = @adapters[bind.adapter]
146
200
  raise WiringError, "unknown adapter #{bind.adapter.inspect}" unless adapter
@@ -149,6 +203,12 @@ module Hecks
149
203
  raise(WiringError, "adapter #{bind.adapter} declares unknown port #{adapter.port.inspect}")
150
204
  end
151
205
 
206
+ # Finds the Ruby module implementing the adapter declared `name`.
207
+ #
208
+ # @param name [String] the adapter's declared name, such as `"PostgresEra"`
209
+ # @return [Module] the adapter module or class under `Hecks::Adapters`
210
+ # @raise [Runtime::WiringError] if no Ruby implementation named `name` exists
211
+ # under `Hecks::Adapters`
152
212
  def adapter_class(name)
153
213
  Adapters.const_get(name)
154
214
  rescue NameError
@@ -164,9 +224,9 @@ module Hecks
164
224
  # unchecked, exactly the defect ADR 0025 §9 names ("role gates
165
225
  # access control by exact string equality ... Governance ...
166
226
  # connected to none of it"). Checked here, at `verify!` — recovered
167
- # and moved, not new: this used to run per-block, at hecksagon
168
- # build time (Bluebook::DSL::HecksagonBuilder#build), which broke
169
- # the moment a domain could be split across multiple hecksagon
227
+ # and moved, not new: running this per-block, at hecksagon
228
+ # build time (Bluebook::DSL::HecksagonBuilder#build), breaks
229
+ # the moment a domain is split across multiple hecksagon
170
230
  # blocks (base + an `environments/<name>.hecksagon` overlay,
171
231
  # Runtime::Loader.boot's `environment:` — see its own comment for
172
232
  # the recovery provenance): every block but the one declaring
@@ -179,7 +239,7 @@ module Hecks
179
239
  # correct (a check against an incomplete, not-yet-merged
180
240
  # hecksagon can never see the real final shape).
181
241
  #
182
- # A provider is RECOGNISED by its declaration, not its name —
242
+ # A provider is recognised by its declaration, not its name —
183
243
  # `authorization_provider_for` answers for the domain's own
184
244
  # chapter too, so Governance (which declares `provides
185
245
  # "authorization"`) passes here because of what it declares, and
@@ -217,6 +277,61 @@ module Hecks
217
277
  bluebook_ir.aggregates.flat_map { |aggregate| aggregate.commands + aggregate.entities.flat_map(&:commands) }
218
278
  end
219
279
 
280
+ # TWO UNRELATED PACKAGES, ONE CHAPTER NAME BY COINCIDENCE — the
281
+ # real risk `Registry#bluebook_sources` exists to catch (found
282
+ # live: a stale `vendor/hecksagain` fork's own copy of Governance/
283
+ # Identity/Deploy, still reachable on 4 consuming apps' own load
284
+ # paths alongside the real gem). `BluebookBuilder.build`'s own
285
+ # accumulation (several files declaring the SAME chapter name ON
286
+ # PURPOSE — `lib/hecks/language/bluebook/*.bluebook` all open
287
+ # `Hecks.bluebook "Bluebook"`) is never touched here — that merge
288
+ # stays unconditional, checked only AFTER every file has loaded,
289
+ # the same "check the merged final result once" timing
290
+ # `refuse_ungoverned_roles!` already uses and for the same reason
291
+ # (a check against an incomplete load can never see the real
292
+ # shape). What distinguishes intentional accumulation from
293
+ # coincidence is PACKAGE ROOT, not file identity: files a real
294
+ # gemspec or a `vendor/` boundary already treats as one unit are
295
+ # expected to share a name; files from two DIFFERENT roots never
296
+ # legitimately do.
297
+ def refuse_cross_package_bluebook_merge!
298
+ @bluebook_sources.each do |name, paths|
299
+ roots = paths.map { |path| package_root_for(path) }.uniq
300
+ next if roots.size <= 1
301
+
302
+ raise WiringError,
303
+ "#{name.inspect} is declared by more than one package: #{roots.join(' and ')} — " \
304
+ "these are two unrelated sources sharing a chapter name by coincidence, not one " \
305
+ "domain split across files, and merging their declarations into one chapter is " \
306
+ "almost certainly a stale/vendored copy left on the load path (paths: " \
307
+ "#{paths.join(', ')})"
308
+ end
309
+ end
310
+
311
+ # THE NEAREST BOUNDARY A PATH ALREADY BELONGS TO — a real
312
+ # gemspec (this IS a package, whatever depends on it or vendors
313
+ # it), or a bare `vendor/` path component, treated as its OWN
314
+ # root regardless of what gemspec might sit above it: vendored
315
+ # code should never be considered "the same package" as whatever
316
+ # it's vendored into, even when nothing else marks the boundary.
317
+ # Neither found, the path's own directory is the root — two
318
+ # files with no closer marker only "belong together" if they are
319
+ # literally the same file.
320
+ def package_root_for(path)
321
+ return path.to_s if path.nil?
322
+
323
+ dir = File.dirname(File.expand_path(path))
324
+ loop do
325
+ return "vendor:#{dir}" if File.basename(dir) == "vendor"
326
+ return dir if Dir.glob(File.join(dir, "*.gemspec")).any?
327
+
328
+ parent = File.dirname(dir)
329
+ return dir if parent == dir
330
+
331
+ dir = parent
332
+ end
333
+ end
334
+
220
335
  # A domain that declares a `process_manager` but whose
221
336
  # `saga_persistence` resolves to `NULL_SAGA_STORE` (no anchor
222
337
  # aggregate, a RemoteRuntime-shaped adapter, an adapter that
@@ -16,12 +16,17 @@ module Hecks
16
16
 
17
17
  attr_reader :root, :bluebooks, :hecksagons, :ports, :adapters, :worlds, :event_log,
18
18
  :reaction_log, :saga_log, :saga_instances, :translations, :saga_mutex,
19
- :saga_dispatch_log, :policy_dispatch_log
19
+ :saga_dispatch_log, :policy_dispatch_log, :bluebook_sources,
20
+ :pending_privacy_markings
20
21
 
22
+ # @param root [String, nil] the booting project's root directory, the base
23
+ # a shared ports/adapters root and a `.world`'s own relative paths resolve
24
+ # against; nil for a registry with no such root
21
25
  def initialize(root: nil)
22
26
  @root = root
23
27
  @bluebooks = {}
24
- @hecksagons = {}
28
+ @bluebook_sources = {}
29
+ @hecksagons = {}
25
30
  @ports = {}
26
31
  @adapters = {}
27
32
  @worlds = {}
@@ -29,6 +34,14 @@ module Hecks
29
34
  @event_log = []
30
35
  @reaction_log = []
31
36
  @saga_log = []
37
+ # A DECLARATIVE FACT, NOT YET A DISPATCHED ONE — `AggregateDoor#
38
+ # mark_sensitive` (called from a `.hecksagon` file, the same way
39
+ # `port`/`persisted_by` already are) appends here at
40
+ # hecksagon-build time; `Runtime::Loader.boot`'s own post-dispatcher
41
+ # step turns each entry into a real `Privacy::Marking.Mark`,
42
+ # idempotently, the same "declared here, taken effect once boot
43
+ # actually has a dispatcher" shape `redrive_outbox!` already has.
44
+ @pending_privacy_markings = []
32
45
  # **Additive, Ruby-only** — never merged into saga_log/reaction_log.
33
46
  # rust/src/kernel/orchestrate.rs ports those two arrays' exact
34
47
  # shape byte-for-byte (spec/rust_conformance_spec.rb's own
@@ -112,6 +125,14 @@ module Hecks
112
125
  # several files (`language/bluebook/*.bluebook`, all `Hecks.bluebook "Bluebook"`)
113
126
  # needs its declarations to accumulate into one builder rather than each
114
127
  # file minting its own and silently discarding the one before.
128
+ #
129
+ # @param name [String, Symbol] the chapter name the builder accumulates
130
+ # declarations for
131
+ # @yield the block that mints a fresh builder, called only the first time
132
+ # `name` is asked for
133
+ # @yieldreturn [Bluebook::DSL::BluebookBuilder] a fresh builder for `name`
134
+ # @return [Bluebook::DSL::BluebookBuilder] the builder already open for
135
+ # `name`, or the block's freshly minted one on the first call
115
136
  def bluebook_builder(name)
116
137
  @bluebook_builders[name.to_s] ||= yield
117
138
  end
@@ -130,20 +151,53 @@ module Hecks
130
151
  # registry/saga_persistence.rb) there is no concurrent caller for
131
152
  # `Hecks/ThreadSharedIvarMutation` to actually be warning about here.
132
153
  # rubocop:disable Hecks/ThreadSharedIvarMutation
154
+ # Registers a loaded chapter, keyed by its own declared name.
155
+ #
156
+ # @param item [Bluebook::Chapter] the loaded, judged chapter
157
+ # @return [Bluebook::Chapter] `item`, unchanged
133
158
  def add_bluebook(item) = @bluebooks[item.name] = item
134
159
 
160
+ # PROVENANCE, SIDE-CHANNEL — which real `.bluebook` file(s)
161
+ # contributed to a chapter name, never part of the exported IR (a
162
+ # boot-time loading fact, not a domain fact) and never Rust-mirrored
163
+ # (the same "additive, Ruby-only" shape `@translations` above already
164
+ # is). Legitimate accumulation (several files declaring the SAME
165
+ # chapter name on purpose — `lib/hecks/language/bluebook/*.bluebook`
166
+ # all open `Hecks.bluebook "Bluebook"`) pushes more than one path
167
+ # here too; that alone is not a problem. What this exists to let
168
+ # `refuse_cross_package_bluebook_merge!` (registry/verification.rb)
169
+ # catch is TWO UNRELATED PACKAGES accumulating into the same name by
170
+ # coincidence — a stale vendored fork's own copy of a real gem's
171
+ # chapter, still reachable on the load path, silently merging its
172
+ # aggregates into the real one via this exact accumulation mechanism.
173
+ def record_bluebook_source(name, path)
174
+ (@bluebook_sources[name.to_s] ||= []) << path
175
+ end
176
+
135
177
  # Merged, not replaced — recovered, not new (see Runtime::Loader
136
178
  # .boot's own comment for the provenance). A domain's hecksagon can
137
179
  # now load in more than one block for the same domain (base file
138
180
  # plus an `environments/<name>.hecksagon` overlay), and the second
139
181
  # block should add to what the first declared, not silently
140
182
  # discard it.
183
+ #
184
+ # @param item [Bluebook::Hecksagon] the declared wiring to register
185
+ # @return [void]
141
186
  def add_hecksagon(item)
142
187
  existing = @hecksagons[item.domain]
143
188
  @hecksagons[item.domain] = existing ? merge_hecksagons(existing, item) : item
144
189
  end
145
190
 
191
+ # Registers a loaded port, keyed by its own declared name.
192
+ #
193
+ # @param item [Bluebook::Port, Bluebook::DomainPort] the loaded port
194
+ # @return [Bluebook::Port, Bluebook::DomainPort] `item`, unchanged
146
195
  def add_port(item) = @ports[item.name] = item
196
+
197
+ # Registers a loaded adapter, keyed by its own declared name.
198
+ #
199
+ # @param item [Bluebook::Adapter] the loaded, judged adapter
200
+ # @return [Bluebook::Adapter] `item`, unchanged
147
201
  def add_adapter(item) = @adapters[item.name] = item
148
202
 
149
203
  # Merged, not replaced — the same generalization for `World` that
@@ -156,12 +210,40 @@ module Hecks
156
210
  # bare verb key and the `"verb:adapter"` qualified key point at the
157
211
  # same resolved hash) — an overlay's key wins over the base's same
158
212
  # key; a key only the base declares survives untouched.
213
+ #
214
+ # @param item [Bluebook::World] the declared world settings to register
215
+ # @return [void]
159
216
  def add_world(item)
160
217
  existing = @worlds[item.domain]
161
218
  @worlds[item.domain] = existing ? merge_worlds(existing, item) : item
162
219
  end
163
220
 
221
+ # Registers a loaded translation.
222
+ #
223
+ # @param item [Bluebook::Translation] the loaded, judged translation
224
+ # @return [Array<Bluebook::Translation>] every translation registered so far,
225
+ # `item` last
164
226
  def add_translation(item) = @translations << item
227
+
228
+ # Declares one attribute of one domain's own aggregate sensitive — called from a
229
+ # terminal `has_<category>(readable_by:)` on a `Bluebook::DSL::AttributePath`
230
+ # (reached by chaining off a bare `Domain::Aggregate` inside a `.hecksagon` file
231
+ # being `Kernel.load`ed, or off an already-installed `AggregateDoor`), same timing
232
+ # (and same thread-safety argument, above) as `add_bluebook`/`add_port`. Recorded,
233
+ # not dispatched: `Runtime::Loader.boot`'s own `seed_privacy_markings!` turns each
234
+ # entry into a real `Privacy::Marking.Mark` once a dispatcher exists.
235
+ #
236
+ # @param domain [String] the marked attribute's own aggregate FQN, e.g.
237
+ # `"Lifeadelics::Registration"`
238
+ # @param attribute_path [String] the dotted path within that aggregate, e.g.
239
+ # `"attendee.medications"`
240
+ # @param category [String] the marking's own sensitivity category, e.g. `"phi"`
241
+ # @param readable_by [String] the Governance role a read must hold, unredacted
242
+ # @return [void]
243
+ def add_pending_privacy_marking(domain:, attribute_path:, category:, readable_by:)
244
+ @pending_privacy_markings << { domain: domain, attribute_path: attribute_path,
245
+ category: category, readable_by: readable_by }
246
+ end
165
247
  # rubocop:enable Hecks/ThreadSharedIvarMutation
166
248
 
167
249
  # {domain name => era ordinal} as resolved by the boot-time era
@@ -176,10 +258,30 @@ module Hecks
176
258
  # an old checkout only — see `initialize`'s own comment on it.
177
259
  attr_reader :superseded_eras
178
260
 
261
+ # Finds a loaded chapter by name.
262
+ #
263
+ # @param name [String, Symbol] the chapter's declared name
264
+ # @return [Bluebook::Chapter, nil] the chapter, or nil if none is registered
265
+ # under `name`
179
266
  def bluebook(name) = @bluebooks[name.to_s]
267
+
268
+ # Finds a domain's registered wiring by name.
269
+ #
270
+ # @param name [String, Symbol] the domain name
271
+ # @return [Bluebook::Hecksagon, nil] the domain's wiring, or nil if none is
272
+ # registered under `name`
180
273
  def hecksagon(name) = @hecksagons[name.to_s]
274
+
275
+ # Finds a domain's registered world settings by name.
276
+ #
277
+ # @param name [String, Symbol] the domain name
278
+ # @return [Bluebook::World, nil] the domain's world, or nil if none is
279
+ # registered under `name`
181
280
  def world(name) = @worlds[name.to_s]
182
281
 
282
+ # Every verb every loaded chapter declares, sorted.
283
+ #
284
+ # @return [Array<String>] every declared verb, across every loaded chapter
183
285
  def verbs = @bluebooks.values.flat_map(&:verbs).sort
184
286
 
185
287
  # The chapter that answers a role check for `domain` — the domain's
@@ -188,6 +290,10 @@ module Hecks
188
290
  # every check for the literal name "Governance": Governance is
189
291
  # recognised by what it declares, and a chapter that declares the
190
292
  # same thing is recognised the same way.
293
+ #
294
+ # @param domain [String, Symbol] the domain whose role checks are being resolved
295
+ # @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s role
296
+ # checks, or nil if none does
191
297
  def authorization_provider_for(domain)
192
298
  names = [domain.to_s, *Array(hecksagon(domain)&.framework_members)]
193
299
  names.filter_map { |name| bluebook(name) }
@@ -195,10 +301,24 @@ module Hecks
195
301
  end
196
302
 
197
303
  # Every loaded chapter declaring `provides "authorization"`.
304
+ #
305
+ # @return [Array<Bluebook::Chapter>] every loaded chapter that provides
306
+ # authorization
198
307
  def authorization_providers
199
308
  @bluebooks.values.select { |chapter| chapter.provides?(Bluebook::Capabilities::AUTHORIZATION) }
200
309
  end
201
310
 
311
+ # Resolves and memoizes `aggregate`'s authoritative repository.
312
+ #
313
+ # @param domain [String, Symbol] name of the domain `aggregate` belongs to
314
+ # @param aggregate [Bluebook::Aggregate] the aggregate to resolve a repository for
315
+ # @return [Persistence::AppendOnly] repository over the aggregate's authoritative
316
+ # adapter, or over a `Memory` adapter when the domain declares no hecksagon
317
+ # @raise [Runtime::WiringError] if the aggregate has no authoritative bind, more than
318
+ # one, or a bind with a role this port does not support; or if the bound adapter is
319
+ # unknown, answers a different verb, is given a setting it does not declare, has no
320
+ # Ruby implementation, or lacks a method its port's `answers` list or the
321
+ # append-only contract (`append`, `project`, `entries`) requires
202
322
  def repository(domain, aggregate)
203
323
  @repositories[[domain.to_s, aggregate.hecks_name]] ||= Ports::Persistence.repository(self, domain, aggregate)
204
324
  end
@@ -216,8 +336,8 @@ module Hecks
216
336
  # that store again, the way `Loader.boot_files` does after
217
337
  # `verify!`.
218
338
  #
219
- # What this is for: a test runner that used to boot a runtime per
220
- # test to get isolation (`Behaviors::Expectations.run_one`) — ~2s a
339
+ # What this is for: a test runner that would otherwise boot a runtime
340
+ # per test to get isolation (`Behaviors::Expectations.run_one`) — ~2s a
221
341
  # boot, 76 chess behaviours = two and a half minutes of booting the
222
342
  # same two files — can now boot once and reset between tests.
223
343
  #
@@ -229,6 +349,8 @@ module Hecks
229
349
  # single-threaded example loop. No production dispatch path calls
230
350
  # this at all — a live Puma worker pool never resets a registry out
231
351
  # from under itself mid-flight.
352
+ #
353
+ # @return [Runtime::Registry] self
232
354
  # rubocop:disable-next Hecks/ThreadSharedIvarMutation
233
355
  def reset_runtime_state!
234
356
  @event_log.clear
@@ -250,6 +372,17 @@ module Hecks
250
372
  # gives, just without a lazy `||=` race on standing it up.
251
373
  attr_reader :capability_graph
252
374
 
375
+ # Resolves and memoizes the repository to read `aggregate` from — a caught-up
376
+ # projection when one is bound and current, otherwise the authoritative repository.
377
+ #
378
+ # @param domain [String, Symbol] name of the domain `aggregate` belongs to
379
+ # @param aggregate [Bluebook::Aggregate] the aggregate to resolve a read
380
+ # repository for
381
+ # @return [Persistence::AppendOnly] the projection repository when one is bound
382
+ # and caught up with the authoritative store; the authoritative repository
383
+ # otherwise
384
+ # @raise [Runtime::WiringError] if the authoritative or projection bind cannot
385
+ # be resolved
253
386
  def read_repository(domain, aggregate)
254
387
  key = [domain.to_s, aggregate.hecks_name]
255
388
  binding = Ports::Projection.binds_for(self, domain, aggregate).first
@@ -264,6 +397,15 @@ module Hecks
264
397
  projection_current?(projection, authoritative) ? projection : authoritative
265
398
  end
266
399
 
400
+ # Reports whether `projection`'s own journal entries and rows agree with
401
+ # `authoritative`'s, entry-for-entry.
402
+ #
403
+ # @param projection [Persistence::AppendOnly] the projection repository to check
404
+ # @param authoritative [Persistence::AppendOnly] the authoritative repository to
405
+ # check `projection` against
406
+ # @return [Boolean] true when `projection` holds the same entries and rows as
407
+ # `authoritative`, in the same order; false on any mismatch, or if comparing
408
+ # them raises
267
409
  def projection_current?(projection, authoritative)
268
410
  projected_entries = projection.entries
269
411
  source_entries = authoritative.entries
@@ -288,6 +430,12 @@ module Hecks
288
430
  # authoritative bind" check is what actually catches a genuine
289
431
  # double-bind; this merge only concatenates, it does not itself
290
432
  # decide which of two binds for the same aggregate wins.
433
+ #
434
+ # @param base [Bluebook::Hecksagon] the domain's already-registered wiring
435
+ # @param overlay [Bluebook::Hecksagon] the newly loaded block's own wiring to
436
+ # fold in
437
+ # @return [Bluebook::Hecksagon] a new wiring with every list-shaped fact
438
+ # concatenated, `base` then `overlay`
291
439
  def merge_hecksagons(base, overlay)
292
440
  Bluebook::Hecksagon.new(
293
441
  domain: base.domain,
@@ -305,6 +453,11 @@ module Hecks
305
453
  # also declares replaces that key's whole resolved hash (the same
306
454
  # all-or-nothing shape `WorldBuilder#method_missing` already builds
307
455
  # each entry as), it does not deep-merge field by field within it.
456
+ #
457
+ # @param base [Bluebook::World] the domain's already-registered world
458
+ # @param overlay [Bluebook::World] the newly loaded block's own world to fold in
459
+ # @return [Bluebook::World] a new world with `overlay`'s scalars winning when
460
+ # present, and `settings` shallow-merged, `overlay`'s keys winning
308
461
  def merge_worlds(base, overlay)
309
462
  Bluebook::World.new(
310
463
  domain: base.domain,
@@ -16,7 +16,7 @@ module Hecks
16
16
  # closely enough that everything built on top of a dispatcher —
17
17
  # `Handle`, `AggregateDoor`, `Facade::Surface` — needs no changes
18
18
  # at all: `Handle#run`'s entire contract is
19
- # `@dispatcher.dispatch("#{fqn}.#{command}", **identity, **args).instance.state`,
19
+ # `@dispatcher.dispatch("#{fqn}.#{command}", to: @id, with: args).instance.state`,
20
20
  # and both classes answer that identically.
21
21
  #
22
22
  # **Reads delegate, writes don't**. `query`/`reference_query` hand off
@@ -31,12 +31,27 @@ module Hecks
31
31
  # against incomplete local state and then merely persisted.
32
32
  class RemoteDispatcher
33
33
  Result = Struct.new(:verb, :instance, :events, keyword_init: true) do
34
+ # Reads the identity of the record the dispatch settled on.
35
+ #
36
+ # @return [String] the settled record's identity
34
37
  def id = instance.id
38
+
39
+ # Reads the settled record's attributes as one Hash.
40
+ #
41
+ # @return [Hash{Symbol => Object}] the settled record's attributes, `:id` merged
42
+ # in last
35
43
  def state = instance.to_h
36
44
  end
37
45
 
38
46
  attr_reader :registry
39
47
 
48
+ # @param registry [Runtime::Registry] the booted registry this dispatcher fronts;
49
+ # read-side calls (`query`/`reference_query`) delegate to a local `Dispatcher`
50
+ # built over the same registry
51
+ # @param region [String] the AWS region the routed Lambda function lives in
52
+ # @param function [String, nil] the `.world`'s own `dispatched_by("Lambda")`
53
+ # function name, when the deployment stack is not named `hecks-<domain>`; nil
54
+ # resolves the function name from `ENV["DOMAIN_NAME"]` or `registry.root`
40
55
  def initialize(registry, region: "us-east-1", function: nil)
41
56
  @registry = registry
42
57
  # `File.basename(registry.root)`, not `bluebooks.keys.first` —
@@ -62,14 +77,55 @@ module Hecks
62
77
  @local = Dispatcher.new(registry)
63
78
  end
64
79
 
65
- # Same deprecation as `Dispatcher#dispatch` — loose keyword facts warn;
66
- # `to:`/`with:` do not.
80
+ # Dispatches a command by verb, routing to the local `Dispatcher` or the
81
+ # remote Lambda depending on the aggregate's bound adapter.
82
+ #
83
+ # Same shape as `Dispatcher#dispatch_flat` — everything but
84
+ # `saga_correlation:` is forwarded through unread, `to:`/`with:`
85
+ # included, and lifted out downstream by whichever path actually
86
+ # dispatches (`@local.dispatch_flat` locally, the flat wire form
87
+ # remotely). Not the strict `to:`/`with:`-only door `Dispatcher#
88
+ # dispatch` is — see that class's own comment for why this file
89
+ # never had one.
90
+ #
91
+ # @param verb [String] the fully qualified verb, `"Domain::Aggregate.Command"`
92
+ # or `"Domain::Aggregate.Entity.Command"`
93
+ # @param saga_correlation [Hash, nil] correlation head => value, stamped on every
94
+ # emitted event when a saga leg causes this dispatch; nil otherwise
95
+ # @param args [Hash] the facts, plus optional `:to`/`:with` keys, read the same
96
+ # way `dispatch_flat` reads them
97
+ # @return [RemoteDispatcher::Result] the verb, settled instance and emitted events
98
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
99
+ # domain or aggregate that is not declared
100
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
101
+ # locally-routed aggregate refuses the call
102
+ # @raise [Runtime::StaleWrite] if concurrent local writers beat this one through
103
+ # every retry, for a locally-routed aggregate
104
+ # @raise [Runtime::WiringError] if the aggregate's adapter or repository cannot be
105
+ # resolved, or the remote call is accepted but reports no mutation for it
106
+ # @raise [Runtime::RemoteRefusal] if the routed Lambda refuses the call
67
107
  def dispatch(verb, saga_correlation: nil, **args)
68
- Dispatcher.deprecate_loose_facts(args.except(:to, :with))
69
108
  dispatch_flat(verb, args.merge(saga_correlation: saga_correlation))
70
109
  end
71
110
 
72
- # Same flat-facts wire form as `Dispatcher#dispatch_flat`.
111
+ # Routes a dispatch to the local `Dispatcher` when the aggregate's bound
112
+ # adapter is not remote-backed (`Ports::Persistence::RemoteRuntime`),
113
+ # otherwise dispatches through the routed Lambda. Same flat-facts wire
114
+ # form as `Dispatcher#dispatch_flat`.
115
+ #
116
+ # @param verb [String] the fully qualified verb, in any shape `dispatch` accepts
117
+ # @param args [Hash] the facts, plus optional Symbol keys `:to`, `:with` and
118
+ # `:saga_correlation`, read as `dispatch`'s keywords of the same names; not mutated
119
+ # @return [RemoteDispatcher::Result] the verb, settled instance and emitted events
120
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
121
+ # domain or aggregate that is not declared
122
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
123
+ # locally-routed aggregate refuses the call
124
+ # @raise [Runtime::StaleWrite] if concurrent local writers beat this one through
125
+ # every retry, for a locally-routed aggregate
126
+ # @raise [Runtime::WiringError] if the aggregate's adapter or repository cannot be
127
+ # resolved, or the remote call is accepted but reports no mutation for it
128
+ # @raise [Runtime::RemoteRefusal] if the routed Lambda refuses the call
73
129
  def dispatch_flat(verb, args = {})
74
130
  args = args.dup
75
131
  saga_correlation = args.delete(:saga_correlation)
@@ -125,16 +181,46 @@ module Hecks
125
181
  Result.new(verb: verb, instance: instance, events: step_events(response))
126
182
  end
127
183
 
184
+ # Delegates to the local `Dispatcher` built over the same registry — see the
185
+ # class comment on why reads, unlike writes, need no remote-specific logic.
186
+ #
187
+ # @param verb [String, Symbol] the query's verb, in one of `Dispatcher#query`'s
188
+ # three shapes
189
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
190
+ # @return [Array<Hash>] see `Dispatcher#query`'s own return
191
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
192
+ # domain, aggregate, entity, query or read model that is not declared
193
+ # @raise [Runtime::NotFound] if a read model's root reference names no record
194
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
195
+ # @raise [KeyError] if a rooted read model is asked without its reference argument
196
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
128
197
  def query(verb, **args) = @local.query(verb, **args)
198
+
199
+ # Delegates to the local `Dispatcher` built over the same registry — see the
200
+ # class comment on why reads, unlike writes, need no remote-specific logic.
201
+ #
202
+ # @param verb [String] the fully qualified query verb, `"Domain::Aggregate.Query"`
203
+ # or `"Domain::Aggregate.Entity.Query"`
204
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
205
+ # @return [Array<Hash>] one Hash per matching record, its state with `:id` merged
206
+ # in last; for an entity query, one Hash per matching element
207
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
208
+ # domain, aggregate, entity or query that is not declared
209
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
210
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
129
211
  def reference_query(verb, **args) = @local.reference_query(verb, **args)
130
212
 
131
- # The full domain's event history, on every call — `{"read":
213
+ # Fetches the full domain's event history, on every call — `{"read":
132
214
  # true}` replays the whole journal (Phase 1's `dispatch::read`),
133
215
  # so its own `events` array already is the complete log, the
134
216
  # same thing `@registry.event_log` would answer for a local
135
217
  # dispatch. Not cached: `AggregateDoor.events`/`Handle#events`
136
218
  # are not called in this codebase's own hot paths today: if that
137
219
  # changes, caching belongs here, not in every caller.
220
+ #
221
+ # @return [Array<Runtime::Event>] every event in the routed Lambda's domain
222
+ # journal, oldest first, with `occurred_at` always nil (the kernel is
223
+ # timestamp-free by design)
138
224
  def events
139
225
  @client.read.fetch("events", []).map { |e| build_event(e) }
140
226
  end