hecks 1.3.0 → 1.4.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 +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +170 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +180 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +11 -11
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +35 -35
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +22 -2
|
@@ -20,17 +20,17 @@ module Hecks
|
|
|
20
20
|
{ "query" => entry[:verb], "args" => args }
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# A
|
|
23
|
+
# A report ask — the bare domain form `entry[:verb]` already carries
|
|
24
24
|
# ("Domain.report_name", no "::"), so `Dispatcher#query` routes it
|
|
25
25
|
# to the read model rather than an aggregate query. A `ReadModel`
|
|
26
26
|
# has no declared `.attributes` the way a `Query` does — its own
|
|
27
|
-
# argument surface is exactly
|
|
27
|
+
# argument surface is exactly one key, `reference_name`, and only
|
|
28
28
|
# for a rooted model (`read_model_actionable?` already gated a
|
|
29
29
|
# rootless one straight into eligibility with nothing to supply).
|
|
30
|
-
# A
|
|
31
|
-
# refuse_object_reference` explicitly
|
|
30
|
+
# A bare scalar, not `identity_shaped` — `ReadModelInterpreter#
|
|
31
|
+
# refuse_object_reference` explicitly rejects a Hash/Value offered
|
|
32
32
|
# here (this is the one place in the whole generator where the
|
|
33
|
-
# subject's own identity must
|
|
33
|
+
# subject's own identity must not be wrapped the way a command
|
|
34
34
|
# argument's would be).
|
|
35
35
|
def build_read_model_step(runtime, entry)
|
|
36
36
|
model = entry[:model]
|
|
@@ -40,20 +40,20 @@ module Hecks
|
|
|
40
40
|
{ "query" => entry[:verb], "args" => args }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
#
|
|
44
|
-
# arguments and identity are fully built,
|
|
43
|
+
# The adversarial layer sits here, and only here — after the step's
|
|
44
|
+
# arguments and identity are fully built, before the one real
|
|
45
45
|
# dispatch this generator makes to learn what the step did, and
|
|
46
46
|
# before the step is returned as corpus data. That ordering is
|
|
47
47
|
# the whole contract: the mutated `args` are what this generator's
|
|
48
48
|
# own inline dispatch sees (so `known_ids` tracking reflects what
|
|
49
|
-
# actually happened),
|
|
49
|
+
# actually happened), and they are the bytes `Fuzzing::Replay`
|
|
50
50
|
# later hands Ruby's runtime and the bytes `JSON.generate({steps:
|
|
51
51
|
# ...})` hands the compiled Rust binary — one step, one payload,
|
|
52
52
|
# both engines. Nothing downstream of this method can tell a
|
|
53
53
|
# mutated step from an ordinary one except by reading the
|
|
54
54
|
# `"adversarial"` metadata it carries (see adversary.rb).
|
|
55
55
|
#
|
|
56
|
-
#
|
|
56
|
+
# Two more draws sit right after the mutation, in a fixed order —
|
|
57
57
|
# a caller (`caller_draw!`, adversary.rb: `role:`/`actor_id:` on
|
|
58
58
|
# the step, bound around this one dispatch exactly the way
|
|
59
59
|
# `Fuzzing::Replay` and `kernel/cli.rs` will later bind it) and
|
|
@@ -70,13 +70,14 @@ module Hecks
|
|
|
70
70
|
mutations = adversarial_mutations!(args, entry, catalog)
|
|
71
71
|
caller, caller_note = caller_draw!(entry, catalog)
|
|
72
72
|
mutations << caller_note if caller_note
|
|
73
|
+
@state_before = state_before(runtime, entry, args)
|
|
73
74
|
|
|
74
75
|
step =
|
|
75
76
|
if dry_run_draw?
|
|
76
77
|
safe_call { as_caller(caller) { runtime.dry_run?(entry[:verb], **symbolize(args)) } }
|
|
77
78
|
{ "dry_run" => entry[:verb], "args" => args }
|
|
78
79
|
else
|
|
79
|
-
outcome = safe_call { as_caller(caller) { runtime.
|
|
80
|
+
outcome = safe_call { as_caller(caller) { runtime.dispatch_flat(entry[:verb], symbolize(args)) } }
|
|
80
81
|
if outcome
|
|
81
82
|
record_outcome(catalog, entry, args)
|
|
82
83
|
@event_count += outcome.events.length
|
|
@@ -93,7 +94,7 @@ module Hecks
|
|
|
93
94
|
def dry_run_draw? = @dry_run.positive? && @random.rand < @dry_run
|
|
94
95
|
|
|
95
96
|
# `Hecks.as_caller` for exactly this block, or a bare yield — the
|
|
96
|
-
#
|
|
97
|
+
# same binding `Fuzzing::Replay` makes from the step's own keys
|
|
97
98
|
# later, so the generator's inline dispatch and both replays see
|
|
98
99
|
# one caller.
|
|
99
100
|
def as_caller(caller, &)
|
|
@@ -104,7 +105,7 @@ module Hecks
|
|
|
104
105
|
|
|
105
106
|
def args_for(attributes, aggregate)
|
|
106
107
|
args = attributes.each_with_object({}) do |attribute, built|
|
|
107
|
-
#
|
|
108
|
+
# An optional argument is sometimes not given, and that is an
|
|
108
109
|
# ordinary payload rather than a damaged one — see
|
|
109
110
|
# OPTIONAL_OMITTED_PROBABILITY for why this cannot live in
|
|
110
111
|
# `malform` below and what it was costing while it did not
|
|
@@ -113,13 +114,13 @@ module Hecks
|
|
|
113
114
|
|
|
114
115
|
if attribute.list?
|
|
115
116
|
value = list_value_for(attribute, aggregate)
|
|
116
|
-
# A list-of-
|
|
117
|
+
# A list-of-entity command attribute has no real example
|
|
117
118
|
# anywhere in this repo's domains — every entity-owned list is
|
|
118
119
|
# populated via a per-element append command instead, never a
|
|
119
120
|
# whole-list command argument — so `list_value_for` (below)
|
|
120
121
|
# answers `nil` for one rather than guessing at an entity's own
|
|
121
122
|
# shape, and this step still skips it exactly as it always
|
|
122
|
-
# has. A list-of-
|
|
123
|
+
# has. A list-of-value-object attribute (`ConsoleSettings::
|
|
123
124
|
# Collection.ReplaceColumns`' own `columns`, `list_of(Column)`)
|
|
124
125
|
# is the real, previously-unfuzzable case this now covers —
|
|
125
126
|
# `sets :columns` imports the owner aggregate's own declared
|
|
@@ -137,9 +138,9 @@ module Hecks
|
|
|
137
138
|
malform(args, attributes, aggregate)
|
|
138
139
|
end
|
|
139
140
|
|
|
140
|
-
# A `list_of`
|
|
141
|
+
# A `list_of` attribute's own value — an array of independently
|
|
141
142
|
# generated elements, each shaped exactly the way a bare (non-list)
|
|
142
|
-
# attribute of the
|
|
143
|
+
# attribute of the same declared element type already is
|
|
143
144
|
# (`ValueGenerator.value_for`), since `list_of(X)`'s own element
|
|
144
145
|
# coercion is `X`'s ordinary shape repeated, not a different one
|
|
145
146
|
# (`Attribute#type` is already unwrapped from `list_of(...)` at
|
|
@@ -157,12 +158,12 @@ module Hecks
|
|
|
157
158
|
Array.new(@random.rand(0..3)) { ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids) }
|
|
158
159
|
end
|
|
159
160
|
|
|
160
|
-
#
|
|
161
|
+
# One malformation at a time, and usually none. A step whose payload is
|
|
161
162
|
# wrong in three ways only ever proves which check runs first ; wrong in
|
|
162
163
|
# exactly one way names the check that fired. And the rate stays low on
|
|
163
164
|
# purpose — a corrupted step is almost always refused, a sequence of
|
|
164
165
|
# refusals reaches no state at all, and bin/fuzz already counts those as
|
|
165
|
-
#
|
|
166
|
+
# silent rather than scoring them.
|
|
166
167
|
def malform(args, attributes, aggregate)
|
|
167
168
|
return args if args.empty? || @random.rand >= MALFORMED_ARGUMENT_PROBABILITY
|
|
168
169
|
|
|
@@ -173,12 +174,12 @@ module Hecks
|
|
|
173
174
|
end
|
|
174
175
|
end
|
|
175
176
|
|
|
176
|
-
#
|
|
177
|
+
# Never the identity. A creating command with no id auto-mints one, and
|
|
177
178
|
# a minted id is deliberately unreproducible — a random hex, never a
|
|
178
179
|
# guessable counter — so dropping it manufactures a step whose outcome
|
|
179
180
|
# cannot be replayed and says nothing about the runtime's behaviour.
|
|
180
181
|
# Every step in the hand-written corpus
|
|
181
|
-
# supplies an id for the same reason. Whether an auto-minted id
|
|
182
|
+
# supplies an id for the same reason. Whether an auto-minted id ought to
|
|
182
183
|
# be reproducible is a real question, but it is not one a payload fuzzer
|
|
183
184
|
# can ask.
|
|
184
185
|
def drop_one(args, aggregate)
|
|
@@ -204,11 +205,11 @@ module Hecks
|
|
|
204
205
|
if entry[:entity]
|
|
205
206
|
parent_scalar = pick_known(aggregate.hecks_name)
|
|
206
207
|
args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, parent_scalar, aggregate)
|
|
207
|
-
#
|
|
208
|
+
# One identity per hop, each drawn from the pool its own
|
|
208
209
|
# parent-plus-hops landed elements in (`entity_pool_key`) —
|
|
209
210
|
# a depth-1 chain draws exactly what it always did; a
|
|
210
211
|
# `Board.Card` chain draws a Board under this Workspace, then
|
|
211
|
-
# a Card under
|
|
212
|
+
# a Card under that Board. Flat args, one head per hop, is
|
|
212
213
|
# the legacy addressing `EntityElement#locate_chain` reads
|
|
213
214
|
# (`args[head]` per identity path); the routed `to: {
|
|
214
215
|
# aggregate:, entities: [...] }` spelling is an adversarial
|
|
@@ -222,19 +223,19 @@ module Hecks
|
|
|
222
223
|
scalars << scalar
|
|
223
224
|
end
|
|
224
225
|
elsif entry[:command].creates?
|
|
225
|
-
# A
|
|
226
|
-
#
|
|
226
|
+
# A composite identity (`identified_by` answering nil with more
|
|
227
|
+
# than one declared path — Behaviour::Identified's own "a
|
|
227
228
|
# composite has no single head" comment) supplies every one of
|
|
228
229
|
# its parts as its own ordinary, individually-declared command
|
|
229
230
|
# attribute already — `RoleAssignment::Assign` takes actor_id/
|
|
230
231
|
# role_name/starts_at directly, `args_for` (above) already
|
|
231
232
|
# generated all three. Forcing a synthetic top-level `id` here
|
|
232
|
-
# too — this codebase's own fallback for the
|
|
233
|
+
# too — this codebase's own fallback for the single-key and the
|
|
233
234
|
# genuinely untyped (`identity_paths.empty?`, no `identified_by`
|
|
234
235
|
# declared at all) cases — hands a composite creating command
|
|
235
236
|
# an argument it never declared at all, refused every time as
|
|
236
237
|
# unknown before this check existed (a creating `Assign`/`Grant`
|
|
237
|
-
# step was never anything
|
|
238
|
+
# step was never anything but refused). `identity_paths.empty?`
|
|
238
239
|
# is the untyped default (falls all the way back to a minted
|
|
239
240
|
# `:id` the runtime itself never declared as an attribute
|
|
240
241
|
# either), which still needs exactly the old minting behavior.
|
|
@@ -248,9 +249,9 @@ module Hecks
|
|
|
248
249
|
end
|
|
249
250
|
end
|
|
250
251
|
|
|
251
|
-
#
|
|
252
|
+
# True only for a genuine multi-field identity — `identified_by`
|
|
252
253
|
# returns nil both for a real composite (`identity_paths.size > 1`)
|
|
253
|
-
# and for the untyped default with
|
|
254
|
+
# and for the untyped default with no identity declared at all
|
|
254
255
|
# (`identity_paths.size == 0`, Behaviour::Identified's own
|
|
255
256
|
# `Array(@identified_by)` fallback) ; only the first of those two
|
|
256
257
|
# has its own parts already sitting in `args` as real, individually-
|
|
@@ -294,7 +295,7 @@ module Hecks
|
|
|
294
295
|
|
|
295
296
|
# A step the runtime declines is not a generator failure — it simply did
|
|
296
297
|
# not take effect, so nothing is recorded and the sequence carries on. The
|
|
297
|
-
# step still goes into the corpus, because a
|
|
298
|
+
# step still goes into the corpus, because a refusal is an answer, and
|
|
298
299
|
# its wording is pinned by the corpus.
|
|
299
300
|
#
|
|
300
301
|
# EvaluationError sits alongside the declared refusals deliberately : a
|
|
@@ -305,10 +306,36 @@ module Hecks
|
|
|
305
306
|
# generator built a step that breaks the interpreter for reasons that have
|
|
306
307
|
# nothing to do with the domain declining a payload.
|
|
307
308
|
def safe_call
|
|
308
|
-
yield
|
|
309
|
-
|
|
309
|
+
result = yield
|
|
310
|
+
@last_outcome = "ok"
|
|
311
|
+
result
|
|
312
|
+
rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError => e
|
|
313
|
+
@last_outcome = e.class.name.split("::").last
|
|
310
314
|
nil
|
|
311
315
|
end
|
|
316
|
+
|
|
317
|
+
# Where the addressed aggregate stood just before this dispatch — its
|
|
318
|
+
# lifecycle value, or `exists`/`absent` when it declares none. Read
|
|
319
|
+
# straight off the repository, never through dispatch, and draws
|
|
320
|
+
# nothing from the RNG, so it changes no generated byte. An identity
|
|
321
|
+
# an adversarial mutation mangled past resolving is `?`.
|
|
322
|
+
def state_before(runtime, entry, args)
|
|
323
|
+
aggregate = entry[:aggregate]
|
|
324
|
+
symbolic = symbolize(args)
|
|
325
|
+
id = Runtime::Identity.of(aggregate, symbolic) || Runtime::Identity.from(aggregate, symbolic, :id)
|
|
326
|
+
return "absent" unless id
|
|
327
|
+
|
|
328
|
+
record = runtime.registry.repository(entry[:verb].split("::").first, aggregate).find(id)
|
|
329
|
+
return "absent" unless record
|
|
330
|
+
|
|
331
|
+
lifecycle = aggregate.lifecycle
|
|
332
|
+
return "exists" unless lifecycle
|
|
333
|
+
|
|
334
|
+
key = record.state.key?(lifecycle.field) ? lifecycle.field : lifecycle.field.to_s
|
|
335
|
+
record.state[key].to_s
|
|
336
|
+
rescue StandardError
|
|
337
|
+
"?"
|
|
338
|
+
end
|
|
312
339
|
end
|
|
313
340
|
end
|
|
314
341
|
end
|
|
@@ -15,13 +15,13 @@ module Hecks
|
|
|
15
15
|
# `{name, note, steps}` shape `spec/corpus/*.json` already uses — so a
|
|
16
16
|
# generated sequence replays as a corpus member, completely unchanged.
|
|
17
17
|
# Boots a throwaway copy of
|
|
18
|
-
# the domain and
|
|
18
|
+
# the domain and dispatches each candidate step for real as it builds the
|
|
19
19
|
# sequence (not just synthesizing plausible-looking JSON) : the only way to
|
|
20
20
|
# know whether a step actually reached a new state, or which id an
|
|
21
21
|
# auto-minted entity landed on, is to run it and watch what happened.
|
|
22
22
|
#
|
|
23
23
|
# Single-call fuzzing mostly misses the bugs this project has actually
|
|
24
|
-
# found — they needed
|
|
24
|
+
# found — they needed state first (a saga leg acting on a transfer that
|
|
25
25
|
# already exists, a reference pointing at a customer already registered).
|
|
26
26
|
# So this tracks what it has created as it goes, the same way
|
|
27
27
|
# spec/banking_state_machine_spec.rb's hand-written generator does, and
|
|
@@ -47,18 +47,18 @@ module Hecks
|
|
|
47
47
|
CREATING_WEIGHT = 2
|
|
48
48
|
|
|
49
49
|
# How often a payload is deliberately the wrong shape. Low, because a
|
|
50
|
-
# refused step reaches no new state and a sequence of them is
|
|
50
|
+
# refused step reaches no new state and a sequence of them is silent.
|
|
51
51
|
MALFORMED_ARGUMENT_PROBABILITY = 0.12
|
|
52
52
|
|
|
53
|
-
#
|
|
53
|
+
# How often an optional argument is simply not given — a fair coin,
|
|
54
54
|
# because that is exactly what `optional:` means: present or absent,
|
|
55
55
|
# both legal, neither the interesting case.
|
|
56
56
|
#
|
|
57
|
-
# This is
|
|
58
|
-
# drops an argument too, but a dropped
|
|
59
|
-
# a refusal writes no record, and bin/fuzz counts it
|
|
57
|
+
# This is not a malformation and must not be filed as one. `malform`
|
|
58
|
+
# drops an argument too, but a dropped required argument is refused,
|
|
59
|
+
# a refusal writes no record, and bin/fuzz counts it silent — so the
|
|
60
60
|
# one outcome worth reaching (a stored record carrying a null, then
|
|
61
|
-
#
|
|
61
|
+
# queried) was unreachable from that path by construction. Until
|
|
62
62
|
# this existed no generated history contained a null at all, which
|
|
63
63
|
# meant `query_answers_match_reference` — the differential that
|
|
64
64
|
# diffs every native adapter against the reference interpreter — had
|
|
@@ -72,11 +72,11 @@ module Hecks
|
|
|
72
72
|
# How strongly an unexercised verb is preferred over one this sequence has
|
|
73
73
|
# already dispatched. Random picking revisits the same handful of verbs and
|
|
74
74
|
# leaves whole commands untouched for a whole run — which is the same
|
|
75
|
-
# "reached no interesting state" problem the
|
|
75
|
+
# "reached no interesting state" problem the silent count reports, seen from
|
|
76
76
|
# the generating end rather than the scoring end.
|
|
77
77
|
UNEXERCISED_WEIGHT = 4
|
|
78
78
|
|
|
79
|
-
# `adversarial:` — the fraction of generated
|
|
79
|
+
# `adversarial:` — the fraction of generated command steps (0.0..1.0)
|
|
80
80
|
# that get one deliberately adversarial argument mutation
|
|
81
81
|
# (adversary.rb — the shapes BUG#7–#16 were found through). `0.0`,
|
|
82
82
|
# the default, draws nothing extra from the seeded RNG, so a seed's
|
|
@@ -89,22 +89,49 @@ module Hecks
|
|
|
89
89
|
# `dry_run_draw?`): `0.0` draws nothing, so every pinned seed is
|
|
90
90
|
# byte-for-byte what it was; `bin/qa_sweep` reads them from
|
|
91
91
|
# `QualityControlDials::ROLE_DRAW_PROBABILITY`/`DRY_RUN_FRACTION`.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
#
|
|
93
|
+
# `prefix:` / `favor:` — the two levers `CoverageCampaign` pulls
|
|
94
|
+
# (coverage_campaign.rb has the why). `prefix:` is `{ "seed", "steps",
|
|
95
|
+
# "favor", "prefix" }`: re-generate that seed's first `steps` attempts
|
|
96
|
+
# (itself recursively prefixed) before this seed's own randomness
|
|
97
|
+
# starts, so a seed can begin from state an earlier seed reached.
|
|
98
|
+
# `favor:` names verbs the picker weights up. `nil`/`[]`, the
|
|
99
|
+
# defaults, draw nothing extra and change nothing: every pinned seed
|
|
100
|
+
# is byte-for-byte what it was.
|
|
101
|
+
def self.generate(domain_path, seed:, steps:, **)
|
|
102
|
+
new(domain_path, seed: seed, steps: steps, **).call
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The same generation, with what it reached — `coverage` is
|
|
106
|
+
# `[[attempt_index, tuple], ...]` (`coverage_tuple`), `verbs` every
|
|
107
|
+
# verb the booted catalog offered, so a campaign can tell a verb it
|
|
108
|
+
# never hit from one that does not exist.
|
|
109
|
+
Trace = Struct.new(:steps, :coverage, :verbs, keyword_init: true)
|
|
110
|
+
|
|
111
|
+
def self.trace(domain_path, seed:, steps:, **)
|
|
112
|
+
generator = new(domain_path, seed: seed, steps: steps, **)
|
|
113
|
+
Trace.new(steps: generator.call, coverage: generator.coverage, verbs: generator.verbs)
|
|
95
114
|
end
|
|
96
115
|
|
|
97
|
-
# How
|
|
116
|
+
# How strongly a `favor:` verb is preferred when it is eligible —
|
|
117
|
+
# the same order of magnitude as an unexercised verb, so favor
|
|
118
|
+
# steers without drowning out what this sequence has not touched.
|
|
119
|
+
FAVOR_WEIGHT = 4
|
|
120
|
+
|
|
121
|
+
attr_reader :coverage, :verbs
|
|
122
|
+
|
|
123
|
+
# How many events the generated sequence actually produced — not
|
|
98
124
|
# steps, not successful dispatches, but the sum of every Result#events
|
|
99
125
|
# length across the run. This is the count bin/fuzz declares as the
|
|
100
126
|
# script's own `expectations.events` claim: whatever was achieved
|
|
101
|
-
#
|
|
127
|
+
# during generation becomes the claim a fresh replay of the same
|
|
102
128
|
# script is held to. Zero means the sequence never
|
|
103
129
|
# reached an interesting state — a fuzzer-effectiveness fact, not a
|
|
104
130
|
# replay one.
|
|
105
131
|
attr_reader :event_count
|
|
106
132
|
|
|
107
|
-
def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0
|
|
133
|
+
def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0,
|
|
134
|
+
prefix: nil, favor: [])
|
|
108
135
|
{ adversarial: adversarial, role_draw: role_draw, dry_run: dry_run }.each do |name, fraction|
|
|
109
136
|
next if fraction.is_a?(Numeric) && fraction.between?(0, 1)
|
|
110
137
|
|
|
@@ -122,41 +149,102 @@ module Hecks
|
|
|
122
149
|
@known_ids = Hash.new { |h, k| h[k] = [] }
|
|
123
150
|
@entity_known_ids = Hash.new { |h, k| h[k] = [] }
|
|
124
151
|
@appended_identities = Hash.new { |h, k| h[k] = [] }
|
|
125
|
-
#
|
|
152
|
+
# Role => [actor ids] this sequence's own successful
|
|
126
153
|
# `Governance::RoleAssignment.Assign` steps granted — what the
|
|
127
154
|
# `actor_known` caller shape draws from (adversary.rb).
|
|
128
155
|
@granted = Hash.new { |h, k| h[k] = [] }
|
|
129
156
|
@precedence_caller = nil
|
|
130
157
|
@exercised = Set.new
|
|
131
158
|
@event_count = 0
|
|
159
|
+
@prefix = prefix
|
|
160
|
+
@favor = Array(favor)
|
|
161
|
+
@own_favor = @favor
|
|
162
|
+
@coverage = []
|
|
163
|
+
@verbs = []
|
|
164
|
+
@attempt = 0
|
|
132
165
|
end
|
|
133
166
|
|
|
134
167
|
def call
|
|
135
168
|
# Real leftover data from ordinary use (bin/console, whatever) lives
|
|
136
169
|
# under the example's data/ — a generator that boots against it
|
|
137
170
|
# starts from state its own known_ids tracking doesn't know about.
|
|
138
|
-
# IsolatedBoot resets that
|
|
171
|
+
# IsolatedBoot resets that and rebinds persistence to Memory, since
|
|
139
172
|
# a Postgres-bound domain's real store lives outside the copied
|
|
140
173
|
# directory entirely and `rm_rf`ing data/ alone cannot reach it —
|
|
141
174
|
# see isolated_boot.rb's own header.
|
|
142
175
|
IsolatedBoot.call(@domain_path, adapter: @adapter) do |copy|
|
|
143
176
|
runtime = Hecks.boot(copy)
|
|
144
177
|
catalog = build_catalog(runtime)
|
|
145
|
-
|
|
178
|
+
@verbs = catalog.values_at(:creating, :instance, :entity_commands, :queries, :entity_queries, :read_models)
|
|
179
|
+
.flatten.map { |entry| entry[:verb] }.uniq
|
|
180
|
+
|
|
181
|
+
steps = []
|
|
182
|
+
if @prefix
|
|
183
|
+
realize_prefix(runtime, catalog, @prefix, prefix_limit(@prefix), steps)
|
|
184
|
+
@random = Random.new(@seed)
|
|
185
|
+
@favor = @own_favor
|
|
186
|
+
end
|
|
187
|
+
@step_count.times { steps << attempt_step(runtime, catalog) }
|
|
188
|
+
steps.compact
|
|
146
189
|
end
|
|
147
190
|
end
|
|
148
191
|
|
|
149
192
|
private
|
|
150
193
|
|
|
194
|
+
# A prefix is the first `limit` attempts of another seed's generation,
|
|
195
|
+
# re-run for real: its own nested prefix first (capped the same way it
|
|
196
|
+
# was capped when that seed was generated), then that seed's own
|
|
197
|
+
# `Random.new(seed)` and favor for the rest. Same inputs, same
|
|
198
|
+
# catalog, same draws — the same steps, and the same known ids and
|
|
199
|
+
# exercised verbs carried forward into this seed.
|
|
200
|
+
#
|
|
201
|
+
# The prefix is on top of this seed's own budget, not out of it. A
|
|
202
|
+
# spliced seed still makes all `steps` attempts of its own after the
|
|
203
|
+
# prefix; a prefix is capped at `steps` attempts, so a spliced
|
|
204
|
+
# sequence is at most twice as long as an unspliced one. Taking the
|
|
205
|
+
# prefix out of the budget (the first version of this) left a spliced
|
|
206
|
+
# seed replaying state already seen with almost nothing left to
|
|
207
|
+
# explore from it — measured: fewer distinct tuples than unguided.
|
|
208
|
+
def realize_prefix(runtime, catalog, spec, limit, steps)
|
|
209
|
+
return 0 unless limit.positive?
|
|
210
|
+
|
|
211
|
+
inner = spec["prefix"]
|
|
212
|
+
used = inner ? realize_prefix(runtime, catalog, inner, [prefix_limit(inner), limit].min, steps) : 0
|
|
213
|
+
@random = Random.new(Integer(spec.fetch("seed")))
|
|
214
|
+
@favor = Array(spec["favor"])
|
|
215
|
+
(limit - used).times { steps << attempt_step(runtime, catalog) }
|
|
216
|
+
limit
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def prefix_limit(spec) = Integer(spec.fetch("steps")).clamp(0, @step_count)
|
|
220
|
+
|
|
151
221
|
def attempt_step(runtime, catalog)
|
|
222
|
+
index = @attempt
|
|
223
|
+
@attempt += 1
|
|
152
224
|
entry = pick(catalog)
|
|
153
225
|
return nil unless entry
|
|
154
226
|
|
|
155
227
|
@exercised << entry[:verb]
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
228
|
+
@state_before = "-"
|
|
229
|
+
step =
|
|
230
|
+
if entry[:query] then build_query_step(runtime, entry)
|
|
231
|
+
elsif entry[:model] then build_read_model_step(runtime, entry)
|
|
232
|
+
else build_command_step(runtime, catalog, entry)
|
|
233
|
+
end
|
|
234
|
+
@coverage << [index, coverage_tuple(entry, step)]
|
|
235
|
+
step
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# `verb | kind | state before | mutation | outcome` — see
|
|
239
|
+
# CoverageCampaign's header for why the unit is this and not the
|
|
240
|
+
# verb. `state` is the addressed aggregate's lifecycle value (or
|
|
241
|
+
# `exists`/`absent` for one without a lifecycle) read just before
|
|
242
|
+
# dispatch; `mutation` names every adversarial mutation and its shape;
|
|
243
|
+
# `outcome` is `ok` or the refusal class `safe_call` rescued.
|
|
244
|
+
def coverage_tuple(entry, step)
|
|
245
|
+
kind = %w[verb query dry_run].find { |key| step.key?(key) }
|
|
246
|
+
mutation = Array(step["adversarial"]).map { |m| [m["mutation"], m["shape"]].compact.join(":") }.join("+")
|
|
247
|
+
[entry[:verb], kind, @state_before, mutation.empty? ? "-" : mutation, @last_outcome].join(" | ")
|
|
160
248
|
end
|
|
161
249
|
end
|
|
162
250
|
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# A failing step list, made small enough to read.
|
|
4
|
+
#
|
|
5
|
+
# `bin/fuzz` always shrank its findings; `bin/qa_sweep` — the loop that
|
|
6
|
+
# actually finds the bugs — never did, so the ledger filled with
|
|
7
|
+
# demonstrations like "seed 25, 25 steps, step 16" and a human cut
|
|
8
|
+
# BUG#40 down to its four real steps by hand. This is `bin/fuzz`'s
|
|
9
|
+
# shrinker lifted out of that script so both callers share it, and
|
|
10
|
+
# made cheaper at the same time: a sweep's candidate check can cost a
|
|
11
|
+
# Rust subprocess (or a real Postgres round trip), where `bin/fuzz`'s
|
|
12
|
+
# only ever cost an in-process replay.
|
|
13
|
+
#
|
|
14
|
+
# The caller owns "same finding". `call` is handed a block that
|
|
15
|
+
# answers true when a candidate step list still reproduces what the
|
|
16
|
+
# original did — `bin/fuzz` compares its own verdict signature,
|
|
17
|
+
# `bin/qa_sweep` compares `Shrinker.signature` of one mode's
|
|
18
|
+
# divergences. This module never replays anything itself, so it has
|
|
19
|
+
# no idea which engine, adapter or comparison it is minimizing for.
|
|
20
|
+
#
|
|
21
|
+
# **Two passes, in order**:
|
|
22
|
+
#
|
|
23
|
+
# 1. Steps, chunks first. Removing one step at a time (what
|
|
24
|
+
# `bin/fuzz` did) costs O(n²) candidate checks on a sequence
|
|
25
|
+
# where most steps are irrelevant. Delta-debugging style: try
|
|
26
|
+
# removing halves, then quarters, … then single steps, keeping
|
|
27
|
+
# any removal that still reproduces; a single-step pass repeats
|
|
28
|
+
# until it changes nothing, so the result is 1-minimal (no one
|
|
29
|
+
# remaining step can be dropped).
|
|
30
|
+
# 2. Arguments, inside each surviving step. Unchanged from `bin/fuzz`
|
|
31
|
+
# (spec/bin_fuzz_spec.rb pins its accumulation contract): drop
|
|
32
|
+
# one key at a time from the step's current args, keep it dropped
|
|
33
|
+
# only while the finding still reproduces.
|
|
34
|
+
#
|
|
35
|
+
# A budget, because a sweep has other targets waiting. `budget:` caps
|
|
36
|
+
# how many candidate checks one call may spend (nil = unbounded, the
|
|
37
|
+
# `bin/fuzz` behaviour). When it runs out the best candidate found so
|
|
38
|
+
# far is returned — every accepted candidate reproduced, so a partial
|
|
39
|
+
# shrink is still a correct, just less small, demonstration.
|
|
40
|
+
module Shrinker
|
|
41
|
+
Result = Struct.new(:steps, :attempts, :exhausted, keyword_init: true)
|
|
42
|
+
|
|
43
|
+
module_function
|
|
44
|
+
|
|
45
|
+
def call(steps, budget: nil, &reproduces)
|
|
46
|
+
raise ArgumentError, "Shrinker.call needs a block answering whether a candidate reproduces" unless reproduces
|
|
47
|
+
|
|
48
|
+
meter = Meter.new(budget)
|
|
49
|
+
current = drop_steps(steps.dup, meter, &reproduces)
|
|
50
|
+
current = drop_arguments(current, meter, &reproduces)
|
|
51
|
+
Result.new(steps: current, attempts: meter.used, exhausted: meter.exhausted?)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def drop_steps(steps, meter, &reproduces)
|
|
55
|
+
current = steps
|
|
56
|
+
chunk = [current.length / 2, 1].max
|
|
57
|
+
loop do
|
|
58
|
+
changed = false
|
|
59
|
+
index = 0
|
|
60
|
+
while index < current.length
|
|
61
|
+
return current if meter.exhausted?
|
|
62
|
+
|
|
63
|
+
candidate = current[0...index] + (current[(index + chunk)..] || [])
|
|
64
|
+
if candidate.empty?
|
|
65
|
+
index += chunk
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if meter.try { reproduces.call(candidate) }
|
|
70
|
+
current = candidate
|
|
71
|
+
changed = true
|
|
72
|
+
else
|
|
73
|
+
index += chunk
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if chunk > 1
|
|
78
|
+
chunk = [chunk / 2, 1].max
|
|
79
|
+
elsif !changed
|
|
80
|
+
break
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
current
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# See the module header's pass 2 — `args` is read by whichever
|
|
87
|
+
# spelling the step actually carries (`key?` first, never `||`,
|
|
88
|
+
# which cannot tell a stored `false` from an absent key).
|
|
89
|
+
def drop_arguments(steps, meter, &reproduces)
|
|
90
|
+
steps.each_index do |position|
|
|
91
|
+
original = args_of(steps[position])
|
|
92
|
+
next unless original.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
original.each_key do |key|
|
|
95
|
+
return steps if meter.exhausted?
|
|
96
|
+
|
|
97
|
+
step = steps[position]
|
|
98
|
+
trimmed = args_of(step).reject { |name, _| name == key }
|
|
99
|
+
candidate = steps.map(&:dup)
|
|
100
|
+
candidate[position] = step.merge("args" => trimmed)
|
|
101
|
+
steps = candidate if meter.try { reproduces.call(candidate) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
steps
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def args_of(step)
|
|
108
|
+
step.key?("args") ? step["args"] : step[:args]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Which finding this is, as a set of stable strings — the identity a
|
|
112
|
+
# shrink candidate has to keep. The `field` alone is too loose for
|
|
113
|
+
# the comparisons that carry lists: "refusals differ" on a 3-step
|
|
114
|
+
# candidate could be a different refusal split than the one the
|
|
115
|
+
# original seed found, and a shrinker that accepted it would hand
|
|
116
|
+
# back a demonstration of the wrong bug. So:
|
|
117
|
+
#
|
|
118
|
+
# refusals/queries/dry_runs/reactions — the field plus every verb
|
|
119
|
+
# (or query) named in the symmetric difference of the two sides;
|
|
120
|
+
# instances — the field plus the aggregate of every top-level key
|
|
121
|
+
# whose two sides disagree (the `#id` suffix dropped);
|
|
122
|
+
# a crash/process finding — the field plus the exception class
|
|
123
|
+
# leading its detail;
|
|
124
|
+
# anything else (a property name, a self-consistency axis) — the
|
|
125
|
+
# field, which already names the finding.
|
|
126
|
+
#
|
|
127
|
+
# `reproduces?` holds when a candidate's signature contains the
|
|
128
|
+
# original's: removing steps may add a second divergence, but it
|
|
129
|
+
# must never lose the one being demonstrated.
|
|
130
|
+
LIST_FIELDS = %w[refusals queries dry_runs reactions].freeze
|
|
131
|
+
|
|
132
|
+
CRASH_FIELDS = %w[crash process generator_crash].freeze
|
|
133
|
+
|
|
134
|
+
def signature(divergences)
|
|
135
|
+
divergences.each_with_object(Set.new) do |divergence, keys|
|
|
136
|
+
field = divergence[:field].to_s
|
|
137
|
+
keys << field
|
|
138
|
+
keys.merge(detail_keys(field, divergence))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def detail_keys(field, divergence)
|
|
143
|
+
left, right = divergence.except(:field, :detail).values.select { |v| v.is_a?(Array) || v.is_a?(Hash) }
|
|
144
|
+
if LIST_FIELDS.include?(field) then list_keys(field, left, right)
|
|
145
|
+
elsif field == "instances" then instance_keys(left, right)
|
|
146
|
+
elsif CRASH_FIELDS.include?(field) && divergence[:detail]
|
|
147
|
+
["#{field}:#{divergence[:detail].to_s[/\A\w+(?:::\w+)*/]}"]
|
|
148
|
+
else []
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def list_keys(field, left, right)
|
|
153
|
+
return [] unless left.is_a?(Array) && right.is_a?(Array)
|
|
154
|
+
|
|
155
|
+
((left - right) + (right - left)).map { |row| "#{field}:#{named(row)}" }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Instance keys are `Aggregate#id` on the wire — the id is whatever
|
|
159
|
+
# the generator minted, so it names the record, not the finding;
|
|
160
|
+
# keeping it would pin every creating step.
|
|
161
|
+
def instance_keys(left, right)
|
|
162
|
+
return [] unless left.is_a?(Hash) && right.is_a?(Hash)
|
|
163
|
+
|
|
164
|
+
(left.keys | right.keys).reject { |key| left[key] == right[key] }
|
|
165
|
+
.map { |key| "instances:#{key.to_s.split('#').first}" }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def reproduces?(original_signature, divergences)
|
|
169
|
+
!divergences.empty? && original_signature.subset?(signature(divergences))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def named(row)
|
|
173
|
+
return row.to_s unless row.is_a?(Hash)
|
|
174
|
+
|
|
175
|
+
key = [%w[verb query policy], %i[verb query policy]].flatten.find { |name| row.key?(name) }
|
|
176
|
+
(key ? row[key] : row).to_s
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Counts candidate checks against the budget.
|
|
180
|
+
class Meter
|
|
181
|
+
attr_reader :used
|
|
182
|
+
|
|
183
|
+
def initialize(budget)
|
|
184
|
+
@budget = budget
|
|
185
|
+
@used = 0
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def exhausted? = !@budget.nil? && @used >= @budget
|
|
189
|
+
|
|
190
|
+
def try
|
|
191
|
+
@used += 1
|
|
192
|
+
yield
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|