hecks 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
@@ -80,24 +80,68 @@ module Hecks
80
80
 
81
81
  module_function
82
82
 
83
+ # Reads an aggregate IR hash's own declared entities.
84
+ #
85
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
86
+ # @return [Array<Hash>] the aggregate's own declared entities ("pieces"); `[]`
87
+ # if it declares none
83
88
  def entities(aggregate) = aggregate["entities"] || []
89
+
90
+ # Reads an aggregate IR hash's own declared commands.
91
+ #
92
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
93
+ # @return [Array<Hash>] the aggregate's own declared commands; `[]` if it
94
+ # declares none
84
95
  def commands(aggregate) = aggregate["commands"] || []
85
96
 
86
97
  # One aggregate's commands, its pieces' included — a form carried by
87
98
  # an entity command is carried by the aggregate that owns it, the
88
99
  # same way `composite_piece`/`piece_lifecycle` already read pieces.
100
+ #
101
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
102
+ # @return [Array<Hash>] `aggregate`'s own commands, plus every entity's own
89
103
  def every_command(aggregate) = commands(aggregate) + entities(aggregate).flat_map { |piece| commands(piece) }
90
104
 
105
+ # Answers whether a command IR hash declares a `corrects` mutation.
106
+ #
107
+ # @param verb [Hash] a string-keyed command IR hash
108
+ # @return [Boolean] true if `verb` declares a `corrects` mutation
91
109
  def corrects?(verb) = (verb["mutations"] || []).any? { |change| change["op"].to_s == "corrects" }
110
+
111
+ # Reads an aggregate IR hash's own declared attributes.
112
+ #
113
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
114
+ # @return [Array<Hash>] the aggregate's own declared attributes; `[]` if it
115
+ # declares none
92
116
  def attributes(aggregate) = aggregate["attributes"] || []
117
+
118
+ # Reads an aggregate IR hash's own declared queries.
119
+ #
120
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
121
+ # @return [Array<Hash>] the aggregate's own declared queries; `[]` if it
122
+ # declares none
93
123
  def queries(aggregate) = aggregate["queries"] || []
124
+
125
+ # Answers whether an attribute IR hash's own declared type is a reference.
126
+ #
127
+ # @param attribute [Hash] a string-keyed attribute IR hash
128
+ # @return [Boolean] true if `attribute`'s own declared type is a reference
94
129
  def reference?(attribute) = attribute["type"].to_s.start_with?("Reference<")
95
130
 
131
+ # Answers whether any of an aggregate's own commands crosses a two-hop given.
132
+ #
133
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
134
+ # @return [Boolean] true if any command's own given crosses at least two
135
+ # references (`TWO_HOP_GIVEN_PATH_LENGTH`)
96
136
  def two_hop_given?(aggregate)
97
137
  commands(aggregate).any? { |verb| (verb["givens"] || []).any? { |given| deep_lookup?(given["ast"]) } }
98
138
  end
99
139
 
100
140
  # A `where` whose field crosses two `/` — `member/sponsor/standing`.
141
+ #
142
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
143
+ # @return [Boolean] true if any query's own where clause crosses at least
144
+ # two `/` hops
101
145
  def multi_hop_where?(aggregate)
102
146
  queries(aggregate).any? { |query| (query["wheres"] || []).any? { |where| where["field"].to_s.count("/") >= 2 } }
103
147
  end
@@ -106,6 +150,10 @@ module Hecks
106
150
  # own reference-typed attributes while carrying a different, non-
107
151
  # reference type — `attribute :member, Handle` against
108
152
  # `reference_to Member`.
153
+ #
154
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
155
+ # @return [Boolean] true if any command redeclares one of the aggregate's
156
+ # own reference-typed attribute names under a non-reference type
109
157
  def revalued_reference?(aggregate)
110
158
  references = attributes(aggregate).select { |held| reference?(held) }.to_set { |held| held["name"].to_s }
111
159
  commands(aggregate).any? do |verb|
@@ -115,6 +163,11 @@ module Hecks
115
163
 
116
164
  # Walks a given's own exported AST for any `lookup` whose path is
117
165
  # long enough to have crossed two references.
166
+ #
167
+ # @param node [Hash, Array, Object] an AST node (or subtree) from an
168
+ # exported given's `"ast"`
169
+ # @return [Boolean] true if any `lookup` node's own path is at least
170
+ # `TWO_HOP_GIVEN_PATH_LENGTH` long
118
171
  def deep_lookup?(node)
119
172
  case node
120
173
  when Hash
@@ -129,21 +182,39 @@ module Hecks
129
182
  end
130
183
 
131
184
  # Every form, answered for one aggregate — the table above, applied.
185
+ #
186
+ # @param aggregate [Hash] a string-keyed aggregate IR hash
187
+ # @return [Hash{String => Boolean}] every `FORMS` name mapped to whether
188
+ # `aggregate` exhibits it
132
189
  def properties(aggregate)
133
190
  FORMS.transform_values { |form| form.call(aggregate) }
134
191
  end
135
192
 
136
193
  # Every unordered pair of forms, each rendered "left + right" in
137
194
  # alphabetical order — the key both gates' excuse tables use.
195
+ #
196
+ # @return [Array<String>] every unordered pair of `FORMS` names, as
197
+ # `"left + right"`
138
198
  def pairs
139
199
  FORMS.keys.combination(2).map { |pair| pair_key(*pair) }
140
200
  end
141
201
 
202
+ # Renders two form names as one sorted pair key.
203
+ #
204
+ # @param left [String] a form name
205
+ # @param right [String] a form name
206
+ # @return [String] `left`/`right`, alphabetically ordered, joined as
207
+ # `"left + right"`
142
208
  def pair_key(left, right) = [left, right].sort.join(" + ")
143
209
 
144
210
  # `held` is `[[aggregate_name, properties], ...]`. Answers which
145
211
  # pairs are met on one aggregate, and by which — a Hash from pair
146
212
  # key to the names carrying it, so a caller can say who.
213
+ #
214
+ # @param held [Array<Array(String, Hash)>] `[aggregate_name, properties]`
215
+ # pairs, `properties` as returned by `#properties`
216
+ # @return [Hash{String => Array<String>}] every met pair key mapped to the
217
+ # aggregate names that carry it
147
218
  def covered_pairs(held)
148
219
  held.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(name, shows), covered|
149
220
  shows.select { |_, present| present }.keys.combination(2).each do |left, right|
@@ -155,6 +226,12 @@ module Hecks
155
226
  # `[[\"Chapter::Aggregate\", properties], ...]` for every aggregate
156
227
  # a string-keyed chapter IR declares — the same walk the golden
157
228
  # spec makes over `spec/golden/ir/*.json`.
229
+ #
230
+ # @param chapter_ir [Hash] a string-keyed chapter IR hash, with `"name"` and
231
+ # `"aggregates"`
232
+ # @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
233
+ # pairs, one per declared aggregate, `properties` as returned by
234
+ # `#properties`
158
235
  def aggregates_in(chapter_ir)
159
236
  (chapter_ir["aggregates"] || []).map do |aggregate|
160
237
  ["#{chapter_ir['name']}::#{aggregate['name']}", properties(aggregate)]
@@ -164,6 +241,10 @@ module Hecks
164
241
  # Where a domain path keeps its bluebooks — see
165
242
  # `Hecks::Corpus.bluebook_files`, the one definition every corpus
166
243
  # walk shares. `nil` when neither shape holds a bluebook.
244
+ #
245
+ # @param domain_path [String] path to a domain directory
246
+ # @return [Array<String>, nil] the domain's own `.bluebook` file paths, or
247
+ # `nil` if it holds none
167
248
  def bluebook_files(domain_path)
168
249
  Hecks::Corpus.bluebook_files(domain_path)
169
250
  end
@@ -176,6 +257,11 @@ module Hecks
176
257
  # domain's own). Only the domain's own chapter is measured — the
177
258
  # first bluebook loaded, the same "target chapter is always first"
178
259
  # fact `bin/project_rust` relies on.
260
+ # @param domain_path [String] path to a domain directory
261
+ # @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
262
+ # pairs, one per aggregate declared on `domain_path`'s own first-loaded
263
+ # chapter (see `Hecks::Projector::Exporter.call`)
264
+ # @raise [ArgumentError] if `domain_path` has no bluebook files to measure
179
265
  def census(domain_path)
180
266
  root = File.expand_path("../../..", __dir__)
181
267
  files = bluebook_files(domain_path)
@@ -27,6 +27,27 @@ module Hecks
27
27
 
28
28
  module_function
29
29
 
30
+ # Checks one generated domain over up to `seeds` generated sequences, stopping
31
+ # at the first divergence found.
32
+ #
33
+ # @param domain_path [String] path to the generated domain directory
34
+ # @param seeds [Integer] maximum number of seeds to try before declaring clean
35
+ # @param steps [Integer] steps to generate per seed
36
+ # @param adversarial [Float] fraction of command steps to mutate adversarially
37
+ # @param binary [String, nil] path to a compiled Rust conformance binary to
38
+ # diff against, or `nil` for a Ruby-only check
39
+ # @param differ [Object, nil] duck-typed comparison helper answering
40
+ # `RustConformanceHelpers`' interface (adapter-defined; constructed ad hoc
41
+ # by callers such as bin/qa_generated_domains), or `nil`
42
+ # @param match [Hash, nil] `{"mode" =>, "signature" =>}` from a parent
43
+ # domain's own finding — only a divergence matching it counts, for
44
+ # domain-level shrinking; `nil` accepts the first divergence found
45
+ # @param shrink_budget [Integer] step-level shrink attempts to spend on a
46
+ # found finding; `0` skips shrinking
47
+ # @return [Hash] `{"status" => "invalid", "error" =>}` if the domain does not
48
+ # boot; `{"status" => "clean", "seeds_run" =>}` if no seed diverged;
49
+ # otherwise a finding merged with `{"status" => "found", "seeds_run" =>}`
50
+ # (see `#finding`), plus `"shrunk_steps"` when `shrink_budget` is positive
30
51
  def run(domain_path, seeds:, steps:, adversarial:, binary: nil, differ: nil, match: nil, shrink_budget: 0)
31
52
  error = boot_error(domain_path)
32
53
  return { "status" => "invalid", "error" => error } if error
@@ -41,6 +62,11 @@ module Hecks
41
62
  { "status" => "clean", "seeds_run" => seeds }
42
63
  end
43
64
 
65
+ # Boots `domain_path` in an isolated copy, to find out whether it boots at all.
66
+ #
67
+ # @param domain_path [String] path to the domain directory to boot
68
+ # @return [String, nil] `nil` if the boot succeeds; otherwise the raising
69
+ # exception's class and first message line
44
70
  def boot_error(domain_path)
45
71
  IsolatedBoot.call(domain_path) { |copy| Hecks.boot(copy) }
46
72
  nil
@@ -48,6 +74,20 @@ module Hecks
48
74
  "#{e.class}: #{e.message.lines.first&.strip}"
49
75
  end
50
76
 
77
+ # Generates one sequence for `seed` and checks it for a divergence.
78
+ #
79
+ # @param domain_path [String] path to the domain directory
80
+ # @param seed [Integer] RNG seed for `SequenceGenerator.generate`
81
+ # @param steps [Integer] steps to generate
82
+ # @param adversarial [Float] fraction of command steps to mutate adversarially
83
+ # @param binary [String, nil] path to a compiled Rust conformance binary, or
84
+ # `nil` for a Ruby-only check
85
+ # @param differ [Object, nil] duck-typed comparison helper answering
86
+ # `RustConformanceHelpers`' interface (adapter-defined), or `nil`
87
+ # @param match [Hash, nil] `{"mode" =>, "signature" =>}` a divergence must
88
+ # match to count, or `nil` to accept the first one found
89
+ # @return [Hash, nil] a finding (see `#finding`) if a matching divergence was
90
+ # found, or if generation itself crashed; `nil` if the sequence was clean
51
91
  def check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
52
92
  sequence = SequenceGenerator.generate(domain_path, seed: seed, steps: steps, adversarial: adversarial)
53
93
  rescue StandardError => e
@@ -60,6 +100,19 @@ module Hecks
60
100
  nil
61
101
  end
62
102
 
103
+ # Runs the comparison appropriate to whether a compiled binary is available:
104
+ # `Differential.diff` against Rust when `binary` is given, otherwise a
105
+ # Ruby-only replay checked for property and self-consistency divergences.
106
+ #
107
+ # @param domain_path [String] path to the domain directory
108
+ # @param sequence [Array<Hash>] the generated step sequence to replay
109
+ # @param binary [String, nil] path to a compiled Rust conformance binary, or
110
+ # `nil` for a Ruby-only check
111
+ # @param differ [Object, nil] duck-typed comparison helper answering
112
+ # `RustConformanceHelpers`' interface (adapter-defined), or `nil`
113
+ # @return [Hash{Symbol => Array<Hash>}] one divergence list per mode checked;
114
+ # `{ruby_only: [{field: "crash", detail:}]}` if the Ruby-only replay itself
115
+ # raised
63
116
  def outcomes(domain_path, sequence, binary, differ)
64
117
  return Differential.diff(differ, domain_path, sequence, binary, modes: DIFFERENTIAL_MODES) if binary
65
118
 
@@ -70,6 +123,18 @@ module Hecks
70
123
  { ruby_only: [{ field: "crash", detail: "#{e.class}: #{e.message}" }] }
71
124
  end
72
125
 
126
+ # Builds a reported finding from a mode's divergences, unless `divergences`
127
+ # is empty or `match` names a different signature.
128
+ #
129
+ # @param seed [Integer] the seed that produced `sequence`
130
+ # @param mode [Symbol] the comparison mode the divergences came from
131
+ # @param divergences [Array<Hash>] divergence entries for `mode`
132
+ # @param sequence [Array<Hash>] the generated step sequence
133
+ # @param match [Hash, nil] `{"mode" =>, "signature" =>}` the finding must
134
+ # match, or `nil` to accept any
135
+ # @return [Hash, nil] `{"seed" =>, "mode" =>, "signature" =>, "steps" =>,
136
+ # "divergences" =>}` if `divergences` is non-empty and matches `match`;
137
+ # `nil` otherwise
73
138
  def finding(seed, mode, divergences, sequence, match)
74
139
  return nil if divergences.empty?
75
140
 
@@ -82,6 +147,17 @@ module Hecks
82
147
 
83
148
  # The step-level half of shrinking, in this same process: the domain
84
149
  # is already as small as the parent could make it.
150
+ #
151
+ # @param domain_path [String] path to the (already domain-shrunk) domain
152
+ # directory
153
+ # @param found [Hash] the finding to shrink, as returned by `#finding`
154
+ # @param binary [String, nil] path to a compiled Rust conformance binary, or
155
+ # `nil` for a Ruby-only check
156
+ # @param differ [Object, nil] duck-typed comparison helper answering
157
+ # `RustConformanceHelpers`' interface (adapter-defined), or `nil`
158
+ # @param budget [Integer] step-level shrink attempts to spend
159
+ # @return [Array<Hash>] the smallest step list found that still reproduces
160
+ # `found`'s divergence signature
85
161
  def shrink(domain_path, found, binary, differ, budget)
86
162
  original = Set.new(found["signature"])
87
163
  mode = found["mode"].to_sym
@@ -32,6 +32,16 @@ module Hecks
32
32
  null
33
33
  ].freeze
34
34
 
35
+ # Builds one value of the wrong shape for `attribute`, chosen from the
36
+ # confusions that are actually meaningful for its declared type.
37
+ #
38
+ # @param attribute [Bluebook::Attribute] the attribute the corrupted value is
39
+ # handed to
40
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares
41
+ # `attribute` (and any value objects it references)
42
+ # @param random [Random] the seeded RNG to draw the kind and value from
43
+ # @return [Object] a value of the wrong shape for `attribute` — an Integer,
44
+ # Float, String, Array, Hash, or boolean, depending on the kind drawn
35
45
  def corrupt(attribute, aggregate, random:)
36
46
  kind = kinds_for(attribute, aggregate).sample(random: random)
37
47
  build(kind, attribute, aggregate, random: random)
@@ -41,6 +51,13 @@ module Hecks
41
51
  # `scalar_for_object` for a plain Integer would just be a second spelling of
42
52
  # `numeral_string`, and a kind that cannot be wrong for the attribute it is
43
53
  # handed teaches the corpus nothing.
54
+ #
55
+ # @param attribute [Bluebook::Attribute] the attribute to find applicable
56
+ # confusions for
57
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares
58
+ # `attribute` (and any value objects it references)
59
+ # @return [Array<Symbol>] the subset of `KINDS` that are a real confusion for
60
+ # `attribute`'s declared type
44
61
  def kinds_for(attribute, aggregate)
45
62
  value_object = aggregate.value_object(attribute.type.to_s)
46
63
  return %i[scalar_for_object array_for_scalar null] if value_object
@@ -49,6 +66,16 @@ module Hecks
49
66
  %i[object_for_scalar array_for_scalar boolean_for_string null]
50
67
  end
51
68
 
69
+ # Builds the actual corrupted value for one already-chosen `kind`.
70
+ #
71
+ # @param kind [Symbol] one of `KINDS`
72
+ # @param attribute [Bluebook::Attribute] the attribute the value will be
73
+ # handed to
74
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares
75
+ # `attribute` (and any value objects it references)
76
+ # @param random [Random] the seeded RNG to draw the value from
77
+ # @return [Object, nil] the corrupted value for `kind`; `nil` if `kind` is not
78
+ # one `KINDS` declares
52
79
  def build(kind, attribute, aggregate, random:)
53
80
  case kind
54
81
  when :object_for_scalar then { "cents" => random.rand(1..1000) }
@@ -64,6 +91,15 @@ module Hecks
64
91
  # legitimately accepts one (that is the standing-in rule every domain relies
65
92
  # on), so the interesting case is a value object with several fields, where a
66
93
  # scalar cannot stand for anything and the refusal has to say so.
94
+ # @param attribute [Bluebook::Attribute] the value-object-typed attribute to
95
+ # build a bare scalar for
96
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares
97
+ # `attribute`'s value object
98
+ # @param random [Random] accepted for a uniform call signature with `#build`;
99
+ # this method draws nothing from it
100
+ # @return [String, Hash] `"a bare scalar"` when a scalar would legitimately be
101
+ # accepted (a single-field value object); otherwise a Hash corrupting the
102
+ # sole field's own type instead
67
103
  def scalar_for(attribute, aggregate, random:)
68
104
  value_object = aggregate.value_object(attribute.type.to_s)
69
105
  sole = value_object&.sole_attribute
@@ -77,6 +113,9 @@ module Hecks
77
113
  # An attribute the command never declared. `refuse_unknown_arguments` is a
78
114
  # real dispatch step (Vocabulary::AggregateDispatchOrder), and nothing
79
115
  # generated had ever exercised it.
116
+ # @param random [Random] the seeded RNG to draw the name and value from
117
+ # @return [Array(String, String)] a `[name, value]` pair naming an argument
118
+ # no command declares
80
119
  def undeclared_argument(random:)
81
120
  name = %w[colour flavour rank note].sample(random: random)
82
121
  [name, %w[red loud third scribbled].sample(random: random)]
@@ -6,6 +6,8 @@ module Hecks
6
6
  module Fuzzing
7
7
  # A fresh, in-process adapter for every ephemeral boot.
8
8
  #
9
+ # ## Why rebind at all
10
+ #
9
11
  # Fuzzing/replay copies a domain to a tmpdir and boots from there
10
12
  # specifically to get zero-history state — `rm_rf`ing the copy's own
11
13
  # `data/` achieves that for a file-based adapter (Memory,
@@ -25,16 +27,18 @@ module Hecks
25
27
  # one ephemeral copy answers through changes. What the domain is
26
28
  # bound to for real deployment is never touched — only this tmp copy.
27
29
  #
28
- # `adapter:` (PRD 02) — Memory is the default and the only mode every
29
- # existing caller still gets with no change. `:sqlite` rebinds to the
30
- # real SQLite adapter instead of the in-memory one, for exactly the
31
- # same reason PRD 02 exists: 15 declared properties (properties.rb)
32
- # and every fuzz/replay run has only ever been checked against
33
- # Memory's own hand-written repository, never against a real,
34
- # persisted, SQL-backed one — and `spec/adapters/query_agreement_spec
35
- # .rb` already found 4 shipped query bugs from exactly that
36
- # comparison, on a fixed corpus far smaller than what the fuzzer
37
- # generates. `:sqlite` rewrites to `"SqlitePersistence"` — the name
30
+ # ## `adapter:` modes
31
+ #
32
+ # `adapter:` (PRD 02) picks which real persistence one ephemeral boot
33
+ # answers through. `:memory` is the default and the only mode every
34
+ # caller that passes no `adapter:` still gets. `:sqlite` rebinds to
35
+ # the real SQLite adapter instead of the in-memory one: 15 declared
36
+ # properties (properties.rb) and every fuzz/replay run only ever
37
+ # exercised Memory's own hand-written repository until this mode
38
+ # existed, and `spec/adapters/query_agreement_spec.rb` already found 4
39
+ # shipped query bugs from exactly that comparison, on a fixed corpus
40
+ # far smaller than what the fuzzer generates. `:sqlite` rewrites to
41
+ # `"SqlitePersistence"` — the name
38
42
  # `lib/hecks/adapters/driven/sqlite.adapter` actually registers under
39
43
  # (`Sqlite` is the class; `SqlitePersistence` is a thin subclass
40
44
  # that's the one real port binding names) — and needs no `.world`
@@ -44,18 +48,16 @@ module Hecks
44
48
  # `root:`, which `Hecks.boot(copy)` passes as this ephemeral copy's
45
49
  # own directory — a fresh, empty `data/` per run, exactly like
46
50
  # Memory's own zero-history guarantee, just backed by a real SQLite
47
- # file instead of a Hash. Stale as of this paragraph's original
48
- # writing — both `Postgres` (PRD 02, docs/prds/02-fuzzer-real-
49
- # adapters.md) and `PostgresEra` (this mode's own header, below,
50
- # `rebind_to_postgres_era!`) since gained real `adapter:` modes here.
51
- # Each writes its own fresh `.world` per `.hecksagon` rather than
52
- # relying on the zero-config default Sqlite/Memory get, and each
53
- # needs a real, reachable Postgres server — but "no place to source a
54
- # connection safely" turned out not to be true: `:postgres` sources
55
- # one shared, permanent scratch database/schema this module itself
56
- # owns (see `FUZZ_POSTGRES_DATABASE`'s own header); `:postgres_era`
57
- # instead requires the caller to supply (and own the lifecycle of) its
58
- # own throwaway `database:`/`schema:`, since its only caller
51
+ # file instead of a Hash. `:postgres` (PRD 02, docs/prds/02-fuzzer-
52
+ # real-adapters.md) and `:postgres_era` (`rebind_to_postgres_era!`'s
53
+ # own header, below) are the two real-Postgres modes. Each writes its
54
+ # own fresh `.world` per `.hecksagon` rather than relying on the
55
+ # zero-config default Sqlite/Memory get, and each needs a real,
56
+ # reachable Postgres server: `:postgres` sources one shared,
57
+ # permanent scratch database/schema this module itself owns (see
58
+ # `FUZZ_POSTGRES_DATABASE`'s own header); `:postgres_era` instead
59
+ # requires the caller to supply (and own the lifecycle of) its own
60
+ # throwaway `database:`/`schema:`, since its only caller
59
61
  # (`bin/qa_sweep --persistence-parity`) already has to manage a
60
62
  # disposable database of its own, never a shared one this module could
61
63
  # safely default to.
@@ -83,6 +85,20 @@ module Hecks
83
85
  # signature per adapter) keeps `SequenceGenerator`/`Replay`'s own
84
86
  # single passthrough (`adapter:`, now joined by these two) uniform
85
87
  # across all four modes.
88
+ #
89
+ # @param domain_path [String] path to the real domain directory to copy and isolate
90
+ # @param adapter [Symbol] which persistence the copy answers through: `:memory`
91
+ # (default), `:sqlite`, `:postgres`, or `:postgres_era`
92
+ # @param database [String, nil] the database name, required only for
93
+ # `adapter: :postgres_era`; ignored by every other adapter
94
+ # @param schema [String, nil] the schema name, required only for
95
+ # `adapter: :postgres_era`; ignored by every other adapter
96
+ # @yield [String] the freshly rebound copy's root directory, ready to boot
97
+ # @yieldreturn [Object] anything; becomes this method's own return value
98
+ # @return [Object] whatever the given block returns
99
+ # @raise [ArgumentError] if `adapter` is not `:memory`, `:sqlite`, `:postgres`, or
100
+ # `:postgres_era`, or if `adapter: :postgres_era` is given without both
101
+ # `database:` and `schema:`
86
102
  def call(domain_path, adapter: :memory, database: nil, schema: nil)
87
103
  Dir.mktmpdir("hecks-fuzz") do |tmp|
88
104
  copy = File.join(tmp, File.basename(domain_path))
@@ -113,6 +129,10 @@ module Hecks
113
129
  # what an isolated boot wants: the copy has to stand alone, since
114
130
  # rebind! rewrites files in it and must not reach back through a
115
131
  # link into the real tree.
132
+ #
133
+ # @param source [String] the real domain directory to copy
134
+ # @param destination [String] the tmpdir path to copy it into; created if missing
135
+ # @return [void]
116
136
  def copy_dereferencing(source, destination)
117
137
  FileUtils.mkdir_p(destination)
118
138
  Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).each do |path|
@@ -136,6 +156,11 @@ module Hecks
136
156
  end
137
157
  end
138
158
 
159
+ # Rewrites every `.hecksagon` in the copy to bind through Memory and drops its
160
+ # `.world` files, so the boot needs no settings at all.
161
+ #
162
+ # @param copy [String] the isolated copy's root directory
163
+ # @return [void]
139
164
  def rebind_to_memory!(copy)
140
165
  rewrite_bindings!(copy, "Memory")
141
166
  strip_translations!(copy)
@@ -162,6 +187,9 @@ module Hecks
162
187
  # needed" property Memory has. `data/` itself is left for Sqlite to
163
188
  # recreate on first write, same as it always was for Memory/Heki —
164
189
  # nothing here creates it up front.
190
+ #
191
+ # @param copy [String] the isolated copy's root directory
192
+ # @return [void]
165
193
  def rebind_to_sqlite!(copy)
166
194
  rewrite_bindings!(copy, "SqlitePersistence")
167
195
  strip_translations!(copy)
@@ -193,6 +221,11 @@ module Hecks
193
221
  FUZZ_POSTGRES_DATABASE = "hecks_fuzz".freeze
194
222
  FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
195
223
 
224
+ # Rewrites every `.hecksagon` in the copy to bind through Postgres, against the
225
+ # shared `FUZZ_POSTGRES_DATABASE`/`FUZZ_POSTGRES_SCHEMA` scratch schema.
226
+ #
227
+ # @param copy [String] the isolated copy's root directory
228
+ # @return [void]
196
229
  def rebind_to_postgres!(copy)
197
230
  require "pg"
198
231
  rewrite_bindings!(copy, "Postgres")
@@ -244,6 +277,8 @@ module Hecks
244
277
  # .available?` already uses) ; the schema inside it is dropped and
245
278
  # recreated on every call, which is what actually isolates one
246
279
  # ephemeral boot's data from the next.
280
+ #
281
+ # @return [void]
247
282
  def ensure_fuzz_schema!
248
283
  # `Adapters::Postgres#initialize` opens one real `PG::Connection`
249
284
  # per aggregate and never explicitly closes it — fine for a
@@ -322,6 +357,12 @@ module Hecks
322
357
  # keyword arguments (never a fallback constant) is what keeps that
323
358
  # ownership from silently drifting back onto this module the way
324
359
  # `:postgres`'s own `FUZZ_POSTGRES_DATABASE` already has.
360
+ #
361
+ # @param copy [String] the isolated copy's root directory
362
+ # @param database [String] the caller-owned throwaway database name
363
+ # @param schema [String] the caller-owned throwaway schema name
364
+ # @return [void]
365
+ # @raise [ArgumentError] if `database` or `schema` is empty
325
366
  def rebind_to_postgres_era!(copy, database:, schema:)
326
367
  require "pg"
327
368
  if database.to_s.empty? || schema.to_s.empty?
@@ -400,6 +441,11 @@ module Hecks
400
441
  # again is the caller's own job (its name and lifecycle belong to
401
442
  # the caller — see `rebind_to_postgres_era!`'s own header), not
402
443
  # something this per-ephemeral-boot helper should ever do mid-sweep.
444
+ #
445
+ # @param database [String] the caller-owned database name, created if it does not
446
+ # already exist
447
+ # @param schema [String] the caller-owned schema name, dropped and recreated
448
+ # @return [void]
403
449
  def ensure_postgres_era_schema!(database:, schema:)
404
450
  GC.start
405
451
 
@@ -467,10 +513,21 @@ module Hecks
467
513
  # an ephemeral boot, deleted outright) — never called for
468
514
  # `:postgres_era` itself, where the bound adapter genuinely is
469
515
  # lineage-capable and the edge causes no refusal to begin with.
516
+ #
517
+ # @param copy [String] the isolated copy's root directory
518
+ # @return [void]
470
519
  def strip_translations!(copy)
471
520
  Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
472
521
  end
473
522
 
523
+ # Rewrites every `persisted_by` bind — aggregate-scoped or bare at the
524
+ # hecksagon's own root — in the copy's `.hecksagon` files to name `adapter_name`,
525
+ # and drops every `projected_by` bind outright.
526
+ #
527
+ # @param copy [String] the isolated copy's root directory
528
+ # @param adapter_name [String] the port binding name to rewrite every
529
+ # `persisted_by` to, such as `"Memory"` or `"SqlitePersistence"`
530
+ # @return [void]
474
531
  def rewrite_bindings!(copy, adapter_name)
475
532
  Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
476
533
  lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
@@ -1,6 +1,6 @@
1
1
  module Hecks
2
2
  module Fuzzing
3
- # The one declared set of FIELDS that leave a comparison — partition,
3
+ # The one declared set of `FIELDS` that leave a comparison — partition,
4
4
  # not filter. Every comparison in lib/hecks/fuzzing that drops a field
5
5
  # before comparing two histories names a group here instead of writing
6
6
  # its own literal `except(...)` list, so the reason for dropping a field
@@ -48,17 +48,29 @@ module Hecks
48
48
 
49
49
  module_function
50
50
 
51
+ # Lists the nondeterministic field names declared for one comparison group.
52
+ #
53
+ # @param group [Symbol] a key of `FIELDS`, such as `:query_row` or `:event`
54
+ # @return [Array<Symbol>] the field names declared for `group`
55
+ # @raise [KeyError] if `group` names no declared group
51
56
  def names(group)
52
57
  FIELDS.fetch(group).keys
53
58
  end
54
59
 
55
60
  # Every declared name, across every group.
61
+ #
62
+ # @return [Array<Symbol>] the union of every group's field names, deduplicated
56
63
  def all_names
57
64
  FIELDS.values.flat_map(&:keys).uniq
58
65
  end
59
66
 
60
67
  # `hash` without `group`'s fields — symbol keys, the shape every
61
68
  # Ruby-side history carries.
69
+ #
70
+ # @param hash [Hash] a symbol-keyed row, event, or history hash to compare
71
+ # @param group [Symbol] a key of `FIELDS` naming which fields to drop
72
+ # @return [Hash] `hash` with `group`'s declared fields removed
73
+ # @raise [KeyError] if `group` names no declared group
62
74
  def strip(hash, group)
63
75
  hash.except(*names(group))
64
76
  end