hecks 2.7.0 → 2.8.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/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +12 -13
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +12 -6
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +3 -3
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +1 -1
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/verification.rb +11 -11
- data/lib/hecks/runtime/registry.rb +8 -4
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +6 -1
- metadata +28 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
module Hecks
|
|
2
|
-
module Fuzzing
|
|
3
|
-
class SequenceGenerator
|
|
4
|
-
# Everything the booted domain offers, indexed for the picker:
|
|
5
|
-
# creating vs instance commands, entity commands, queries, and the
|
|
6
|
-
# populators that predict where an appended element lands.
|
|
7
|
-
module Catalog
|
|
8
|
-
private
|
|
9
|
-
|
|
10
|
-
def build_catalog(runtime)
|
|
11
|
-
creating = []
|
|
12
|
-
instance = []
|
|
13
|
-
entity_commands = []
|
|
14
|
-
queries = []
|
|
15
|
-
entity_queries = []
|
|
16
|
-
read_models = []
|
|
17
|
-
|
|
18
|
-
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
19
|
-
bluebook.aggregates.each do |aggregate|
|
|
20
|
-
aggregate.commands.each do |command|
|
|
21
|
-
entry = { verb: "#{domain_name}::#{aggregate.hecks_name}.#{command.hecks_name}",
|
|
22
|
-
command: command, aggregate: aggregate }
|
|
23
|
-
(command.creates? ? creating : instance) << entry
|
|
24
|
-
end
|
|
25
|
-
aggregate.queries.each do |query|
|
|
26
|
-
queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{query.name}",
|
|
27
|
-
query: query, aggregate: aggregate }
|
|
28
|
-
end
|
|
29
|
-
catalog_entities(domain_name, aggregate, entity_commands, entity_queries)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# **The bare domain form** — "Domain.report_name", no "::" — the
|
|
33
|
-
# same shape `Dispatcher#query` itself branches on to route a
|
|
34
|
-
# read-model ask apart from an aggregate query. A report was
|
|
35
|
-
# never in this catalog at all before: `aggregation_matches_
|
|
36
|
-
# recompute` (count/median) has only ever been exercised by
|
|
37
|
-
# hand-built specs, never a single real generated sequence,
|
|
38
|
-
# because nothing here ever asked one. `model:`, not
|
|
39
|
-
# `aggregate:` — a rootless report has no aggregate of its
|
|
40
|
-
# own to be eligible against (see picker.rb's own use of
|
|
41
|
-
# `model.reference_target`).
|
|
42
|
-
bluebook.read_models.each do |model|
|
|
43
|
-
read_models << { verb: "#{domain_name}.#{model.query_name}", model: model }
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
{ creating: creating, instance: instance, entity_commands: entity_commands,
|
|
48
|
-
queries: queries, entity_queries: entity_queries, read_models: read_models,
|
|
49
|
-
populators: populators(runtime),
|
|
50
|
-
# The aggregates a query filters on, and the fields it compares
|
|
51
|
-
# (query_binding.rb).
|
|
52
|
-
query_bindings: build_query_bindings(runtime),
|
|
53
|
-
# Every `role "..."` any command in the boot declares — the
|
|
54
|
-
# "wrong hat" pool the caller draw's `mismatched` shape picks
|
|
55
|
-
# from (adversary.rb `other_role`).
|
|
56
|
-
roles: (creating + instance + entity_commands).filter_map { |e| e[:command].role }
|
|
57
|
-
.map(&:to_s).reject(&:empty?).uniq.sort,
|
|
58
|
-
# The grant verb every loaded authorization provider declares
|
|
59
|
-
# (`provides "authorization", grant: ...`) — what the caller
|
|
60
|
-
# draw steers at a declared role and records as a real grant.
|
|
61
|
-
# Read off the declaration, never the literal Governance name.
|
|
62
|
-
grant_verbs: runtime.registry.authorization_providers
|
|
63
|
-
.filter_map { |chapter| chapter.provided_verb(Bluebook::Capabilities::AUTHORIZATION, :grant) }
|
|
64
|
-
.sort,
|
|
65
|
-
# Which aggregates this corpus can actually make one of — the ones
|
|
66
|
-
# `satisfiable?` is entitled to wait for.
|
|
67
|
-
creatable: creating.to_set { |entry| entry[:aggregate].hecks_name } }
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Every entity, at every depth — `Card` nested inside `Board`
|
|
71
|
-
# inside `Workspace` (qa/stress_domains/nested_pieces) walks in
|
|
72
|
-
# as `chain: [Board, Card]`, the exact hop list
|
|
73
|
-
# `EntityInterpreter::Resolution.of` resolves the dotted verb
|
|
74
|
-
# back into. Before this walk existed the catalog only ever read
|
|
75
|
-
# `aggregate.entities` one level down, so a two-hop entity
|
|
76
|
-
# command (BUG#11's whole class) could never be generated at all
|
|
77
|
-
# — the one shape the differential harness most needed to reach
|
|
78
|
-
# was structurally absent from every sequence it ever produced.
|
|
79
|
-
# `entity:` stays the last hop (what every existing reader means
|
|
80
|
-
# by "the entity"); `chain:` is the whole path.
|
|
81
|
-
#
|
|
82
|
-
# Entity queries stay one hop deep, exactly as before — a nested
|
|
83
|
-
# entity's query has no established wire spelling this generator
|
|
84
|
-
# can vouch for, and nothing in the corpus declares one.
|
|
85
|
-
def catalog_entities(domain_name, aggregate, entity_commands, entity_queries)
|
|
86
|
-
each_entity_chain(aggregate) do |chain|
|
|
87
|
-
entity = chain.last
|
|
88
|
-
path = chain.map(&:hecks_name).join(".")
|
|
89
|
-
entity.commands.each do |command|
|
|
90
|
-
entity_commands << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{path}.#{command.hecks_name}",
|
|
91
|
-
command: command, aggregate: aggregate, entity: entity, chain: chain }
|
|
92
|
-
end
|
|
93
|
-
next unless chain.size == 1
|
|
94
|
-
|
|
95
|
-
entity.queries.each do |query|
|
|
96
|
-
entity_queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{path}.#{query.name}",
|
|
97
|
-
query: query, aggregate: aggregate, entity: entity }
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Depth-first, parents before children, in declaration order — so
|
|
103
|
-
# the depth-1 entries land in `entity_commands` in exactly the
|
|
104
|
-
# order they always did (a pinned seed's picker pool is the same
|
|
105
|
-
# pool it was), and a nested entity's own entries follow its
|
|
106
|
-
# parent's.
|
|
107
|
-
def each_entity_chain(owner, chain = [], &block)
|
|
108
|
-
owner.entities.each do |entity|
|
|
109
|
-
path = chain + [entity]
|
|
110
|
-
yield path
|
|
111
|
-
each_entity_chain(entity, path, &block)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Which command, on which owner, appends to which entity list — so a
|
|
116
|
-
# successful dispatch can predict the identity the element it just
|
|
117
|
-
# added landed on. Entity#identified_by is filled by `Array(current).size + 1`
|
|
118
|
-
# (CommandInterpreter#entity_element) when the append's own field
|
|
119
|
-
# mapping doesn't already assign it — the common case, predicted here.
|
|
120
|
-
# A domain whose append explicitly assigns identity through a mapped
|
|
121
|
-
# argument is covered too, without guessing: whatever value this
|
|
122
|
-
# generator supplied for that argument at dispatch time is the
|
|
123
|
-
# identity, and gets recorded directly (see `record_outcome`).
|
|
124
|
-
#
|
|
125
|
-
# `owner_chain:` — `[]` for an aggregate-level append (`Board.AddList`,
|
|
126
|
-
# `Folder.AddSlip`), the entity path for an entity-level one
|
|
127
|
-
# (`Board.AddCard` appending into `Board.cards`, owner_chain
|
|
128
|
-
# `[Board]`) — so `record_outcome` can key the appended element
|
|
129
|
-
# under the exact parent-plus-hops it landed beneath.
|
|
130
|
-
# `identity_arguments:` — every identity head the mapping sources
|
|
131
|
-
# from a command argument (a composite entity identity has several),
|
|
132
|
-
# what the adversarial duplicate-identity mutation replays;
|
|
133
|
-
# `identity_argument:` stays the single-head reading the existing
|
|
134
|
-
# auto-mint prediction already keys on.
|
|
135
|
-
def populators(runtime)
|
|
136
|
-
runtime.registry.bluebooks.each_value.flat_map do |bluebook|
|
|
137
|
-
bluebook.aggregates.flat_map do |aggregate|
|
|
138
|
-
owners = [[aggregate, []]]
|
|
139
|
-
each_entity_chain(aggregate) { |chain| owners << [chain.last, chain] }
|
|
140
|
-
|
|
141
|
-
owners.flat_map do |owner, chain|
|
|
142
|
-
owner.commands.filter_map { |command| populator_for(aggregate, owner, chain, command) }
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def populator_for(aggregate, owner, chain, command)
|
|
149
|
-
append = command.mutations.find { |mutation| mutation.op == :append }
|
|
150
|
-
return unless append
|
|
151
|
-
|
|
152
|
-
list_attribute = owner.attribute(append.target)
|
|
153
|
-
return unless list_attribute&.list?
|
|
154
|
-
|
|
155
|
-
entity = owner.entities.find { |candidate| candidate.hecks_name == list_attribute.type.to_s }
|
|
156
|
-
return unless entity
|
|
157
|
-
|
|
158
|
-
identity_field = entity.identified_by
|
|
159
|
-
mapped = identity_field && append.source[identity_field]
|
|
160
|
-
identity_arguments = entity.identity_heads.filter_map do |head|
|
|
161
|
-
source = append.source[head]
|
|
162
|
-
source if source.is_a?(Symbol)
|
|
163
|
-
end
|
|
164
|
-
{ command: command, aggregate: aggregate, owner: owner, owner_chain: chain, entity: entity,
|
|
165
|
-
identity_field: identity_field, identity_argument: mapped.is_a?(Symbol) ? mapped : nil,
|
|
166
|
-
identity_arguments: identity_arguments }
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
require_relative "../value_generator"
|
|
2
|
-
require_relative "../../naming"
|
|
3
|
-
|
|
4
|
-
module Hecks
|
|
5
|
-
module Fuzzing
|
|
6
|
-
class SequenceGenerator
|
|
7
|
-
# What a successful step created, and how a later step draws on it —
|
|
8
|
-
# the state tracking that makes multi-step sequences reach further
|
|
9
|
-
# than single calls ever did.
|
|
10
|
-
module OutcomeTracker
|
|
11
|
-
private
|
|
12
|
-
|
|
13
|
-
def record_outcome(catalog, entry, args)
|
|
14
|
-
aggregate = entry[:aggregate]
|
|
15
|
-
parent_scalar = identity_scalar_of(aggregate, args)
|
|
16
|
-
|
|
17
|
-
@known_ids[aggregate.hecks_name] << parent_scalar if entry[:entity].nil? && entry[:command].creates?
|
|
18
|
-
record_grant(args) if catalog[:grant_verbs].include?(entry[:verb])
|
|
19
|
-
|
|
20
|
-
populator = populator_for_entry(catalog, entry)
|
|
21
|
-
return unless populator
|
|
22
|
-
|
|
23
|
-
key = append_pool_key(populator, args)
|
|
24
|
-
|
|
25
|
-
# The auto-minted case (CommandInterpreter#entity_element): the
|
|
26
|
-
# element just landed at count-so-far + 1. The explicit case: this
|
|
27
|
-
# generator itself supplied whatever value sits at
|
|
28
|
-
# args[identity_argument] — no guessing needed, it's read straight
|
|
29
|
-
# back.
|
|
30
|
-
new_id =
|
|
31
|
-
if populator[:identity_argument]
|
|
32
|
-
ValueGenerator.scalar_of(args[populator[:identity_argument].to_s])
|
|
33
|
-
else
|
|
34
|
-
(@entity_known_ids[key].size + 1).to_s
|
|
35
|
-
end
|
|
36
|
-
@entity_known_ids[key] << new_id
|
|
37
|
-
|
|
38
|
-
# The caller-supplied identity tuple, kept whole — every mapped
|
|
39
|
-
# identity argument and the exact value this step offered for it
|
|
40
|
-
# — so the adversarial duplicate-identity mutation (BUG#13's own
|
|
41
|
-
# shape, `Folder.AddSlip` twice under the same `reference`) can
|
|
42
|
-
# offer it again later against the same parent, composite
|
|
43
|
-
# identities included (BUG#13's fix explicitly did not cover
|
|
44
|
-
# those; this is how a sequence gets to ask).
|
|
45
|
-
return if populator[:identity_arguments].empty?
|
|
46
|
-
|
|
47
|
-
@appended_identities[key] << populator[:identity_arguments].to_h { |name| [name.to_s, args[name.to_s]] }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# A grant this sequence made for real — the authorization provider's
|
|
51
|
-
# declared `grant:` verb (`Governance::RoleAssignment.Assign` in
|
|
52
|
-
# every boot today) succeeded with these exact args, so `actor_id` now holds
|
|
53
|
-
# `role_name` in this boot's own store, on both replay sides. The
|
|
54
|
-
# `actor_known` caller shape (adversary.rb) replays that identity
|
|
55
|
-
# against a command gated on the same role: the one way a
|
|
56
|
-
# generated step reaches `holds_role?`'s authorized branch rather
|
|
57
|
-
# than only its refusing one.
|
|
58
|
-
def record_grant(args)
|
|
59
|
-
role = ValueGenerator.scalar_of(args["role_name"]).to_s
|
|
60
|
-
actor = ValueGenerator.scalar_of(args["actor_id"]).to_s
|
|
61
|
-
@granted[role] << actor unless role.empty? || actor.empty?
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# The pool an appended element lands in — the aggregate's own
|
|
65
|
-
# identity, then one scalar per owning hop (an aggregate-level
|
|
66
|
-
# append has none; `Board.AddCard` has `Board`'s own `number`,
|
|
67
|
-
# read straight back off the args this step addressed it by).
|
|
68
|
-
def append_pool_key(populator, args)
|
|
69
|
-
parent_scalar = identity_scalar_of(populator[:aggregate], args)
|
|
70
|
-
owner_scalars = populator[:owner_chain].map do |piece|
|
|
71
|
-
ValueGenerator.scalar_of(args[(piece.identified_by || :id).to_s])
|
|
72
|
-
end
|
|
73
|
-
entity_pool_key(populator[:aggregate].hecks_name,
|
|
74
|
-
populator[:owner_chain].map(&:hecks_name) + [populator[:entity].hecks_name],
|
|
75
|
-
[parent_scalar] + owner_scalars)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# `"Agg.Board#w1"` for a depth-1 pool — byte-identical to the key
|
|
79
|
-
# this always used, so nothing a pinned seed draws from moves —
|
|
80
|
-
# and `"Agg.Board.Card#w1/1"` one hop deeper.
|
|
81
|
-
def entity_pool_key(aggregate_name, chain_names, scalars)
|
|
82
|
-
"#{aggregate_name}.#{chain_names.join('.')}##{scalars.join('/')}"
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# The scalar this step's own aggregate identity resolves to, from the
|
|
86
|
-
# args a creating (or entity-populating) command actually
|
|
87
|
-
# dispatched — a single declared head (or the untyped default
|
|
88
|
-
# `:id`) reads straight off its own top-level arg the way this
|
|
89
|
-
# always has (`ValueGenerator.scalar_of`, opening the identity value
|
|
90
|
-
# object). A genuinely composite identity (`composite_identity?`,
|
|
91
|
-
# shared with StepBuilder#add_identity! — see its own comment) has
|
|
92
|
-
# no such top-level arg at all: `add_identity!` deliberately leaves
|
|
93
|
-
# a composite creating command's own individually-declared fields
|
|
94
|
-
# alone rather than forcing a synthetic `id` neither the bluebook
|
|
95
|
-
# nor the command ever declared, so this joins those fields itself,
|
|
96
|
-
# in the same declaration order `Runtime::Identity.of` joins them
|
|
97
|
-
# at dispatch (`Naming.identity`, `Naming::IDENTITY_JOIN`) — which
|
|
98
|
-
# is exactly what makes the result usable later as the bare `id:`
|
|
99
|
-
# a composite Revoke/act-again command expects (`Identity.from`'s
|
|
100
|
-
# own untyped fallback, reached when `identity_of` finds no
|
|
101
|
-
# top-level field args to join, reads a bare `id:` straight
|
|
102
|
-
# through unattributed).
|
|
103
|
-
def identity_scalar_of(aggregate, args)
|
|
104
|
-
parent_key = (aggregate.identified_by || :id).to_s
|
|
105
|
-
return ValueGenerator.scalar_of(args[parent_key]) unless composite_identity?(aggregate)
|
|
106
|
-
|
|
107
|
-
parts = aggregate.identity_paths.map { |path| ValueGenerator.scalar_of(args[path.to_s.split(".").first]) }
|
|
108
|
-
Naming.identity(parts)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def pick_known(name)
|
|
112
|
-
pool = @known_ids[name]
|
|
113
|
-
return ValueGenerator.random_id(@random) if pool.empty? || @random.rand < ValueGenerator::INVALID_REFERENCE_PROBABILITY
|
|
114
|
-
|
|
115
|
-
pool.sample(random: @random)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def pick_entity_known(key)
|
|
119
|
-
pool = @entity_known_ids[key]
|
|
120
|
-
return ValueGenerator.random_id(@random) if pool.empty? || @random.rand < ValueGenerator::INVALID_REFERENCE_PROBABILITY
|
|
121
|
-
|
|
122
|
-
pool.sample(random: @random)
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
module Hecks
|
|
2
|
-
module Fuzzing
|
|
3
|
-
class SequenceGenerator
|
|
4
|
-
# Which step to try next: eligibility (what is possible from the
|
|
5
|
-
# state so far) and weighting (what is likely to reach somewhere
|
|
6
|
-
# new).
|
|
7
|
-
module Picker
|
|
8
|
-
private
|
|
9
|
-
|
|
10
|
-
def pick(catalog)
|
|
11
|
-
rest = catalog[:queries].dup
|
|
12
|
-
catalog[:instance].each { |entry| rest << entry if actionable?(catalog, entry) }
|
|
13
|
-
catalog[:entity_commands].each { |entry| rest << entry if actionable?(catalog, entry) }
|
|
14
|
-
catalog[:entity_queries].each { |entry| rest << entry if @known_ids[entry[:aggregate].hecks_name].any? }
|
|
15
|
-
catalog[:read_models].each { |entry| rest << entry if read_model_actionable?(entry) }
|
|
16
|
-
|
|
17
|
-
makers = catalog[:creating].select { |entry| satisfiable?(catalog, entry) }
|
|
18
|
-
pool = rest + makers.flat_map { |entry| [entry] * creating_weight(rest.size) }
|
|
19
|
-
pool += deep_entity_bias(rest) if adversarial?
|
|
20
|
-
|
|
21
|
-
steer(pool).sample(random: @random)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# BUG#11's own preference (adversary.rb) — an entity command two
|
|
25
|
-
# or more hops deep is weighted up the same way an unexercised
|
|
26
|
-
# verb is, only in adversarial mode: the eligibility rules above
|
|
27
|
-
# still decide what is possible, and a default-mode pool is
|
|
28
|
-
# exactly the pool it always was.
|
|
29
|
-
def deep_entity_bias(rest)
|
|
30
|
-
deep = rest.select { |entry| (entry[:chain] || []).size >= Adversary::DEEP_ENTITY_DEPTH }
|
|
31
|
-
deep * Adversary::DEEP_ENTITY_WEIGHT
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# While there is nothing to find, making something is the only useful move.
|
|
35
|
-
#
|
|
36
|
-
# A flat weight is right once the domain has records in it, and badly
|
|
37
|
-
# wrong before: banking declares ten queries, and from an empty store
|
|
38
|
-
# exactly one creating command is satisfiable — Customer.Register, the
|
|
39
|
-
# verb the whole cascade waits on. Two entries against ten left runs
|
|
40
|
-
# spending their entire budget querying a store nothing had been written
|
|
41
|
-
# to ; one seed answered ten empty queries in a row and emitted nothing
|
|
42
|
-
# at all.
|
|
43
|
-
#
|
|
44
|
-
# So while nothing exists, creation matches everything else put together.
|
|
45
|
-
# After that the ordinary weight applies and the run gets on with
|
|
46
|
-
# exercising what it made.
|
|
47
|
-
def creating_weight(rest_size)
|
|
48
|
-
return CREATING_WEIGHT if @known_ids.each_value.any?(&:any?)
|
|
49
|
-
|
|
50
|
-
[rest_size, CREATING_WEIGHT].max
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def actionable?(catalog, entry)
|
|
54
|
-
@known_ids[entry[:aggregate].hecks_name].any? && satisfiable?(catalog, entry)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# A rootless report (no `reference_to` at all) has nothing to wait
|
|
58
|
-
# for — it reads whole tables, the same "always eligible" position
|
|
59
|
-
# `catalog[:queries]` itself takes. A rooted one needs a real
|
|
60
|
-
# instance of its own `reference_target` to ask about first, same
|
|
61
|
-
# rule `actionable?` already gives an instance command — asking
|
|
62
|
-
# `Banking.disputed_payment_count` before any Account exists would
|
|
63
|
-
# just be refused (`refuse_object_reference`/`NotFound`) the same
|
|
64
|
-
# way `Account.Credit` would be, for the identical reason.
|
|
65
|
-
def read_model_actionable?(entry)
|
|
66
|
-
entry[:model].reference_target.nil? || @known_ids[entry[:model].reference_target].any?
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# A command that references nothing that exists cannot succeed, so it is
|
|
70
|
-
# not offered until something does.
|
|
71
|
-
#
|
|
72
|
-
# `ValueGenerator.reference_value` has no real id to hand over when its
|
|
73
|
-
# pool is empty, so it mints `missing-…` and the dispatch is refused
|
|
74
|
-
# before it starts — a guaranteed waste of a step, and worse, one that
|
|
75
|
-
# never fills the pool it was waiting on. Banking cascades from it:
|
|
76
|
-
# Account.Open needs a Customer, ATMCard.Issue needs an Account,
|
|
77
|
-
# Transfer.Request needs two, so a run that opened with the wrong verb
|
|
78
|
-
# spent its whole budget being refused.
|
|
79
|
-
#
|
|
80
|
-
# This is the rule instance commands already follow — `known_ids.any?` —
|
|
81
|
-
# applied to what a command references rather than to what it acts on.
|
|
82
|
-
# A target no creating command in this corpus can make (a cross-domain
|
|
83
|
-
# reference, which `CommandRules#resolve_references` skips anyway) is
|
|
84
|
-
# exempt, or the whole domain would starve waiting for it.
|
|
85
|
-
def satisfiable?(catalog, entry)
|
|
86
|
-
entry[:command].attributes.select(&:reference?).all? do |attribute|
|
|
87
|
-
target = attribute.type.target_name.to_s
|
|
88
|
-
!catalog[:creatable].include?(target) || @known_ids[target].any?
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Coverage-guided rather than uniformly random : a verb this sequence has
|
|
93
|
-
# not dispatched yet is weighted up, so a run spends its budget on the
|
|
94
|
-
# commands it has not reached instead of re-rolling the ones it has. The
|
|
95
|
-
# eligibility rules above still decide what is possible — this only decides
|
|
96
|
-
# what is likely, so a verb gated behind state it does not have yet stays
|
|
97
|
-
# out of the pool entirely rather than being preferred forever.
|
|
98
|
-
def steer(pool)
|
|
99
|
-
fresh = pool.reject { |entry| @exercised.include?(entry[:verb]) }
|
|
100
|
-
steered = fresh.empty? ? pool : pool + (fresh * UNEXERCISED_WEIGHT)
|
|
101
|
-
favor(steered)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# `favor:` (CoverageCampaign's rare verbs) — weighted up when
|
|
105
|
-
# eligible, never made eligible. An empty favor list returns the
|
|
106
|
-
# pool untouched, so an unguided seed draws exactly what it did.
|
|
107
|
-
def favor(pool)
|
|
108
|
-
return pool if @favor.empty?
|
|
109
|
-
|
|
110
|
-
favored = pool.uniq.select { |entry| @favor.include?(entry[:verb]) }
|
|
111
|
-
pool + (favored * SequenceGenerator::FAVOR_WEIGHT)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
require_relative "../../query_specification/field_path"
|
|
2
|
-
require_relative "../../runtime/value"
|
|
3
|
-
|
|
4
|
-
module Hecks
|
|
5
|
-
module Fuzzing
|
|
6
|
-
class SequenceGenerator
|
|
7
|
-
# Query arguments drawn from what the sequence has already written.
|
|
8
|
-
#
|
|
9
|
-
# A query is only a real test of an adapter when its argument names a row
|
|
10
|
-
# that exists. A `where(site_reference: :site_reference)` asked with a
|
|
11
|
-
# freshly generated word matches nothing on every adapter, so an adapter
|
|
12
|
-
# that compiles the comparison wrongly (and so also matches nothing) is
|
|
13
|
-
# indistinguishable from a correct one. Every argument `args_for` draws
|
|
14
|
-
# is independent of the store, and the chance that one lands on a stored
|
|
15
|
-
# value is small enough that a whole persistence-parity sweep can pass
|
|
16
|
-
# without a single query returning a row.
|
|
17
|
-
#
|
|
18
|
-
# ## What is written, and what is bound
|
|
19
|
-
#
|
|
20
|
-
# After each command that took effect, every aggregate a query filters
|
|
21
|
-
# on is read back from the repository, and the values of exactly the
|
|
22
|
-
# fields those queries compare (`bound_fields`) are remembered as one
|
|
23
|
-
# row per record. A row is what the store holds, not what a command was
|
|
24
|
-
# given, so a value a `sets` mapping renamed, a policy wrote in another
|
|
25
|
-
# aggregate, or a lifecycle transition assigned is covered too. A later
|
|
26
|
-
# query step usually takes its arguments from one remembered row, so
|
|
27
|
-
# several parameters filter the same record. The rest of the time it
|
|
28
|
-
# keeps the generated argument, which is what reaches the empty answer.
|
|
29
|
-
#
|
|
30
|
-
# ## Why a separate random stream
|
|
31
|
-
#
|
|
32
|
-
# The choice draws from its own `Random`, seeded from the sequence's
|
|
33
|
-
# seed and restarted wherever `@random` is (`restart_random`). Drawing
|
|
34
|
-
# from `@random` would shift every later step of every seed. This way
|
|
35
|
-
# each seed generates the same commands, in the same order, with the
|
|
36
|
-
# same arguments as before, and only the arguments of a query step
|
|
37
|
-
# change. A query changes no state, so nothing downstream moves.
|
|
38
|
-
module QueryBinding
|
|
39
|
-
# How often a query step whose aggregate has written rows takes its
|
|
40
|
-
# arguments from one of them. High, because a query that misses a
|
|
41
|
-
# stored row tests nothing, and the miss is still drawn the other
|
|
42
|
-
# quarter of the time.
|
|
43
|
-
BOUND_QUERY_PROBABILITY = 0.75
|
|
44
|
-
|
|
45
|
-
# Comparators whose argument is one scalar or value, so a stored value
|
|
46
|
-
# is a legitimate operand. `in` takes a list and `none_in_state` an
|
|
47
|
-
# `"Aggregate:state"` string; a stored value is neither.
|
|
48
|
-
BINDABLE_OPERATORS = %i[eq ne lt lte gt gte].freeze
|
|
49
|
-
|
|
50
|
-
# Keeps the binding stream apart from `@random`'s, which is seeded
|
|
51
|
-
# with the bare seed.
|
|
52
|
-
BINDING_SEED_OFFSET = 0x9e3779b1
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
# `{"Domain::Aggregate" => {domain:, aggregate:, fields: [String]}}`
|
|
57
|
-
# for every aggregate some query filters on, where `fields` is the
|
|
58
|
-
# union of the stored fields those queries compare.
|
|
59
|
-
def build_query_bindings(runtime)
|
|
60
|
-
runtime.registry.bluebooks.each_with_object({}) do |(domain_name, bluebook), bindings|
|
|
61
|
-
bluebook.aggregates.each do |aggregate|
|
|
62
|
-
fields = aggregate.queries.flat_map { |query| bound_fields(query).values }.uniq
|
|
63
|
-
next if fields.empty?
|
|
64
|
-
|
|
65
|
-
bindings["#{domain_name}::#{aggregate.hecks_name}"] =
|
|
66
|
-
{ domain: domain_name, aggregate: aggregate, fields: fields }
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# `{parameter name => stored field}` for each `where` clause whose
|
|
72
|
-
# operand is one of the query's own parameters. A clause on a literal,
|
|
73
|
-
# or aimed at another aggregate's field (`target`), binds nothing.
|
|
74
|
-
def bound_fields(query)
|
|
75
|
-
query.wheres.each_with_object({}) do |clause, fields|
|
|
76
|
-
next unless clause.value.is_a?(Symbol) && clause.target.nil?
|
|
77
|
-
next unless BINDABLE_OPERATORS.include?(clause.op.to_sym)
|
|
78
|
-
|
|
79
|
-
fields[clause.value.to_s] ||= clause.field.to_s
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Remembers the compared fields of every stored record of every
|
|
84
|
-
# aggregate a query filters on, as one row per distinct record shape.
|
|
85
|
-
def harvest_written_rows(runtime, catalog)
|
|
86
|
-
catalog[:query_bindings].each do |key, binding|
|
|
87
|
-
repository = runtime.registry.repository(binding[:domain], binding[:aggregate])
|
|
88
|
-
repository.all.each do |record|
|
|
89
|
-
row = binding[:fields].to_h { |field| [field, written_value(field_of(record, field))] }.compact
|
|
90
|
-
@written_rows[key] << row unless row.empty? || @written_rows[key].include?(row)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def field_of(record, field) = QuerySpecification::FieldPath.dig(record.state, field)
|
|
96
|
-
|
|
97
|
-
# A stored value as the JSON-shaped argument a step carries: string
|
|
98
|
-
# keys, nested values opened. `nil` for anything a query argument
|
|
99
|
-
# cannot be (a missing value, a list, a timestamp object).
|
|
100
|
-
def written_value(value)
|
|
101
|
-
case value
|
|
102
|
-
when Runtime::Value then written_value(value.to_h)
|
|
103
|
-
when Hash then value.to_h { |key, inner| [key.to_s, written_value(inner)] }
|
|
104
|
-
when String, Integer, Float, true, false then value
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Replaces the generated arguments of a query step with a remembered
|
|
109
|
-
# row's, most of the time. Only a parameter the step already carries
|
|
110
|
-
# is replaced, so an optional argument left out, or a required one a
|
|
111
|
-
# malformation dropped, stays that way.
|
|
112
|
-
def bind_to_written_row!(args, entry)
|
|
113
|
-
return if entry[:entity]
|
|
114
|
-
|
|
115
|
-
rows = @written_rows[entry[:verb].split(".").first]
|
|
116
|
-
params = bound_fields(entry[:query]).select { |param, _| args.key?(param) }
|
|
117
|
-
return if rows.empty? || params.empty?
|
|
118
|
-
return unless @binding_random.rand < BOUND_QUERY_PROBABILITY
|
|
119
|
-
|
|
120
|
-
row = rows.select { |candidate| params.values.any? { |field| candidate.key?(field) } }
|
|
121
|
-
.sample(random: @binding_random)
|
|
122
|
-
params.each do |param, field|
|
|
123
|
-
args[param] = shaped_like(args[param], row[field]) if row&.key?(field)
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# A single-field value object is written `{"value" => x}` but may be
|
|
128
|
-
# asked with the bare `x`; keeps whichever spelling the generator
|
|
129
|
-
# already chose for this argument.
|
|
130
|
-
def shaped_like(current, written)
|
|
131
|
-
return written unless written.is_a?(Hash) && written.size == 1 && !current.is_a?(Hash)
|
|
132
|
-
|
|
133
|
-
written.values.first
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|