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
@@ -1,5 +1,4 @@
1
1
  require_relative "errors"
2
- require_relative "../deprecation"
3
2
  require_relative "refusal_wording"
4
3
  require_relative "caller"
5
4
  require_relative "invocation"
@@ -121,22 +120,21 @@ module Hecks
121
120
  # Runs one command, entity command or port operation, then every policy and saga reaction
122
121
  # its events are owed.
123
122
  #
124
- # Loose keyword facts are deprecated (roadmap I3) — `dispatch(verb,
125
- # amount: 5)` still works, and warns once per call site; pass
126
- # `with: { amount: 5 }` (and the receiver in `to:`) instead. The
127
- # keyword door closes in `LEGACY_ARGS_REMOVAL`. `bin/codemod_legacy_
128
- # dispatch_args` rewrites existing callers.
123
+ # The receiver in `to:`, the facts in `with:`, and nothing else.
124
+ # Loose keyword facts — `dispatch(verb, amount: 5)`, one bag holding
125
+ # both the route and the payload — were deprecated in 1.3.x and are
126
+ # gone: Ruby now refuses them itself, by name ("unknown keyword:
127
+ # :amount"). Code holding a bag of data rather than written keywords
128
+ # calls `dispatch_flat` below; that door is not going anywhere.
129
129
  #
130
130
  # @param verb [String] the fully qualified verb: `"Domain::Aggregate.Command"`,
131
131
  # `"Domain::Aggregate.Entity.Command"` or `"Domain::Aggregate.Port.Operation"`
132
132
  # @param to [String, Hash, nil] the receiver: an aggregate identity, or an entity route
133
133
  # Hash with `:aggregate` and one of `:entity`/`:entities`; nil when the facts carry
134
134
  # the identity themselves
135
- # @param with [Hash, nil] the command's facts, keyed by argument name (String or Symbol);
136
- # may not be combined with loose keyword facts
135
+ # @param with [Hash, nil] the command's facts, keyed by argument name (String or Symbol)
137
136
  # @param saga_correlation [Hash, nil] correlation head => value, stamped on every
138
137
  # emitted event when a saga leg causes this dispatch; nil otherwise
139
- # @param legacy_args [Hash{Symbol => Object}] deprecated loose keyword facts
140
138
  # @return [Runtime::Dispatcher::Result] the verb, settled instance (nil for a port
141
139
  # operation), emitted events, execution plan and persistence outcome
142
140
  # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
@@ -145,39 +143,20 @@ module Hecks
145
143
  # the call (`GivenNotMet`, `TypeMismatch`, `Unauthorized`, `NotFound`, …)
146
144
  # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
147
145
  # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
148
- # @raise [Deprecation::Error] if loose keyword facts are given where
149
- # `Deprecation.raise_on!(:legacy_dispatch_args)` applies
150
- def dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args)
151
- Dispatcher.deprecate_loose_facts(legacy_args)
152
- dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, legacy_args: legacy_args)
153
- end
154
-
155
- LEGACY_ARGS_REMOVAL = "1.5.0".freeze
156
- LEGACY_ARGS_WARNING =
157
- "passing command facts to dispatch as loose keyword arguments is deprecated and will be removed in " \
158
- "hecks #{LEGACY_ARGS_REMOVAL} — pass them as `with: { ... }`, with the receiver identity in `to:` " \
159
- "(bin/codemod_legacy_dispatch_args rewrites existing callers)".freeze
160
-
161
- # Warns, once per call site, that a dispatch passed its facts as loose keyword arguments.
162
- #
163
- # @param legacy_args [Hash] the loose keyword facts a dispatch received; empty means
164
- # nothing deprecated was used, and nothing is warned
165
- # @return [nil] always, whether or not a warning was issued
166
- # @raise [Deprecation::Error] if `legacy_args` is not empty where
167
- # `Deprecation.raise_on!(:legacy_dispatch_args)` applies
168
- def self.deprecate_loose_facts(legacy_args)
169
- Deprecation.call(:legacy_dispatch_args, LEGACY_ARGS_WARNING) unless legacy_args.empty?
146
+ def dispatch(verb, to: nil, with: nil, saga_correlation: nil)
147
+ dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, flat: {})
170
148
  end
171
149
 
172
150
  # Dispatches a verb whose receiver and facts arrive together in one flat Hash.
173
151
  #
174
- # **The flat-facts wire form** — one Hash, not keywords, and not
175
- # deprecated: the shape `spec/corpus/*.json` steps, the Rust kernel's
176
- # `cli.rs` contract, a reaction without a `with:` projection, and the
177
- # self-hosted meta-domain all carry. Routes exactly as
178
- # `dispatch(verb, **args)` does: a Symbol `:to`, `:with` or
152
+ # **The flat-facts wire form** — one Hash, not keywords: the shape
153
+ # `spec/corpus/*.json` steps, the Rust kernel's `cli.rs` contract, a
154
+ # reaction without a `with:` projection, and the self-hosted
155
+ # meta-domain all carry, where the receiver's identity is one of the
156
+ # keys because that is how the wire spells it. Routes exactly as the
157
+ # removed keyword door did: a Symbol `:to`, `:with` or
179
158
  # `:saga_correlation` key is lifted out as that keyword, everything
180
- # else is a fact (so a String "to" key stays a fact).
159
+ # else is a fact (so a String "to" key stays a fact, as it did).
181
160
  # Framework code that replays data calls this; application code
182
161
  # calls `dispatch(verb, to:, with:)`.
183
162
  #
@@ -196,10 +175,10 @@ module Hecks
196
175
  to = facts.delete(:to)
197
176
  with = facts.delete(:with)
198
177
  saga_correlation = facts.delete(:saga_correlation)
199
- dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, legacy_args: facts)
178
+ dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, flat: facts)
200
179
  end
201
180
 
202
- def dispatch_invocation(verb, to:, with:, saga_correlation:, legacy_args:)
181
+ def dispatch_invocation(verb, to:, with:, saga_correlation:, flat:)
203
182
  domain, aggregate_name, command_name = parse(verb)
204
183
  aggregate = resolve_aggregate(domain, aggregate_name, verb)
205
184
 
@@ -222,12 +201,12 @@ module Hecks
222
201
  operation = port.operation(sub) ||
223
202
  raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "port_no_operation",
224
203
  port: head, operation: sub))
225
- invocation = Invocation.from_call(verb, to: to, with: with, legacy: legacy_args,
204
+ invocation = Invocation.from_call(verb, to: to, with: with, flat: flat,
226
205
  receiver: :port, aggregate: aggregate) { operation }
227
206
  [nil, @port_ops.call(domain, aggregate, operation, invocation), nil, nil, :enqueue]
228
207
  else
229
208
  resolution = nil
230
- invocation = Invocation.from_call(verb, to: to, with: with, legacy: legacy_args,
209
+ invocation = Invocation.from_call(verb, to: to, with: with, flat: flat,
231
210
  receiver: :entity, entity_depth: command_name.count(".")) do
232
211
  (resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
233
212
  end
@@ -235,7 +214,7 @@ module Hecks
235
214
  end
236
215
  else
237
216
  command = command_of(aggregate, aggregate_name, command_name)
238
- invocation = Invocation.from_call(verb, to: to, with: with, legacy: legacy_args) { command }
217
+ invocation = Invocation.from_call(verb, to: to, with: with, flat: flat) { command }
239
218
  @commands.call(domain, aggregate, command, invocation, saga_correlation)
240
219
  end
241
220
 
@@ -328,13 +307,13 @@ module Hecks
328
307
  # `to:`/`with:` are not keywords of this method — a key named
329
308
  # either is an ordinary fact here (BUG#131), so both go in as nil.
330
309
  resolution = nil
331
- invocation = Invocation.from_call(verb, to: nil, with: nil, legacy: args, receiver: :entity) do
310
+ invocation = Invocation.from_call(verb, to: nil, with: nil, flat: args, receiver: :entity) do
332
311
  (resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
333
312
  end
334
313
  @entities.call(domain, aggregate, resolution, invocation, dry_run: true)
335
314
  else
336
315
  command = command_of(aggregate, aggregate_name, command_name)
337
- invocation = Invocation.from_call(verb, to: nil, with: nil, legacy: args) { command }
316
+ invocation = Invocation.from_call(verb, to: nil, with: nil, flat: args) { command }
338
317
  @commands.call(domain, aggregate, command, invocation, dry_run: true)
339
318
  end
340
319
 
@@ -360,7 +339,9 @@ module Hecks
360
339
  # @param to [String, Hash, nil] the receiving aggregate's identity; when nil it is read
361
340
  # from the facts, by the operation's reference or identity attribute
362
341
  # @param with [Hash, nil] the operation's facts, keyed by argument name
363
- # @param legacy_args [Hash{Symbol => Object}] deprecated loose keyword facts
342
+ # @param flat [Hash] the wire form `dispatch_flat` takes, for the driving adapter
343
+ # holding a decoded webhook rather than written keywords; it is where the operation's
344
+ # own reference attribute is read from and lifted into `to:` when `to:` is nil
364
345
  # @return [Array<Runtime::Event>] the events the operation announced: one per declared
365
346
  # `emits` for an inbound operation, the one answering or refusing event for an
366
347
  # outbound one
@@ -369,10 +350,7 @@ module Hecks
369
350
  # is malformed
370
351
  # @raise [Runtime::NotFound] if the receiving aggregate record does not exist
371
352
  # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
372
- # @raise [Deprecation::Error] if loose keyword facts are given where
373
- # `Deprecation.raise_on!(:legacy_dispatch_args)` applies
374
- def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args)
375
- Dispatcher.deprecate_loose_facts(legacy_args)
353
+ def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, flat: {})
376
354
  aggregate = resolve_aggregate(domain, aggregate_name, "#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}")
377
355
  port = aggregate.port(port_name) ||
378
356
  raise(UnknownVerb, "#{aggregate_name} has no port #{port_name.inspect}")
@@ -380,7 +358,7 @@ module Hecks
380
358
  raise(UnknownVerb, "#{port_name} has no operation #{operation_name.inspect}")
381
359
 
382
360
  invocation = Invocation.from_call("#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}",
383
- to: to, with: with, legacy: legacy_args,
361
+ to: to, with: with, flat: flat,
384
362
  receiver: :port, aggregate: aggregate) { operation }
385
363
  announced = @port_ops.call(domain, aggregate, operation, invocation)
386
364
 
@@ -33,6 +33,9 @@ module Hecks
33
33
  UNMATCHABLE = Object.new.freeze
34
34
  private_constant :UNMATCHABLE
35
35
 
36
+ # Walks `chain`, one hop per entry, and returns the located element (or
37
+ # `instance` itself, when `chain` is empty).
38
+ #
36
39
  # **One hop per chain entry**. `container` starts as `instance` (the root
37
40
  # aggregate record) and becomes each just-located element in turn —
38
41
  # Dispatch's own element is found inside the Handler element
@@ -52,6 +55,22 @@ module Hecks
52
55
  # split's own consequence: an entity's own identity may arrive via
53
56
  # `to:` rather than duplicated into `args`, so each hop is offered
54
57
  # its routed identity ahead of falling back to `args`.
58
+ #
59
+ # @param root_aggregate [Bluebook::Aggregate] the root aggregate record's
60
+ # own construct, held constant across every hop for coercion
61
+ # @param chain [Array<Bluebook::Entity>] the entity chain to walk, root-first
62
+ # @param instance [Runtime::Instance] the root aggregate record to walk from
63
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments,
64
+ # read for each hop's own identity when `route` does not supply it
65
+ # @param command_name [String] the command name, quoted in a refusal
66
+ # @param route [Runtime::Routing::Envelope, nil] the call's resolved routing
67
+ # envelope, if any; its own `entities` supply each hop's identity first
68
+ # @return [Runtime::Instance, Hash{Symbol => Object}] the located element;
69
+ # `instance` itself, unchanged, when `chain` is empty
70
+ # @raise [Runtime::UnknownVerb] if a hop's owner declares no list attribute
71
+ # for that entity
72
+ # @raise [Runtime::NotFound] if a hop's identity is absent from `args`, or no
73
+ # element matches it
55
74
  def locate_chain(root_aggregate, chain, instance, args, command_name, route = nil)
56
75
  container = instance
57
76
  owner = root_aggregate
@@ -81,6 +100,24 @@ module Hecks
81
100
  # rubocop:disable-next Metrics/CyclomaticComplexity
82
101
  # rubocop:disable-next Metrics/PerceivedComplexity
83
102
  # rubocop:disable-next Metrics/MethodLength
103
+ #
104
+ # @param root_aggregate [Bluebook::Aggregate] the root aggregate record's own
105
+ # construct, used for value-object coercion
106
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose own
107
+ # attribute declares the list `entity` is searched in
108
+ # @param entity [Bluebook::Entity] the entity type being located
109
+ # @param command_name [String] the command name, quoted in a refusal
110
+ # @param container [Runtime::Instance, Hash{Symbol => Object}] the record or
111
+ # element holding the list to search
112
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments,
113
+ # read for the element's own identity when `routed_identity` is nil
114
+ # @param routed_identity [String, nil] the routing envelope's own identity
115
+ # string for this hop, matched directly instead of deriving one from `args`
116
+ # @return [Hash{Symbol => Object}] a fresh copy of the located element; the
117
+ # owning list inside `container` is replaced with a fresh copy too
118
+ # @raise [Runtime::UnknownVerb] if `owner` declares no list attribute for `entity`
119
+ # @raise [Runtime::NotFound] if an identity part is absent from `args`
120
+ # (`routed_identity` nil only), or no element matches
84
121
  def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
85
122
  entity_name = entity.hecks_name
86
123
  list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
@@ -173,6 +210,12 @@ module Hecks
173
210
  # that does not name its fields is refused when the bluebook loads ("an
174
211
  # entity says what it is known by", "an identity part names something"),
175
212
  # so by the time a dispatch arrives here there is always a path to dig.
213
+ #
214
+ # @param entity [Bluebook::Entity] the entity type whose identity paths
215
+ # are read
216
+ # @param element [Hash{Symbol => Object}] the stored element to read an
217
+ # identity off
218
+ # @return [String] the element's identity, joined from its declared parts
176
219
  def element_identity(entity, element)
177
220
  parts = entity.identity_paths.map do |path|
178
221
  head = path.to_s.split(".").first.to_sym
@@ -215,8 +258,32 @@ module Hecks
215
258
  # intentional mirroring, which is what lets the two be diffed
216
259
  # against each other when one gets a fix the other needs too.
217
260
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
261
+ # Applies one declared mutation to an entity element, in place.
262
+ #
218
263
  # `pre` — the element as it was before this command (C4.2): every
219
264
  # read below goes through it, every write lands on `element`.
265
+ #
266
+ # @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`/
267
+ # `arithmetic`/`multiply`/`clamp`/`sign_of` are read through
268
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
269
+ # coercion
270
+ # @param entity [Bluebook::Entity] the entity type `element` is an instance of
271
+ # @param element [Hash{Symbol => Object}] the element to mutate; written in place
272
+ # @param mutation [Bluebook::Mutation] the declared mutation to apply
273
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments,
274
+ # a mutation's source may read from
275
+ # @param pre [Hash{Symbol => Object}] the element as it stood before this
276
+ # command; every read goes through this, every write lands on `element`
277
+ # @return [void]
278
+ # @raise [Runtime::TypeMismatch] if a mutation's own value cannot be coerced,
279
+ # or an arithmetic op's operands are not numeric or compatible value objects
280
+ # @raise [Runtime::InvariantViolation] if a coerced or arithmetic-derived value
281
+ # object breaks one of its own invariants
282
+ # @raise [Runtime::AlreadyExists] if an `:append` mints a nested entity whose
283
+ # identity collides with an existing element
284
+ # @raise [Bluebook::Expression::EvaluationError] if an arithmetic op's product
285
+ # does not fit a signed 64-bit Integer, or is a non-finite Float
286
+ # @raise [Runtime::WiringError] if `mutation.op` names no handled mutation kind
220
287
  def apply_to_element(rules, aggregate, entity, element, mutation, args, pre = element)
221
288
  case mutation.op
222
289
  when :set
@@ -269,22 +336,47 @@ module Hecks
269
336
  end
270
337
  end
271
338
 
339
+ # Rewraps a plain-Numeric arithmetic result into `attribute`'s own declared
340
+ # value-object type, when the arithmetic itself ran unwrapped.
341
+ #
272
342
  # `MutationApplier#rewrap_arithmetic_result`'s own entity-scoped
273
343
  # twin, byte-for-byte the same fix — see that method's own
274
344
  # comment for the full "phantom-field asymmetric wrapping" story.
275
345
  # A no-op whenever `current` was already a Value (the arithmetic
276
346
  # call already returned one) or the mutation targets no declared
277
347
  # attribute at all.
348
+ #
349
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
350
+ # coercion
351
+ # @param attribute [Bluebook::Attribute, nil] the mutated field's own declared
352
+ # attribute; a no-op when nil (targets no declared attribute)
353
+ # @param current [Object] the field's pre-mutation value, as read off `pre`
354
+ # @param result [Object] the arithmetic op's own result
355
+ # @return [Runtime::Value, Object] `result` unchanged when already a `Value`,
356
+ # `current` is a `Value`, or `attribute` is nil; otherwise `result` coerced
357
+ # into `attribute`'s own declared type
278
358
  def rewrap_arithmetic_result(aggregate, attribute, current, result)
279
359
  return result if current.is_a?(Value) || attribute.nil? || result.is_a?(Value)
280
360
 
281
361
  Value.for_attribute(aggregate, attribute, result)
282
362
  end
283
363
 
364
+ # Resolves an `:append` mutation's own field source against the offered
365
+ # arguments, falling back to the element's own current field.
366
+ #
284
367
  # `MutationApplier#resolve_append_source`'s own entity-scoped
285
368
  # twin — a caller-supplied arg first, falling back to the
286
369
  # element's own current field (never the parent instance's) when
287
370
  # it isn't one.
371
+ #
372
+ # @param source [Symbol, Object] the mutation's own field source: a Symbol
373
+ # names a command argument or, failing that, an element field; anything
374
+ # else is returned as is
375
+ # @param element [Hash{Symbol => Object}] the element to fall back to reading
376
+ # from
377
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments
378
+ # @return [Object, nil] the named argument's value; failing that, the named
379
+ # element field's value; `source` itself when it is not a Symbol
288
380
  def resolve_element_append_source(source, element, args)
289
381
  return source unless source.is_a?(Symbol)
290
382
  return args[source] if args.key?(source)
@@ -314,6 +406,20 @@ module Hecks
314
406
  # every declared attribute the append mapping doesn't name gets its
315
407
  # own default the same way a fresh aggregate's own attributes
316
408
  # already do (`Instance.defaults`), whichever branch built `fields`.
409
+ #
410
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
411
+ # coercion
412
+ # @param entity [Bluebook::Entity] the entity type `element`'s mutated list
413
+ # attribute belongs to
414
+ # @param element [Hash{Symbol => Object}] the element being appended to, read
415
+ # as the source for a field the mutation's own map does not supply directly
416
+ # @param mutation [Bluebook::Mutation] the declared `:append` mutation
417
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments
418
+ # @return [Array] `element[mutation.target]`'s existing elements, frozen deep,
419
+ # with the newly built element (a `Runtime::Value`, or a Hash for an entity
420
+ # or untyped element) appended last
421
+ # @raise [Runtime::AlreadyExists] if the appended element is a nested entity
422
+ # whose identity collides with an existing one
317
423
  def appended_to_element(aggregate, entity, element, mutation, args)
318
424
  fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
319
425
  element_type = entity.attribute(mutation.target)&.type
@@ -389,6 +495,15 @@ module Hecks
389
495
  # what "the default" means. Additive only — a key `fields` already
390
496
  # holds (the append mapping, an auto-minted identity, a lifecycle
391
497
  # default) is never overwritten.
498
+ #
499
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
500
+ # coercion
501
+ # @param entity [Bluebook::Entity] the entity type whose declared attributes
502
+ # are defaulted
503
+ # @param fields [Hash{Symbol => Object}] the element fields already resolved;
504
+ # written in place
505
+ # @return [Hash{Symbol => Object}] `fields`, with every declared attribute it
506
+ # did not already hold filled with its own default
392
507
  def fill_declared_defaults(aggregate, entity, fields)
393
508
  entity.attributes.each do |attribute|
394
509
  next if fields.key?(attribute.name)
@@ -404,6 +519,18 @@ module Hecks
404
519
  # the list this targets is itself entity-typed — see
405
520
  # `list_element_match?`, below, which both this and
406
521
  # `MutationApplier#removed` now share.
522
+ #
523
+ # @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`
524
+ # is read through
525
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
526
+ # coercion
527
+ # @param entity [Bluebook::Entity] the entity type `element`'s mutated list
528
+ # attribute belongs to
529
+ # @param element [Hash{Symbol => Object}] the element being removed from
530
+ # @param mutation [Bluebook::Mutation] the declared `:remove` mutation
531
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments
532
+ # @return [Array] `element[mutation.target]`'s existing elements, with any
533
+ # matching the resolved remove target left out
407
534
  def removed_from_element(rules, aggregate, entity, element, mutation, args)
408
535
  value = rules.resolve_source(mutation.source, args)
409
536
  attribute = entity.attribute(mutation.target)
@@ -443,6 +570,17 @@ module Hecks
443
570
  # "matches" means — the same reasoning this file's own header
444
571
  # gives for centralizing `locate_chain`/`element_of` once rather
445
572
  # than twice.
573
+ #
574
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, to resolve
575
+ # whether `attribute`'s own type is an entity
576
+ # @param attribute [Bluebook::Attribute, nil] the list attribute `element`
577
+ # belongs to
578
+ # @param element [Object] the stored list element to check: a Hash for an
579
+ # entity-typed list, a `Runtime::Value` otherwise
580
+ # @param value [Object] the `remove:` target to match `element` against
581
+ # @return [Boolean] whole-value equality for a non-entity-typed list;
582
+ # identity-field equality for an entity-typed one with a single identity
583
+ # head; false for a composite or absent identity
446
584
  def list_element_match?(aggregate, attribute, element, value)
447
585
  entity = attribute&.list? ? Value.find_entity(aggregate, attribute.type.to_s) : nil
448
586
  return element == value unless entity
@@ -453,24 +591,27 @@ module Hecks
453
591
  element.is_a?(Hash) && element[head] == value
454
592
  end
455
593
 
456
- # BUG#13 (PR #549) — the same check #hydrate gives every creating
594
+ # Refuses a caller-supplied or composite identity that already names an
595
+ # element on `current`.
596
+ #
597
+ # BUG#13 — the same check #hydrate gives every creating
457
598
  # aggregate command (`repository.find(id)`,
458
- # `command_interpreter.rb`), one level down. Originally lived in
459
- # `MutationApplier` (mutation_applier.rb), called only from
599
+ # `command_interpreter.rb`), one level down. Shared with
600
+ # `MutationApplier` (mutation_applier.rb), called from
460
601
  # `#entity_element` — an aggregate's own entity list (`Workspace.
461
- # boards`, `Ledger.entries`). Moved here (BUG#145) so `#appended_
602
+ # boards`, `Ledger.entries`) — and, moved here (BUG#145), from `#appended_
462
603
  # to_element`, above — an entity's own nested entity list one hop
463
- # further in (`Board.cards`) — can share it too, rather than
464
- # reimplementing it a second time the same way `#list_element_
604
+ # further in (`Board.cards`) — so neither call site reimplements
605
+ # it a second time, the same way `#list_element_
465
606
  # match?` already avoids that split for `remove:`.
466
607
  #
467
608
  # Reached, at the aggregate-owned call site, only on the two
468
609
  # branches that do not auto-mint: a caller-supplied identity (the
469
610
  # field is already in the append's own field map) or a composite
470
611
  # one (`entity.identified_by` is nil for those — Runtime::
471
- # Identified#derive_identity). Neither used to check the sibling
612
+ # Identified#derive_identity). Without this, neither checks the sibling
472
613
  # list at all: a second LogVisit with the same date+sequence, or a
473
- # second IssueKey with the same serial, appended a silent
614
+ # second IssueKey with the same serial, would append a silent
474
615
  # duplicate — worse than an ordinary duplicate row, because
475
616
  # `EntityElement#element_of`'s own `find_index` always matches the
476
617
  # first match, so the second becomes permanently unaddressable by
@@ -491,6 +632,18 @@ module Hecks
491
632
  # hops up). Used for that naming purpose only (`owner.hecks_name`) —
492
633
  # never for `Value`/namespace resolution, which is why an `Entity`
493
634
  # (not just an `Aggregate`) is a valid thing to pass here.
635
+ #
636
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct named
637
+ # in a refusal as what the duplicate "already exists on"
638
+ # @param entity [Bluebook::Entity] the entity type being checked for a
639
+ # colliding identity
640
+ # @param current [Array<Hash>, Object] the entity's own existing elements;
641
+ # coerced through `Array()`, so a single element or nil is also accepted
642
+ # @param fields [Hash{Symbol => Object}] the new element's own fields, whose
643
+ # identity heads are checked against every element in `current`
644
+ # @return [void]
645
+ # @raise [Runtime::AlreadyExists] if `current` already holds an element
646
+ # whose identity heads match `fields`'s own
494
647
  def check_entity_collision(owner, entity, current, fields)
495
648
  heads = entity.identity_heads
496
649
  return if heads.empty?
@@ -24,8 +24,8 @@ module Hecks
24
24
  class EntityInterpreter
25
25
  include Interpreting
26
26
  # The same payload gate aggregate commands and port operations already
27
- # run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md): this
28
- # class used to run neither refuse_unknown_arguments nor
27
+ # run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md): without
28
+ # this, this class runs neither refuse_unknown_arguments nor
29
29
  # refuse_absent_arguments, on a comment claiming "an entity inherits
30
30
  # its aggregate's own gate." Nothing on the entity dispatch path ever
31
31
  # ran one — confirmed live, `LedgerEntry.Reverse` accepted an
@@ -83,7 +83,19 @@ module Hecks
83
83
  # reads the declaring command — after `to:` is parsed, before the
84
84
  # facts are.
85
85
  Resolution = Data.define(:entity_names, :chain, :command_name, :command) do
86
+ # Resolves a dotted entity verb against `aggregate` into the entity
87
+ # chain it walks and the command it names.
88
+ #
86
89
  # Refuses UnknownVerb for an unknown entity or command.
90
+ #
91
+ # @param aggregate [Bluebook::Aggregate] the root aggregate the dotted verb
92
+ # is resolved against
93
+ # @param dotted [String, Symbol] the entity verb, with the leading
94
+ # "Domain::Aggregate." already stripped (e.g. `"Handler.Dispatch.Bind"`)
95
+ # @return [EntityInterpreter::Resolution] the resolved entity names, chain,
96
+ # command name and command class
97
+ # @raise [Runtime::UnknownVerb] if `dotted` names no entity, or the located
98
+ # entity declares no such command
87
99
  def self.of(aggregate, dotted)
88
100
  *entity_names, command_name = dotted.to_s.split(".")
89
101
  if entity_names.empty?
@@ -119,6 +131,9 @@ module Hecks
119
131
  private_class_method :walk
120
132
  end
121
133
 
134
+ # @param registry [Runtime::Registry] the booted registry this interpreter reads
135
+ # @param rules [Runtime::CommandRules] the shared rules engine (admissibility,
136
+ # references, arithmetic, authorization, emission) dispatch runs through
122
137
  def initialize(registry, rules:)
123
138
  @registry = registry
124
139
  @rules = rules
@@ -136,6 +151,26 @@ module Hecks
136
151
  # `resolution` is #resolve's answer; `invocation` the
137
152
  # `Runtime::Invocation` `Dispatcher` built — `ctx.args` is its
138
153
  # `to_args`, `ctx.route` its `target`.
154
+ #
155
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
156
+ # @param aggregate [Bluebook::Aggregate] the root aggregate owning the entity
157
+ # chain the command targets
158
+ # @param resolution [EntityInterpreter::Resolution] the resolved entity chain
159
+ # and command, as `Resolution.of` builds it
160
+ # @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for
161
+ # this call
162
+ # @param dry_run [Boolean] whether to run every step through validation without
163
+ # saving, emitting or enqueueing
164
+ # @return [Array(Runtime::Instance, Array<Runtime::Event>,
165
+ # Runtime::DependencyPlanning::Plan, Ports::Persistence::Execution,
166
+ # Array<Runtime::Outbox::Row>)] the settled parent aggregate instance, emitted
167
+ # events, execution plan, persistence outcome and outbox rows — the last
168
+ # three nil on a dry run, which skips save/emit/outbox
169
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
170
+ # given/ensures/invariant/authorization/admissibility rule refuses
171
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every
172
+ # retry (`MAX_STALE_WRITE_RETRIES`)
173
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
139
174
  def call(domain, aggregate, resolution, invocation, dry_run: false)
140
175
  chain = resolution.chain
141
176
  entity = chain.last
@@ -239,7 +274,7 @@ module Hecks
239
274
  # command's `corrects` names even exist" is checked here too, once,
240
275
  # before the entity's own `given`s.
241
276
  #
242
- # **Admissibility is checked against the parent/root, not the entity** —
277
+ # Admissibility is checked against the parent/root, not the entity —
243
278
  # deliberately `ctx.instance`/`ctx.aggregate` (the parent aggregate
244
279
  # record and the root aggregate construct), never `ctx.view`/
245
280
  # `ctx.entity` (the entity's own pre-mutation view/construct). This
@@ -399,12 +434,12 @@ module Hecks
399
434
  end
400
435
 
401
436
  # `locate_chain`/`element_of`/`element_identity`/`apply_to_element` and
402
- # their own helpers used to live here — moved to `Runtime::EntityElement`
403
- # (see that file's own header) so `CommandInterpreter`'s own
404
- # `delegate_to_entity` step can locate and mutate the same element the
405
- # same way, against an aggregate record already held in memory. `call`,
406
- # above, and every `step_*` method reach them through that module now;
407
- # nothing about the steps themselves changed.
437
+ # their own helpers live in `Runtime::EntityElement`
438
+ # (see that file's own header), shared rather than kept here, so
439
+ # `CommandInterpreter`'s own `delegate_to_entity` step can locate and
440
+ # mutate the same element the same way, against an aggregate record
441
+ # already held in memory. `call`, above, and every `step_*` method
442
+ # reach them through that module.
408
443
  end
409
444
  end
410
445
  end
@@ -2,6 +2,10 @@ require_relative "value/invariant_violation"
2
2
  require_relative "../vocabulary"
3
3
 
4
4
  module Hecks
5
+ # What runs a booted domain: dispatch, the command/entity/query/policy/saga
6
+ # interpreters, the registry a boot assembles, and the errors below —
7
+ # everything downstream of a `.bluebook`/`.hecksagon`/`.world` declaration.
8
+ # See `lib/hecks/runtime.rb` for the module's own facade and boot entry points.
5
9
  module Runtime
6
10
  class UnknownVerb < StandardError; end
7
11
  class EnsuresNotMet < StandardError; end
@@ -21,11 +25,21 @@ module Hecks
21
25
  class GivenNotMet < StandardError
22
26
  attr_reader :detail
23
27
 
28
+ # @param message [String, nil] the refusal text, pinned byte-for-byte across specs
29
+ # @param detail [String, nil] the failing comparison's resolved operands ("left: X,
30
+ # right: Y"), or nil when the given's top-level shape is not a bare comparison
24
31
  def initialize(message = nil, detail: nil)
25
32
  super(message)
26
33
  @detail = detail
27
34
  end
28
35
 
36
+ # Renders `message` with `detail` appended, for a human reading an unhandled refusal.
37
+ #
38
+ # @param highlight [Boolean] unused; accepted for compatibility with `Exception#
39
+ # detailed_message`'s own signature
40
+ # @param opts [Hash] unused; accepted for compatibility with `Exception#
41
+ # detailed_message`'s own signature
42
+ # @return [String] `message`, with `" (#{detail})"` appended when `detail` is present
29
43
  def detailed_message(highlight: false, **opts)
30
44
  base = super
31
45
  detail ? "#{base} (#{detail})" : base
@@ -119,10 +133,10 @@ module Hecks
119
133
  #
120
134
  # Everything else is a defect : a NoMethodError in an interpreter, a
121
135
  # NameError from a missing constant, a TypeError from a bad assumption. A
122
- # blanket `rescue StandardError` used to fold both into one line —
123
- # `delivered: false, reason: "..."` — so a crash in the runtime was
124
- # indistinguishable from a rule doing its job, and read as normal operation
125
- # in the log.
136
+ # blanket `rescue StandardError` would fold both into one line —
137
+ # `delivered: false, reason: "..."` — making a crash in the runtime
138
+ # indistinguishable from a rule doing its job, reading as normal
139
+ # operation in the log.
126
140
  #
127
141
  # UnknownVerb is one of these, and deliberately : a cross-domain policy
128
142
  # (`across "Notifications"`) fires in deployments where that domain is not
@@ -11,20 +11,25 @@ module Hecks
11
11
  # one correlating on a different field. Absent for any event no saga
12
12
  # dispatch caused, which is most of them.
13
13
  Event = Struct.new(:name, :aggregate, :id, :payload, :occurred_at, :correlation, keyword_init: true) do
14
+ # Freezes the event deep, so nothing about it can change after it happens.
15
+ #
14
16
  # An emitted event is a record of something that happened, and a
15
17
  # mutable audit trail is not one. The payload — the domain fact the
16
18
  # event carries — is frozen through on emission: freezing the Hash
17
19
  # alone would leave every value in it editable in place, which is
18
20
  # the shape all four previous freezing bugs had.
19
21
  #
20
- # The whole event, not just its payload. Correlation used to be
21
- # merged onto already-emitted events by `Dispatcher#dispatch`, which
22
- # is what kept an event writable after it had happened; it is set at
23
- # construction now, because it is part of the transaction and known
24
- # from `dispatch`'s own argument before anything is emitted.
22
+ # The whole event, not just its payload. Correlation is set at
23
+ # construction rather than merged in here by `Dispatcher#dispatch`
24
+ # after the event already exists, because it is part of the
25
+ # transaction and known from `dispatch`'s own argument before
26
+ # anything is emitted — that is what keeps an event immutable once
27
+ # it exists.
25
28
  #
26
29
  # The log stays appendable: new events are still recorded. It is
27
30
  # each event that stops changing once it exists.
31
+ #
32
+ # @return [void]
28
33
  def emit!
29
34
  Freezer.deep(payload)
30
35
  Freezer.deep(correlation)