hecks 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -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)
@@ -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) ||