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.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +29 -5
- data/lib/hecks/projector.rb +74 -6
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +11 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- 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
|
-
#
|
|
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
|
-
#
|
|
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`)
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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
|
-
#
|
|
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`
|
|
7
|
-
# there a Cargo feature named after this directory?
|
|
8
|
-
# `:differential`, else `:ruby_only`",
|
|
9
|
-
# for `--persistence-parity` ("does any .hecksagon bind PostgresEra?")
|
|
10
|
-
#
|
|
11
|
-
# WASM front)
|
|
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
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
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
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
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
|
|
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
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
#
|
|
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
|
|
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
|