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
@@ -11,14 +11,16 @@ module Hecks
11
11
  # Rust subprocess (or a real Postgres round trip), where `bin/fuzz`'s
12
12
  # only ever cost an in-process replay.
13
13
  #
14
- # The caller owns "same finding". `call` is handed a block that
14
+ # ## Ownership of "same finding"
15
+ #
16
+ # The caller owns it. `call` is handed a block that
15
17
  # answers true when a candidate step list still reproduces what the
16
18
  # original did — `bin/fuzz` compares its own verdict signature,
17
19
  # `bin/qa_sweep` compares `Shrinker.signature` of one mode's
18
20
  # divergences. This module never replays anything itself, so it has
19
21
  # no idea which engine, adapter or comparison it is minimizing for.
20
22
  #
21
- # **Two passes, in order**:
23
+ # ## Two passes, in order
22
24
  #
23
25
  # 1. Steps, chunks first. Removing one step at a time (what
24
26
  # `bin/fuzz` did) costs O(n²) candidate checks on a sequence
@@ -32,6 +34,8 @@ module Hecks
32
34
  # one key at a time from the step's current args, keep it dropped
33
35
  # only while the finding still reproduces.
34
36
  #
37
+ # ## Budget
38
+ #
35
39
  # A budget, because a sweep has other targets waiting. `budget:` caps
36
40
  # how many candidate checks one call may spend (nil = unbounded, the
37
41
  # `bin/fuzz` behaviour). When it runs out the best candidate found so
@@ -42,6 +46,23 @@ module Hecks
42
46
 
43
47
  module_function
44
48
 
49
+ # Shrinks `steps` to the smallest step list the block still accepts as
50
+ # reproducing the original finding — pass 1 (steps), then pass 2
51
+ # (arguments), as this module's own header describes.
52
+ #
53
+ # @param steps [Array<Hash>] the step list to shrink, each step a command,
54
+ # query, or read-model hash as `SequenceGenerator` produces
55
+ # @param budget [Integer, nil] maximum candidate checks to spend; nil for
56
+ # unbounded
57
+ # @yield [candidate] tests whether a candidate step list still reproduces
58
+ # the finding being shrunk
59
+ # @yieldparam candidate [Array<Hash>] a step-dropped or argument-trimmed
60
+ # copy of `steps`
61
+ # @yieldreturn [Boolean] true if `candidate` still reproduces the finding
62
+ # @return [Fuzzing::Shrinker::Result] `steps:` the shrunk step list,
63
+ # `attempts:` how many candidate checks were spent, `exhausted:` whether
64
+ # the budget ran out before shrinking finished
65
+ # @raise [ArgumentError] if no block is given
45
66
  def call(steps, budget: nil, &reproduces)
46
67
  raise ArgumentError, "Shrinker.call needs a block answering whether a candidate reproduces" unless reproduces
47
68
 
@@ -51,6 +72,18 @@ module Hecks
51
72
  Result.new(steps: current, attempts: meter.used, exhausted: meter.exhausted?)
52
73
  end
53
74
 
75
+ # Runs pass 1 of the shrink: delta-debugging removal of step chunks,
76
+ # halving the chunk size each full pass until single steps are tried
77
+ # and nothing more can be dropped.
78
+ #
79
+ # @param steps [Array<Hash>] the step list to shrink
80
+ # @param meter [Fuzzing::Shrinker::Meter] the shared budget meter
81
+ # @yield [candidate] tests whether a candidate step list still reproduces
82
+ # the finding being shrunk
83
+ # @yieldparam candidate [Array<Hash>] `steps` with one chunk removed
84
+ # @yieldreturn [Boolean] true if `candidate` still reproduces the finding
85
+ # @return [Array<Hash>] the smallest step list pass 1 could reach, or the
86
+ # best found so far if the budget ran out first
54
87
  def drop_steps(steps, meter, &reproduces)
55
88
  current = steps
56
89
  chunk = [current.length / 2, 1].max
@@ -83,9 +116,23 @@ module Hecks
83
116
  current
84
117
  end
85
118
 
119
+ # Runs pass 2 of the shrink: drops one argument key at a time from
120
+ # each surviving step's args, keeping the drop only while the
121
+ # finding still reproduces.
122
+ #
86
123
  # See the module header's pass 2 — `args` is read by whichever
87
124
  # spelling the step actually carries (`key?` first, never `||`,
88
125
  # which cannot tell a stored `false` from an absent key).
126
+ #
127
+ # @param steps [Array<Hash>] the step list, already step-shrunk by `drop_steps`
128
+ # @param meter [Fuzzing::Shrinker::Meter] the shared budget meter
129
+ # @yield [candidate] tests whether a candidate step list still reproduces
130
+ # the finding being shrunk
131
+ # @yieldparam candidate [Array<Hash>] `steps` with one step's one argument
132
+ # key dropped
133
+ # @yieldreturn [Boolean] true if `candidate` still reproduces the finding
134
+ # @return [Array<Hash>] `steps` with every argument key `drop_arguments`
135
+ # accepted dropped
89
136
  def drop_arguments(steps, meter, &reproduces)
90
137
  steps.each_index do |position|
91
138
  original = args_of(steps[position])
@@ -104,6 +151,11 @@ module Hecks
104
151
  steps
105
152
  end
106
153
 
154
+ # Reads a step's argument hash, whichever key spelling it uses.
155
+ #
156
+ # @param step [Hash] one step, string- or symbol-keyed
157
+ # @return [Hash, nil] the step's `"args"` value if the step is string-keyed,
158
+ # else its `:args` value (nil if neither key is present)
107
159
  def args_of(step)
108
160
  step.key?("args") ? step["args"] : step[:args]
109
161
  end
@@ -131,6 +183,15 @@ module Hecks
131
183
 
132
184
  CRASH_FIELDS = %w[crash process generator_crash].freeze
133
185
 
186
+ # Computes which finding a set of divergences is — the stable identity
187
+ # a shrink candidate has to keep. See this method's own preceding
188
+ # comment for what each field kind contributes.
189
+ #
190
+ # @param divergences [Array<Hash>] divergence entries, each at least
191
+ # `{field:, detail:}` plus whichever mode-specific keys carry the two
192
+ # compared sides
193
+ # @return [Set<String>] stable identity strings: each divergence's `field`,
194
+ # plus `detail_keys`' own per-field detail strings
134
195
  def signature(divergences)
135
196
  divergences.each_with_object(Set.new) do |divergence, keys|
136
197
  field = divergence[:field].to_s
@@ -139,6 +200,13 @@ module Hecks
139
200
  end
140
201
  end
141
202
 
203
+ # Extracts the finer-grained identity strings one divergence's field
204
+ # contributes beyond the field name itself.
205
+ #
206
+ # @param field [String] the divergence's `field`, stringified
207
+ # @param divergence [Hash] the divergence entry `field` came from
208
+ # @return [Array<String>] extra identity strings for `field`'s own kind
209
+ # (list, `instances`, or crash/process); empty for any other field
142
210
  def detail_keys(field, divergence)
143
211
  left, right = divergence.except(:field, :detail).values.select { |v| v.is_a?(Array) || v.is_a?(Hash) }
144
212
  if LIST_FIELDS.include?(field) then list_keys(field, left, right)
@@ -149,15 +217,33 @@ module Hecks
149
217
  end
150
218
  end
151
219
 
220
+ # Names the rows present on one side of a list-field divergence and not
221
+ # the other.
222
+ #
223
+ # @param field [String] the divergence's field name, prefixed onto each key
224
+ # @param left [Array, nil] one side's list; a non-Array yields no keys
225
+ # @param right [Array, nil] the other side's list; a non-Array yields no keys
226
+ # @return [Array<String>] `"field:name"` for every row in the symmetric
227
+ # difference of `left` and `right`; empty unless both are arrays
152
228
  def list_keys(field, left, right)
153
229
  return [] unless left.is_a?(Array) && right.is_a?(Array)
154
230
 
155
231
  ((left - right) + (right - left)).map { |row| "#{field}:#{named(row)}" }
156
232
  end
157
233
 
234
+ # Names the top-level keys the two sides of an `instances` divergence
235
+ # disagree on.
236
+ #
158
237
  # Instance keys are `Aggregate#id` on the wire — the id is whatever
159
238
  # the generator minted, so it names the record, not the finding;
160
239
  # keeping it would pin every creating step.
240
+ #
241
+ # @param left [Hash, nil] one side's instance snapshot; a non-Hash yields no keys
242
+ # @param right [Hash, nil] the other side's instance snapshot; a non-Hash
243
+ # yields no keys
244
+ # @return [Array<String>] `"instances:Aggregate"` for every top-level key
245
+ # the two sides disagree on, its `#id` suffix dropped; empty unless both
246
+ # are hashes
161
247
  def instance_keys(left, right)
162
248
  return [] unless left.is_a?(Hash) && right.is_a?(Hash)
163
249
 
@@ -165,10 +251,26 @@ module Hecks
165
251
  .map { |key| "instances:#{key.to_s.split('#').first}" }
166
252
  end
167
253
 
254
+ # Checks that a candidate's own divergences still demonstrate the
255
+ # finding being shrunk.
256
+ #
257
+ # @param original_signature [Set<String>] the finding's own signature, as
258
+ # returned by `signature`
259
+ # @param divergences [Array<Hash>] the candidate's own divergence entries,
260
+ # in `signature`'s own shape
261
+ # @return [Boolean] true if `divergences` is non-empty and its signature is
262
+ # a superset of `original_signature`
168
263
  def reproduces?(original_signature, divergences)
169
264
  !divergences.empty? && original_signature.subset?(signature(divergences))
170
265
  end
171
266
 
267
+ # Names one row from a list-field divergence, for `list_keys`.
268
+ #
269
+ # @param row [Object] one item from a list-field divergence's array;
270
+ # typically a Hash carrying a `verb`, `query`, or `policy` key (string
271
+ # or symbol)
272
+ # @return [String] `row`'s `verb`/`query`/`policy` value, stringified, if
273
+ # `row` is a Hash carrying one of those keys; otherwise `row.to_s`
172
274
  def named(row)
173
275
  return row.to_s unless row.is_a?(Hash)
174
276
 
@@ -180,13 +282,23 @@ module Hecks
180
282
  class Meter
181
283
  attr_reader :used
182
284
 
285
+ # @param budget [Integer, nil] maximum candidate checks to allow; nil for unbounded
183
286
  def initialize(budget)
184
287
  @budget = budget
185
288
  @used = 0
186
289
  end
187
290
 
291
+ # Reports whether this meter's budget has been fully spent.
292
+ #
293
+ # @return [Boolean] true if a budget is set and every allotted check has
294
+ # been used
188
295
  def exhausted? = !@budget.nil? && @used >= @budget
189
296
 
297
+ # Spends one candidate check against the budget and runs the block.
298
+ #
299
+ # @yield runs the candidate check being counted
300
+ # @yieldreturn [Object] the block's own result
301
+ # @return [Object] whatever the block returns
190
302
  def try
191
303
  @used += 1
192
304
  yield
@@ -12,16 +12,24 @@ module Hecks
12
12
  # `QualityControlDials::STRUCTURAL_REFUSAL_BOUNDARY`.
13
13
  #
14
14
  # The construct comes straight from the manifest entry, written by the
15
- # generator branch that made the skip (`Projector::SkipReason`). This
16
- # module used to guess it from the Ruby declaration's `to_h` keys, which
17
- # was coarser than codegen's own decision and could drift from it.
15
+ # generator branch that made the skip (`Projector::SkipReason`), rather
16
+ # than guessed from the Ruby declaration's `to_h` keys, which is coarser
17
+ # than codegen's own decision and can drift from it.
18
18
  module StructuralSkips
19
19
  module_function
20
20
 
21
+ # Attributes each skipped verb to the construct family its manifest
22
+ # entry names.
23
+ #
21
24
  # One entry per skipped verb: `{ verb:, constructs: [...] }`, sorted by
22
25
  # verb so the printed observation is stable across seeds and runs. A
23
26
  # verb the manifest doesn't declare answers `unknown`, which no
24
27
  # boundary admits.
28
+ #
29
+ # @param gaps [Fuzzing::RustGapManifest] the binary's not-generated manifest entries
30
+ # @param verbs [Array<String>] the skipped query/read-model verbs to attribute
31
+ # @return [Array<Hash{Symbol => Object}>] one `{ verb: String, constructs: Array<String> }`
32
+ # per verb, in sorted-verb order
25
33
  def attribute(gaps, verbs)
26
34
  verbs.sort.map do |verb|
27
35
  entry = gaps.not_generated(verb)
@@ -29,7 +37,13 @@ module Hecks
29
37
  end
30
38
  end
31
39
 
32
- # Every skipped verb whose constructs are not all inside `boundary`.
40
+ # Selects the attributed entries whose constructs are not all inside `boundary`.
41
+ #
42
+ # @param attributed [Array<Hash{Symbol => Object}>] entries as returned by `attribute`
43
+ # @param boundary [Array<String, Symbol>] the construct families the documented codegen
44
+ # boundary admits
45
+ # @return [Array<Hash{Symbol => Object}>] the subset of `attributed` naming an
46
+ # unadmitted construct, or no construct at all
33
47
  def outside_boundary(attributed, boundary)
34
48
  admitted = boundary.map(&:to_s)
35
49
  attributed.select { |entry| entry[:constructs].empty? || (entry[:constructs] - admitted).any? }
@@ -40,6 +40,14 @@ module Hecks
40
40
  # streak does not exceed wins — rows are read in order, so the table
41
41
  # must be ascending, and the last row's `Float::INFINITY` is what
42
42
  # makes it the ceiling rather than a gap.
43
+ #
44
+ # @param streak [Integer] `Target.clean_streak`, the number of consecutive clean sweeps
45
+ # @param tiers [Array<Hash{Symbol => Numeric}>] ascending `{ upto:, seeds:, steps: }` rows;
46
+ # defaults to `DEFAULT_TIERS`
47
+ # @return [Array(Integer, Integer)] `[seeds, steps]` for the first row `streak` fits under
48
+ # @raise [ArgumentError] if `streak` is negative
49
+ # @raise [ArgumentError] if no row in `tiers` covers `streak` (the last row must declare
50
+ # `upto: Float::INFINITY`)
43
51
  def for_streak(streak, tiers: DEFAULT_TIERS)
44
52
  raise ArgumentError, "streak must not be negative" if streak.negative?
45
53
 
@@ -3,26 +3,30 @@ module Hecks
3
3
  # What a sweep target can actually be checked for, read off the
4
4
  # filesystem — never off a stored list.
5
5
  #
6
- # `bin/qa_sweep` used to decide its one comparison mode inline: "is
7
- # there a Cargo feature named after this directory? then
8
- # `:differential`, else `:ruby_only`", and a separate hand-typed abort
9
- # for `--persistence-parity` ("does any .hecksagon bind PostgresEra?").
10
- # Every further mode the practice adds (era boundary, concurrency, a
11
- # WASM front) would have grown one more inline `if`, each one a
6
+ # `bin/qa_sweep` does not decide its comparison modes with inline
7
+ # conditionals — "is there a Cargo feature named after this directory?
8
+ # then `:differential`, else `:ruby_only`", a separate hand-typed abort
9
+ # for `--persistence-parity` ("does any .hecksagon bind PostgresEra?"),
10
+ # and so on for every further mode the practice adds (era boundary,
11
+ # concurrency, a WASM front) — because each such inline `if` is a
12
12
  # policy decision hiding in a script. This module is those decisions
13
13
  # as data: `MODE_REQUIREMENTS` says which capabilities each mode
14
14
  # needs, `infer` says which capabilities a target's own directory
15
15
  # actually has, and `resolve` is the one rule that joins them —
16
16
  # `modes_to_run = enabled ∩ eligible`.
17
17
  #
18
- # Inference decides; a stored list only records. `Target.capabilities`
19
- # (qa/bluebook/quality_control.bluebook, once PR-1's era lands) is
20
- # written by the runner from exactly this inference at release time
21
- # so `Target.EligibleFor(mode)` can audit the rotation from the
22
- # ledger alone — but the runner re-infers every sweep, because a
23
- # stored list that lags yesterday's Cargo feature is precisely the
24
- # "quiet divergence" (the chapter's own opening comment) this whole
25
- # practice exists to hunt. Nothing here ever reads the ledger.
18
+ # ## Inference decides; a stored list only records
19
+ #
20
+ # `Target.capabilities` (qa/bluebook/quality_control.bluebook, once
21
+ # PR-1's era lands) is written by the runner from exactly this
22
+ # inference at release time so `Target.EligibleFor(mode)` can audit
23
+ # the rotation from the ledger alone — but the runner re-infers every
24
+ # sweep, because a stored list that lags yesterday's Cargo feature is
25
+ # precisely the "quiet divergence" (the chapter's own opening comment)
26
+ # this whole practice exists to hunt. Nothing here ever reads the
27
+ # ledger.
28
+ #
29
+ # ## Regex provenance
26
30
  #
27
31
  # Every regex is one the harness already owned, moved here rather than
28
32
  # re-derived, and each one's provenance is named beside it so a
@@ -121,10 +125,19 @@ module Hecks
121
125
  # exists because that pass pays for real Postgres I/O per dispatch).
122
126
  DEFERRED_MODES = %i[persistence_parity adapter_parity_postgres era_boundary concurrency].freeze
123
127
 
128
+ # Reads a target directory to find out which capabilities it actually has.
129
+ #
124
130
  # Sorted, plain strings — comma-joined by the runner into the
125
131
  # `Target.Release(capabilities:)` value object and printed verbatim
126
132
  # on the `resolved modes:` line, so the same spelling is what a
127
133
  # human reads, what `--all` parses back, and what the ledger stores.
134
+ #
135
+ # @param domain_path [String] filesystem path to the target domain's directory
136
+ # @param rust_dir [String] path to the Rust project root, checked for a matching
137
+ # Cargo feature; defaults to this repo's own `rust/` directory
138
+ # @return [Array<String>] the target's capabilities, sorted; a subset of
139
+ # `sqlite`, `rust`, `postgres_era`, `translations`, `governance`, `role_gated`,
140
+ # `tenant`, `sagas`
128
141
  def infer(domain_path, rust_dir: File.expand_path("../../../rust", __dir__))
129
142
  capabilities = %w[sqlite]
130
143
  capabilities << "rust" if rust_feature?(domain_path, rust_dir)
@@ -137,6 +150,12 @@ module Hecks
137
150
  capabilities.sort
138
151
  end
139
152
 
153
+ # Answers whether `mode` can run against a target with `capabilities`.
154
+ #
155
+ # @param mode [Symbol, String] a key of `MODE_REQUIREMENTS`, such as `:differential`
156
+ # @param capabilities [Array<String>] the target's capabilities, as `infer` returns
157
+ # @return [Boolean] whether `capabilities` covers every capability `mode` requires
158
+ # @raise [ArgumentError] if `mode` names no entry in `MODE_REQUIREMENTS`
140
159
  def eligible?(mode, capabilities)
141
160
  required = MODE_REQUIREMENTS.fetch(mode.to_sym) { raise ArgumentError, "unknown sweep mode #{mode.inspect}" }
142
161
  (required - capabilities).empty?
@@ -146,12 +165,26 @@ module Hecks
146
165
  # on, in the dial's own declaration order — that order is preserved
147
166
  # so the printed line reads the same way the dial does. Then the
148
167
  # single exclusion named on `MODE_REQUIREMENTS`.
168
+ #
169
+ # @param enabled [Array<Symbol, String>] modes turned on, in the dial's own
170
+ # declaration order
171
+ # @param capabilities [Array<String>] the target's capabilities, as `infer` returns
172
+ # @return [Array<Symbol>] `enabled`'s eligible modes, in `enabled`'s order, with
173
+ # `:ruby_only` dropped whenever `:differential` is also eligible
149
174
  def resolve(enabled, capabilities)
150
175
  resolved = enabled.map(&:to_sym).select { |mode| eligible?(mode, capabilities) }
151
176
  resolved.delete(:ruby_only) if resolved.include?(:differential)
152
177
  resolved
153
178
  end
154
179
 
180
+ # Answers whether `rust_dir`'s Cargo.toml declares a feature named after
181
+ # `domain_path`'s own directory.
182
+ #
183
+ # @param domain_path [String] filesystem path to the target domain's directory;
184
+ # its basename, lowercased, is the feature name looked up
185
+ # @param rust_dir [String] path to the Rust project root (holds `Cargo.toml`)
186
+ # @return [Boolean] whether a matching Cargo feature is declared; false if
187
+ # `rust_dir` has no `Cargo.toml`
155
188
  def rust_feature?(domain_path, rust_dir)
156
189
  cargo_toml = File.join(rust_dir, "Cargo.toml")
157
190
  return false unless File.file?(cargo_toml)
@@ -161,10 +194,17 @@ module Hecks
161
194
  features.match?(/^#{Regexp.escape(feature)}\s*=\s*\[\]/)
162
195
  end
163
196
 
197
+ # Answers whether any `.hecksagon` under `domain_path` attaches a member
198
+ # that provides authorization.
199
+ #
164
200
  # The capability label stays "governance" — it is the value
165
201
  # `bin/qa_sweep` writes into the QualityControl ledger's
166
202
  # `Target.capabilities`, and renaming it is a ledger change, not
167
203
  # part of dropping the name check.
204
+ #
205
+ # @param domain_path [String] filesystem path to the target domain's directory
206
+ # @return [Boolean] whether the domain attaches a member that provides the
207
+ # `Bluebook::Capabilities::AUTHORIZATION` capability
168
208
  def authorization_attached?(domain_path)
169
209
  attached = Dir.glob(File.join(domain_path, "**", "*.hecksagon"))
170
210
  .flat_map { |path| File.read(path).scan(FRAMEWORK_ATTACHED).flatten }.uniq
@@ -172,6 +212,13 @@ module Hecks
172
212
  attached.intersect?(providers)
173
213
  end
174
214
 
215
+ # Answers whether any file under `domain_path` matching `glob` contains `pattern`.
216
+ #
217
+ # @param domain_path [String] filesystem path to the target domain's directory
218
+ # @param glob [String] a `Dir.glob` pattern relative to `domain_path`, such as
219
+ # `"*.bluebook"`
220
+ # @param pattern [Regexp] the pattern each matching file's contents is tested against
221
+ # @return [Boolean] whether any matching file's contents match `pattern`
175
222
  def any_file?(domain_path, glob, pattern)
176
223
  Dir.glob(File.join(domain_path, "**", glob)).any? { |path| File.read(path).match?(pattern) }
177
224
  end
@@ -100,11 +100,11 @@ module Hecks
100
100
  CLOCK_OR_COUNT_NAME_PATTERN = /clock|instant|expir|ttl|\bnow\b|timestamp|epoch|count/i
101
101
  # NaN and +/-Infinity — the real find (see `spec/runtime/
102
102
  # numeric_boundary_spec.rb`): `Value::Coercion#check_numeric_fields`
103
- # used to let all three sail through untyped-checked (each really
104
- # is a Float), reaching either `CommandRules::Arithmetic#clamp`
105
- # (raw `ArgumentError`, not a domain refusal) or `JSON.generate`
103
+ # type-checks all three (each really is a Float) before they can
104
+ # reach either `CommandRules::Arithmetic#clamp` (raw
105
+ # `ArgumentError`, not a domain refusal) or `JSON.generate`
106
106
  # (`JSON::GeneratorError`, also not a domain refusal) — both fixed
107
- # at the source now, so these are safe to generate. -0.0 is
107
+ # at the source, so these are safe to generate. -0.0 is
108
108
  # deliberately included too even though it was already safe
109
109
  # (finite, round-trips through JSON as `-0.0` cleanly) — a signed
110
110
  # zero is exactly the kind of boundary a hand-written corpus never
@@ -125,6 +125,18 @@ module Hecks
125
125
  # just named "address"; the VO's own name is where "email" lives. A
126
126
  # combined hint catches both spellings without needing to guess which
127
127
  # level a domain happened to name the thing on.
128
+ #
129
+ # @param attribute [Bluebook::Attribute] the attribute to generate a value for
130
+ # @param aggregate [Bluebook::Aggregate] the aggregate `attribute` belongs to;
131
+ # resolves a same-chapter identity value object
132
+ # @param random [Random] the RNG driving every draw this call makes
133
+ # @param known_ids [Hash{String => Array<String>}] known real ids, keyed by
134
+ # aggregate or entity name; a reference draws from this pool
135
+ # @param context [String, nil] the enclosing value object's own declared name,
136
+ # for name-aware primitive generation; nil at the top level
137
+ # @return [String, Integer, Float, Boolean, Hash] a JSON-safe value in the corpus's
138
+ # own shape: a nested Hash for a value-object-typed attribute, a bare id String
139
+ # for a reference, or a plain primitive otherwise
128
140
  def value_for(attribute, aggregate, random:, known_ids: {}, context: nil)
129
141
  return reference_value(attribute, random: random, known_ids: known_ids) if attribute.reference?
130
142
 
@@ -143,6 +155,20 @@ module Hecks
143
155
  primitive(attribute.type.to_s, random: random, name: "#{context} #{attribute.name}")
144
156
  end
145
157
 
158
+ # Generates a nested value for a whole value object: a random admitted
159
+ # row for a closed set, or a value per declared attribute otherwise —
160
+ # occasionally unwrapped to a bare scalar for a genuinely single-field
161
+ # value object.
162
+ #
163
+ # @param value_object [Bluebook::ValueObject] the value object to generate a value for
164
+ # @param aggregate [Bluebook::Aggregate] the aggregate `value_object` is reached
165
+ # from, forwarded to nested `value_for` calls
166
+ # @param random [Random] the RNG driving every draw this call makes
167
+ # @param known_ids [Hash{String => Array<String>}] known real ids, keyed by
168
+ # aggregate or entity name, forwarded to nested `value_for` calls
169
+ # @return [Hash, Object] `{field name => value, ...}` for each declared attribute
170
+ # or closed-set member; unwrapped to that lone value directly when the value
171
+ # object has exactly one field and the bare-scalar draw hits
146
172
  def object_for(value_object, aggregate, random:, known_ids:)
147
173
  fields =
148
174
  if value_object.closed_set? && !value_object.members.empty?
@@ -172,17 +198,31 @@ module Hecks
172
198
  # A combination that (almost certainly) isn't one of the closed set's
173
199
  # admitted rows — deliberately, to exercise the refusal a `one_of`
174
200
  # exists to enforce, not just its happy path.
201
+ #
202
+ # @param value_object [Bluebook::ValueObject] the closed-set value object to
203
+ # generate a non-admitted combination for
204
+ # @param random [Random] the RNG driving every draw this call makes
205
+ # @return [Hash] `{field name => value, ...}` for every declared attribute, each
206
+ # drawn independently rather than sampled from an admitted row
175
207
  def invalid_member(value_object, random:)
176
208
  value_object.attributes.to_h do |field|
177
209
  [field.name.to_s, primitive(field.type.to_s, random: random, name: field.name.to_s)]
178
210
  end
179
211
  end
180
212
 
181
- # The ID itself. This minted `{"value" => id}` back when a reference was
182
- # stored wrapped ; the payload gate refuses that shape now, so a fuzzer
183
- # still emitting it would have every generated reference refused and
184
- # the silent guard would report the fuzzer broken rather than
185
- # the runtime.
213
+ # The ID itself, unwrapped: the payload gate refuses a reference stored
214
+ # wrapped as `{"value" => id}`, so a fuzzer emitting that shape would
215
+ # have every generated reference refused and the silent guard would
216
+ # report the fuzzer broken rather than the runtime.
217
+ #
218
+ # @param attribute [Bluebook::Attribute] the reference-typed attribute to
219
+ # generate a value for
220
+ # @param random [Random] the RNG driving every draw this call makes
221
+ # @param known_ids [Hash{String => Array<String>}] known real ids, keyed by
222
+ # aggregate or entity name; looked up under `attribute.type.target_name`
223
+ # @return [String] a real id drawn from the matching pool most of the time; a
224
+ # fabricated `"missing-..."` id when the pool is empty or the invalid-reference
225
+ # draw hits
186
226
  def reference_value(attribute, random:, known_ids:)
187
227
  pool = known_ids[attribute.type.target_name.to_s] || []
188
228
  return "missing-#{random.bytes(4).unpack1('H*')}" if pool.empty? || random.rand < INVALID_REFERENCE_PROBABILITY
@@ -190,6 +230,16 @@ module Hecks
190
230
  pool.sample(random: random)
191
231
  end
192
232
 
233
+ # Generates a value for one Ruby-primitive-typed attribute.
234
+ #
235
+ # @param type_name [String] the primitive type name: `"String"`, `"Integer"`,
236
+ # `"Float"`, `"TrueClass"`, or `"FalseClass"`
237
+ # @param random [Random] the RNG driving every draw this call makes
238
+ # @param name [String, nil] a name hint (attribute name, optionally
239
+ # context-prefixed) that draws an email-/currency-shaped string or a
240
+ # clock/count-shaped integer
241
+ # @return [String, Integer, Float, Boolean] a value of the type `type_name` names
242
+ # @raise [ArgumentError] if `type_name` names anything else
193
243
  def primitive(type_name, random:, name: nil)
194
244
  case type_name
195
245
  when "String" then string_value(random, name: name)
@@ -200,6 +250,14 @@ module Hecks
200
250
  end
201
251
  end
202
252
 
253
+ # Generates a String value, name-aware for the couple of shapes that
254
+ # need to look a specific way to get past their own invariant.
255
+ #
256
+ # @param random [Random] the RNG driving every draw this call makes
257
+ # @param name [String, nil] a name hint; an `"email"` or `"currency"` match
258
+ # draws a shaped value instead of random words
259
+ # @return [String] an edge-case string, an email address, a currency code, or
260
+ # 1-3 random words joined by spaces
203
261
  def string_value(random, name: nil)
204
262
  return STRING_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
205
263
  return email_value(random) if name&.match?(/email/i)
@@ -208,15 +266,26 @@ module Hecks
208
266
  Array.new(random.rand(1..3)) { WORDS.sample(random: random) }.join(" ")
209
267
  end
210
268
 
269
+ # Generates a fabricated, syntactically valid email address.
270
+ #
271
+ # @param random [Random] the RNG driving every draw this call makes
272
+ # @return [String] a `"word@word.example"` address
211
273
  def email_value(random)
212
274
  "#{WORDS.sample(random: random)}@#{WORDS.sample(random: random)}.example"
213
275
  end
214
276
 
277
+ # Generates an Integer value, skewed positive.
278
+ #
215
279
  # Skewed positive : `cents.positive?`/`!cents.negative?`-style
216
280
  # invariants are common across this codebase's example domains, and a
217
281
  # sequence that can never get past one never reaches the state a deeper
218
282
  # bug would need. Zero and negative are still real, reachable outcomes —
219
283
  # via the edge-case pool, deliberately, not by starving them entirely.
284
+ #
285
+ # @param random [Random] the RNG driving every draw this call makes
286
+ # @param name [String, nil] a name hint; a clock/count-shaped match narrows
287
+ # the edge-case pool to `SAFE_INTEGER_EDGE_CASES`
288
+ # @return [Integer] an edge-case integer sometimes, else a random count in 1..1000
220
289
  def integer_value(random, name: nil)
221
290
  if random.rand < EDGE_CASE_PROBABILITY
222
291
  pool = clock_or_count_shaped?(name) ? SAFE_INTEGER_EDGE_CASES : INTEGER_EDGE_CASES
@@ -226,10 +295,20 @@ module Hecks
226
295
  random.rand(1..1000)
227
296
  end
228
297
 
298
+ # Reports whether a field name reads as a clock reading or a
299
+ # policy-capped count, per `CLOCK_OR_COUNT_NAME_PATTERN`'s own comment.
300
+ #
301
+ # @param name [String, nil] the name to test
302
+ # @return [Boolean] true if `name` matches the clock/count name pattern
229
303
  def clock_or_count_shaped?(name)
230
304
  name.to_s.match?(CLOCK_OR_COUNT_NAME_PATTERN)
231
305
  end
232
306
 
307
+ # Generates a Float value.
308
+ #
309
+ # @param random [Random] the RNG driving every draw this call makes
310
+ # @return [Float] an edge-case float sometimes, else a random value in
311
+ # 0.01..1000.0, rounded to 2 decimal places
233
312
  def float_value(random)
234
313
  return FLOAT_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
235
314
 
@@ -239,11 +318,20 @@ module Hecks
239
318
  # The bare scalar a generated identity value stands for, for recording into
240
319
  # `known_ids`. An identity is declared as a value object, so this opens one ;
241
320
  # a reference pointing at this record is already that scalar and needs no
242
- # opening at all. The two used to be the same reading and are not any more.
321
+ # opening at all — the two are different readings, not interchangeable.
322
+ #
323
+ # @param identity_value [Hash, Object] a generated identity value, as `value_for`
324
+ # produced it — a Hash for a multi-field value object, or the bare scalar already
325
+ # for a single-field one
326
+ # @return [String] the identity's bare scalar, stringified
243
327
  def scalar_of(identity_value)
244
328
  identity_value.is_a?(Hash) ? identity_value.values.first.to_s : identity_value.to_s
245
329
  end
246
330
 
331
+ # Generates a fabricated id unrelated to anything `known_ids` tracks.
332
+ #
333
+ # @param random [Random] the RNG driving every draw this call makes
334
+ # @return [String] a `"gen-"`-prefixed id with 8 random hex characters
247
335
  def random_id(random)
248
336
  "gen-#{random.bytes(4).unpack1('H*')}"
249
337
  end