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.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +13 -0
  3. data/lib/hecks/adapters/driven/memory.rb +1 -1
  4. data/lib/hecks/adapters/driven/postgres.rb +1 -1
  5. data/lib/hecks/adapters/driven/prism.rb +1 -1
  6. data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
  7. data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
  8. data/lib/hecks/behaviors/expectations.rb +2 -2
  9. data/lib/hecks/bluebook/assembly/contract.rb +1 -1
  10. data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
  11. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  12. data/lib/hecks/bluebook/assembly.rb +1 -1
  13. data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
  14. data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
  15. data/lib/hecks/bluebook/capabilities.rb +2 -2
  16. data/lib/hecks/bluebook/chapter.rb +1 -1
  17. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
  18. data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
  19. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
  20. data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
  21. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
  22. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
  23. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
  24. data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
  25. data/lib/hecks/bluebook/dsl/world_builder.rb +5 -0
  26. data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
  27. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
  28. data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
  29. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
  30. data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
  31. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
  32. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  33. data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
  34. data/lib/hecks/bluebook/model_check.rb +12 -13
  35. data/lib/hecks/cache_dir.rb +103 -0
  36. data/lib/hecks/cli/document.rb +104 -0
  37. data/lib/hecks/cli/ir.rb +47 -0
  38. data/lib/hecks/cli/mcp.rb +32 -0
  39. data/lib/hecks/cli/mcp_door.rb +461 -0
  40. data/lib/hecks/cli/model_check.rb +270 -0
  41. data/lib/hecks/cli/project_cli.rb +123 -0
  42. data/lib/hecks/cli/project_diagrams.rb +43 -0
  43. data/lib/hecks/cli/run.rb +219 -0
  44. data/lib/hecks/cli/smoke_test.rb +59 -0
  45. data/lib/hecks/cli/stores.rb +54 -0
  46. data/lib/hecks/cli.rb +166 -0
  47. data/lib/hecks/facade/handle.rb +2 -2
  48. data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
  49. data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
  50. data/lib/hecks/mcp_door_scope.rb +176 -0
  51. data/lib/hecks/ports/persistence/append_only.rb +1 -1
  52. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +1 -1
  53. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
  54. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
  55. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
  56. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
  57. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
  58. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +12 -6
  59. data/lib/hecks/ports/persistence/state_codec.rb +1 -1
  60. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  61. data/lib/hecks/projections/deploy/lambda.rb +3 -3
  62. data/lib/hecks/projections/reference.rb +7 -1
  63. data/lib/hecks/projector/exporter.rb +4 -4
  64. data/lib/hecks/projector/target.rb +1 -1
  65. data/lib/hecks/release/runner/ci_publisher.rb +96 -0
  66. data/lib/hecks/release/runner/commands.rb +68 -0
  67. data/lib/hecks/release/runner/console.rb +52 -0
  68. data/lib/hecks/release/runner/gem_publisher.rb +44 -0
  69. data/lib/hecks/release/runner/git.rb +48 -0
  70. data/lib/hecks/release/runner/npm_publisher.rb +89 -0
  71. data/lib/hecks/release/runner/preflight.rb +119 -0
  72. data/lib/hecks/release/runner/published.rb +55 -0
  73. data/lib/hecks/release/runner/tagger.rb +87 -0
  74. data/lib/hecks/release/runner.rb +244 -0
  75. data/lib/hecks/router/namespace_installer.rb +1 -1
  76. data/lib/hecks/runtime/command_interpreter.rb +3 -3
  77. data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
  78. data/lib/hecks/runtime/entity_element.rb +1 -1
  79. data/lib/hecks/runtime/entity_interpreter.rb +1 -1
  80. data/lib/hecks/runtime/invocation.rb +1 -1
  81. data/lib/hecks/runtime/loader.rb +1 -1
  82. data/lib/hecks/runtime/outbox.rb +1 -1
  83. data/lib/hecks/runtime/reaction_invocation.rb +3 -3
  84. data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
  85. data/lib/hecks/runtime/reference_hop.rb +4 -4
  86. data/lib/hecks/runtime/registry/verification.rb +11 -11
  87. data/lib/hecks/runtime/registry.rb +8 -4
  88. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  89. data/lib/hecks/runtime/value/coercion.rb +3 -3
  90. data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
  91. data/lib/hecks/storehouse.rb +18 -12
  92. data/lib/hecks/version.rb +1 -1
  93. data/lib/hecks/vocabulary.rb +5 -0
  94. data/lib/hecks.rb +6 -1
  95. metadata +28 -58
  96. data/lib/hecks/bench/cli.rb +0 -74
  97. data/lib/hecks/bench/environment.rb +0 -100
  98. data/lib/hecks/bench/postgres_probe.rb +0 -41
  99. data/lib/hecks/bench/report.rb +0 -116
  100. data/lib/hecks/bench/ruby_runner.rb +0 -151
  101. data/lib/hecks/bench/run.rb +0 -55
  102. data/lib/hecks/bench/rust_runner.rb +0 -165
  103. data/lib/hecks/bench/stats.rb +0 -75
  104. data/lib/hecks/bench/suite.rb +0 -172
  105. data/lib/hecks/bench/workload.rb +0 -137
  106. data/lib/hecks/bench.rb +0 -28
  107. data/lib/hecks/codemod.rb +0 -443
  108. data/lib/hecks/corpus.rb +0 -479
  109. data/lib/hecks/doc/reference.rb +0 -585
  110. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  111. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  112. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  113. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  114. data/lib/hecks/fuzzing/differential.rb +0 -192
  115. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  116. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  117. data/lib/hecks/fuzzing/form_census.rb +0 -285
  118. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  119. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  120. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  121. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  122. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  123. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  124. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  125. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  126. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  127. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  128. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  129. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  130. data/lib/hecks/fuzzing/properties.rb +0 -267
  131. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  132. data/lib/hecks/fuzzing/replay.rb +0 -930
  133. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  134. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  135. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  136. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  137. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  138. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  139. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  140. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  141. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  142. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  143. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  144. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  145. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  146. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  147. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  148. data/lib/hecks/fuzzing.rb +0 -23
  149. data/lib/hecks/grammar/evolve.rb +0 -524
  150. 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