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
@@ -49,6 +49,11 @@ module Hecks
49
49
  #
50
50
  # Real target: SafeDepositBox's own Visit — `invariant("a written
51
51
  # note is not blank") { !note || !note.text.to_s.empty? }`.
52
+ #
53
+ # @param history [Hash] a replayed history as returned by `Replay.call`
54
+ # @return [true, String] true if every stored record satisfies its own
55
+ # (and its own entities') declared invariants; otherwise a message
56
+ # naming the record and the invariant it violates
52
57
  def stored_records_satisfy_declared_invariants(history)
53
58
  bluebooks = history.fetch(:bluebooks)
54
59
 
@@ -76,6 +81,12 @@ module Hecks
76
81
  # a.list? && a.type.to_s == entity.hecks_name }`), independently
77
82
  # reapplied here against a stored record's own plain Hash state
78
83
  # rather than a live `Instance`.
84
+ # @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] the
85
+ # construct whose own `list_of` entities to check
86
+ # @param owner_state [Hash] `owner_construct`'s own stored state
87
+ # @param key [String] the top-level record key, for the message
88
+ # @return [String, nil] a message naming the first violating piece found,
89
+ # at any nesting depth; `nil` if every piece satisfies its invariants
79
90
  def check_piece_invariants(owner_construct, owner_state, key)
80
91
  owner_construct.entities.each do |entity|
81
92
  next if entity.invariants.empty?
@@ -122,6 +133,10 @@ module Hecks
122
133
  # a live or rehydrated instance sitting in a state the procedure
123
134
  # never declares is the saga-durability twin of
124
135
  # `lifecycle_values_are_declared` above.
136
+ # @param history [Hash] a replayed history as returned by `Replay.call`
137
+ # @return [true, String] true if every saga instance holds a declared
138
+ # state and its own memory survives a checkpoint round-trip; otherwise a
139
+ # message naming the process manager, correlation, and problem
125
140
  def sagas_rehydrate_cleanly(history)
126
141
  bluebook = history.fetch(:bluebook)
127
142
  process_managers = bluebook.process_managers.to_h { |pm| [pm.name, pm] }
@@ -163,6 +178,10 @@ module Hecks
163
178
  # dispatch is the claim then, not "dispatched to zero rows," and a
164
179
  # policy that dispatched anyway despite a failing guard is as real
165
180
  # a finding as a row it skipped.
181
+ # @param history [Hash] a replayed history as returned by `Replay.call`
182
+ # @return [true, String] true if every fan-out finding's actual dispatches
183
+ # match its independently computed expected row set; otherwise a message
184
+ # naming the policy, event, and disagreement
166
185
  def fanout_dispatches_once_per_matching_row(history)
167
186
  offenders = history.fetch(:fan_outs).filter_map do |finding|
168
187
  expected = finding[:expected_row_ids]
@@ -211,6 +230,10 @@ module Hecks
211
230
  # branch reads as its own precondition."
212
231
  # rubocop:disable-next Metrics/CyclomaticComplexity
213
232
  # rubocop:disable-next Metrics/PerceivedComplexity
233
+ # @param history [Hash] a replayed history as returned by `Replay.call`
234
+ # @return [true, String] true if every eligible count/median report answer
235
+ # matches an independent recomputation; otherwise a message naming the
236
+ # query and the disagreement
214
237
  def aggregation_matches_recompute(history)
215
238
  bluebook = history.fetch(:bluebook)
216
239
 
@@ -257,6 +280,11 @@ module Hecks
257
280
  # of count/median, nest_rows in place of recompute_median.
258
281
  # rubocop:disable-next Metrics/CyclomaticComplexity
259
282
  # rubocop:disable-next Metrics/PerceivedComplexity
283
+ #
284
+ # @param history [Hash] a replayed history as returned by `Replay.call`
285
+ # @return [true, String] true if every eligible group_by report answer matches
286
+ # an independent recompute; otherwise a semicolon-joined message naming each
287
+ # offending query
260
288
  def group_by_matches_recompute(history)
261
289
  bluebook = history.fetch(:bluebook)
262
290
 
@@ -290,6 +318,11 @@ module Hecks
290
318
  # per `group_by` field in declared order, leaf is the row with
291
319
  # every grouped field stripped (already spent, as the keys that
292
320
  # reached it).
321
+ # @param rows [Array<Hash>] materialized, symbol-keyed rows to nest
322
+ # @param fields [Array<Symbol>] the `group_by` fields, in declared order
323
+ # @return [Hash] one level of nesting per field, in order; the leaf under
324
+ # each key path is the first row in that group with every grouped field
325
+ # stripped
293
326
  def nest_rows(rows, fields)
294
327
  field, *rest = fields
295
328
  rows.group_by { |row| row[field] }.transform_values do |group|
@@ -305,6 +338,16 @@ module Hecks
305
338
  # finds the matching attribute, then narrowed by the report's own
306
339
  # `where` clauses via the same `InMemory.holds?` the interpreter's
307
340
  # `execute` calls.
341
+ # @param bluebook [Bluebook::Chapter] the bluebook the report belongs to
342
+ # @param instances [Hash] the snapshot to read rows from (`history[:instances]`
343
+ # shape, or the query's own `instances_at`)
344
+ # @param domain [String] the domain name the reduced aggregate belongs to
345
+ # @param model [Bluebook::ReadModel] the report being recomputed
346
+ # @param reduced_head [Hash] the many-side aggregate head being reduced, from
347
+ # `model.aggregate_heads`
348
+ # @param args [Hash] the query's own arguments, for FK matching and `where`
349
+ # evaluation
350
+ # @return [Array<Hash>] every eligible row's own state, `id:` merged in
308
351
  def eligible_rows(bluebook, instances, domain, model, reduced_head, args)
309
352
  aggregate = bluebook.aggregate(reduced_head[:aggregate])
310
353
  prefix = "#{domain}::#{reduced_head[:aggregate]}#"
@@ -337,6 +380,11 @@ module Hecks
337
380
  # → the average of the two middle values, as a Float; empty → nil,
338
381
  # never zero, so a caller cannot mistake "nothing to average" for
339
382
  # "averaged to zero."
383
+ # @param rows [Array<Hash>] eligible rows, as returned by `#eligible_rows`
384
+ # @param field [Symbol] the field to average
385
+ # @return [Object, Float, nil] the true middle value for an odd count, the
386
+ # Float average of the two middle values for an even count, or `nil` for
387
+ # an empty `rows`
340
388
  def recompute_median(rows, field)
341
389
  values = rows.map { |state| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, field)) }
342
390
  .compact.sort
@@ -22,6 +22,10 @@ module Hecks
22
22
  # a coercion bug, a stale string surviving a rename, a default
23
23
  # that drifted from the declared set, would all show up here as a
24
24
  # value nothing upstream would have predicted.
25
+ #
26
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
27
+ # @return [true, String] true if every instance's lifecycle field holds a declared
28
+ # state; otherwise a semicolon-joined message naming each offending instance
25
29
  def lifecycle_values_are_declared(history)
26
30
  bluebook = history.fetch(:bluebook)
27
31
  declared = {}
@@ -54,6 +58,11 @@ module Hecks
54
58
  # handler names would mean the runtime moved state the language
55
59
  # never authorized — the same trust ModelCheck's static reachability
56
60
  # rests on, checked here against what a run actually did.
61
+ #
62
+ # @param history [Hash] a replayed history as returned by `Replay.call`
63
+ # @return [true, String] true if every logged advance matches a declared
64
+ # handler edge; otherwise a message naming the process manager and pair
65
+ # that does not
57
66
  def saga_advances_follow_declared_handlers(history)
58
67
  bluebook = history.fetch(:bluebook)
59
68
  edges = Hash.new { |h, k| h[k] = [] }
@@ -86,6 +95,15 @@ module Hecks
86
95
  # comparison depended on, anything. Two independent replays, not a
87
96
  # cached one compared to itself, so a bug that corrupts the first
88
97
  # run's own bookkeeping cannot pass by agreeing with itself.
98
+ #
99
+ # @param domain_path [String] path to the domain directory to boot, such as
100
+ # `"examples/pizzas"`
101
+ # @param steps [Array<Hash>] the step list to replay twice
102
+ # @param adapter [Symbol] persistence adapter to boot with (`:memory`,
103
+ # `:postgres`, or `:postgres_era`)
104
+ # @return [true, String] true if both replays produce identical histories
105
+ # (after stripping declared nondeterministic fields); otherwise a message
106
+ # naming the step count that diverged
89
107
  def replay_is_deterministic(domain_path, steps, adapter: :memory)
90
108
  first = Replay.call(domain_path, steps, adapter: adapter)
91
109
  second = Replay.call(domain_path, steps, adapter: adapter)
@@ -10,6 +10,8 @@ module Hecks
10
10
  # capture, one entry per step whose dispatch enqueued at least one
11
11
  # row):
12
12
  #
13
+ # ## The two checks
14
+ #
13
15
  # 1. "delivery is inline by default" — a row this replay's own
14
16
  # dispatch enqueued must not still be `pending`/`claimed` once
15
17
  # that same call returns (nothing here ever simulates a
@@ -38,10 +40,9 @@ module Hecks
38
40
  # it should have dispatched to) is `fanout_dispatches_once_
39
41
  # per_matching_row`'s job, not this one's.
40
42
  #
41
- # A `saga:` row has no equivalent second check, deliberately — this
42
- # was the first shape this property shipped with, and it was wrong,
43
- # caught live against `examples/banking` before this comment
44
- # existed: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
43
+ # ## Why a `saga:` row is exempt from check 2
44
+ #
45
+ # Deliberately: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
45
46
  # handler_for matching the event name alone) says nothing about
46
47
  # whether a correlation resolves or a live instance exists, and
47
48
  # `begin_saga`/`end_saga` (saga_interpreter.rb) both have silent,
@@ -57,11 +58,18 @@ module Hecks
57
58
  # `saga_log` entries is therefore not a finding — only check 1
58
59
  # applies to it.
59
60
  #
60
- # **Not a grammar construct** — `FEATURE_COVERAGE`'s own `dry_runs_
61
- # leave_no_trace` precedent: the outbox is a runtime door
62
- # (`Runtime::Outbox`), not a word a bluebook declares, so there is
63
- # no feature string here to claim.
61
+ # ## Not a grammar construct
62
+ #
63
+ # `FEATURE_COVERAGE`'s own `dry_runs_leave_no_trace` precedent: the
64
+ # outbox is a runtime door (`Runtime::Outbox`), not a word a
65
+ # bluebook declares, so there is no feature string here to claim.
64
66
  module Outbox
67
+ # Checks every outbox row a replay's history recorded against the
68
+ # outbox's own contract (see this file's own header).
69
+ #
70
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
71
+ # @return [true, String] true if every outbox row satisfies the contract; otherwise
72
+ # a semicolon-joined message naming each offending row
65
73
  def outbox_rows_match_reactions(history)
66
74
  bluebooks = history.fetch(:bluebooks, {})
67
75
 
@@ -72,6 +80,17 @@ module Hecks
72
80
  offenders.empty? || offenders.join("; ")
73
81
  end
74
82
 
83
+ # Checks one outbox row against the outbox's own contract.
84
+ #
85
+ # @param row [Hash] one `Runtime::Outbox::Row#to_h` entry, `trace[:rows]`'s shape:
86
+ # at least `:status`, `:delivery_id`, `:consumer`, `:event`, and (when failed)
87
+ # `:error`
88
+ # @param trace [Hash] this row's outbox trace entry, one of `history[:outbox_traces]`,
89
+ # carrying `:rows` and `:reactions`
90
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
91
+ # keyed by domain name
92
+ # @return [Array<String>] zero or one offending message naming what this row got
93
+ # wrong; empty when the row is fine or its status names no check
75
94
  def outbox_row_offenders(row, trace, bluebooks)
76
95
  on = row.dig(:event, :name)
77
96
 
@@ -90,9 +109,20 @@ module Hecks
90
109
  end
91
110
  end
92
111
 
112
+ # Checks one delivered `policy:` outbox row for a missing `reaction_log` entry.
113
+ #
93
114
  # See this file's own header for why a `saga:` row is exempt: its
94
115
  # own `listens?` gives no such guarantee, unlike a policy's single,
95
116
  # deterministic `where` gate.
117
+ #
118
+ # @param row [Hash] the delivered outbox row being checked
119
+ # @param on [String] the row's event name, `row.dig(:event, :name)`
120
+ # @param trace [Hash] this row's outbox trace entry, carrying `:reactions`
121
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
122
+ # keyed by domain name
123
+ # @return [Array<String>] zero or one offending message; empty when the row is
124
+ # not a policy row, the policy is undeclared or fans out, or the policy's
125
+ # `where` does not independently re-evaluate true
96
126
  def outbox_delivered_policy_offenders(row, on, trace, bluebooks)
97
127
  kind, fqn = row[:consumer].to_s.split(":", 2)
98
128
  return [] unless kind == "policy"
@@ -101,11 +131,14 @@ module Hecks
101
131
  return [] if trace[:reactions].any? { |entry| entry[:policy] == name && entry[:on] == on }
102
132
 
103
133
  policy = bluebooks[home]&.policies&.find { |candidate| candidate.name == name }
104
- return [] unless policy # nothing declared under this name — inconclusive, not a claimed mismatch
105
- return [] if policy.fans_out? # fan-out row count is fanout_dispatches_once_per_matching_row's job
134
+ # Nothing declared under this name — inconclusive, not a claimed mismatch.
135
+ return [] unless policy
136
+ # Fan-out row count is fanout_dispatches_once_per_matching_row's job.
137
+ return [] if policy.fans_out?
106
138
 
107
139
  held = independently_re_evaluate_policy_where(policy, row[:event])
108
- return [] if held != true # false, or inconclusive (the where itself raised) — never a claimed mismatch
140
+ # False, or inconclusive (the where itself raised) — never a claimed mismatch.
141
+ return [] if held != true
109
142
 
110
143
  ["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) drained as delivered, but no matching " \
111
144
  "reaction_log entry exists and the policy's own where clause independently re-evaluates true — " \
@@ -128,6 +161,11 @@ module Hecks
128
161
  # claimed pass or a claimed mismatch from a resolution this replay
129
162
  # cannot actually reproduce" discipline `build_guard_check`'s own
130
163
  # rescue clause already follows.
164
+ #
165
+ # @param policy [Bluebook::Policy] the policy whose `where` clause is re-evaluated
166
+ # @param event [Hash] the outbox row's own recorded event, read for `:payload`
167
+ # @return [Boolean, nil] whether `policy`'s `where` holds against `event`'s payload,
168
+ # or nil if the where clause cannot be re-evaluated from it
131
169
  def independently_re_evaluate_policy_where(policy, event)
132
170
  return true if policy.where.to_s.empty?
133
171
 
@@ -21,20 +21,24 @@ module Hecks
21
21
  # drifted before — an adapter that accepts what the reference says
22
22
  # matches nothing, or orders what it refuses to order, shows up
23
23
  # here as a finding no self-referential adapter spec could see.
24
- # M23 — `Replay` now runs the native and reference engines
25
- # independently (each in its own begin/rescue — see that file's own
26
- # comment at the capture site), so this property can tell apart what
27
- # used to be indistinguishable: "both engines refused" (fine — the
28
- # ask was genuinely bad, nothing to compare) from "one refused and
29
- # the other did not" (a real divergence — the two engines disagree
30
- # about whether the ask was even valid, never mind what it answers).
31
- # `native_refused`/`reference_refused` are read by key presence, not
32
- # truthiness — `Replay` only ever adds `:error`/`:reference_error`
33
- # to an entry when that side actually raised, so an absent key is an
34
- # unambiguous "this side answered." A read-model ask (no reference
35
- # twin attempted at all, `asked[:query]` without "::") is skipped
36
- # entirely, same as always — there is no second engine to disagree
37
- # with.
24
+ # M23 — `Replay` runs the native and reference engines independently
25
+ # (each in its own begin/rescue — see that file's own comment at the
26
+ # capture site), so this property can tell apart "both engines
27
+ # refused" (fine — the ask was genuinely bad, nothing to compare)
28
+ # from "one refused and the other did not" (a real divergence — the
29
+ # two engines disagree about whether the ask was even valid, never
30
+ # mind what it answers). `native_refused`/`reference_refused` are
31
+ # read by key presence, not truthiness — `Replay` only ever adds
32
+ # `:error`/`:reference_error` to an entry when that side actually
33
+ # raised, so an absent key is an unambiguous "this side answered." A
34
+ # read-model ask (no reference twin attempted at all, `asked[:query]`
35
+ # without "::") is skipped entirely, same as always — there is no
36
+ # second engine to disagree with.
37
+ #
38
+ # @param history [Hash] a replayed history as returned by `Replay.call`
39
+ # @return [true, String] true if every native/reference query answer, and every
40
+ # refusal of one, agrees; otherwise a message naming the offending query, its
41
+ # args, and how the two engines disagreed
38
42
  def query_answers_match_reference(history)
39
43
  offenders = history.fetch(:queries).filter_map do |asked|
40
44
  next unless asked[:query].is_a?(String) && asked[:query].include?("::")
@@ -83,6 +87,11 @@ module Hecks
83
87
  # `skipped`/`expected` across method boundaries as params/returns
84
88
  # for a sequence that's only ever computed once, in this order.
85
89
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
+ #
91
+ # @param history [Hash] a replayed history as returned by `Replay.call`
92
+ # @return [true, String] true if every paged query's real answer matches an
93
+ # independent order/offset/limit recomputation over its eligible rows;
94
+ # otherwise a message naming the query, its args, and the two answers
86
95
  def paging_offset_partitions_correctly(history)
87
96
  bluebooks = history.fetch(:bluebooks)
88
97
 
@@ -119,6 +128,13 @@ module Hecks
119
128
  # corpus site yet, and the "one many-side head, one aggregate,
120
129
  # no FK-join" shape #query_eligible_rows assumes doesn't hold for
121
130
  # one.
131
+ #
132
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
133
+ # keyed by domain name
134
+ # @param verb [String] the asked verb, `"Domain::Aggregate.Query"`
135
+ # @return [Bluebook::Query, nil] the declared query, or nil if `verb` names an
136
+ # entity-level query path, or a domain, aggregate, or query not found among
137
+ # `bluebooks`
122
138
  def query_for_verb(bluebooks, verb)
123
139
  domain, aggregate_name, query_path = Naming.split_verb(verb)
124
140
  return nil unless query_path && !query_path.include?(".")
@@ -128,6 +144,9 @@ module Hecks
128
144
  aggregate&.query(query_path)
129
145
  end
130
146
 
147
+ # Independently recomputes which of an aggregate's own stored rows a
148
+ # query's `wheres` admit.
149
+ #
131
150
  # A query's own rows — unlike #eligible_rows (a ReadModel's
132
151
  # reduced/grouped many-side head, possibly FK-joined against a
133
152
  # root), a Query always asks about its own owning aggregate
@@ -149,6 +168,18 @@ module Hecks
149
168
  # nil, and declared every genuinely-eligible row ineligible — a
150
169
  # false property violation against a correct runtime answer,
151
170
  # reproducible on an untouched main with this same 4-step script.
171
+ #
172
+ # @param instances [Hash{String => Hash}] `history[:instances]`, or an
173
+ # `:instances_at` snapshot from a replayed query entry
174
+ # @param domain [String] the domain name the target aggregate belongs to
175
+ # @param aggregate_name [String] the target aggregate's own declared name
176
+ # @param wheres [Array<QuerySpecification::Common::WhereClause>] the clauses
177
+ # every returned row must satisfy
178
+ # @param args [Hash] the query's own call args, for a clause whose value is a
179
+ # Symbol naming one
180
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
181
+ # by domain name; needed only to resolve a `/` hop clause
182
+ # @return [Array<Hash>] each admitted row's own state, merged with its `id:`
152
183
  def query_eligible_rows(instances, domain, aggregate_name, wheres, args, bluebooks: {})
153
184
  aggregate = bluebooks[domain]&.aggregate(aggregate_name)
154
185
  prefix = "#{domain}::#{aggregate_name}#"
@@ -166,6 +197,9 @@ module Hecks
166
197
  end
167
198
  end
168
199
 
200
+ # Resolves one hop of a `/`-chained clause into a local `in` clause against
201
+ # the target aggregate's own ids.
202
+ #
169
203
  # `Runtime::ReferenceHop#fold`, independently restated over the
170
204
  # replay's own `:instances_at` snapshot instead of live
171
205
  # repositories — the same shape every other recompute in this
@@ -180,6 +214,20 @@ module Hecks
180
214
  # live fold builds. A clause with no `/`, or one whose head this
181
215
  # aggregate's declarations cannot resolve, passes through
182
216
  # untouched and evaluates locally as it always did.
217
+ #
218
+ # @param instances [Hash{String => Hash}] `history[:instances]`, or an
219
+ # `:instances_at` snapshot from a replayed query entry
220
+ # @param domain [String] the domain name `aggregate` belongs to
221
+ # @param aggregate [Bluebook::Aggregate, nil] the aggregate `clause` is
222
+ # evaluated against; nil skips hop resolution entirely
223
+ # @param clause [QuerySpecification::Common::WhereClause] the clause to resolve
224
+ # @param args [Hash] the query's own call args, passed through to the inner
225
+ # hop's own `query_eligible_rows` recursion
226
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
227
+ # by domain name
228
+ # @return [QuerySpecification::Common::WhereClause] `clause` unchanged if it has
229
+ # no resolvable hop head; otherwise a local `in` clause over the matching
230
+ # target ids
183
231
  def resolve_hop_clause(instances, domain, aggregate, clause, args, bluebooks)
184
232
  return clause unless aggregate && QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes)
185
233
 
@@ -194,9 +242,15 @@ module Hecks
194
242
  QuerySpecification::Common::WhereClause.new(field: hop.attribute.name, op: "in", value: ids)
195
243
  end
196
244
 
245
+ # Resolves a declared limit/offset value against the query's own call args.
246
+ #
197
247
  # `QueryInterpreter#resolve_query_value`, reproduced: a declared
198
248
  # limit/offset is either a literal or a Symbol naming an argument
199
249
  # the caller supplied.
250
+ #
251
+ # @param value [Integer, Symbol] the declared limit/offset value
252
+ # @param args [Hash] the query's own call args
253
+ # @return [Object] `value` unchanged if not a Symbol; otherwise `args[value]`
200
254
  def resolve_paging_value(value, args)
201
255
  value.is_a?(Symbol) ? args[value] : value
202
256
  end
@@ -22,11 +22,15 @@ module Hecks
22
22
  # any history a valid domain produces, independent of which seed
23
23
  # produced it.
24
24
  #
25
+ # ## Property shape
26
+ #
25
27
  # Each property is `name => ->(history) { true/false, or a message
26
28
  # string naming what broke }` — a truthy return (including `true`)
27
29
  # is a pass; a String return is a failure, and the string is the
28
30
  # finding. `history` is Replay's return shape.
29
31
  #
32
+ # ## Feature coverage
33
+ #
30
34
  # Every property declares the language feature it covers, in
31
35
  # `FEATURE_COVERAGE` below — a "Construct#attribute" pair spelled
32
36
  # exactly as `Bluebook::MetaValidator.grammar_registry` names it,
@@ -161,18 +165,19 @@ module Hecks
161
165
  # Not a place to hide a real gap — a feature belongs here only once
162
166
  # the specific enforcing code path has been read and confirmed, the
163
167
  # same discipline `spec/fuzzing/meta_domain_coverage_spec.rb` demands
164
- # of `KNOWN_GAPS` in the other direction. `Entity#identified_by` was
165
- # checked for this category once before and found not to qualify —
166
- # `command_interpreter.rb`'s `AlreadyExists` refusal was given to
167
- # every creating aggregate command uniformly, and MutationApplier
168
- # (command_interpreter/mutation_applier.rb) had no matching check on
169
- # an entity's own append. It does now: #check_entity_collision runs
170
- # unconditionally on both branches an entity identity can arrive by
171
- # (caller-supplied, or composite — the two the auto-mint branch
172
- # doesn't cover), the same way command_interpreter#hydrate's own
173
- # check is unconditional for every creating aggregate command. Real,
174
- # confirmed live before the fix (SafeDepositBox's Visit/KeyIssuance —
175
- # see spec/runtime/safe_deposit_box_spec.rb).
168
+ # of `KNOWN_GAPS` in the other direction. `Entity#identified_by`
169
+ # illustrates the discipline: `command_interpreter.rb`'s
170
+ # `AlreadyExists` refusal covers every creating aggregate command
171
+ # uniformly, but nothing else on its own covers an entity's own
172
+ # append — so this entry names the path that actually does,
173
+ # `MutationApplier#check_entity_collision` (command_interpreter/
174
+ # mutation_applier.rb), which runs unconditionally on both branches
175
+ # an entity identity can arrive by (caller-supplied, or composite —
176
+ # the two the auto-mint branch doesn't cover), the same way
177
+ # `command_interpreter#hydrate`'s own check is unconditional for
178
+ # every creating aggregate command. The collision this closes is
179
+ # real, not hypothetical: SafeDepositBox's Visit/KeyIssuance entities
180
+ # reproduce it, confirmed by spec/runtime/safe_deposit_box_spec.rb.
176
181
  GUARANTEED_BY_CONSTRUCTION = {
177
182
  "Aggregate#attributes" => "every field's pattern/closed-set/type passes through Value.build's one coercion " \
178
183
  "door (value/coercion.rb#check_patterns, value/admission.rb) before it can exist " \
@@ -230,9 +235,13 @@ module Hecks
230
235
  "Member#pairs" => "one level into ValueObject#rows — same door"
231
236
  }.freeze
232
237
 
233
- # The standard battery, run over one replayed history — everything
234
- # except determinism, which needs to replay twice itself and so
235
- # takes the steps directly rather than a single history.
238
+ # Runs the standard property battery over one replayed history —
239
+ # everything except determinism, which needs to replay twice itself
240
+ # and so takes the steps directly rather than a single history.
241
+ #
242
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
243
+ # @return [Hash{Symbol => true, String}] each property name mapped to `true`
244
+ # (passed) or a message string naming what broke
236
245
  def check(history)
237
246
  { lifecycle_values_are_declared: lifecycle_values_are_declared(history),
238
247
  saga_advances_follow_declared_handlers: saga_advances_follow_declared_handlers(history),
@@ -83,6 +83,13 @@ module Hecks
83
83
  DEFAULT_PATH = File.expand_path("../../../qa/settings.yml", __dir__)
84
84
 
85
85
  class << self
86
+ # Loads and validates `qa/settings.yml` (or `path`), returning a frozen instance.
87
+ #
88
+ # @param path [String] path to the YAML settings file; defaults to `DEFAULT_PATH`
89
+ # @return [Hecks::Fuzzing::QaSettings] the validated, frozen settings
90
+ # @raise [ArgumentError] if `path` does not exist, is not valid YAML, is not a
91
+ # YAML mapping at the top level, is missing a required key, declares an
92
+ # unknown key, or gives a value the wrong type for its dial
86
93
  def load(path = DEFAULT_PATH)
87
94
  raise ArgumentError, "qa settings file not found: #{path}" unless File.file?(path)
88
95
 
@@ -97,6 +104,11 @@ module Hecks
97
104
  end
98
105
  end
99
106
 
107
+ # @param raw [Hash] parsed YAML settings keyed by symbol, one entry per dial in
108
+ # `EXPECTED_TYPES`
109
+ # @param path [String] path to the settings file, used only in error messages
110
+ # @raise [ArgumentError] if `raw` is missing a required key, declares an unknown
111
+ # key, or gives a value the wrong type for its dial
100
112
  def initialize(raw, path)
101
113
  missing = EXPECTED_TYPES.keys - raw.keys
102
114
  raise ArgumentError, "#{path} is missing #{missing.sort.join(', ')}" if missing.any?