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
@@ -31,7 +31,7 @@ module Hecks
31
31
  # both directions.
32
32
  DISPATCH_ORDER = Hecks::Vocabulary.symbols("AggregateDispatchOrder")
33
33
 
34
- # **A last-resort safety valve, not the normal outcome path** — see
34
+ # A last-resort safety valve, not the normal outcome path — see
35
35
  # `Runtime::StaleWrite`'s own comment. Two concurrent writers
36
36
  # against one aggregate resolve through exactly one retry in the
37
37
  # ordinary case (the loser's retried hydrate reads the winner's now-
@@ -41,14 +41,17 @@ module Hecks
41
41
  # two-writer case.
42
42
  MAX_STALE_WRITE_RETRIES = 5
43
43
 
44
- # Every cross-step local `call` used to thread through its own literal
45
- # sequence, held in one place now that the sequence is data-driven —
44
+ # Every cross-step local held in one place, now that the sequence is
45
+ # data-driven rather than `call`'s own literal method-call sequence —
46
46
  # `result` and `transition`/`old_state` default to nil until the step
47
- # that sets them runs, same as they were unset locals before that point.
47
+ # that sets them runs, the same as unset locals would be before that point.
48
48
  Context = Struct.new(:domain, :aggregate, :command, :args, :repository, :instance, :transition, :old_state,
49
49
  :result, :correlation, :route, :plan, :strategy, :persistence_outcome, :pending_delegation,
50
50
  :dry_run, :correction_bindings, :outbox_rows, :invocation)
51
51
 
52
+ # @param registry [Runtime::Registry] the booted registry this interpreter reads
53
+ # @param rules [Runtime::CommandRules] the shared rules engine (admissibility,
54
+ # references, arithmetic, authorization, emission) dispatch runs through
52
55
  def initialize(registry, rules:)
53
56
  @registry = registry
54
57
  @rules = rules
@@ -69,6 +72,27 @@ module Hecks
69
72
  # `invocation` — the `Runtime::Invocation` `Dispatcher` built for this
70
73
  # call. `ctx.args` is `invocation.to_args` (the same Hash routing
71
74
  # always handed this method), `ctx.route` its `target`.
75
+ #
76
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
77
+ # @param aggregate [Bluebook::Aggregate] the aggregate the command acts on
78
+ # @param command [Class] the command class (`Bluebook::Command` subclass) to dispatch
79
+ # @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for
80
+ # this call
81
+ # @param correlation [Hash{Symbol => Object}, nil] correlation head => value,
82
+ # stamped on every emitted event when a saga leg causes this dispatch; nil
83
+ # otherwise
84
+ # @param dry_run [Boolean] whether to run every step through validation without
85
+ # saving, emitting or enqueueing
86
+ # @return [Array(Runtime::Instance, Array<Runtime::Event>,
87
+ # Runtime::DependencyPlanning::Plan, Ports::Persistence::Execution,
88
+ # Array<Runtime::Outbox::Row>)] the settled instance, emitted events,
89
+ # execution plan, persistence outcome and outbox rows — the last
90
+ # three nil on a dry run, which skips save/emit/outbox
91
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
92
+ # given/ensures/invariant/authorization/admissibility rule refuses
93
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every
94
+ # retry (`MAX_STALE_WRITE_RETRIES`)
95
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
72
96
  def call(domain, aggregate, command, invocation, correlation = nil, dry_run: false)
73
97
  args = invocation.to_args
74
98
  route = invocation.target
@@ -273,9 +297,9 @@ module Hecks
273
297
  # `ensures`/`enforce_invariants`/`save` steps still run after
274
298
  # this one. The target's emission is parked and performed by
275
299
  # `step_emit`, after the parent committed — where every other
276
- # command's events are emitted too. (Before this, the entity
277
- # leg's events were on the event log and in the adapter before
278
- # the parent could refuse.)
300
+ # command's events are emitted too. (Without parking it here, the
301
+ # entity leg's events would be on the event log and in the adapter
302
+ # before the parent could refuse.)
279
303
  ctx.pending_delegation = [target_command, target_args]
280
304
  end
281
305
  end
@@ -350,7 +374,7 @@ module Hecks
350
374
 
351
375
  def persist_instance(ctx)
352
376
  if ctx.strategy == DependencyPlanning::ATOMIC_PUT
353
- # **A second creation is not a fresh one** — see
377
+ # A second creation is not a fresh one — see
354
378
  # hydrate_complete_state's own comment; the
355
379
  # same refusal, on the same terms, for the
356
380
  # complete-state path. `insert_only:` asks the
@@ -441,8 +465,9 @@ module Hecks
441
465
  # `:delegate` mutation.
442
466
  if ctx.pending_delegation
443
467
  target_command, target_args = ctx.pending_delegation
444
- # The same dispatch, so the same correlation — a saga-driven
445
- # door's events used to lose their stamp here.
468
+ # The same dispatch, so the same correlation — without
469
+ # threading `ctx.correlation` through, a saga-driven door's
470
+ # events would lose their stamp here.
446
471
  next @rules.emit(target_command, ctx.domain, ctx.aggregate, ctx.instance, target_args, ctx.repository,
447
472
  ctx.correlation)
448
473
  end
@@ -562,7 +587,7 @@ module Hecks
562
587
  aggregate: aggregate.hecks_name,
563
588
  identity: identity_reading(aggregate)))
564
589
 
565
- # **A second creation is not a fresh one** — `creates?` on an identity
590
+ # A second creation is not a fresh one — `creates?` on an identity
566
591
  # a record already exists under refuses (`AlreadyExists`) rather
567
592
  # than silently overwriting it, the same refusal `hydrate_prior_
568
593
  # or_initial`'s own body gives for its own complete-but-state-
@@ -604,7 +629,7 @@ module Hecks
604
629
  identity: identity_reading(aggregate)))
605
630
  found = repository.find(id)
606
631
 
607
- # **A second creation is not a fresh one** — see hydrate_complete_
632
+ # A second creation is not a fresh one — see hydrate_complete_
608
633
  # state's own comment; the same refusal, on the same terms, for
609
634
  # a complete-but-state-dependent command (one with a `given`
610
635
  # reading its own prior state, which is what routes here instead
@@ -614,10 +639,10 @@ module Hecks
614
639
  # prior state this branch exists to supply), so only a genuine
615
640
  # creation reusing an already-occupied identity is a duplicate.
616
641
  # `SafeDepositBox.Rent` is exactly this shape — `given("box is
617
- # vacant")` makes it state-dependent, so a second Rent used to
618
- # silently hydrate the existing box as "prior state" and refuse
619
- # for the wrong reason (not vacant) instead of the right one
620
- # (already exists).
642
+ # vacant")` makes it state-dependent, so without this check a
643
+ # second Rent would silently hydrate the existing box as "prior
644
+ # state" and refuse for the wrong reason (not vacant) instead of
645
+ # the right one (already exists).
621
646
  if found && command.creates?
622
647
  raise(AlreadyExists, RefusalWording.render_site("AlreadyExists", "creating_duplicate",
623
648
  command: command.hecks_name, aggregate: aggregate.hecks_name,
@@ -628,7 +653,7 @@ module Hecks
628
653
  found ? found.dup : Instance.new(aggregate: aggregate, id: id, args: args)
629
654
  end
630
655
 
631
- # **The join, the dig, and the reading** — all shared with `EntityInterpreter`
656
+ # The join, the dig, and the reading — all shared with `EntityInterpreter`
632
657
  # now, in `Runtime::Identity`, rather than kept as two copies that could
633
658
  # only ever drift. See that module for the reasoning ; these three stay
634
659
  # here, at the old names, purely so nothing below has to change.
@@ -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