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
@@ -4,7 +4,9 @@ require_relative "nondeterministic"
4
4
 
5
5
  module Hecks
6
6
  module Fuzzing
7
- # **The second differential axis** — bin/qa_sweep's own `diff_ruby_vs_rust`
7
+ # ## The second differential axis
8
+ #
9
+ # bin/qa_sweep's own `diff_ruby_vs_rust`
8
10
  # compares two different engines (the Ruby interpreter vs the compiled
9
11
  # Rust kernel) against the same persistence (Memory, always — see
10
12
  # `SequenceGenerator`'s own header: sequence generation stays
@@ -27,6 +29,8 @@ module Hecks
27
29
  # Memory, unconditionally, no matter what `directory.world` itself
28
30
  # declares.
29
31
  #
32
+ # ## A separate axis, not a replacement
33
+ #
30
34
  # Not a replacement for `bin/qa_sweep`'s own Ruby-vs-Rust differential
31
35
  # mode — a genuinely separate axis, opt-in (`--persistence-parity`),
32
36
  # because this one pays for a real `PG.connect` and real SQL per
@@ -34,7 +38,9 @@ module Hecks
34
38
  # own `--persistence-parity` handling for the seed-count dial that
35
39
  # keeps that cost bounded.
36
40
  #
37
- # Generalized to `left:`/`right:` — originally hardcoded to Memory vs
41
+ # ## `left:`/`right:`
42
+ #
43
+ # Generalized — originally hardcoded to Memory vs
38
44
  # PostgresEra (the only pairing that existed), now any two of
39
45
  # `IsolatedBoot`'s own adapter symbols (`:memory`, `:sqlite`,
40
46
  # `:postgres`, `:postgres_era`). Defaults preserve the original
@@ -49,7 +55,9 @@ module Hecks
49
55
  # loop instead of needing a deferred wave of its own the way
50
56
  # PostgresEra does.
51
57
  #
52
- # `database:`/`schema:` — required only when `:postgres_era` is one of
58
+ # ## `database:`/`schema:`
59
+ #
60
+ # Required only when `:postgres_era` is one of
53
61
  # the two adapters (the caller — today, only `bin/qa_sweep` — owns the
54
62
  # disposable database's whole lifecycle: created before the sweep,
55
63
  # dropped after — see that script's own comment, and the discipline
@@ -81,6 +89,21 @@ module Hecks
81
89
  # sides identically (a `Runtime::Value`, a `Symbol` key, a `Time`
82
90
  # nobody asked for — none of that survives an accidental leak into
83
91
  # this comparison unnoticed).
92
+ #
93
+ # @param domain_path [String] path to the domain directory to boot
94
+ # @param steps [Array<Hash>] the step list to replay against both adapters
95
+ # @param left [Symbol] the first adapter to replay against, one of
96
+ # `IsolatedBoot`'s adapter symbols (`:memory`, `:sqlite`, `:postgres`,
97
+ # `:postgres_era`)
98
+ # @param right [Symbol] the second adapter to replay against, same set as `left`
99
+ # @param database [String, nil] connection identity for `:postgres_era`; required
100
+ # when `left` or `right` is `:postgres_era`, ignored otherwise
101
+ # @param schema [String, nil] disposable schema name for `:postgres_era`; required
102
+ # when `left` or `right` is `:postgres_era`, ignored otherwise
103
+ # @return [Array<Hash>] divergence entries, each `{field: String, left => Object,
104
+ # right => Object}` — `left`'s and `right`'s own adapter symbols become the
105
+ # entry's own keys, holding each side's JSON-shaped value for that field; empty
106
+ # if both sides agree on every field
84
107
  def diff(domain_path, steps, left: :memory, right: :postgres_era, database: nil, schema: nil)
85
108
  left_result = Replay.call(domain_path, steps, adapter: left, database: database, schema: schema)
86
109
  right_result = Replay.call(domain_path, steps, adapter: right, database: database, schema: schema)
@@ -95,8 +118,25 @@ module Hecks
95
118
  divergences
96
119
  end
97
120
 
121
+ # Round-trips `value` through JSON, the same wire-precision reduction
122
+ # `diff_ruby_vs_rust` uses so `Hash#==`/`Array#==` compares plain,
123
+ # JSON-shaped data on both sides.
124
+ #
125
+ # @param value [Object] any JSON-serializable value from a replay result
126
+ # @return [Object] `value`, JSON-round-tripped: Symbol keys become Strings,
127
+ # and any non-JSON-native value surfaces as its own JSON form
98
128
  def as_json(value) = JSON.parse(JSON.generate(value))
99
129
 
130
+ # Compares both sides' stored instances.
131
+ #
132
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
133
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
134
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
135
+ # this entry's own key
136
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
137
+ # this entry's own key
138
+ # @return [Array<Hash>] one `{field: "instances", left => Object, right => Object}`
139
+ # entry if the two sides' JSON-shaped instances differ; empty otherwise
100
140
  def diff_instances(left_result, right_result, left, right)
101
141
  l = as_json(left_result[:instances])
102
142
  r = as_json(right_result[:instances])
@@ -105,6 +145,16 @@ module Hecks
105
145
  [{ field: "instances", left => l, right => r }]
106
146
  end
107
147
 
148
+ # Compares both sides' emitted events.
149
+ #
150
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
151
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
152
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
153
+ # this entry's own key
154
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
155
+ # this entry's own key
156
+ # @return [Array<Hash>] one `{field: "events", left => Object, right => Object}`
157
+ # entry if the two sides' JSON-shaped events differ; empty otherwise
108
158
  def diff_events(left_result, right_result, left, right)
109
159
  l = as_json(left_result[:events])
110
160
  r = as_json(right_result[:events])
@@ -113,11 +163,22 @@ module Hecks
113
163
  [{ field: "events", left => l, right => r }]
114
164
  end
115
165
 
166
+ # Compares both sides' refusals.
167
+ #
116
168
  # `verb:`/`kind:` normalized to plain strings the same way
117
169
  # `diff_ruby_vs_rust`'s own `ruby_refusals` mapping does — both
118
170
  # sides here already answer strings (`Replay#refusal_kind` always
119
171
  # returns one), so this is belt-and-suspenders consistency with the
120
172
  # sibling mode's own shape, not a real coercion.
173
+ #
174
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
175
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
176
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
177
+ # this entry's own key
178
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
179
+ # this entry's own key
180
+ # @return [Array<Hash>] one `{field: "refusals", left => Object, right => Object}`
181
+ # entry if the two sides' normalized refusals differ; empty otherwise
121
182
  def diff_refusals(left_result, right_result, left, right)
122
183
  normalize = lambda do |refusals|
123
184
  refusals.map { |r| { "verb" => r[:verb].to_s, "kind" => r[:kind].to_s, "error" => r[:error] } }
@@ -129,9 +190,20 @@ module Hecks
129
190
  [{ field: "refusals", left => l, right => r }]
130
191
  end
131
192
 
193
+ # Compares both sides' query answers.
194
+ #
132
195
  # `Nondeterministic`'s `query_row` group dropped from every entry —
133
196
  # the same group `Differential.diff` drops, for the reason declared
134
197
  # there (already covered by `diff_instances` above).
198
+ #
199
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
200
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
201
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
202
+ # this entry's own key
203
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
204
+ # this entry's own key
205
+ # @return [Array<Hash>] one `{field: "queries", left => Object, right => Object}`
206
+ # entry if the two sides' stripped, JSON-shaped queries differ; empty otherwise
135
207
  def diff_queries(left_result, right_result, left, right)
136
208
  strip = ->(rows) { rows.map { |row| Nondeterministic.strip(row, :query_row) } }
137
209
  l = as_json(strip.call(left_result[:queries]))
@@ -141,6 +213,16 @@ module Hecks
141
213
  [{ field: "queries", left => l, right => r }]
142
214
  end
143
215
 
216
+ # Compares both sides' saga logs.
217
+ #
218
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
219
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
220
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
221
+ # this entry's own key
222
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
223
+ # this entry's own key
224
+ # @return [Array<Hash>] one `{field: "sagas", left => Object, right => Object}`
225
+ # entry if the two sides' JSON-shaped sagas differ; empty otherwise
144
226
  def diff_sagas(left_result, right_result, left, right)
145
227
  l = as_json(left_result[:sagas])
146
228
  r = as_json(right_result[:sagas])
@@ -149,6 +231,16 @@ module Hecks
149
231
  [{ field: "sagas", left => l, right => r }]
150
232
  end
151
233
 
234
+ # Compares both sides' reaction logs.
235
+ #
236
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
237
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
238
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
239
+ # this entry's own key
240
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
241
+ # this entry's own key
242
+ # @return [Array<Hash>] one `{field: "reactions", left => Object, right => Object}`
243
+ # entry if the two sides' JSON-shaped reactions differ; empty otherwise
152
244
  def diff_reactions(left_result, right_result, left, right)
153
245
  l = as_json(left_result[:reactions])
154
246
  r = as_json(right_result[:reactions])
@@ -45,6 +45,11 @@ module Hecks
45
45
  # mutation is invisible to both doors today — this property is the
46
46
  # only thing anywhere, on either engine, that would ever catch one
47
47
  # going wrong.
48
+ #
49
+ # @param history [Hash] a replayed history as returned by `Replay.call`
50
+ # @return [true, String] true if every event emitted by a `corrects`-bearing
51
+ # command has a matching, strictly earlier corrected event in the same
52
+ # history; otherwise a message listing every unmatched correction
48
53
  def corrections_reference_an_emitted_event(history)
49
54
  violations = []
50
55
 
@@ -72,6 +77,17 @@ module Hecks
72
77
  violations.empty? || violations.uniq.join("; ")
73
78
  end
74
79
 
80
+ # Finds every occurrence of `produced_event_name`, on `aggregate_key`, with no
81
+ # matching `corrected_event` for the same id appearing earlier in `events`.
82
+ #
83
+ # @param events [Array<Hash>] `history[:events]`, in occurrence order
84
+ # @param aggregate_key [String] `"domain::AggregateName"` the events belong to
85
+ # @param produced_event_name [String] name of the event a `corrects` mutation's
86
+ # command emits
87
+ # @param corrected_event [String] name of the event the mutation claims to correct
88
+ # @param command_name [String] the command's own `hecks_name`, for the message
89
+ # @return [Array<String>] one message per occurrence with no matching earlier
90
+ # corrected event; empty when every occurrence is matched
75
91
  def unmatched_corrections(events, aggregate_key, produced_event_name, corrected_event, command_name)
76
92
  own_events = events.each_with_index.select do |event, _index|
77
93
  event[:name] == produced_event_name && event[:aggregate] == aggregate_key
@@ -90,6 +106,15 @@ module Hecks
90
106
  end
91
107
  end
92
108
 
109
+ # Yields every command declared on `owner`, then recurses into each of its
110
+ # entities to yield theirs too, at any nesting depth (ADR 0026, S17).
111
+ #
112
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the aggregate or entity
113
+ # whose own commands, and whose entities' commands, to walk
114
+ # @yield [command] once per declared command, aggregate-level or nested
115
+ # @yieldparam command [Bluebook::Command] a command declared on `owner` or one
116
+ # of its entities
117
+ # @return [void]
93
118
  def each_command_including_entities(owner, &block)
94
119
  owner.commands.each(&block)
95
120
  owner.entities.each { |entity| each_command_including_entities(entity, &block) }
@@ -16,6 +16,12 @@ module Hecks
16
16
  # hand-built history, an older corpus) are skipped, not failed: no
17
17
  # claim, no finding.
18
18
  module DryRuns
19
+ # Checks that every dry-run step left neither the event count nor any
20
+ # instance's state changed.
21
+ #
22
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
23
+ # @return [true, String] true if every dry-run trace shows no change; otherwise
24
+ # a semicolon-joined message naming each offending dry run
19
25
  def dry_runs_leave_no_trace(history)
20
26
  offenders = Array(history[:dry_run_traces]).filter_map do |entry|
21
27
  before = entry[:before]
@@ -67,7 +73,8 @@ module Hecks
67
73
  # argument binding on a fan-out dispatch that produces a perfectly
68
74
  # normal-looking log entry (`delivered: true`) and would only ever
69
75
  # surface as a downstream assertion failure, if it surfaces at
70
- # all" — PR #325's own defect class, one level over.
76
+ # all" — a defect class one level over from what
77
+ # `fanout_dispatches_once_per_matching_row` already catches.
71
78
  #
72
79
  # Real targets: Settlement (mixed literal/correlation-head/event-
73
80
  # payload/memory-fallback bindings across three legs, plus a
@@ -75,6 +82,11 @@ module Hecks
75
82
  # the forward Credit leg carries), ExternalSettlement, Onboarding
76
83
  # (no compensation leg, by design — nothing to check there beyond
77
84
  # the forward leg's own event-payload binding).
85
+ #
86
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
87
+ # @return [true, String] true if every saga and policy dispatch's own bound args
88
+ # match an independent re-derivation of their `with_spec`; otherwise a
89
+ # semicolon-joined message naming each offending dispatch
78
90
  def dispatch_binding_fidelity(history)
79
91
  saga_offenders = history.fetch(:saga_dispatches, []).filter_map do |entry|
80
92
  expected = resolve_dispatch_binding(entry)
@@ -102,6 +114,11 @@ module Hecks
102
114
  # the current triggering event's own payload, or — the fallback —
103
115
  # the saga's own carried memory (seeded from the starting event's
104
116
  # payload, at begin_saga).
117
+ #
118
+ # @param entry [Hash] one `history[:saga_dispatches]` entry: at least
119
+ # `:with_spec`, `:correlation_head`, `:instance`, `:event_payload`, and `:memory`
120
+ # @return [Hash{Symbol => Object}] the resolved, materialized args `with_spec`
121
+ # should produce, keyed by symbol
105
122
  def resolve_dispatch_binding(entry)
106
123
  entry[:with_spec].to_h do |key, value|
107
124
  resolved = if !value.is_a?(Symbol) then value
@@ -117,6 +134,11 @@ module Hecks
117
134
  # policy holds no correlation and no memory, so `payload` (the
118
135
  # triggering event's own payload, already merged with a fan-out
119
136
  # row's id when there is one) is the whole source.
137
+ #
138
+ # @param entry [Hash] one `history[:policy_dispatches]` entry: at least
139
+ # `:with_spec` and `:payload`
140
+ # @return [Hash{Symbol => Object}] the resolved, materialized args `with_spec`
141
+ # should produce, keyed by symbol
120
142
  def resolve_trigger_binding(entry)
121
143
  entry[:with_spec].to_h do |key, value|
122
144
  resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
@@ -209,6 +231,14 @@ module Hecks
209
231
  # corpus shape).
210
232
  RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp set].freeze
211
233
 
234
+ # Checks that every recomputable mutation an entity-owned command
235
+ # applied landed on the same after-state an independent
236
+ # recomputation of the same rule produces.
237
+ #
238
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
239
+ # @return [true, String] true if every recomputable mutation's after-state
240
+ # matches an independent recomputation; otherwise a semicolon-joined message
241
+ # naming each offending mutation
212
242
  def mutations_match_recompute(history)
213
243
  bluebooks = history.fetch(:bluebooks)
214
244
 
@@ -249,6 +279,12 @@ module Hecks
249
279
  # value-object type against the root's own namespace only, the
250
280
  # same reason `EntityElement#locate_chain` threads `root_aggregate`
251
281
  # through every hop separately from each hop's own `owner`.
282
+ #
283
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
284
+ # keyed by domain name
285
+ # @param verb [String] the dispatched verb to resolve
286
+ # @return [Bluebook::Aggregate, nil] the root aggregate `verb`'s domain and
287
+ # aggregate name, or nil if `verb` names no domain
252
288
  def aggregate_for_verb(bluebooks, verb)
253
289
  domain_name, aggregate_name, = Naming.split_verb(verb)
254
290
  return nil unless domain_name
@@ -268,6 +304,15 @@ module Hecks
268
304
  # each hop's own `owner`. Re-derived independently from
269
305
  # `entry[:verb]` alone, the same reasoning `aggregate_for_verb`'s
270
306
  # own comment gives.
307
+ #
308
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
309
+ # keyed by domain name
310
+ # @param verb [String] the dispatched verb to resolve
311
+ # @return [Bluebook::Aggregate, Bluebook::Entity, nil] the construct that
312
+ # declares the mutation's own target: the root aggregate for an
313
+ # aggregate-owned command, or the owning entity for a dot-shaped
314
+ # (entity-owned) command; nil if `verb` names no command path or no
315
+ # resolvable aggregate
271
316
  def owner_for_verb(bluebooks, verb)
272
317
  domain_name, aggregate_name, command_path = Naming.split_verb(verb)
273
318
  return nil unless command_path
@@ -280,6 +325,21 @@ module Hecks
280
325
  aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
281
326
  end
282
327
 
328
+ # Dispatches to the recompute rule for `mutation.op`.
329
+ #
330
+ # @param mutation [Bluebook::Mutation] the mutation to recompute
331
+ # @param current [Object] the target field's before-value, as captured in
332
+ # `entry[:before]`
333
+ # @param args [Hash] the step's own raw args, as captured in `entry[:args]`
334
+ # @param before_scope [Hash] the entity element's own before-state
335
+ # (`entry[:before]`), the source an append's caller-omitted field resolves against
336
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
337
+ # namespace resolution
338
+ # @param command [Bluebook::Command] the command that declared `mutation`
339
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct that
340
+ # declares `mutation.target`; defaults to `aggregate`
341
+ # @return [Object, Symbol] the recomputed after-value for `mutation.op`, or
342
+ # `:unrecomputable` if the raw material doesn't fit the op's own contract
283
343
  def recompute_mutation(mutation, current, args, before_scope, aggregate, command, owner = aggregate)
284
344
  case mutation.op
285
345
  when :append
@@ -312,19 +372,16 @@ module Hecks
312
372
  # (the root aggregate, for value-object namespace resolution
313
373
  # only — `value_object_for(aggregate, attribute.type)` — the
314
374
  # same aggregate/owner split `owner_for_verb`'s own comment
315
- # explains for `recompute_append`). An earlier version of this
316
- # method mirrored `MutationApplier#apply`'s `:set` branch instead
317
- # (StateRef-aware, coerced against `aggregate.attribute` rather
318
- # than `owner.attribute`) and false-positived on every real
319
- # entity-owned `sets` in the corpus — `NestedPieces::Workspace.
320
- # Board.Label` (`sets :label`) has no `:label` attribute on
321
- # `Workspace` at all, so `Value.for(aggregate, :label, raw)`
322
- # silently passed the raw, uncoerced String through instead of
323
- # wrapping it as `BoardLabel`, and the comparison below then
324
- # disagreed with the real, correctly-coerced `after` state on
325
- # every single run — caught immediately by running this against
326
- # `qa/stress_domains/nested_pieces` before this comment existed,
327
- # not by inspection alone.
375
+ # explains for `recompute_append`). Coercing against
376
+ # `owner.attribute` rather than `aggregate.attribute` matters
377
+ # concretely: `NestedPieces::Workspace.Board.Label` (`sets
378
+ # :label`) has no `:label` attribute on `Workspace` at all, so
379
+ # coercing against the aggregate would silently pass the raw,
380
+ # uncoerced String through instead of wrapping it as
381
+ # `BoardLabel`, and the comparison below would then disagree with
382
+ # the real, correctly-coerced `after` state on every single run —
383
+ # a real false positive, confirmed by running this against
384
+ # `qa/stress_domains/nested_pieces`, not by inspection alone.
328
385
  #
329
386
  # Reusing `Value.for_attribute` for the coercion step, rather
330
387
  # than re-deriving it, is the same "coercion is its own already-
@@ -341,6 +398,17 @@ module Hecks
341
398
  # real dispatch was itself malformed — `:unrecomputable`, not a
342
399
  # crash, the same discipline every other branch in this method
343
400
  # already follows for the generator's own deliberate malforming.
401
+ #
402
+ # @param source [Symbol, Object] the mutation's own declared source: an argument
403
+ # name to resolve from `args`, or a literal value already
404
+ # @param args [Hash] the step's own raw args
405
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
406
+ # namespace resolution
407
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity, nil] the construct that
408
+ # declares `target`
409
+ # @param target [Symbol, nil] the mutation's own target field name
410
+ # @return [Object, Symbol] the recomputed, materialized value, or
411
+ # `:unrecomputable` if resolving or coercing the source raises
344
412
  def recompute_set(source, args, aggregate, owner, target)
345
413
  raw = resolve_mutation_source(source, args)
346
414
  attribute = owner&.attribute(target)
@@ -393,6 +461,22 @@ module Hecks
393
461
  # through it via `Instance.defaults`), not the new glue
394
462
  # (`fill_declared_defaults` itself) this property exists to
395
463
  # catch a drift in.
464
+ #
465
+ # @param current [Object] the target field's before-value (an Array of elements)
466
+ # @param source_map [Hash{Symbol => Object}] the append mutation's own field map:
467
+ # each appended field's name to its source (an argument name Symbol, or a literal)
468
+ # @param before_scope [Hash] the entity element's own before-state, the source a
469
+ # caller-omitted field resolves against
470
+ # @param args [Hash] the step's own raw args
471
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
472
+ # namespace resolution
473
+ # @param command [Bluebook::Command] the command that declared the mutation, for
474
+ # coercing a caller-supplied arg
475
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose nested
476
+ # entity `target` may declare; defaults to `aggregate`
477
+ # @param target [Symbol, nil] the append mutation's own target field name
478
+ # @return [Array] `current` (coerced to an Array) with the newly built,
479
+ # symbol-keyed element appended
396
480
  def recompute_append(current, source_map, before_scope, args, aggregate, command, owner = aggregate, target = nil)
397
481
  fields = source_map.transform_values do |source|
398
482
  resolve_mutation_append_field(source, before_scope, args, aggregate, command)
@@ -415,6 +499,17 @@ module Hecks
415
499
  # (`Card` is `Board.entities`, never `Workspace.entities`), the
416
500
  # same distinction `EntityElement#appended_to_element`'s own fix
417
501
  # draws.
502
+ #
503
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, forwarded to
504
+ # `Instance.default_for`
505
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity, nil] the construct whose
506
+ # nested entity `target` may declare
507
+ # @param target [Symbol, nil] the append mutation's own target field name
508
+ # @param fields [Hash{Symbol => Object}] the field map already resolved by
509
+ # `recompute_append`
510
+ # @return [Hash{Symbol => Object}] `fields` unchanged if `target` is nil or does
511
+ # not name a nested entity; otherwise `fields` with every declared attribute it
512
+ # doesn't already hold filled in with its own default
418
513
  def fill_recompute_declared_defaults(aggregate, owner, target, fields)
419
514
  return fields unless target
420
515
 
@@ -430,6 +525,18 @@ module Hecks
430
525
  fields
431
526
  end
432
527
 
528
+ # Resolves one appended field's own value from its declared source.
529
+ #
530
+ # @param source [Symbol, Object] an append field's own source: an argument name,
531
+ # or a literal value
532
+ # @param before_scope [Hash] the entity element's own before-state
533
+ # @param args [Hash] the step's own raw args
534
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
535
+ # namespace resolution
536
+ # @param command [Bluebook::Command] the command that declared the mutation
537
+ # @return [Object] `before_scope[source]` if `source` names no caller-supplied
538
+ # arg; `source` unchanged if it is not a Symbol; otherwise the coerced,
539
+ # materialized arg value
433
540
  def resolve_mutation_append_field(source, before_scope, args, aggregate, command)
434
541
  return source unless source.is_a?(Symbol)
435
542
  return before_scope[source] unless args.key?(source)
@@ -455,6 +562,15 @@ module Hecks
455
562
  # throughout this whole property, so the eventual `symbolize_deep`
456
563
  # comparison is always materialized-against-materialized, never a
457
564
  # live `Value` against a Hash.
565
+ #
566
+ # @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
567
+ # namespace resolution
568
+ # @param command [Bluebook::Command] the command whose own declared attribute
569
+ # `source` may name
570
+ # @param source [Symbol] the argument name to coerce
571
+ # @param raw [Object] the raw arg value to coerce
572
+ # @return [Object] `raw`, coerced and materialized, if `source` names a declared
573
+ # attribute of `command`; `raw` unchanged otherwise
458
574
  def coerce_recompute_append_arg(aggregate, command, source, raw)
459
575
  attribute = command.attribute(source)
460
576
  return raw unless attribute
@@ -463,6 +579,13 @@ module Hecks
463
579
  end
464
580
 
465
581
  # `MutationApplier#removed`'s own value-equality match, reproduced.
582
+ #
583
+ # @param current [Object] the target field's before-value (an Array of elements)
584
+ # @param source [Symbol, Object] the remove mutation's own source: an argument
585
+ # name, or a literal value already
586
+ # @param args [Hash] the step's own raw args
587
+ # @return [Array] `current` (coerced to an Array) with every element matching the
588
+ # resolved target value removed
466
589
  def recompute_remove(current, source, args)
467
590
  target = symbolize_deep(resolve_mutation_source(source, args))
468
591
  Array(current).reject { |element| symbolize_deep(element) == target }
@@ -475,6 +598,12 @@ module Hecks
475
598
  # `current ||= 0` — the same phantom-field fallback #multiply
476
599
  # itself already gives (unaffected by this session's #clamp fix,
477
600
  # since #multiply never needed one).
601
+ #
602
+ # @param current [Hash, Numeric, nil] the target field's before-value
603
+ # @param amount [Object] the resolved multiplier
604
+ # @return [Hash, Numeric, Symbol] the scaled field/value, or `:unrecomputable` if
605
+ # `amount` is not Numeric or `current` is neither a Hash with a numeric field
606
+ # nor a Numeric
478
607
  def recompute_multiply(current, amount)
479
608
  return :unrecomputable unless amount.is_a?(Numeric)
480
609
 
@@ -498,6 +627,12 @@ module Hecks
498
627
  # is always a literal `[min, max]`, never an argument reference
499
628
  # (MutationApplier's own comment on why `resolve_source` is
500
629
  # skipped for clamp) — so nothing here reads `args` for it at all.
630
+ #
631
+ # @param current [Hash, Numeric, nil] the target field's before-value
632
+ # @param bounds [Object] the mutation's own declared `[min, max]` literal
633
+ # @return [Hash, Numeric, Symbol] the clamped field/value, or `:unrecomputable`
634
+ # if `bounds` is not a 2-element Array or `current` is neither a Hash with a
635
+ # numeric field nor a Numeric
501
636
  def recompute_clamp(current, bounds)
502
637
  return :unrecomputable unless bounds.is_a?(Array) && bounds.size == 2
503
638
 
@@ -518,6 +653,11 @@ module Hecks
518
653
  # `CommandRules::Arithmetic#resolve_source`, reproduced: a
519
654
  # mutation's source is either the name of an argument or a
520
655
  # literal, told apart by type.
656
+ #
657
+ # @param source [Symbol, Object] an argument name to resolve from `args`, or a
658
+ # literal value already
659
+ # @param args [Hash] the step's own raw args
660
+ # @return [Object] `args[source]` if `source` is a Symbol; `source` unchanged otherwise
521
661
  def resolve_mutation_source(source, args)
522
662
  source.is_a?(Symbol) ? args[source] : source
523
663
  end
@@ -531,6 +671,10 @@ module Hecks
531
671
  # both sides are normalized the same way first. Recursive, since
532
672
  # an appended/removed element can itself nest a value object
533
673
  # (RemoveTag's own `Tag` argument, `{"key"=>..., "value"=>...}`).
674
+ #
675
+ # @param value [Object] a Hash, Array, or scalar to normalize
676
+ # @return [Object] `value` with every Hash key symbolized, recursively through
677
+ # nested Hashes and Arrays; any other value unchanged
534
678
  def symbolize_deep(value)
535
679
  case value
536
680
  when Hash then value.to_h { |key, val| [key.to_sym, symbolize_deep(val)] }
@@ -54,6 +54,11 @@ module Hecks
54
54
  # branches shares logic with the others beyond that shared setup.
55
55
  # rubocop:disable-next Metrics/CyclomaticComplexity
56
56
  # rubocop:disable-next Metrics/PerceivedComplexity
57
+ #
58
+ # @param history [Hash] a replayed history as returned by `Replay.call`
59
+ # @return [true, String] true if every tenant-scoped query answer, and every
60
+ # refusal of one, agrees with `TenantScope.apply`'s own contract; otherwise a
61
+ # message naming the offending query, its args, and what it should have done
57
62
  def authorize_scopes_or_refuses(history)
58
63
  bluebooks = history.fetch(:bluebooks)
59
64
 
@@ -116,6 +121,14 @@ module Hecks
116
121
  # those; the raised class does not.
117
122
  GUARD_REFUSAL_KINDS = %w[Hecks::Runtime::GivenNotMet Hecks::Runtime::EnsuresNotMet].freeze
118
123
 
124
+ # Checks that every given/ensures refusal a run actually raised names a rule
125
+ # the refusing command actually declares.
126
+ #
127
+ # @param history [Hash] a replayed history as returned by `Replay.call`
128
+ # @return [true, String] true if every guard refusal's quoted description matches
129
+ # a `given`/`ensures` the refusing command (or its delegation target) declares;
130
+ # otherwise a message naming the refusal and the declared descriptions it does
131
+ # not match
119
132
  def guard_refusals_are_declared(history)
120
133
  bluebooks = history.fetch(:bluebooks)
121
134
 
@@ -190,6 +203,12 @@ module Hecks
190
203
  # false positive.
191
204
  # rubocop:disable-next Metrics/CyclomaticComplexity
192
205
  # rubocop:disable-next Metrics/PerceivedComplexity
206
+ #
207
+ # @param history [Hash] a replayed history as returned by `Replay.call`
208
+ # @return [true, String] true if every stored record's `reference_to` attribute
209
+ # points at a record sharing the same tenant value (or has no resolvable
210
+ # target, or neither side declares a tenant field); otherwise a message naming
211
+ # the referencing record, the reference, and the tenant mismatch
193
212
  def commands_respect_tenant_scope(history)
194
213
  bluebooks = history.fetch(:bluebooks)
195
214
  instances = history.fetch(:instances)
@@ -236,6 +255,10 @@ module Hecks
236
255
  # ..., tenant:` on any of its own queries — not every aggregate
237
256
  # is tenant-scoped, and one that isn't has nothing for this
238
257
  # property to check either side of.
258
+ #
259
+ # @param aggregate [Bluebook::Aggregate] the aggregate to look up
260
+ # @return [Symbol, nil] the tenant field name, or nil if no query on the
261
+ # aggregate declares `authorize ..., tenant:`
239
262
  def tenant_field_for(aggregate)
240
263
  authorization = aggregate.queries.filter_map(&:authorization).find(&:tenant)
241
264
  authorization&.tenant&.to_sym
@@ -267,6 +290,14 @@ module Hecks
267
290
  # own guards first, then every delegation target's; an offence is
268
291
  # only a description neither declares. Found live mining chess's
269
292
  # history: every refused move through a door read as undeclared.
293
+ #
294
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
295
+ # keyed by domain name
296
+ # @param verb [String] the dispatched verb, `"Domain::Aggregate.Command"` or
297
+ # `"Domain::Aggregate.Entity.Command"`
298
+ # @param command [Bluebook::Command] the command `verb` resolves to
299
+ # @return [Array<String>] `command`'s own declared given/ensures descriptions,
300
+ # plus every `delegate`d command's, in that order
270
301
  def effective_guard_descriptions(bluebooks, verb, command)
271
302
  own = command.guard_descriptions
272
303
  delegated = command.mutations.select { |m| m.op == :delegate }.flat_map do |delegation|
@@ -277,6 +308,14 @@ module Hecks
277
308
  own + delegated
278
309
  end
279
310
 
311
+ # Resolves a dispatched verb back to the declared command it names.
312
+ #
313
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
314
+ # keyed by domain name
315
+ # @param verb [String] the dispatched verb, `"Domain::Aggregate.Command"` or
316
+ # `"Domain::Aggregate.Entity.Command"`
317
+ # @return [Bluebook::Command, nil] the declared command, or nil if `verb`'s
318
+ # domain, aggregate, entity, or command is not found among `bluebooks`
280
319
  def command_for_verb(bluebooks, verb)
281
320
  domain, aggregate_name, command_path = Naming.split_verb(verb)
282
321
  return nil unless command_path
@@ -337,6 +376,11 @@ module Hecks
337
376
  # different named precondition `"customer is not closed"` instead
338
377
  # (a suspended customer must still be freezable), so it is not a
339
378
  # `"customer is active"` example, just the same mechanism.
379
+ #
380
+ # @param history [Hash] a replayed history as returned by `Replay.call`
381
+ # @return [true, String] true if every recomputed `enforce_givens`/
382
+ # `enforce_lifecycle_guard` check agrees with what the real dispatch did;
383
+ # otherwise a message naming the verb and the disagreement
340
384
  def lifecycle_guard_and_given_violations_are_refused(history)
341
385
  offenders = history.fetch(:guard_checks).filter_map do |check|
342
386
  next if check[:recomputed_refused] == check[:actual_refused]