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
|
@@ -15,9 +15,11 @@ module Hecks
|
|
|
15
15
|
# compare it against. This module asks a different question of one
|
|
16
16
|
# engine at a time: does it agree with itself?
|
|
17
17
|
#
|
|
18
|
-
# Three checks
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# ## Three checks
|
|
19
|
+
#
|
|
20
|
+
# Run on the same already-generated sequence and its resulting live
|
|
21
|
+
# state — no second fuzzing pass, no re-dispatch through the command
|
|
22
|
+
# layer:
|
|
21
23
|
#
|
|
22
24
|
# 1. `check_rehydration` — does reloading an aggregate from its
|
|
23
25
|
# own durable journal reproduce exactly the state a live dispatch
|
|
@@ -29,30 +31,31 @@ module Hecks
|
|
|
29
31
|
# 3. `check_value_object_round_trip` — does every value object the
|
|
30
32
|
# sequence actually built survive `to_json` then rebuilt back?
|
|
31
33
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# `
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
34
|
+
# ## The rehydration path, found, not guessed
|
|
35
|
+
#
|
|
36
|
+
# hecks is not event-sourced at the aggregate level — there is no
|
|
37
|
+
# `AccountOpened`-shaped log a `CommandInterpreter` folds to rebuild
|
|
38
|
+
# state. What there is, real and already shipping, is
|
|
39
|
+
# `Ports::Persistence::AppendOnly` (lib/hecks/ports/persistence/
|
|
40
|
+
# append_only.rb): every adapter accepts the same `Entry` stream
|
|
41
|
+
# (`operation`, `id`, the full state after that command — not a
|
|
42
|
+
# delta) and answers `#entries`; `#recover!` — "an append is durable
|
|
43
|
+
# before a projection is attempted; replaying the log restores a
|
|
44
|
+
# snapshot/table after a crash in that small window" — is called on
|
|
45
|
+
# every repository this runtime ever builds (`RepositoryFactory
|
|
46
|
+
# .build`'s own `recover: true` default). That is the production
|
|
47
|
+
# cold-rehydration path. Reusing `#recover!` directly against the
|
|
48
|
+
# live adapter would prove nothing, though: `Fuzzing::Replay` runs
|
|
49
|
+
# against `Adapters::Memory` (`IsolatedBoot`'s own default), and
|
|
50
|
+
# Memory's own `Entry#state` is a shallow `instance.state.dup` — the
|
|
51
|
+
# exact same `Runtime::Value` objects a command produced ride along
|
|
52
|
+
# unchanged, so folding them straight back through Memory's own
|
|
53
|
+
# `#project` is a tautology that can never fail (`Value#for_attribute`
|
|
54
|
+
# 's own `value.is_a?(self) && value.type_name == ...` branch passes
|
|
55
|
+
# an already-typed value straight through, no rebuilding at all).
|
|
53
56
|
#
|
|
54
|
-
# So `
|
|
55
|
-
#
|
|
57
|
+
# So `fold!`, below, feeds the same entries through `Adapters::Heki`
|
|
58
|
+
# instead — a real, already-shipped, disk-backed adapter
|
|
56
59
|
# (examples/banking's own `persisted_by("Heki")`), in a throwaway
|
|
57
60
|
# directory. Writing forces every value through `JSON.generate`
|
|
58
61
|
# (Heki's own journal line, its own compressed snapshot); reading
|
|
@@ -76,6 +79,11 @@ module Hecks
|
|
|
76
79
|
# could hand `Runtime::Instance` undecoded state and the codec's
|
|
77
80
|
# guarantee would have a bypass in exactly the check that exists to
|
|
78
81
|
# compare stored state against live state.
|
|
82
|
+
#
|
|
83
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate this store holds
|
|
84
|
+
# @param settings [Hash] adapter settings, forwarded to `Adapters::Heki.new`
|
|
85
|
+
# @param root [String, nil] the directory this store reads/writes under
|
|
86
|
+
# @return [Adapters::Heki] a fresh, codec-boundary-guarded Heki instance
|
|
79
87
|
def guarded_heki(**) = Ports::Persistence::CodecBoundary.guard!(Adapters::Heki.new(**))
|
|
80
88
|
|
|
81
89
|
# The whole pass — called once, with the runtime still live (inside
|
|
@@ -87,6 +95,14 @@ module Hecks
|
|
|
87
95
|
# below, run and collected — kept as three independently callable
|
|
88
96
|
# methods (not fused into one shared fold) so a spec proving one
|
|
89
97
|
# check can fire never has to reason about the other two.
|
|
98
|
+
#
|
|
99
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
100
|
+
# booted runtime this replay just dispatched through
|
|
101
|
+
# @param history [Hash] the replayed history `Replay.call` is about to return
|
|
102
|
+
# @return [Hash{Symbol => Array<Hash>}] the five checks' own findings, keyed
|
|
103
|
+
# `:rehydration`, `:idempotency`, `:value_object_round_trip`,
|
|
104
|
+
# `:saga_rehydration`, `:saga_redelivery_idempotency` — each value an empty
|
|
105
|
+
# Array when that check found nothing
|
|
90
106
|
def check(runtime, history)
|
|
91
107
|
{ rehydration: check_rehydration(runtime), idempotency: check_idempotency(runtime),
|
|
92
108
|
value_object_round_trip: check_value_object_round_trip(history),
|
|
@@ -95,6 +111,12 @@ module Hecks
|
|
|
95
111
|
end
|
|
96
112
|
|
|
97
113
|
# Check 1 — rehydrate-from-journal == live state.
|
|
114
|
+
#
|
|
115
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
116
|
+
# booted runtime this replay just dispatched through
|
|
117
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:domain`, `:aggregate`,
|
|
118
|
+
# `:live`, `:rehydrated`) per aggregate whose cold-read state disagrees with its
|
|
119
|
+
# live state; empty when every touched aggregate rehydrates cleanly
|
|
98
120
|
def check_rehydration(runtime)
|
|
99
121
|
each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
|
|
100
122
|
live = snapshot(repository)
|
|
@@ -115,6 +137,12 @@ module Hecks
|
|
|
115
137
|
# replay-specific bug (leaked state between applications, a
|
|
116
138
|
# double-applied effect) that a single, one-shot cold read could
|
|
117
139
|
# never see, even one that already agrees with live state.
|
|
140
|
+
#
|
|
141
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
142
|
+
# booted runtime this replay just dispatched through
|
|
143
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:domain`, `:aggregate`,
|
|
144
|
+
# `:once`, `:twice`) per aggregate whose second fold disagrees with its first;
|
|
145
|
+
# empty when every touched aggregate folds idempotently
|
|
118
146
|
def check_idempotency(runtime)
|
|
119
147
|
each_touched_repository(runtime).filter_map do |domain_name, aggregate, repository, entries|
|
|
120
148
|
Dir.mktmpdir("hecks-self-consistency") do |tmp|
|
|
@@ -162,6 +190,12 @@ module Hecks
|
|
|
162
190
|
# positive `nil` already produced once — `instances` and `events`
|
|
163
191
|
# alone already reach every value object a generated sequence
|
|
164
192
|
# actually persisted or announced.
|
|
193
|
+
#
|
|
194
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
195
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:type`, `:original`, and
|
|
196
|
+
# either `:rehydrated` or `:error`) per value object whose `to_json`-then-`build`
|
|
197
|
+
# round trip disagrees with (or raises on) the original; empty when every value
|
|
198
|
+
# object found round-trips cleanly
|
|
165
199
|
def check_value_object_round_trip(history)
|
|
166
200
|
bluebooks = history[:bluebooks] || {}
|
|
167
201
|
seen = {}.compare_by_identity
|
|
@@ -235,6 +269,14 @@ module Hecks
|
|
|
235
269
|
# not the settled snapshot this history exists to describe), so
|
|
236
270
|
# there is no ground truth to compare it against here. Written as an
|
|
237
271
|
# empty array on the way in and never read back on the way out.
|
|
272
|
+
#
|
|
273
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
274
|
+
# booted runtime this replay just dispatched through
|
|
275
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
276
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:domain`,
|
|
277
|
+
# `:process_manager`, `:live`, `:rehydrated`) per process manager whose cold-read
|
|
278
|
+
# saga instances disagree with the ones this replay persisted; empty when every
|
|
279
|
+
# process manager with persisted instances rehydrates cleanly
|
|
238
280
|
def check_saga_rehydration(runtime, history)
|
|
239
281
|
saga_instances = history[:saga_instances] || {}
|
|
240
282
|
each_domain_process_manager(runtime).filter_map do |domain_name, process_manager|
|
|
@@ -325,6 +367,14 @@ module Hecks
|
|
|
325
367
|
# the redelivered event name (the ordinary, expected case once a
|
|
326
368
|
# saga has moved past the leg that produced its own current
|
|
327
369
|
# checkpoint) is exactly what this proves stays put.
|
|
370
|
+
#
|
|
371
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
372
|
+
# booted runtime this replay just dispatched through
|
|
373
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
374
|
+
# @return [Array<Hash>] one divergence Hash (`:field`, `:domain`,
|
|
375
|
+
# `:process_manager`, `:correlation`, `:on`, `:before`, `:after`) per correlation
|
|
376
|
+
# whose redelivered event visibly changed state/memory on a rehydrated
|
|
377
|
+
# checkpoint; empty when every redelivered correlation stayed put
|
|
328
378
|
def check_saga_idempotency(runtime, history)
|
|
329
379
|
saga_instances = history[:saga_instances] || {}
|
|
330
380
|
interpreter = Runtime::SagaInterpreter.new(runtime.registry, door: runtime)
|
|
@@ -375,19 +425,25 @@ module Hecks
|
|
|
375
425
|
# comment). This check has no Ruby side to spare — it is asking the
|
|
376
426
|
# Rust binary whether it agrees with itself, so `emitted_*` fields
|
|
377
427
|
# are exactly as real a fact to compare as any other. Stripping them
|
|
378
|
-
# here
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
#
|
|
386
|
-
#
|
|
387
|
-
#
|
|
388
|
-
#
|
|
389
|
-
#
|
|
390
|
-
#
|
|
428
|
+
# here would silently delete them from the returned seed-round-trip
|
|
429
|
+
# result while leaving them present on the original `seed_instances`
|
|
430
|
+
# a caller passes in — an asymmetric comparison that would report
|
|
431
|
+
# every record carrying one as a rehydration divergence,
|
|
432
|
+
# unconditionally, on every domain that has one at all. Confirmed
|
|
433
|
+
# live against `examples/banking` (`Banking::Account`'s own
|
|
434
|
+
# `corrects` reaction) by comparing this method's own answer against
|
|
435
|
+
# the compiled binary's raw stdout for the identical seed call: the
|
|
436
|
+
# raw round trip preserves `emitted_fee_applied` correctly. `spec/
|
|
437
|
+
# self_consistency_rust_spec.rb`'s own "banking" example pins this
|
|
438
|
+
# against a real domain going forward.
|
|
439
|
+
#
|
|
440
|
+
# @param binary [String] path to the compiled Rust conformance binary
|
|
441
|
+
# @param _differ [Object] a `RustConformanceHelpers`-including instance, unused here
|
|
442
|
+
# @param seed_instances [Hash] the `"instances"` shape to seed the fresh invocation
|
|
443
|
+
# with, in place of replaying `steps`
|
|
444
|
+
# @return [Hash] the fresh invocation's own `"instances"`, or a
|
|
445
|
+
# `{"__self_consistency_error__" => String}` Hash if the binary exited nonzero or
|
|
446
|
+
# the binary itself reported an `"error"`
|
|
391
447
|
def rust_seed_round_trip(binary, _differ, seed_instances)
|
|
392
448
|
stdout, status = Open3.capture2(binary, stdin_data: JSON.generate({ "steps" => [], "seed" => seed_instances }))
|
|
393
449
|
return { "__self_consistency_error__" => "rust binary exited #{status.exitstatus}: #{stdout}" } \
|
|
@@ -404,6 +460,13 @@ module Hecks
|
|
|
404
460
|
# state, unchanged. `live_instances` is `rust_output["instances"]`
|
|
405
461
|
# — the exact same value `bin/qa_sweep`'s own differential compare
|
|
406
462
|
# already diffed against Ruby, reused here rather than re-derived.
|
|
463
|
+
#
|
|
464
|
+
# @param binary [String] path to the compiled Rust conformance binary
|
|
465
|
+
# @param differ [Object] a `RustConformanceHelpers`-including instance, forwarded to
|
|
466
|
+
# `rust_seed_round_trip` unused
|
|
467
|
+
# @param live_instances [Hash] the prior invocation's own `"instances"` output
|
|
468
|
+
# @return [Array<Hash>] a single divergence Hash (`:field`, `:live`, `:rehydrated`)
|
|
469
|
+
# if seeding with `live_instances` fails to reproduce it; empty otherwise
|
|
407
470
|
def check_rust_rehydration(binary, differ, live_instances)
|
|
408
471
|
rehydrated = rust_seed_round_trip(binary, differ, live_instances)
|
|
409
472
|
return [] if rehydrated == live_instances
|
|
@@ -416,6 +479,14 @@ module Hecks
|
|
|
416
479
|
# same "replay it again, byte for byte" claim `check_idempotency`
|
|
417
480
|
# proves for Ruby, aimed at the one rehydration door this compiled
|
|
418
481
|
# binary actually has.
|
|
482
|
+
#
|
|
483
|
+
# @param binary [String] path to the compiled Rust conformance binary
|
|
484
|
+
# @param differ [Object] a `RustConformanceHelpers`-including instance, forwarded to
|
|
485
|
+
# `rust_seed_round_trip` unused
|
|
486
|
+
# @param live_instances [Hash] the prior invocation's own `"instances"` output to
|
|
487
|
+
# seed the first round trip with
|
|
488
|
+
# @return [Array<Hash>] a single divergence Hash (`:field`, `:once`, `:twice`) if a
|
|
489
|
+
# second seed round trip disagrees with the first; empty otherwise
|
|
419
490
|
def check_rust_idempotency(binary, differ, live_instances)
|
|
420
491
|
once = rust_seed_round_trip(binary, differ, live_instances)
|
|
421
492
|
twice = rust_seed_round_trip(binary, differ, once)
|
|
@@ -432,6 +503,12 @@ module Hecks
|
|
|
432
503
|
# finding a "clean, nothing touched" report would mean anything
|
|
433
504
|
# for. Mirrors `Replay#snapshot_instances`' own
|
|
434
505
|
# `bluebooks.each { aggregates.each { repository(...) } }` walk.
|
|
506
|
+
#
|
|
507
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
508
|
+
# booted runtime this replay just dispatched through
|
|
509
|
+
# @return [Array<Array(String, Bluebook::Aggregate, Ports::Persistence::AppendOnly,
|
|
510
|
+
# Array<Ports::Persistence::Entry>)>] one `[domain_name, aggregate, repository,
|
|
511
|
+
# entries]` tuple per aggregate with at least one journal entry
|
|
435
512
|
def each_touched_repository(runtime)
|
|
436
513
|
found = []
|
|
437
514
|
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
@@ -446,6 +523,12 @@ module Hecks
|
|
|
446
523
|
found
|
|
447
524
|
end
|
|
448
525
|
|
|
526
|
+
# Materializes a repository's own stored records, keyed by id, for comparing
|
|
527
|
+
# against a cold read of the same aggregate.
|
|
528
|
+
#
|
|
529
|
+
# @param repository [Ports::Persistence::AppendOnly] the repository to read
|
|
530
|
+
# @return [Hash{String => Object}] every stored record's id (stringified) mapped to
|
|
531
|
+
# its materialized state
|
|
449
532
|
def snapshot(repository)
|
|
450
533
|
repository.all.to_h { |record| [record.id.to_s, Runtime::Value.materialize(record.state)] }
|
|
451
534
|
end
|
|
@@ -461,6 +544,11 @@ module Hecks
|
|
|
461
544
|
# identical to "ran and found nothing," which is deliberate: neither
|
|
462
545
|
# check has a positive "passed" artifact to report either way (see
|
|
463
546
|
# this file's own header on why silence is never a claimed pass).
|
|
547
|
+
#
|
|
548
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
549
|
+
# booted runtime this replay just dispatched through
|
|
550
|
+
# @return [Array<Array(String, Bluebook::ProcessManager)>] one `[domain_name,
|
|
551
|
+
# process_manager]` tuple per process manager any loaded bluebook declares
|
|
464
552
|
def each_domain_process_manager(runtime)
|
|
465
553
|
found = []
|
|
466
554
|
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
@@ -472,6 +560,11 @@ module Hecks
|
|
|
472
560
|
# The live-side ground truth, key-shape-normalized (see `deep_
|
|
473
561
|
# stringify_keys`'s own comment) so it compares fairly against a
|
|
474
562
|
# real Heki round trip's own shallow-symbolize convention.
|
|
563
|
+
#
|
|
564
|
+
# @param persisted [Hash{String, Symbol => Hash}] `history[:saga_instances]`'s own
|
|
565
|
+
# per-process-manager entry, keyed by correlation, each holding `:state`/`:memory`
|
|
566
|
+
# @return [Hash{String => Hash}] the same rows, keyed by stringified correlation,
|
|
567
|
+
# with `:memory` recursively string-keyed
|
|
475
568
|
def normalize_saga_rows(persisted)
|
|
476
569
|
persisted.each_with_object({}) do |(correlation, saga), rows|
|
|
477
570
|
rows[correlation.to_s] = { state: saga[:state], memory: deep_stringify_keys(saga[:memory]) }
|
|
@@ -482,8 +575,16 @@ module Hecks
|
|
|
482
575
|
# `@store`/`@saga_store`, so `#each_saga` is forced back through
|
|
483
576
|
# `read_snapshot`/`replay_journal`, real bytes off real disk, not
|
|
484
577
|
# whatever the writer that just wrote them still holds in its own
|
|
485
|
-
# process memory (the same reason `fold!`,
|
|
578
|
+
# process memory (the same reason `fold!`, below, opens a second
|
|
486
579
|
# `Adapters::Heki` instance rather than reading its own writer back).
|
|
580
|
+
#
|
|
581
|
+
# @param anchor [Bluebook::Aggregate] any aggregate of the owning domain, used only
|
|
582
|
+
# to open a `Heki` store at the right domain/root
|
|
583
|
+
# @param tmp [String] the throwaway directory the saga store was written into
|
|
584
|
+
# @param domain_name [String] the owning domain's name, threaded into the store's
|
|
585
|
+
# own `settings`
|
|
586
|
+
# @return [Hash{String => Hash}] every persisted correlation's `:state`/`:memory`,
|
|
587
|
+
# with `:memory` recursively string-keyed
|
|
487
588
|
def cold_read_saga_rows(anchor, tmp, domain_name)
|
|
488
589
|
reader = guarded_heki(aggregate: anchor, root: tmp, settings: { domain: domain_name })
|
|
489
590
|
reader.each_saga.with_object({}) do |(_pm, correlation, state, memory, _completed), rows|
|
|
@@ -513,6 +614,10 @@ module Hecks
|
|
|
513
614
|
# key, a changed value, a missing field) — exactly the kind (b)'s
|
|
514
615
|
# own seeded fixture in `spec/fuzzing/self_consistency_saga_spec.rb`
|
|
515
616
|
# proves this still catches.
|
|
617
|
+
#
|
|
618
|
+
# @param value [Object] any value; only Hash and Array are recursed into
|
|
619
|
+
# @return [Object] `value` with every Hash key (at every nesting level) replaced by
|
|
620
|
+
# its own `to_s`; anything else is returned unchanged
|
|
516
621
|
def deep_stringify_keys(value)
|
|
517
622
|
case value
|
|
518
623
|
when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify_keys(v) }
|
|
@@ -540,6 +645,17 @@ module Hecks
|
|
|
540
645
|
# "hand-rolled approximation" this check exists to avoid; `send` on
|
|
541
646
|
# an interpreter sharing this same `runtime`'s own registry is the
|
|
542
647
|
# real thing, not a copy of it.
|
|
648
|
+
#
|
|
649
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
650
|
+
# booted runtime this replay just dispatched through
|
|
651
|
+
# @param interpreter [Runtime::SagaInterpreter] the interpreter whose (private)
|
|
652
|
+
# `saga_correlation` re-derives this conversation's own correlation
|
|
653
|
+
# @param process_manager [Bluebook::ProcessManager] the process manager whose saga
|
|
654
|
+
# log is walked
|
|
655
|
+
# @param correlation [Object] the correlation identifier to match, as
|
|
656
|
+
# `saga_correlation` produces it — typically a String
|
|
657
|
+
# @return [Runtime::Event, nil] the real domain event this correlation's current
|
|
658
|
+
# checkpoint last advanced on, or nil if it was only ever `begin_saga`'d
|
|
543
659
|
def last_advancing_event(runtime, interpreter, process_manager, correlation)
|
|
544
660
|
entry = runtime.registry.saga_log.reverse_each.find do |row|
|
|
545
661
|
row[:process_manager] == process_manager.name && row[:instance] == correlation &&
|
|
@@ -560,6 +676,21 @@ module Hecks
|
|
|
560
676
|
# fix) belongs right where it guards `interpreter.advance`, not in
|
|
561
677
|
# a helper a reader would have to jump to just to see what is and
|
|
562
678
|
# isn't being restored around that one call.
|
|
679
|
+
#
|
|
680
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the still-live
|
|
681
|
+
# booted runtime this replay just dispatched through
|
|
682
|
+
# @param interpreter [Runtime::SagaInterpreter] the interpreter to redeliver through
|
|
683
|
+
# @param domain_name [String] the owning domain's name
|
|
684
|
+
# @param process_manager [Bluebook::ProcessManager] the process manager under test
|
|
685
|
+
# @param anchor [Bluebook::Aggregate] any aggregate of the owning domain, used only
|
|
686
|
+
# to open a `Heki` store at the right domain/root
|
|
687
|
+
# @param correlation [Object] the conversation's own correlation identifier
|
|
688
|
+
# @param saga [Hash] this correlation's persisted `:state`/`:memory`
|
|
689
|
+
# @param redelivery [Runtime::Event] the real domain event to redeliver
|
|
690
|
+
# @return [Hash, nil] a divergence Hash (`:field`, `:domain`, `:process_manager`,
|
|
691
|
+
# `:correlation`, `:on`, `:before`, `:after`) if the redelivery visibly changed
|
|
692
|
+
# state/memory; nil if it left the checkpoint unchanged, or if the cold-read
|
|
693
|
+
# checkpoint could not be found at all
|
|
563
694
|
def check_one_saga_redelivery(runtime, interpreter, domain_name, process_manager, anchor,
|
|
564
695
|
correlation, saga, redelivery)
|
|
565
696
|
# rubocop:disable-next Metrics/BlockLength
|
|
@@ -621,6 +752,13 @@ module Hecks
|
|
|
621
752
|
# second time" — `writer` already holds everything the first fold
|
|
622
753
|
# wrote, so a second fold re-applies the identical operations on
|
|
623
754
|
# top, and the two cold reads either agree (idempotent) or don't.
|
|
755
|
+
#
|
|
756
|
+
# @param writer [Adapters::Heki] the open store to append `entries` into
|
|
757
|
+
# @param tmp [String] the throwaway directory `writer` was opened against
|
|
758
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `entries` belongs to
|
|
759
|
+
# @param entries [Array<Ports::Persistence::Entry>] the journal entries to fold in
|
|
760
|
+
# @return [Hash{String => Object}] every stored record's id (stringified) mapped to
|
|
761
|
+
# its materialized state, read back cold
|
|
624
762
|
def fold!(writer, tmp, aggregate, entries)
|
|
625
763
|
entries.each do |entry|
|
|
626
764
|
writer.append(entry)
|
|
@@ -655,6 +793,17 @@ module Hecks
|
|
|
655
793
|
# answer — identity, not `Value#==`, is the right notion of
|
|
656
794
|
# "already found" here (two different value objects that happen to
|
|
657
795
|
# hold equal fields are still two separate round trips to prove).
|
|
796
|
+
#
|
|
797
|
+
# @param node [Object] the value to walk; a `Runtime::Value` is recorded and
|
|
798
|
+
# recursed into by field, a Hash or Array is recursed into by value/element,
|
|
799
|
+
# anything else is ignored
|
|
800
|
+
# @param found [Array<Array(Runtime::Value, Bluebook::Aggregate, nil)>] accumulator
|
|
801
|
+
# this method appends `[value, aggregate]` pairs to, in place
|
|
802
|
+
# @param seen [Hash] a `compare_by_identity` Hash used as a found-by-identity set,
|
|
803
|
+
# mutated in place
|
|
804
|
+
# @param aggregate [Bluebook::Aggregate, nil] the aggregate `node` belongs to, carried
|
|
805
|
+
# along unchanged for every value found beneath it
|
|
806
|
+
# @return [void]
|
|
658
807
|
def walk_value_objects(node, found, seen, aggregate)
|
|
659
808
|
case node
|
|
660
809
|
when Runtime::Value
|
|
@@ -8,6 +8,8 @@ module Hecks
|
|
|
8
8
|
# The argument shapes the Ruby/Rust divergences were actually found
|
|
9
9
|
# through, injected on purpose, on every domain.
|
|
10
10
|
#
|
|
11
|
+
# ## Why these shapes
|
|
12
|
+
#
|
|
11
13
|
# Ten bugs in one QA session (BUG#7–#16, `QualityControl`'s own
|
|
12
14
|
# ledger) clustered in a handful of mechanisms — and several were
|
|
13
15
|
# only ever reachable because one domain happened to declare the
|
|
@@ -16,7 +18,7 @@ module Hecks
|
|
|
16
18
|
# but-null `to:` was misread as a routing envelope (BUG#16), a blank
|
|
17
19
|
# creating identity minted a phantom aggregate in Rust (BUG#15), a
|
|
18
20
|
# single-field closed-set argument offered as bare `null` refused
|
|
19
|
-
# with different
|
|
21
|
+
# with different kinds on the two engines (BUG#14). No other domain
|
|
20
22
|
# in the rotation could ever have found any of those, because
|
|
21
23
|
# nothing generated ever produced the shape. This module produces
|
|
22
24
|
# them — for a configurable fraction of command steps, chosen and
|
|
@@ -24,15 +26,19 @@ module Hecks
|
|
|
24
26
|
# already draws from, so `generate(domain, seed:, steps:,
|
|
25
27
|
# adversarial:)` stays exactly as reproducible per seed as it was.
|
|
26
28
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
29
|
+
# ## Opt-in, by construction
|
|
30
|
+
#
|
|
31
|
+
# `adversarial: 0.0` (the default) returns before drawing a single
|
|
32
|
+
# random number, so every pinned seed in spec/fuzzing,
|
|
33
|
+
# spec/rust_conformance_fuzz_spec.rb, bin/fuzz and bin/generate
|
|
34
|
+
# produces byte-for-byte what it produced before this module
|
|
35
|
+
# existed. `bin/qa_sweep` turns it on (reading
|
|
32
36
|
# `QualityControlDials::ADVERSARIAL_FRACTION`) — that script is the
|
|
33
37
|
# one place a mutated step's own divergence is a finding rather than
|
|
34
38
|
# a red CI gate.
|
|
35
39
|
#
|
|
40
|
+
# ## When it applies
|
|
41
|
+
#
|
|
36
42
|
# One mutation per step, applied in `StepBuilder#build_command_step`
|
|
37
43
|
# after the arguments and identity are built and before the step's
|
|
38
44
|
# own inline dispatch — so the generator's own `known_ids` tracking
|
|
@@ -122,7 +122,7 @@ module Hecks
|
|
|
122
122
|
# shape, and this step still skips it exactly as it always
|
|
123
123
|
# has. A list-of-value-object attribute (`ConsoleSettings::
|
|
124
124
|
# Collection.ReplaceColumns`' own `columns`, `list_of(Column)`)
|
|
125
|
-
# is the real
|
|
125
|
+
# is the real case this covers —
|
|
126
126
|
# `sets :columns` imports the owner aggregate's own declared
|
|
127
127
|
# `list_of` attribute onto the command verbatim (Command
|
|
128
128
|
# Builder#resolve_bare_set!), so it is a required, ordinary
|
|
@@ -98,6 +98,26 @@ module Hecks
|
|
|
98
98
|
# `favor:` names verbs the picker weights up. `nil`/`[]`, the
|
|
99
99
|
# defaults, draw nothing extra and change nothing: every pinned seed
|
|
100
100
|
# is byte-for-byte what it was.
|
|
101
|
+
#
|
|
102
|
+
# Generates one random-but-valid step sequence for `domain_path`, dispatching
|
|
103
|
+
# each step for real against a throwaway boot as it builds it.
|
|
104
|
+
#
|
|
105
|
+
# @param domain_path [String] path to the domain directory to boot
|
|
106
|
+
# @param seed [Integer] RNG seed; every draw this run makes is reproducible
|
|
107
|
+
# from it
|
|
108
|
+
# @param steps [Integer] number of generation attempts to make
|
|
109
|
+
# @param adapter [Symbol] persistence adapter to boot with (default `:memory`)
|
|
110
|
+
# @param adversarial [Float] fraction of command steps to mutate adversarially
|
|
111
|
+
# (default `0.0`, drawing nothing extra)
|
|
112
|
+
# @param role_draw [Float] fraction of gated commands to draw a caller for
|
|
113
|
+
# (default `0.0`, drawing nothing extra)
|
|
114
|
+
# @param dry_run [Float] fraction of command steps dispatched as dry runs
|
|
115
|
+
# (default `0.0`, drawing nothing extra)
|
|
116
|
+
# @param prefix [Hash, nil] another seed's own generation spec to replay
|
|
117
|
+
# first (`{"seed" =>, "steps" =>, "favor" =>, "prefix" =>}`), or `nil`
|
|
118
|
+
# @param favor [Array<String>, Array<Symbol>] verbs the picker weights up
|
|
119
|
+
# @return [Array<Hash>] the generated step list, each a command, query, or
|
|
120
|
+
# read-model step
|
|
101
121
|
def self.generate(domain_path, seed:, steps:, **)
|
|
102
122
|
new(domain_path, seed: seed, steps: steps, **).call
|
|
103
123
|
end
|
|
@@ -108,6 +128,15 @@ module Hecks
|
|
|
108
128
|
# never hit from one that does not exist.
|
|
109
129
|
Trace = Struct.new(:steps, :coverage, :verbs, keyword_init: true)
|
|
110
130
|
|
|
131
|
+
# Generates one sequence exactly like `.generate`, but also returns the
|
|
132
|
+
# coverage and verb data a campaign needs, which `.generate` discards.
|
|
133
|
+
#
|
|
134
|
+
# @param domain_path [String] path to the domain directory to boot
|
|
135
|
+
# @param seed [Integer] RNG seed; every draw this run makes is reproducible
|
|
136
|
+
# from it
|
|
137
|
+
# @param steps [Integer] number of generation attempts to make
|
|
138
|
+
# @return [Hecks::Fuzzing::SequenceGenerator::Trace] the generated steps,
|
|
139
|
+
# the coverage tuples reached, and every verb the booted catalog offered
|
|
111
140
|
def self.trace(domain_path, seed:, steps:, **)
|
|
112
141
|
generator = new(domain_path, seed: seed, steps: steps, **)
|
|
113
142
|
Trace.new(steps: generator.call, coverage: generator.coverage, verbs: generator.verbs)
|
|
@@ -130,6 +159,19 @@ module Hecks
|
|
|
130
159
|
# replay one.
|
|
131
160
|
attr_reader :event_count
|
|
132
161
|
|
|
162
|
+
# @param domain_path [String] path to the domain directory to boot
|
|
163
|
+
# @param seed [Integer] RNG seed; every draw this run makes is reproducible
|
|
164
|
+
# from it
|
|
165
|
+
# @param steps [Integer] number of generation attempts `#call` will make
|
|
166
|
+
# @param adapter [Symbol] persistence adapter to boot with
|
|
167
|
+
# @param adversarial [Float] fraction of command steps to mutate adversarially
|
|
168
|
+
# @param role_draw [Float] fraction of gated commands to draw a caller for
|
|
169
|
+
# @param dry_run [Float] fraction of command steps dispatched as dry runs
|
|
170
|
+
# @param prefix [Hash, nil] another seed's own generation spec to replay
|
|
171
|
+
# first, or `nil`
|
|
172
|
+
# @param favor [Array<String>, Array<Symbol>] verbs the picker weights up
|
|
173
|
+
# @raise [ArgumentError] if `adversarial`, `role_draw`, or `dry_run` is not a
|
|
174
|
+
# Numeric between 0.0 and 1.0
|
|
133
175
|
def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0,
|
|
134
176
|
prefix: nil, favor: [])
|
|
135
177
|
{ adversarial: adversarial, role_draw: role_draw, dry_run: dry_run }.each do |name, fraction|
|
|
@@ -164,6 +206,11 @@ module Hecks
|
|
|
164
206
|
@attempt = 0
|
|
165
207
|
end
|
|
166
208
|
|
|
209
|
+
# Runs the generation this instance was configured for, against a fresh,
|
|
210
|
+
# isolated boot of `@domain_path`.
|
|
211
|
+
#
|
|
212
|
+
# @return [Array<Hash>] the generated step list, each a command, query, or
|
|
213
|
+
# read-model step; a picker miss that produced no step is dropped
|
|
167
214
|
def call
|
|
168
215
|
# Real leftover data from ordinary use (bin/console, whatever) lives
|
|
169
216
|
# under the example's data/ — a generator that boots against it
|