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
@@ -37,6 +37,8 @@ module Hecks
37
37
  # identifying the field, so a rule that reads it fails loud
38
38
  # instead of quietly wrong.
39
39
  class GuardState
40
+ # @param instance [Runtime::Instance] the record a `given`/`ensures`/
41
+ # `invariant` rule reads state from
40
42
  def initialize(instance)
41
43
  @instance = instance
42
44
  @declared = instance.respond_to?(:aggregate) ? instance.aggregate.attributes.to_h { |a| [a.name, a] } : {}
@@ -55,8 +57,23 @@ module Hecks
55
57
  @projected = owner.respond_to?(:projected_fields) ? owner.projected_fields.to_h { |f| [f.name, f] } : {}
56
58
  end
57
59
 
60
+ # Reports whether `name` is a declared attribute, a projected field, or
61
+ # a key `instance`'s own state actually holds.
62
+ #
63
+ # @param name [String, Symbol] the field name to check
64
+ # @return [Boolean] true if `name` is readable one of those three ways
58
65
  def key?(name) = @declared.key?(name.to_sym) || @projected.key?(name.to_sym) || @instance.key?(name)
59
66
 
67
+ # Reads one field, the way a `given`/`ensures`/`invariant` rule expects
68
+ # absence to read.
69
+ #
70
+ # @param name [String, Symbol] the field name to read
71
+ # @return [Object, nil] the stored value; nil for a declared, optional
72
+ # attribute the record predates
73
+ # @raise [Runtime::AttributeAbsent] if `name` is a declared, non-optional
74
+ # attribute the record's own state does not hold
75
+ # @raise [Runtime::ProjectionAbsent] if `name` is a `projects` field the
76
+ # record's own state does not hold
60
77
  def [](name)
61
78
  return @instance[name] if @instance.key?(name)
62
79
 
@@ -101,7 +118,7 @@ module Hecks
101
118
  # found this — TypeError, not a refusal. Command-level
102
119
  # dereferencing is the one thing that is supposed to override
103
120
  # its own source argument for exactly this reason; args still
104
- # wins over stored owner state (unchanged from before this fix).
121
+ # wins over stored owner state.
105
122
  # `parent:` is an entity command's own parent aggregate record
106
123
  # (EntityInterpreter's `ctx.instance` — "the parent aggregate
107
124
  # record", its own doc comment) — the entity's containment, not a
@@ -109,9 +126,9 @@ module Hecks
109
126
  # `dereference`'s attribute scan the way `owner`'s do. Hydrated
110
127
  # the same shape regardless: the parent's own state, merged so
111
128
  # the dereferenced hash wins over the raw reference it replaces
112
- # (ADR 0025 dropped the `_id` suffix that used to keep the two
129
+ # (ADR 0025 drops the `_id` suffix that once kept the two
113
130
  # apart by name, so `parent.state.merge(dereference(...))` is
114
- # now load-bearing, not redundant) — plus its own references
131
+ # load-bearing, not redundant) — plus its own references
115
132
  # dereferenced one level in, so `parent.customer.status` (a
116
133
  # parent aggregate reaching its own customer) resolves the same
117
134
  # way `account.customer.status` does for a command-level reference.
@@ -122,14 +139,34 @@ module Hecks
122
139
  # `enforce_ensures`, below — it is a fresh local binding a
123
140
  # `corrects` command introduces, not a real argument/state field
124
141
  # a caller could collide with by accident.
142
+ #
143
+ # @param subject [Runtime::Instance] the pre-mutation record a `given` is
144
+ # checked against
145
+ # @param command [Class] the command class (`Bluebook::Command` subclass)
146
+ # whose declared `givens` are checked
147
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments,
148
+ # readable by a given
149
+ # @param domain [String, Symbol] the domain a reference-typed argument is
150
+ # dereferenced in
151
+ # @param declaring [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
152
+ # `command` is declared on; also checks `enforce_lifecycle_guard` when given
153
+ # @param parent [Runtime::Instance, nil] an entity command's own parent
154
+ # aggregate record, readable as `parent.*`; nil for an aggregate command
155
+ # @param correction [Hash{Symbol => Object}] the `{as_name => payload}`
156
+ # bindings a `corrects` command's located old event offers
157
+ # @return [void]
158
+ # @raise [Runtime::GivenNotMet] if a declared `given` does not hold
159
+ # @raise [Runtime::LifecycleRefused] if `declaring` is given and the command's
160
+ # own `from:` guard refuses the record's current lifecycle state
161
+ # @raise [Bluebook::Expression::EvaluationError] if a given's own rule cannot
162
+ # be evaluated (an unresolvable field, a bad comparison)
125
163
  def enforce_givens(subject, command, args, domain:, declaring: nil, parent: nil, correction: {})
126
164
  state = GuardState.new(subject)
127
165
  # A rule may only read within its own aggregate boundary (S12,
128
166
  # ADR 0025) — `subject`'s own stored references are no longer
129
- # dereferenced here at all. What used to be a live query against
130
- # another aggregate's own repository is now just `subject`'s own
131
- # state: a `projects :customer_status, from: :"customer.status"`
132
- # field is a regular stored attribute, already present in
167
+ # dereferenced here at all: a `projects :customer_status, from:
168
+ # :"customer.status"` field is just `subject`'s own
169
+ # field, a regular stored attribute, already present in
133
170
  # `subject`/`state` with no hydration step needed. `dereference`
134
171
  # is still called on `command`/`args`, below — that is a
135
172
  # different case the ADR explicitly keeps in bounds ("its command
@@ -180,16 +217,64 @@ module Hecks
180
217
  # one, made deliberately: `as:` reads as "the instance being
181
218
  # corrected," which is naturally the latest fact on record, not
182
219
  # an arbitrary one.
220
+ # Reads the durable event history a `corrects` mutation is judged against.
221
+ #
183
222
  # C9.2 (docs/semantics/bluebook-semantics.md) — a correction target
184
223
  # is judged against the record's durable history: the events the
185
224
  # aggregate's own store recorded (`AppendOnly#events`), which
186
225
  # survive a restart the way the Rust kernel's persisted
187
226
  # `emitted_<event>` flag does. The in-process log is the fallback
188
227
  # only for an adapter that records no readable history.
228
+ #
229
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
230
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose repository is read
231
+ # @return [Array<Runtime::Event>] the aggregate's own durably recorded events
232
+ # if its repository keeps a readable log, otherwise the registry's in-process
233
+ # event log
189
234
  def correction_history(domain, aggregate)
190
235
  @registry.repository(domain, aggregate).events || @registry.event_log
191
236
  end
192
237
 
238
+ # Locates each `:corrects` mutation's own already-emitted target event, and
239
+ # binds every `as:`-named one for `given`/`ensures` to reference.
240
+ #
241
+ # `corrects` — CommandBuilder#corrects_impl's own comment. Not
242
+ # expressible as an ordinary `given`: "has this exact record
243
+ # already emitted this exact event" is not a predicate over the
244
+ # record's own fields, it is a fact about the event log, so it is
245
+ # raised structurally here, the same way NotFound/AlreadyExists
246
+ # are, rather than through the expression evaluator. The build-
247
+ # time half — does anything in this aggregate ever emit the named
248
+ # event at all — is `AggregateBuilder#seal_correction_targets`;
249
+ # this is the dispatch-time half — has this record actually done
250
+ # so yet.
251
+ #
252
+ # Also locates the matched event now, not just its existence, and
253
+ # returns a `{as_name => payload}` bindings hash — one entry per
254
+ # `:corrects` mutation that named an `as:` — so `given`/`ensures`
255
+ # on a corrects-bearing command can reference the located old
256
+ # event by that name, the same shape `enforce_ensures`'s own
257
+ # `old:` binding already has (CommandBuilder#corrects_impl's own
258
+ # comment: `as:` was stored, from the start, specifically to be
259
+ # wired into the evaluator once a real runtime consumer existed —
260
+ # this is that consumer). `.reverse.find` — the most recent
261
+ # matching event, if this record has somehow emitted the same
262
+ # correction target more than once; the prior existence-only
263
+ # check never had to make this choice, so it's a genuinely new
264
+ # one, made deliberately: `as:` reads as "the instance being
265
+ # corrected," which is naturally the latest fact on record, not
266
+ # an arbitrary one.
267
+ #
268
+ # @param instance [Runtime::Instance] the record being corrected
269
+ # @param aggregate [Bluebook::Aggregate] the aggregate `instance` belongs to
270
+ # @param command [Class] the command class (`Bluebook::Command` subclass)
271
+ # whose `:corrects` mutations are located
272
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
273
+ # @return [Hash{Symbol => Object}] the located event's payload, keyed by
274
+ # each `:corrects` mutation's own `as:` name; empty for a mutation with
275
+ # no `as:`
276
+ # @raise [Runtime::NothingToCorrect] if a `:corrects` mutation names an
277
+ # event `instance` has never emitted
193
278
  def enforce_correction_target(instance, aggregate, command, domain:)
194
279
  bindings = {}
195
280
  command.mutations.each do |mutation|
@@ -222,6 +307,16 @@ module Hecks
222
307
  # `admissible_transition`, right below, for the transition this
223
308
  # is deliberately not reusing (its own `StateTransition#target`
224
309
  # is required, and a guard-only command has none to give it).
310
+ #
311
+ # @param declaring [Bluebook::Aggregate, Bluebook::Entity] the construct
312
+ # whose lifecycle field is checked
313
+ # @param command [Class] the command class (`Bluebook::Command` subclass)
314
+ # whose `from:` guard is checked
315
+ # @param subject [Runtime::Instance] the pre-mutation record to read the
316
+ # current lifecycle state off
317
+ # @return [void]
318
+ # @raise [Runtime::LifecycleRefused] if `command` declares `from:` and the
319
+ # record's current lifecycle state is not one of them
225
320
  def enforce_lifecycle_guard(declaring, command, subject)
226
321
  return unless command.from
227
322
 
@@ -232,12 +327,12 @@ module Hecks
232
327
  # Routed through RefusalWording's own "transition_blocked"
233
328
  # template — the same one #admissible_transition, right below,
234
329
  # already raises LifecycleRefused through for the same
235
- # refusal class. This used to hand-roll its own wording
330
+ # refusal class, rather than hand-rolling its own wording
236
331
  # inline ("...only runs from..." vs. the template's "...moves
237
- # it only from...") — two shapes for one refusal kind, so
332
+ # it only from..."), two shapes for one refusal kind, so
238
333
  # anything string-matching a LifecycleRefused message (a
239
- # property, a spec, a caller) had to know both existed rather
240
- # than one.
334
+ # property, a spec, a caller) would have to know both existed
335
+ # rather than one.
241
336
  raise LifecycleRefused,
242
337
  RefusalWording.render_site("LifecycleRefused", "transition_blocked",
243
338
  command: command.hecks_name, field: lifecycle.field,
@@ -258,6 +353,25 @@ module Hecks
258
353
  # Not new to ensures — `given` lives under the same rule — but an
259
354
  # ensures is more likely to collide, since it typically re-reads a
260
355
  # field the command just took in to mutate it.
356
+ #
357
+ # @param subject [Runtime::Instance] the settled, post-mutation record an
358
+ # `ensures` is checked against
359
+ # @param command [Class] the command class (`Bluebook::Command` subclass)
360
+ # whose declared `ensures` are checked
361
+ # @param args [Hash{String, Symbol => Object}] the offered command arguments,
362
+ # readable by an ensures unless `subject` shares the same field name
363
+ # @param old [Hash{Symbol => Object}, nil] the pre-mutation state, readable
364
+ # as `old.*`; nil when `command` declares no ensures (dup skipped upstream)
365
+ # @param domain [String, Symbol] the domain a reference-typed argument is
366
+ # dereferenced in
367
+ # @param parent [Runtime::Instance, nil] an entity command's own parent
368
+ # aggregate record, readable as `parent.*`; nil for an aggregate command
369
+ # @param correction [Hash{Symbol => Object}] the `{as_name => payload}`
370
+ # bindings a `corrects` command's located old event offers
371
+ # @return [void]
372
+ # @raise [Runtime::EnsuresNotMet] if a declared `ensures` does not hold
373
+ # @raise [Bluebook::Expression::EvaluationError] if an ensures's own rule
374
+ # cannot be evaluated (an unresolvable field, a bad comparison)
261
375
  def enforce_ensures(subject, command, args, old:, domain:, parent: nil, correction: {})
262
376
  state = GuardState.new(subject)
263
377
  # S12, ADR 0025 — same boundary reasoning as enforce_givens
@@ -302,9 +416,21 @@ module Hecks
302
416
  # No `dereference` (S12, ADR 0025) — an invariant may only read
303
417
  # `subject`'s own boundary, same rule `enforce_givens`/
304
418
  # `enforce_ensures` now hold to. No invariant in the corpus has
305
- # ever read across a `reference_to` (verified before this
306
- # change), so this is not a migration, just closing the same
307
- # capability off here that was already unused.
419
+ # ever read across a `reference_to` (verified by grep), so this
420
+ # is not a migration, just closing the same capability off here
421
+ # that was already unused.
422
+ #
423
+ # @param subject [Runtime::Instance] the settled aggregate record an
424
+ # invariant is checked against
425
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose declared
426
+ # `invariants` are checked
427
+ # @param domain [String, Symbol] the domain `aggregate` belongs to, passed
428
+ # through to `check_entity_invariants`
429
+ # @return [void]
430
+ # @raise [Runtime::InvariantViolation] if a declared invariant does not hold,
431
+ # on `aggregate` itself or any of its entities
432
+ # @raise [Bluebook::Expression::EvaluationError] if an invariant's own rule
433
+ # cannot be evaluated (an unresolvable field, a bad comparison)
308
434
  def enforce_invariants(subject, aggregate, domain:)
309
435
  state = GuardState.new(subject)
310
436
  attrs = {}
@@ -338,6 +464,17 @@ module Hecks
338
464
  # matching list attribute) is a static-analysis gap for a
339
465
  # future gate, not a runtime concern here — `next` past it
340
466
  # rather than raising mid-enforcement for an unrelated command.
467
+ #
468
+ # @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] the
469
+ # construct whose nested entities are checked
470
+ # @param owner_instance [Runtime::Instance] the settled record holding
471
+ # `owner_construct`'s own entity lists
472
+ # @param domain [String, Symbol] the domain `owner_construct` belongs to
473
+ # @return [void]
474
+ # @raise [Runtime::InvariantViolation] if a declared entity invariant does
475
+ # not hold, on any element or its own nested entities
476
+ # @raise [Bluebook::Expression::EvaluationError] if an invariant's own rule
477
+ # cannot be evaluated (an unresolvable field, a bad comparison)
341
478
  def check_entity_invariants(owner_construct, owner_instance, domain:)
342
479
  owner_construct.entities.each do |entity|
343
480
  next if entity.invariants.empty?
@@ -364,6 +501,20 @@ module Hecks
364
501
  end
365
502
  end
366
503
 
504
+ # Finds the lifecycle transition `command` admits from `subject`'s current
505
+ # state, if `declaring` declares a lifecycle and `command` moves it.
506
+ #
507
+ # @param declaring [Bluebook::Aggregate, Bluebook::Entity] the construct
508
+ # whose lifecycle is checked
509
+ # @param command [Class] the command class (`Bluebook::Command` subclass)
510
+ # to find a declared transition for
511
+ # @param subject [Runtime::Instance] the pre-mutation record to read the
512
+ # current lifecycle state off
513
+ # @return [Bluebook::StateTransition, nil] the admitted transition, or nil
514
+ # when `declaring` has no lifecycle or `command` declares no transition
515
+ # @raise [Runtime::LifecycleRefused] if `command` declares one or more
516
+ # transitions, each constrained by its own `from:`, and none admits the
517
+ # record's current lifecycle state
367
518
  def admissible_transition(declaring, command, subject)
368
519
  lifecycle = declaring.lifecycle
369
520
  return nil unless lifecycle
@@ -279,6 +279,9 @@ module Hecks
279
279
  bounded(current * amount, "multiply", current, amount, "*")
280
280
  end
281
281
 
282
+ # Bounds an attribute's current value into `[min, max]`, on a bare number or on
283
+ # the one numeric field the wrapping value object carries.
284
+ #
282
285
  # Vendored addition, not (yet) upstream hecks (migration plan
283
286
  # task 4, i106): bound the current value into `[min, max]` -- no
284
287
  # "amount" to combine, so it does not go through
@@ -286,6 +289,15 @@ module Hecks
286
289
  # it clamps whichever single numeric field the wrapping value
287
290
  # object carries (a synthesised wrapper always carries exactly
288
291
  # one, per Part 3a's auto-synthesis).
292
+ #
293
+ # @param current [Numeric, Runtime::Value, nil] the attribute's pre-dispatch value; nil
294
+ # (never set) counts as 0
295
+ # @param bounds [Array<Numeric>] the two-element `[min, max]` range to clamp into
296
+ # @param target [Symbol, String] name of the attribute, used only to word a refusal
297
+ # @return [Numeric, Runtime::Value] the clamped value: a `Runtime::Value` when
298
+ # `current` is one, otherwise a bare number the caller re-wraps
299
+ # @raise [Runtime::TypeMismatch] if `current` is a value object with no single
300
+ # numeric field, or is neither numeric nor a value object
289
301
  def clamp(current, bounds, target)
290
302
  min, max = bounds
291
303
  # The same `current ||= 0` #arithmetic/#multiply both give a
@@ -294,11 +306,11 @@ module Hecks
294
306
  # attribute with no declared `default:` (genuinely absent,
295
307
  # `Instance.defaults`/`#default_for`) hit TypeMismatch on the
296
308
  # first clamp. (#arithmetic/#multiply's own absent-current gap
297
- # was a real, separate bug this comment used to describe wrong —
298
- # they did not "silently treat the same absent field as zero";
299
- # they raised too, blaming a perfectly valid `amount` for not
300
- # being an Integer when it was one, just still Money-wrapped.
301
- # Fixed alongside this one — see #unwrap_single_numeric_field.)
309
+ # is a real, separate bug: they do not silently treat an absent
310
+ # field as zero; they raise too, blaming a perfectly valid
311
+ # `amount` for not being an Integer when it is one, just still
312
+ # Money-wrapped. Fixed alongside this one — see
313
+ # #unwrap_single_numeric_field.)
302
314
  current ||= 0
303
315
  if current.is_a?(Value)
304
316
  fields = current.to_h
@@ -23,6 +23,15 @@ module Hecks
23
23
  # Shared by CommandInterpreter and EntityInterpreter — an entity command
24
24
  # can declare a reference-typed attribute the same way an aggregate
25
25
  # command can, even though nothing in the real corpus does yet.
26
+ #
27
+ # @param domain [String, Symbol] the domain the referenced aggregate is
28
+ # resolved in
29
+ # @param command [Class] the command or port-operation class (`Bluebook::Command`
30
+ # or `PortOperation` subclass) whose reference-typed attributes are checked
31
+ # @param args [Hash{Symbol => Object}] the offered, already-coerced argument values
32
+ # @return [void]
33
+ # @raise [Runtime::NotFound] if a reference-typed argument names an identity that
34
+ # does not exist in its target aggregate's repository
26
35
  def resolve_references(domain, command, args)
27
36
  command.attributes.each do |attribute|
28
37
  next unless attribute.reference?
@@ -41,6 +50,20 @@ module Hecks
41
50
  # Structural references are checked again against the settled state.
42
51
  # This is what makes a `has_many` declared on an aggregate honest even
43
52
  # when a command supplies its list through an ordinary typed argument.
53
+ #
54
+ # @param domain [String, Symbol] the domain the referenced aggregate is
55
+ # resolved in
56
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
57
+ # whose reference-typed attributes are checked against `state`
58
+ # @param state [Hash{Symbol => Object}] the settled, post-mutation state to
59
+ # check every reference and relationship cardinality against
60
+ # @return [void]
61
+ # @raise [Runtime::TypeMismatch] if a required `has_one`/`belongs_to`
62
+ # relationship holds nil
63
+ # @raise [Runtime::NotFound] if a reference-typed field names an identity
64
+ # that does not exist in its target aggregate's repository
65
+ # @raise [Runtime::Unauthorized] if a referenced record belongs to a
66
+ # different tenant than `construct`'s own record
44
67
  def resolve_state_references(domain, construct, state)
45
68
  own_tenant_field = tenant_field_for(construct)
46
69
 
@@ -73,6 +96,14 @@ module Hecks
73
96
  # checking only command arguments would let a required relationship be
74
97
  # persisted as nil. `has_many` admits zero members, so its empty list is
75
98
  # already a valid cardinality and needs no presence refusal.
99
+ #
100
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
101
+ # `attribute` is declared on, named in a refusal
102
+ # @param attribute [Bluebook::Attribute] the reference-typed attribute to check
103
+ # @param held [Object, nil] the field's settled value
104
+ # @return [void]
105
+ # @raise [Runtime::TypeMismatch] if `attribute` declares a required (non-optional,
106
+ # non-list) `has_one`/`belongs_to` relationship and `held` is nil
76
107
  def validate_relationship_cardinality(construct, attribute, held)
77
108
  return if attribute.relationship.nil? || attribute.list?
78
109
  return unless held.nil? && !attribute.optional?
@@ -84,15 +115,31 @@ module Hecks
84
115
  end
85
116
 
86
117
  # The reference resolves itself — through the chapter's own IR, so the
87
- # bluebook's declared heads are the index. This used to regex the target's
88
- # name out of "Reference<Customer>" and then search
89
- # `registry.bluebook(domain).aggregates` for it — and later reached the
90
- # target through Ruby's constant tree, a class thrown away for its `.ir`
91
- # the moment it was found.
118
+ # bluebook's declared heads are the index, rather than regexing the
119
+ # target's name out of "Reference<Customer>" and searching
120
+ # `registry.bluebook(domain).aggregates` for it, or reaching the
121
+ # target through Ruby's constant tree, a class thrown away for its
122
+ # `.ir` the moment it was found.
123
+ #
124
+ # @param attribute [Bluebook::Attribute] the reference-typed attribute to resolve
125
+ # @return [Bluebook::Aggregate, nil] the target aggregate the reference names,
126
+ # or nil if it cannot be resolved
127
+ # @raise [Bluebook::DSL::Malformed] if the reference does not know which
128
+ # aggregate declares it
92
129
  def referenced_aggregate(attribute)
93
130
  attribute.type.resolve
94
131
  end
95
132
 
133
+ # Refuses a reference-typed value whose target identity does not exist.
134
+ #
135
+ # @param domain [String, Symbol] the domain `target` is resolved in
136
+ # @param target [Bluebook::Aggregate] the referenced aggregate
137
+ # @param held [Object, Array<Object>] the offered value: a single reference,
138
+ # or, when `list:` is true, an Array of them
139
+ # @param list [Boolean] whether `held` is list-shaped
140
+ # @return [void]
141
+ # @raise [Runtime::NotFound] if any referenced identity does not exist in
142
+ # `target`'s repository
96
143
  def validate_reference_values(domain, target, held, list:)
97
144
  values = list ? Array(held) : [held]
98
145
  values.each do |value|
@@ -132,6 +179,22 @@ module Hecks
132
179
  # — one change, both interpreters covered, no new vocabulary step
133
180
  # to keep in sync with `Vocabulary::AggregateDispatchOrder`/
134
181
  # `EntityDispatchOrder`.
182
+ #
183
+ # @param domain [String, Symbol] the domain `target` is resolved in
184
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
185
+ # `attribute` is declared on, named in a refusal
186
+ # @param attribute [Bluebook::Attribute] the reference-typed attribute being
187
+ # checked
188
+ # @param target [Bluebook::Aggregate] the referenced aggregate
189
+ # @param held [Object, Array<Object>] the offered value: a single reference,
190
+ # or, when `attribute.list?` is true, an Array of them
191
+ # @param state [Hash{Symbol => Object}] `construct`'s own settled state, read
192
+ # for its own tenant field's value
193
+ # @param own_tenant_field [Symbol, nil] `construct`'s own declared tenant field;
194
+ # a no-op if nil or absent from `state`
195
+ # @return [void]
196
+ # @raise [Runtime::Unauthorized] if a referenced record's own tenant field
197
+ # disagrees with `construct`'s
135
198
  def enforce_tenant_boundary(domain, construct, attribute, target, held, state, own_tenant_field)
136
199
  return unless own_tenant_field && state.key?(own_tenant_field)
137
200
 
@@ -172,11 +235,19 @@ module Hecks
172
235
  # (`Entity.queries` is always empty in the real corpus), so this
173
236
  # answers `nil` for every entity without needing to special-case
174
237
  # one.
238
+ #
239
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
240
+ # whose declared queries are searched
241
+ # @return [Symbol, nil] the tenant-scoping field name, or nil if `construct`
242
+ # declares no query with `authorize policy, tenant: :field`
175
243
  def tenant_field_for(construct)
176
244
  authorization = construct.queries.filter_map(&:authorization).find(&:tenant)
177
245
  authorization&.tenant&.to_sym
178
246
  end
179
247
 
248
+ # Renders a reference-typed value as the plain string key its target
249
+ # record is looked up by.
250
+ #
180
251
  # `value` is the referenced record's own id, exactly as `Identity.of`
181
252
  # would build it for that record — a bare scalar for a single-field
182
253
  # identity (the overwhelming common case; Banking's own plain
@@ -184,18 +255,23 @@ module Hecks
184
255
  # materialize_unwrapped` is a no-op passthrough here), or a
185
256
  # `Naming.identity`-joined string for a compound one
186
257
  # (`belongs_to Translation, as: :translation_ref` — Translation's
187
- # own `identified_by :domain, :from, :to`, three fields). Before
188
- # this, plain `value.to_s` on that compound case's own coerced
189
- # Value hit Ruby's default `Object#to_s` (a raw, run-to-run-random
190
- # memory address) instead of joining the record's real id — found
191
- # live via bin/fuzz on the self-hosted "translation" domain
192
- # (replay_is_deterministic), the same class of gap `Identity.from`
258
+ # own `identified_by :domain, :from, :to`, three fields). Without
259
+ # `materialize_unwrapped`, plain `value.to_s` on that compound case's
260
+ # own coerced Value would hit Ruby's default `Object#to_s` (a raw,
261
+ # run-to-run-random memory address) instead of joining the record's
262
+ # real id — found live via bin/fuzz on the self-hosted "translation"
263
+ # domain (replay_is_deterministic), the same class of gap `Identity.from`
193
264
  # already had for a compound `identified_by`'s own bare (undotted)
194
265
  # attribute paths. `materialize_unwrapped` recurses a multi-
195
266
  # attribute value object to a plain Hash keyed by attribute name,
196
267
  # in declaration order — `Naming.identity` on `.values` reproduces
197
268
  # the identical join `Identity.of` itself would produce for the
198
269
  # same fields.
270
+ #
271
+ # @param value [Object] the offered reference value: a `Runtime::Value`, a
272
+ # scalar, or anything `Value.materialize_unwrapped` can open
273
+ # @return [String] the key to look the referenced record up by; empty when
274
+ # `value` names no id
199
275
  def reference_key(value)
200
276
  unwrapped = Value.materialize_unwrapped(value)
201
277
  return Naming.identity(unwrapped.values).to_s if unwrapped.is_a?(Hash)
@@ -203,8 +279,8 @@ module Hecks
203
279
  unwrapped.to_s
204
280
  end
205
281
 
206
- # A command argument's own related record, reachable by name from
207
- # `given`/`ensures` — `disputed_by.status`, say, `CardPayment
282
+ # Hydrates a command argument's own related record, reachable by name
283
+ # from `given`/`ensures` — `disputed_by.status`, say, `CardPayment
208
284
  # .Dispute`'s own fresh `Reference<Customer>` argument — without
209
285
  # teaching the pure expression evaluator anything about
210
286
  # repositories. The lookup happens here, once, before evaluation;
@@ -213,23 +289,21 @@ module Hecks
213
289
  #
214
290
  # `owner` narrowed to `command` only (S12, ADR 0025 — "rules
215
291
  # confined to their own aggregate boundary"): dereferencing the
216
- # declaring aggregate/entity's own stored `reference_to` used to
217
- # be the other half of this method's job — a live query against
218
- # another aggregate's own repository, every time a `given`/
219
- # `ensures`/`invariant` ran. That half is gone; a cross-aggregate
220
- # fact a rule needs now has to be a `projects`-maintained local
221
- # field (`AggregateBuilder#projects_impl`'s own comment), already
222
- # present in `subject`'s own state, no hydration needed. A
223
- # reference-typed command argument stays in bounds, though — the
224
- # ADR's own boundary list names "its command arguments" as
225
- # readable, and nothing is stored yet for a fresh argument to
226
- # project from; resolving it once here, synchronous with this
227
- # command's own admission, is a different shape from a live query
228
- # against an already-persisted reference. `enforce_givens`/
292
+ # declaring aggregate/entity's own stored `reference_to` is not part
293
+ # of this method's job — a cross-aggregate fact a rule needs has to
294
+ # be a `projects`-maintained local field (`AggregateBuilder#
295
+ # projects_impl`'s own comment), already present in `subject`'s own
296
+ # state, no hydration needed, rather than a live query against
297
+ # another aggregate's own repository on every `given`/`ensures`/
298
+ # `invariant` run. A reference-typed command argument stays in
299
+ # bounds, though — the ADR's own boundary list names "its command
300
+ # arguments" as readable, and nothing is stored yet for a fresh
301
+ # argument to project from; resolving it once here, synchronous
302
+ # with this command's own admission, is a different shape from a
303
+ # live query against an already-persisted reference. `enforce_givens`/
229
304
  # `enforce_ensures` are this method's only two remaining callers,
230
305
  # both passing `command`/`args`, never a `subject`'s own
231
- # aggregate — verified before this comment was written, not
232
- # assumed.
306
+ # aggregate — verified by grep, not assumed.
233
307
  #
234
308
  # Recurses into what it finds, so a chain deeper than one hop
235
309
  # still resolves in one pass. Depth-bounded rather than cycle-
@@ -239,6 +313,22 @@ module Hecks
239
313
  DEREFERENCE_DEPTH = 4
240
314
  private_constant :DEREFERENCE_DEPTH
241
315
 
316
+ # Hydrates `owner`'s own reference-typed attributes into the records they name.
317
+ #
318
+ # @param domain [String, Symbol] the domain a referenced aggregate is
319
+ # resolved in
320
+ # @param owner [Class, Bluebook::Aggregate, Bluebook::Entity, nil] the
321
+ # construct whose reference-typed attributes are dereferenced; nil (or a
322
+ # depth of zero) short-circuits to an empty Hash
323
+ # @param source [Hash{Symbol => Object}] the offered payload to read each
324
+ # reference-typed attribute's raw id from
325
+ # @param depth [Integer] how many more hops to recurse into a resolved
326
+ # reference's own reference-typed fields
327
+ # @return [Hash{Symbol => Object}] one entry per resolved reference-typed
328
+ # attribute, keyed by the attribute name with a trailing `_id` stripped,
329
+ # valued at the referenced record's state merged with its own dereferenced
330
+ # references; an attribute with no id, an unresolvable target, or no
331
+ # matching record contributes nothing
242
332
  def dereference(domain, owner, source, depth: DEREFERENCE_DEPTH)
243
333
  return {} if depth <= 0 || owner.nil?
244
334
 
@@ -17,12 +17,28 @@ module Hecks
17
17
  :unresolved_dependencies,
18
18
  keyword_init: true
19
19
  ) do
20
+ # Reports whether every owner field this command could touch is a known,
21
+ # deterministic write.
22
+ #
23
+ # @return [Boolean] `complete_state`
20
24
  def complete_state? = complete_state
25
+
26
+ # Reports whether the command needs no prior state at all (implies `complete_state?`).
27
+ #
28
+ # @return [Boolean] `state_independent`
21
29
  def state_independent? = state_independent
22
30
 
31
+ # Chooses the dispatch strategy this plan's proof and the adapter's own
32
+ # capabilities together allow.
33
+ #
23
34
  # Capability negotiation is correctness-first: an optimization is
24
35
  # selected only when both the semantic proof and adapter capability
25
36
  # are present. This is planning data only; no runtime path calls it yet.
37
+ #
38
+ # @param capabilities [Array<String, Symbol>] the repository's declared capabilities
39
+ # @return [Symbol] `DependencyPlanning::ATOMIC_PUT` when the plan is complete,
40
+ # state-independent, and the adapter declares that capability;
41
+ # `DependencyPlanning::TRANSACTIONAL_FALLBACK` otherwise
26
42
  def strategy_for(capabilities: [])
27
43
  return TRANSACTIONAL_FALLBACK unless complete_state? && state_independent?
28
44
  return TRANSACTIONAL_FALLBACK unless capabilities.map(&:to_sym).include?(ATOMIC_PUT)
@@ -38,13 +54,27 @@ module Hecks
38
54
  module ExpressionReads
39
55
  module_function
40
56
 
57
+ # Finds every dotted path a canonical expression reads, without evaluating it.
58
+ #
41
59
  # Read the same parsed canonical-expression nodes the evaluator uses.
42
60
  # A generic Struct walk keeps this additive when the expression grammar
43
61
  # gains a composed node; only Lookup nodes carry domain dependencies.
62
+ #
63
+ # @param canonical [String] the canonical expression text, such as a rule's `canonical`
64
+ # @return [Array<String>] the dotted paths the expression reads
44
65
  def paths(canonical)
45
66
  collect(Bluebook::Expression::Evaluator.parse(canonical), Set.new)
46
67
  end
47
68
 
69
+ # Walks one parsed expression node, collecting the dotted paths it reads.
70
+ #
71
+ # @param node [Object] a node from `Bluebook::Expression::Evaluator.parse` — a
72
+ # `Bluebook::Expression::Resolver::Lookup`, a `Bluebook::Expression::Resolver::
73
+ # BlockPredicate`, a `Struct` composed of further nodes, an `Array` of them, or any
74
+ # other value (a literal), which contributes no paths
75
+ # @param bound_names [Set<String>] names locally bound by an enclosing block
76
+ # predicate's own parameter, excluded from the result rather than reported as reads
77
+ # @return [Array<String>] the dotted paths read under `node`, excluding `bound_names`
48
78
  def collect(node, bound_names)
49
79
  case node
50
80
  when Bluebook::Expression::Resolver::Lookup
@@ -87,8 +117,20 @@ module Hecks
87
117
  # unresolved regardless of correctness. Defaults to `aggregate`
88
118
  # (a no-op) for the plain-aggregate case — `CommandInterpreter`'s
89
119
  # own call site never needed to change.
120
+ #
121
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the command
122
+ # is dispatched against; an entity for an entity-owned command
123
+ # @param command [Bluebook::Command] the command to analyze
124
+ # @param root_aggregate [Bluebook::Aggregate] the owning aggregate a `parent.*` read
125
+ # resolves against; defaults to `aggregate` for a plain-aggregate command
126
+ # @return [Hecks::Runtime::DependencyPlanning::Plan] the derived, frozen plan
90
127
  def self.call(aggregate:, command:, root_aggregate: aggregate) = new(aggregate, command, root_aggregate).call
91
128
 
129
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct the command
130
+ # is dispatched against
131
+ # @param command [Bluebook::Command] the command to analyze
132
+ # @param root_aggregate [Bluebook::Aggregate] the owning aggregate `parent.*` reads
133
+ # resolve against; defaults to `aggregate`
92
134
  def initialize(aggregate, command, root_aggregate = aggregate)
93
135
  @aggregate = aggregate
94
136
  @command = command
@@ -126,6 +168,9 @@ module Hecks
126
168
  @unresolved = Set.new
127
169
  end
128
170
 
171
+ # Runs the analysis and derives the command's dependency plan.
172
+ #
173
+ # @return [Hecks::Runtime::DependencyPlanning::Plan] the derived, frozen plan
129
174
  def call
130
175
  analyze_initial_state
131
176
  analyze_mutations