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
@@ -23,19 +23,38 @@ module Hecks
23
23
  module Identity
24
24
  module_function
25
25
 
26
+ # Reads one key off `hash` by whichever of its Symbol/String spelling
27
+ # is actually present, favoring the Symbol spelling.
28
+ #
26
29
  # A hash read that decides which spelling of a key answers by
27
30
  # presence, never by `||` — a bare `||` treats a genuinely-held
28
31
  # `false` the same as an absent key and falls through to the other
29
32
  # spelling, landing on `nil` instead of the real, stored answer.
33
+ #
34
+ # @param hash [Hash] the hash to read, potentially keyed by either spelling
35
+ # @param key [String, Symbol] the key to look up
36
+ # @return [Object, nil] the value under `key.to_sym` if that key is present,
37
+ # otherwise the value under `key` as given; nil if neither is a key of `hash`
30
38
  def hash_lookup(hash, key)
31
39
  sym = key.to_sym
32
40
  hash.key?(sym) ? hash[sym] : hash[key]
33
41
  end
34
42
 
43
+ # Digs `path`'s fields out of `held`, past the head a caller already
44
+ # consumed to look `held` up.
45
+ #
35
46
  # The head names the attribute and is consumed by whoever looked the value
36
47
  # up; what is left is the walk down into it. A path with no fields to walk
37
48
  # — an aggregate that declares no identity and falls back to `id` — hands
38
49
  # back what it was given, because there is nothing declared to dig for.
50
+ #
51
+ # @param path [String, Symbol] the dotted identity path (`"number.value"`) or a
52
+ # bare head (`:id`); only the segments after the head are walked
53
+ # @param held [Object] the value already looked up for the path's head — a
54
+ # `Runtime::Value`, a Hash, or a plain scalar
55
+ # @return [Object, nil] `held` unchanged when `path` has no fields past its head;
56
+ # otherwise the value found by walking those fields, or nil when a segment
57
+ # is missing or the value being dug into is not a Hash
39
58
  def scalar(path, held)
40
59
  _head, *fields = path.to_s.split(".")
41
60
  return held if fields.empty?
@@ -45,6 +64,9 @@ module Hecks
45
64
  end
46
65
  end
47
66
 
67
+ # Derives `construct`'s identity string from `args`, joining every
68
+ # declared identity part in declaration order.
69
+ #
48
70
  # The identity is the join of its parts, in declaration order. Shared by
49
71
  # `CommandInterpreter` (an aggregate acting on itself) and
50
72
  # `EntityInterpreter` (a piece addressed through its aggregate) — a piece
@@ -59,23 +81,49 @@ module Hecks
59
81
  # rather than half of one. Half an identity names nothing, and joining what
60
82
  # did arrive would silently name a different record on every dispatch — the
61
83
  # precise failure that minting an id caused, arrived at by another road.
84
+ #
85
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct whose
86
+ # `identity_paths` are resolved
87
+ # @param args [Hash{Symbol => Object}] the offered payload to resolve each
88
+ # identity path against
89
+ # @param value_owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose
90
+ # namespace a value-object identity part is coerced against; `construct` itself
91
+ # unless the caller passes the owning aggregate for an entity
92
+ # @return [String, nil] the joined identity string, or nil when `construct`
93
+ # declares no identity path or any resolved part is nil or blank
62
94
  def of(construct, args, value_owner: construct)
63
95
  paths = construct.identity_paths
64
96
  return nil if paths.empty?
65
97
 
66
98
  parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
67
99
  # A blank part names nothing, the same as an absent one — an ID is a
68
- # scalar, and "" is not a fact about anything. This used to check only
69
- # `nil?`, so a canonical text extracted as "" (an expression whose
70
- # source did not survive extraction) resolved to a real, empty-string
100
+ # scalar, and "" is not a fact about anything. Checking only `nil?`
101
+ # would let a canonical text extracted as "" (an expression whose
102
+ # source did not survive extraction) resolve to a real, empty-string
71
103
  # identity — a record addressable by an id no caller could have meant.
72
104
  return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
73
105
 
74
106
  Naming.identity(parts)
75
107
  end
76
108
 
109
+ # Resolves one identity path (or a bare head such as `:id`) against `args`.
110
+ #
77
111
  # A path digs into the value object that carries the identity, so what is
78
112
  # stored is the scalar inside it rather than the object serialised whole.
113
+ #
114
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct `key` is
115
+ # checked against when it is a bare head
116
+ # @param args [Hash{Symbol => Object}] the offered payload to resolve `key` against
117
+ # @param key [String, Symbol, nil] the identity path to resolve, dotted
118
+ # (`"number.value"`) or bare (`:id`); nil resolves to nil
119
+ # @param value_owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose
120
+ # namespace a value-object head is coerced against; `construct` itself unless
121
+ # the caller passes the owning aggregate for an entity
122
+ # @return [String, Object, nil] the resolved identity text — a String once dug
123
+ # through a dotted path or coerced through a declared value-object attribute;
124
+ # the raw `args[key]` value, unconverted, when the bare head names no declared
125
+ # identity attribute of `construct`; nil when `key` is nil, absent from `args`,
126
+ # or a dotted walk finds nothing
79
127
  def from(construct, args, key, value_owner: construct)
80
128
  return nil unless key
81
129
 
@@ -127,12 +175,21 @@ module Hecks
127
175
  Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
128
176
  end
129
177
 
178
+ # Renders `construct`'s identity paths for a refusal message.
179
+ #
130
180
  # How an identity reads when the runtime has to name it in a refusal — the
131
181
  # paths as they were declared, so the message quotes the bluebook back.
182
+ #
183
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct whose
184
+ # declared identity paths are rendered
185
+ # @return [String] `construct`'s identity paths, comma-separated, exactly as declared
132
186
  def reading(construct)
133
187
  construct.identity_paths.join(", ")
134
188
  end
135
189
 
190
+ # Resolves the best identity string available for `construct` from `args`,
191
+ # without raising, for use as a lock key only.
192
+ #
136
193
  # Best-effort, for a lock key only — `Runtime::AggregateLock`'s own
137
194
  # per-record striping needs some id to key on before dispatch has run
138
195
  # far enough to hydrate for real, so this walks the identical chain
@@ -143,6 +200,17 @@ module Hecks
143
200
  # from the raw, pre-normalized payload this runs against" — the
144
201
  # caller locks by aggregate type alone in that case (coarser, still
145
202
  # correct, just less concurrent).
203
+ #
204
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct being
205
+ # located for the lock key
206
+ # @param args [Hash{Symbol => Object}] the raw, pre-normalized payload to resolve
207
+ # an identity from
208
+ # @param route [Runtime::Routing::Envelope, nil] the call's resolved routing
209
+ # envelope, if any; its own `aggregate` identity is tried first
210
+ # @param reference_key [Symbol, nil] the command's reference-key fallback,
211
+ # tried last if given
212
+ # @return [String, Object, nil] the best-effort identity to lock by, or nil
213
+ # when nothing resolves (the caller then locks by aggregate type alone)
146
214
  def best_effort(construct, args, route = nil, reference_key: nil)
147
215
  route&.aggregate ||
148
216
  of(construct, args) ||
@@ -31,20 +31,34 @@ module Hecks
31
31
  # holding it when they mint a brand-new record). See
32
32
  # `materialize_identity!` for why a composite identity needs it.
33
33
  #
34
- # `hydrate:` — ON BY DEFAULT, and every existing caller keeps getting
34
+ # `hydrate:` — on by default, and every existing caller keeps getting
35
35
  # exactly what it always got: `state` re-walked through
36
36
  # `hydrate_with_defaults` (declared defaults filled, every attribute
37
37
  # re-coerced through `Value.for_attribute`, an entity list's every
38
38
  # element rebuilt and re-validated). `false` is for exactly one
39
39
  # caller (`Adapters::Memory#build_instance`, judge-bootstrapping
40
40
  # only — see its own header) that already knows `state` needs none
41
- # of that: it is a shallow dup of an ALREADY-hydrated, ALREADY-
41
+ # of that: it is a shallow dup of an already-hydrated, already-
42
42
  # validated live `Instance`'s own state, not a raw value pulled off
43
43
  # a wire. Skipping the re-walk is what turns a `list_of` entity's Nth
44
44
  # save from O(N) (re-hydrating every element saved so far, for every
45
45
  # save) into O(1) — the quadratic cost `Adapters::Memory`'s own
46
46
  # header traces start to finish. `CodecBoundary.check_state!` still
47
47
  # runs either way ; only the re-hydration is skipped.
48
+ #
49
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct this
50
+ # record's state is declared by
51
+ # @param id [String, nil] this record's identity; nil for a record whose identity
52
+ # is not yet resolvable (e.g. an entity element hydrated before its own view exists)
53
+ # @param state [Hash{Symbol => Object}, nil] the record's stored (or partial)
54
+ # attribute values; nil for a brand-new record, hydrated entirely from declared
55
+ # defaults
56
+ # @param args [Hash{Symbol => Object}, nil] the original command payload, offered
57
+ # only by a fresh creation; materializes a composite identity's own head
58
+ # attributes
59
+ # @param hydrate [Boolean] whether `state` is re-walked through
60
+ # `hydrate_with_defaults`; false only for a caller that already knows `state` is
61
+ # already hydrated and validated
48
62
  def initialize(aggregate:, id:, state: nil, args: nil, hydrate: true)
49
63
  @aggregate = aggregate
50
64
  @id = id
@@ -68,6 +82,12 @@ module Hecks
68
82
  # written — arrives filled instead of nil. Only declared defaults
69
83
  # fill in; an attribute with no default stays absent, exactly as
70
84
  # stored.
85
+ #
86
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
87
+ # declared attributes and defaults `state` is hydrated against
88
+ # @param state [Hash{Symbol => Object}] the raw stored (or partial) attribute values
89
+ # @return [Hash{Symbol => Object}] `state` coerced through every declared attribute,
90
+ # with any missing declared-default attribute filled in
71
91
  def self.hydrate_with_defaults(aggregate, state)
72
92
  hydrated = Value.hydrate(aggregate, state)
73
93
  defaults(aggregate).each do |name, value|
@@ -76,6 +96,13 @@ module Hecks
76
96
  hydrated
77
97
  end
78
98
 
99
+ # Builds a fresh record's starting state: every declared attribute's
100
+ # default value, an empty frozen Array for a `list_of` attribute, and
101
+ # the lifecycle field's declared starting value when `aggregate` has one.
102
+ #
103
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
104
+ # declared attributes and lifecycle are read
105
+ # @return [Hash{Symbol => Object}] the default state, keyed by attribute name
79
106
  def self.defaults(aggregate)
80
107
  state = aggregate.attributes.to_h do |attr|
81
108
  # Frozen, like a list that has had something appended to it.
@@ -88,6 +115,16 @@ module Hecks
88
115
  state
89
116
  end
90
117
 
118
+ # Resolves one attribute's default value: its own declared `default:`
119
+ # if it has one, otherwise a value object built entirely from its own
120
+ # members' defaults, when every member declares one.
121
+ #
122
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
123
+ # `attribute` belongs to
124
+ # @param attribute [Bluebook::Attribute] the attribute to resolve a default for
125
+ # @return [Runtime::Value, Object, nil] the coerced default value; nil when
126
+ # `attribute` declares no default and either names no value object or names
127
+ # one with a member that itself declares no default
91
128
  def self.default_for(aggregate, attribute)
92
129
  return Value.for_attribute(aggregate, attribute, attribute.default) unless attribute.default.nil?
93
130
  # An entity's members hydrate through the same path but an entity
@@ -100,10 +137,24 @@ module Hecks
100
137
  Value.build(value_object, {}, aggregate)
101
138
  end
102
139
 
140
+ # Reads one state field by name.
141
+ #
142
+ # @param name [String, Symbol] the declared attribute name to read
143
+ # @return [Object, nil] the field's current value; nil if `name` is not a key
144
+ # of `state`
103
145
  def [](name) = @state[name.to_sym]
104
146
 
147
+ # Reports whether `state` holds a value for `name`.
148
+ #
149
+ # @param name [String, Symbol] the declared attribute name to check
150
+ # @return [Boolean] true if `state` has a key for `name`
105
151
  def key?(name) = @state.key?(name.to_sym)
106
152
 
153
+ # Writes one state field by name.
154
+ #
155
+ # @param name [String, Symbol] the declared attribute name to write
156
+ # @param value [Object] the value to store
157
+ # @return [Object] `value`, unchanged
107
158
  def []=(name, value)
108
159
  @state[name.to_sym] = value
109
160
  end
@@ -118,6 +169,8 @@ module Hecks
118
169
  @state.key?(name) || super
119
170
  end
120
171
 
172
+ # Renders this record's wire/storage shape, state plus its identity.
173
+ #
121
174
  # `id: @id` last, not first — see Facade::Handle#to_h's own comment
122
175
  # for the full story (the same fix, landed there first): an
123
176
  # aggregate free to declare its own attribute literally named `id`
@@ -125,8 +178,13 @@ module Hecks
125
178
  # attribute's own wrapped value object sitting in `@state[:id]` —
126
179
  # merging `@state` on top of `{ id: @id }` let it silently clobber
127
180
  # the correct bare identity. `@id` merged last always wins.
181
+ #
182
+ # @return [Hash{Symbol => Object}] `state`, with `id` merged in last
128
183
  def to_h = @state.merge(id: @id)
129
184
 
185
+ # Copies this record, deep enough that a mutation on the copy cannot
186
+ # reach the original's own state.
187
+ #
130
188
  # A copy a mutation may touch. Every adapter but Memory hands `find`
131
189
  # a freshly-decoded Instance already; Memory's holds the record it
132
190
  # eventually saves — the same state Hash, aliased. Before `ensures`
@@ -139,6 +197,8 @@ module Hecks
139
197
  # record through this, never through the adapter's own return value
140
198
  # directly, so a refused ensures leaves the stored record untouched
141
199
  # regardless of which adapter is holding it.
200
+ #
201
+ # @return [Runtime::Instance] a copy of this record, with its own state Hash
142
202
  def dup
143
203
  copy = super
144
204
  copy.state = @state.dup
@@ -157,11 +217,11 @@ module Hecks
157
217
  # `identified_by` to fall back to `:id` for — `@aggregate.identified_by`
158
218
  # is nil the moment there is more than one head (`Behaviour::Identified
159
219
  # #derive_identity`), so the single-head branch below never runs for
160
- # it at all. A creating command that declares those heads as ordinary
161
- # attributes but doesn't also `sets` them (redundant with the identity
162
- # the command's own args already named) used to persist every head as
163
- # nil — the id correctly named the record, but the record's own
164
- # attributes forgot what named it.
220
+ # it at all. Without this, a creating command that declares those heads
221
+ # as ordinary attributes but doesn't also `sets` them (redundant with
222
+ # the identity the command's own args already named) would persist
223
+ # every head as nil — the id correctly naming the record, but the
224
+ # record's own attributes forgetting what named it.
165
225
  #
166
226
  # Filled from `args`, never from splitting `@id` back apart — the
167
227
  # same reason the single-head branch below won't guess a multi-path
@@ -8,11 +8,19 @@ module Hecks
8
8
  # in CommandInterpreter and EntityInterpreter, where they could only
9
9
  # ever drift.
10
10
  module Interpreting
11
+ # Ruby's module-inclusion hook. Gives `interpreter` its own `trace`
12
+ # accessor, since a class including this module needs its own copy
13
+ # rather than one shared across every interpreter.
14
+ #
11
15
  # Each including interpreter gets its own `trace` — set by a spec to
12
16
  # observe dispatch order (Vocabulary::AggregateDispatchOrder and
13
17
  # Vocabulary::EntityDispatchOrder in language/bluebook/vocabulary.bluebook);
14
18
  # nil in production, always — one array push and a nil check per step
15
19
  # is the entire cost of leaving this in.
20
+ #
21
+ # @param interpreter [Class] the class (`CommandInterpreter`, `EntityInterpreter`)
22
+ # including this module
23
+ # @return [void]
16
24
  def self.included(interpreter)
17
25
  interpreter.singleton_class.attr_accessor :trace
18
26
  end
@@ -32,9 +40,9 @@ module Hecks
32
40
  # read off its own generated Vocabulary::*DispatchOrder table —
33
41
  # vocabulary.bluebook, via lib/hecks/vocabulary.rb) by `send`ing
34
42
  # each declared step name against the including interpreter's own
35
- # `step_<name>` handler, in declared order. What used to be `call`'s own
36
- # literal sequence of method calls is now data driving that sequence —
37
- # tracing a real dispatch and comparing it to the declaration is
43
+ # `step_<name>` handler, in declared order. Data drives that sequence,
44
+ # rather than `call` spelling it out as a literal sequence of method
45
+ # calls — tracing a real dispatch and comparing it to the declaration is
38
46
  # tautological once `call` mechanically follows the declaration; a
39
47
  # conditional step (assign_creation_attributes, advance_lifecycle) still
40
48
  # has to guard itself at the top of its own handler and skip tracing
@@ -116,8 +124,8 @@ module Hecks
116
124
  # refuse_absent_arguments are separate DISPATCH_ORDER steps now (the
117
125
  # declared vocabulary lists all three as flat, sequential members, not
118
126
  # one nesting the other two), and EntityInterpreter never had them here
119
- # at all (an entity inherits its aggregate's own gate). One copy,
120
- # shared, rather than the two identical ones that used to drift.
127
+ # at all (an entity inherits its aggregate's own gate). One shared
128
+ # copy, rather than two identical ones that can only ever drift apart.
121
129
  def normalize_args(aggregate, command, args)
122
130
  coerce_declared_arguments(aggregate, command, args)
123
131
  end
@@ -9,7 +9,8 @@ module Hecks
9
9
  # (`target`, a `Routing::Envelope` or nil), and every fact the caller did
10
10
  # or did not offer. Built once per dispatch by `Invocation.from_call`, the
11
11
  # only place the runtime interprets the shape of a call (`to:` vs `with:`
12
- # vs loose keyword arguments, a port operation's reference attribute
12
+ # vs the flat facts hash `dispatch_flat` carries, a port operation's
13
+ # reference attribute
13
14
  # lifted into `to:`). `Runtime::Routing.envelope`/`.payload` delegate here
14
15
  # and no longer hold that logic themselves.
15
16
  #
@@ -21,12 +22,12 @@ module Hecks
21
22
  # - `Invocation::Present` — the key was offered with a value
22
23
  #
23
24
  # Keys are kept exactly as offered: `with:` keys are symbolized (as they
24
- # always were), loose keyword arguments keep whatever key the caller
25
- # used. Undeclared keys a caller offered are kept too — refusing them is
25
+ # always were), a flat facts hash keeps whatever key the wire used. Undeclared keys a caller
26
+ # offered are kept too — refusing them is
26
27
  # still `refuse_unknown_arguments`' job, a dispatch step, not this one.
27
28
  #
28
- # PR I1 changes no behavior: every interpreter still reads `ctx.args`,
29
- # which is `#to_args` — the same Hash `Routing.payload` used to return.
29
+ # I1 changes no behavior: every interpreter still reads `ctx.args`,
30
+ # which is `#to_args` — the same Hash `Routing.payload` returns.
30
31
  # Roadmap I2 moves the Ruby `decode_arguments` step onto `facts` itself.
31
32
  Invocation = Data.define(:verb, :target, :facts)
32
33
 
@@ -41,6 +42,7 @@ module Hecks
41
42
  # The class of the two frozen marker singletons below — never
42
43
  # instantiated anywhere else.
43
44
  class Marker
45
+ # @param name [String] the marker's own label, used by #inspect/#to_s
44
46
  def initialize(name)
45
47
  @name = name
46
48
  freeze
@@ -54,22 +56,51 @@ module Hecks
54
56
  Absent = Marker.new("Absent")
55
57
  Null = Marker.new("Null")
56
58
 
59
+ # @param verb [String] the fully qualified verb this invocation dispatches
60
+ # @param target [Routing::Envelope, nil] the resolved receiver, or nil when
61
+ # the facts carry the identity themselves
62
+ # @param facts [Hash{String, Symbol => Object}] every candidate fact, keyed by
63
+ # offered (or declared) name; frozen, deep-duped, before being stored
57
64
  def initialize(verb:, target:, facts:)
58
65
  super(verb: verb, target: target, facts: facts.dup.freeze)
59
66
  end
60
67
 
68
+ # Reads the fact recorded under `name`.
69
+ #
61
70
  # The fact recorded under `name` — `Absent` for a name never offered
62
71
  # and never declared either.
72
+ #
73
+ # @param name [String, Symbol] the fact name to look up
74
+ # @return [Invocation::Present, Invocation::Null, Invocation::Absent] the
75
+ # recorded marker for `name`
63
76
  def fact(name) = facts.fetch(name, Absent)
64
77
 
78
+ # Reports whether `name` was offered with a real, non-nil value.
79
+ #
80
+ # @param name [String, Symbol] the fact name to check
81
+ # @return [Boolean] true if `name` was offered with a non-nil value
65
82
  def present?(name) = fact(name).is_a?(Present)
83
+
84
+ # Reports whether `name` was offered with an explicit nil.
85
+ #
86
+ # @param name [String, Symbol] the fact name to check
87
+ # @return [Boolean] true if `name` was offered with an explicit nil
66
88
  def null?(name) = fact(name).equal?(Null)
89
+
90
+ # Reports whether `name` was never offered.
91
+ #
92
+ # @param name [String, Symbol] the fact name to check
93
+ # @return [Boolean] true if `name` was never offered (and, if declared, left out)
67
94
  def absent?(name) = fact(name).equal?(Absent)
68
95
 
69
96
  # A Present fact's value; nil for an explicit Null. Raises KeyError for
70
97
  # an Absent fact — "never offered" has no value, and answering nil
71
98
  # would re-conflate it with an explicit null, the very ambiguity this
72
99
  # type exists to remove. Ask `absent?`/`present?` first.
100
+ #
101
+ # @param name [String, Symbol] the fact name to read
102
+ # @return [Object, nil] the offered value; nil for an explicit Null fact
103
+ # @raise [KeyError] if `name` names an Absent fact (never offered)
73
104
  def value(name)
74
105
  case (found = fact(name))
75
106
  when Present then found.value
@@ -78,9 +109,12 @@ module Hecks
78
109
  end
79
110
  end
80
111
 
81
- # The legacy args hash, byte for byte what `Routing.payload` returned
112
+ # The flat args hash, byte for byte what `Routing.payload` returned
82
113
  # before this type existed: offered keys in offered order, Absent keys
83
114
  # omitted, Null keys mapped to nil. A fresh Hash every call.
115
+ #
116
+ # @return [Hash{String, Symbol => Object}] the offered facts by name; a fact
117
+ # offered as nil is kept as nil
84
118
  def to_args
85
119
  facts.each_with_object({}) do |(name, found), args|
86
120
  next if found.equal?(Absent)
@@ -90,7 +124,7 @@ module Hecks
90
124
  end
91
125
 
92
126
  class << self
93
- # **The one reading of a call's shape**. `receiver:` picks which of the
127
+ # The one reading of a call's shape. `receiver:` picks which of the
94
128
  # three dispatch shapes this is, because each has always checked its
95
129
  # parts in its own order and a malformed call's refusal depends on
96
130
  # that order:
@@ -107,24 +141,57 @@ module Hecks
107
141
  # the point in that order where the caller always resolved it.
108
142
  # `aggregate:` is the owning aggregate construct, read for `:port`
109
143
  # only.
110
- def from_call(verb, to:, with:, legacy:, receiver: :aggregate, entity_depth: 0, aggregate: nil, &declaring)
144
+ #
145
+ # @param verb [String] the fully qualified verb being dispatched
146
+ # @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
147
+ # `aggregate:` and `entity:`/`entities:`; nil for no explicit receiver
148
+ # @param with [Hash, nil] the command's facts, keyed by attribute name; nil
149
+ # when the caller offers `flat` instead
150
+ # @param flat [Hash] the command's facts as a flat args hash, used when `with`
151
+ # is nil; for `:port`, may also carry the receiver identity under a
152
+ # reference attribute's own name
153
+ # @param receiver [Symbol] which of the three dispatch shapes this is:
154
+ # `:aggregate`, `:entity` or `:port`
155
+ # @param entity_depth [Integer] the number of entity-hop identities `to:`
156
+ # must carry, for `:entity` only
157
+ # @param aggregate [Bluebook::Aggregate, nil] the owning aggregate construct,
158
+ # read for `:port` only
159
+ # @yieldreturn [Class] the declaring command or port-operation class (anything
160
+ # with `hecks_name` and `attributes`)
161
+ # @return [Runtime::Invocation] the built invocation
162
+ # @raise [Runtime::TypeMismatch] if `to:` is malformed, its entity count is
163
+ # wrong, or (`:port` only) the operation resolves no receiving aggregate
164
+ # @raise [Runtime::UnknownArgument] if `with:` names an attribute the command
165
+ # does not declare
166
+ # @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
167
+ # @raise [ArgumentError] if `receiver` is none of `:aggregate`, `:entity` or `:port`
168
+ def from_call(verb, to:, with:, flat:, receiver: :aggregate, entity_depth: 0, aggregate: nil, &declaring)
111
169
  case receiver
112
170
  when :aggregate
113
171
  command = declaring.call
114
- facts = facts_for(command, with: with, legacy: legacy)
172
+ facts = facts_for(command, with: with, flat: flat)
115
173
  new(verb: verb, target: route(to), facts: facts)
116
174
  when :entity
117
175
  target = route(to, entity_depth: entity_depth)
118
176
  command = declaring.call
119
- new(verb: verb, target: target, facts: facts_for(command, with: with, legacy: legacy))
177
+ new(verb: verb, target: target, facts: facts_for(command, with: with, flat: flat))
120
178
  when :port
121
- port_call(verb, aggregate, declaring.call, to: to, with: with, legacy: legacy)
179
+ port_call(verb, aggregate, declaring.call, to: to, with: with, flat: flat)
122
180
  else
123
181
  raise ArgumentError, "unknown receiver #{receiver.inspect}"
124
182
  end
125
183
  end
126
184
 
127
185
  # `to:` as a `Routing::Envelope`, or nil when no `to:` was given.
186
+ #
187
+ # @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
188
+ # `aggregate:` and `entity:`/`entities:`; nil for no receiver
189
+ # @param entity_depth [Integer] the number of entity-hop identities `to`
190
+ # must carry
191
+ # @return [Routing::Envelope, nil] the resolved envelope, or nil when `to`
192
+ # is nil
193
+ # @raise [Runtime::TypeMismatch] if `to` is malformed, or its entity count
194
+ # does not match `entity_depth`
128
195
  def route(to, entity_depth: 0)
129
196
  return nil if to.nil?
130
197
 
@@ -146,8 +213,23 @@ module Hecks
146
213
  # The offered facts for `declaring`, as Absent/Null/Present — offered
147
214
  # keys first, in offered order, then every declared attribute that was
148
215
  # not offered, as Absent.
149
- def facts_for(declaring, with:, legacy:)
150
- offered = offered_facts(declaring, with: with, legacy: legacy)
216
+ #
217
+ # @param declaring [Class] the command or port-operation class (anything
218
+ # with `hecks_name` and `attributes`) whose declared attributes are read
219
+ # @param with [Hash, nil] the command's facts, keyed by attribute name; nil
220
+ # when the caller offers `flat` instead
221
+ # @param flat [Hash] the command's facts as a flat args hash, used when `with`
222
+ # is nil
223
+ # @return [Hash{String, Symbol => Invocation::Absent, Invocation::Null,
224
+ # Invocation::Present}] each candidate fact keyed by its offered (or
225
+ # declared) name
226
+ # @raise [Runtime::TypeMismatch] if both `with` and a non-empty `flat` are
227
+ # given, or if `with` is not a Hash
228
+ # @raise [Runtime::UnknownArgument] if `with:` names an attribute `declaring`
229
+ # does not declare
230
+ # @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
231
+ def facts_for(declaring, with:, flat:)
232
+ offered = offered_facts(declaring, with: with, flat: flat)
151
233
  facts = offered.each_with_object({}) do |(name, value), found|
152
234
  found[name] = value.nil? ? Null : Present.new(value: value)
153
235
  end
@@ -160,10 +242,10 @@ module Hecks
160
242
 
161
243
  private
162
244
 
163
- # **The port operation shape** — formerly `Dispatcher#port_invocation`.
245
+ # **The port operation shape** — moved from `Dispatcher#port_invocation`.
164
246
  #
165
247
  # A Reference-typed attribute naming the owning aggregate is routing,
166
- # not a fact: lifted out of the loose kwargs into `to:` when no `to:`
248
+ # not a fact: lifted out of the flat facts into `to:` when no `to:`
167
249
  # was given. A `to:`-declared operation carries no Reference
168
250
  # attribute at all (PortOperationBuilder#initialize's own comment),
169
251
  # so its receiver is read — not removed — from the plain attribute
@@ -173,30 +255,30 @@ module Hecks
173
255
  # with attribute"; a real AbsentArgument confirmed this before `[]`
174
256
  # replaced `delete`). Composite identity is not attempted — `.first`
175
257
  # only, no domain in the corpus needs more for a port operation.
176
- def port_call(verb, aggregate, operation, to:, with:, legacy:)
177
- legacy = legacy.dup
258
+ def port_call(verb, aggregate, operation, to:, with:, flat:)
259
+ flat = flat.dup
178
260
  identity = operation.identity_attribute(aggregate.hecks_name)
179
- if to.nil? && identity && legacy.key?(identity.name)
180
- to = legacy.delete(identity.name)
261
+ if to.nil? && identity && flat.key?(identity.name)
262
+ to = flat.delete(identity.name)
181
263
  elsif to.nil? && operation.to == aggregate.hecks_name
182
264
  identity_name = Array(aggregate.identified_by).first
183
- to = legacy[identity_name] if identity_name && legacy.key?(identity_name)
265
+ to = flat[identity_name] if identity_name && flat.key?(identity_name)
184
266
  end
185
267
 
186
268
  target = route(to)
187
269
  raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless target
188
270
 
189
- new(verb: verb, target: target, facts: facts_for(operation, with: with, legacy: legacy))
271
+ new(verb: verb, target: target, facts: facts_for(operation, with: with, flat: flat))
190
272
  end
191
273
 
192
274
  # BUG#7 — a non-Hash `to:` must be a String, matching Rust's
193
275
  # `RoutingEnvelope::from_json` (kernel/routing.rs), which refuses
194
276
  # anything neither a JSON string nor object before the domain payload
195
277
  # is examined. Surfaced on `examples/roster`'s `Mark`, whose own
196
- # attribute is literally named `to`: a flat-kwargs dispatch steals
197
- # that key into this parameter, and an out-of-range Integer used to be
198
- # accepted as the aggregate identity, leaving Mark's `to` fact absent
199
- # (AbsentArgument in Ruby, TypeMismatch in Rust). See
278
+ # attribute is literally named `to`: a flat-facts dispatch steals
279
+ # that key into this parameter, and without this check an out-of-range
280
+ # Integer would be accepted as the aggregate identity, leaving Mark's
281
+ # `to` fact absent (AbsentArgument in Ruby, TypeMismatch in Rust). See
200
282
  # spec/runtime/routing_envelope_shape_spec.rb.
201
283
  def scalar_envelope(to)
202
284
  return [to, []] if to.is_a?(String)
@@ -214,11 +296,11 @@ module Hecks
214
296
 
215
297
  # BUG#18 — an entity route naming no entity (`entities: []`, or
216
298
  # neither key) refuses here, unconditionally, before entity_depth is
217
- # consulted: for an aggregate-level command (depth 0) `[].size == 0`
218
- # used to satisfy the depth check and let the degenerate Hash reach
219
- # the command's own validation. Rust's `RoutingEnvelope::from_json`
220
- # always refused it at this point. A bare aggregate identity String
221
- # remains the ordinary aggregate-command shape.
299
+ # consulted: for an aggregate-level command (depth 0), without this,
300
+ # `[].size == 0` would satisfy the depth check and let the degenerate
301
+ # Hash reach the command's own validation. Rust's `RoutingEnvelope::
302
+ # from_json` always refused it at this point. A bare aggregate identity
303
+ # String remains the ordinary aggregate-command shape.
222
304
  def entity_identities(hash)
223
305
  raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
224
306
 
@@ -230,16 +312,16 @@ module Hecks
230
312
 
231
313
  # `with:` is deliberately strict: a caller choosing the explicit
232
314
  # envelope cannot smuggle receiver identity back into the payload,
233
- # and may not mix it with loose keyword arguments. Without `with:`
234
- # (nil or false) the loose keyword arguments are the facts, unread —
315
+ # and may not mix it with a flat facts hash. Without `with:`
316
+ # (nil or false) the flat facts are the facts, unread —
235
317
  # whether `to:` was given never enters this decision (BUG#17).
236
- def offered_facts(declaring, with:, legacy:)
237
- if with && !legacy.empty?
318
+ def offered_facts(declaring, with:, flat:)
319
+ if with && !flat.empty?
238
320
  raise TypeMismatch,
239
- "dispatch takes command facts in with:, not both with: and loose keyword arguments"
321
+ "dispatch takes command facts in with:, not both with: and a flat facts hash"
240
322
  end
241
323
 
242
- return legacy unless with
324
+ return flat unless with
243
325
  raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
244
326
 
245
327
  offered = with.transform_keys(&:to_sym)