hecks 1.2.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 +60 -2
- 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/postgres_era.adapter +5 -0
- 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 +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -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 +27 -3
- 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 +122 -86
- 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 +301 -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 +183 -20
- 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 +361 -0
- 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 +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- 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 +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -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 +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- 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 +160 -58
- 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 +44 -6
- 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 +338 -83
- 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 +29 -6
- 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 +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- 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 +15 -22
- 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 +132 -24
- 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 +64 -67
- 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 +94 -19
- 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 +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- 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 +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- 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 -112
- 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 +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- 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 +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- 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 +207 -5
- 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 +43 -2
|
@@ -7,6 +7,7 @@ require_relative "sequence_generator/catalog"
|
|
|
7
7
|
require_relative "sequence_generator/picker"
|
|
8
8
|
require_relative "sequence_generator/step_builder"
|
|
9
9
|
require_relative "sequence_generator/outcome_tracker"
|
|
10
|
+
require_relative "sequence_generator/adversary"
|
|
10
11
|
|
|
11
12
|
module Hecks
|
|
12
13
|
module Fuzzing
|
|
@@ -14,13 +15,13 @@ module Hecks
|
|
|
14
15
|
# `{name, note, steps}` shape `spec/corpus/*.json` already uses — so a
|
|
15
16
|
# generated sequence replays as a corpus member, completely unchanged.
|
|
16
17
|
# Boots a throwaway copy of
|
|
17
|
-
# the domain and
|
|
18
|
+
# the domain and dispatches each candidate step for real as it builds the
|
|
18
19
|
# sequence (not just synthesizing plausible-looking JSON) : the only way to
|
|
19
20
|
# know whether a step actually reached a new state, or which id an
|
|
20
21
|
# auto-minted entity landed on, is to run it and watch what happened.
|
|
21
22
|
#
|
|
22
23
|
# Single-call fuzzing mostly misses the bugs this project has actually
|
|
23
|
-
# found — they needed
|
|
24
|
+
# found — they needed state first (a saga leg acting on a transfer that
|
|
24
25
|
# already exists, a reference pointing at a customer already registered).
|
|
25
26
|
# So this tracks what it has created as it goes, the same way
|
|
26
27
|
# spec/banking_state_machine_spec.rb's hand-written generator does, and
|
|
@@ -28,13 +29,16 @@ module Hecks
|
|
|
28
29
|
#
|
|
29
30
|
# One concern per file beside this one: what the domain offers
|
|
30
31
|
# (sequence_generator/catalog.rb), which step to try next (picker.rb),
|
|
31
|
-
# how a step is built and dispatched (step_builder.rb),
|
|
32
|
-
# success taught us (outcome_tracker.rb)
|
|
32
|
+
# how a step is built and dispatched (step_builder.rb), what a
|
|
33
|
+
# success taught us (outcome_tracker.rb), and — opt-in, `adversarial:`
|
|
34
|
+
# — the argument shapes real Ruby/Rust divergences were found through
|
|
35
|
+
# (adversary.rb).
|
|
33
36
|
class SequenceGenerator
|
|
34
37
|
include Catalog
|
|
35
38
|
include Picker
|
|
36
39
|
include StepBuilder
|
|
37
40
|
include OutcomeTracker
|
|
41
|
+
include Adversary
|
|
38
42
|
|
|
39
43
|
# Creating commands are always eligible ; weighting them heavier (not
|
|
40
44
|
# exclusively — a domain with only one or two aggregates would starve
|
|
@@ -43,18 +47,18 @@ module Hecks
|
|
|
43
47
|
CREATING_WEIGHT = 2
|
|
44
48
|
|
|
45
49
|
# How often a payload is deliberately the wrong shape. Low, because a
|
|
46
|
-
# 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.
|
|
47
51
|
MALFORMED_ARGUMENT_PROBABILITY = 0.12
|
|
48
52
|
|
|
49
|
-
#
|
|
53
|
+
# How often an optional argument is simply not given — a fair coin,
|
|
50
54
|
# because that is exactly what `optional:` means: present or absent,
|
|
51
55
|
# both legal, neither the interesting case.
|
|
52
56
|
#
|
|
53
|
-
# This is
|
|
54
|
-
# drops an argument too, but a dropped
|
|
55
|
-
# 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
|
|
56
60
|
# one outcome worth reaching (a stored record carrying a null, then
|
|
57
|
-
#
|
|
61
|
+
# queried) was unreachable from that path by construction. Until
|
|
58
62
|
# this existed no generated history contained a null at all, which
|
|
59
63
|
# meant `query_answers_match_reference` — the differential that
|
|
60
64
|
# diffs every native adapter against the reference interpreter — had
|
|
@@ -68,62 +72,179 @@ module Hecks
|
|
|
68
72
|
# How strongly an unexercised verb is preferred over one this sequence has
|
|
69
73
|
# already dispatched. Random picking revisits the same handful of verbs and
|
|
70
74
|
# leaves whole commands untouched for a whole run — which is the same
|
|
71
|
-
# "reached no interesting state" problem the
|
|
75
|
+
# "reached no interesting state" problem the silent count reports, seen from
|
|
72
76
|
# the generating end rather than the scoring end.
|
|
73
77
|
UNEXERCISED_WEIGHT = 4
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
# `adversarial:` — the fraction of generated command steps (0.0..1.0)
|
|
80
|
+
# that get one deliberately adversarial argument mutation
|
|
81
|
+
# (adversary.rb — the shapes BUG#7–#16 were found through). `0.0`,
|
|
82
|
+
# the default, draws nothing extra from the seeded RNG, so a seed's
|
|
83
|
+
# output is byte-for-byte what it was before the option existed;
|
|
84
|
+
# any positive value is just as deterministic per seed, since every
|
|
85
|
+
# choice the mutation makes comes from the same `Random.new(seed)`.
|
|
86
|
+
#
|
|
87
|
+
# `role_draw:` / `dry_run:` — two more opt-in fractions with the
|
|
88
|
+
# identical contract (adversary.rb's `caller_draw!`, step_builder.rb's
|
|
89
|
+
# `dry_run_draw?`): `0.0` draws nothing, so every pinned seed is
|
|
90
|
+
# byte-for-byte what it was; `bin/qa_sweep` reads them from
|
|
91
|
+
# `QualityControlDials::ROLE_DRAW_PROBABILITY`/`DRY_RUN_FRACTION`.
|
|
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)
|
|
77
114
|
end
|
|
78
115
|
|
|
79
|
-
# 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
|
|
80
124
|
# steps, not successful dispatches, but the sum of every Result#events
|
|
81
125
|
# length across the run. This is the count bin/fuzz declares as the
|
|
82
126
|
# script's own `expectations.events` claim: whatever was achieved
|
|
83
|
-
#
|
|
127
|
+
# during generation becomes the claim a fresh replay of the same
|
|
84
128
|
# script is held to. Zero means the sequence never
|
|
85
129
|
# reached an interesting state — a fuzzer-effectiveness fact, not a
|
|
86
130
|
# replay one.
|
|
87
131
|
attr_reader :event_count
|
|
88
132
|
|
|
89
|
-
def initialize(domain_path, seed:, steps:, adapter: :memory
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@
|
|
98
|
-
@
|
|
133
|
+
def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0,
|
|
134
|
+
prefix: nil, favor: [])
|
|
135
|
+
{ adversarial: adversarial, role_draw: role_draw, dry_run: dry_run }.each do |name, fraction|
|
|
136
|
+
next if fraction.is_a?(Numeric) && fraction.between?(0, 1)
|
|
137
|
+
|
|
138
|
+
raise ArgumentError, "#{name}: must be a fraction between 0.0 and 1.0, got #{fraction.inspect}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
@domain_path = domain_path
|
|
142
|
+
@seed = seed
|
|
143
|
+
@step_count = steps
|
|
144
|
+
@adapter = adapter
|
|
145
|
+
@adversarial = adversarial.to_f
|
|
146
|
+
@role_draw = role_draw.to_f
|
|
147
|
+
@dry_run = dry_run.to_f
|
|
148
|
+
@random = Random.new(seed)
|
|
149
|
+
@known_ids = Hash.new { |h, k| h[k] = [] }
|
|
150
|
+
@entity_known_ids = Hash.new { |h, k| h[k] = [] }
|
|
151
|
+
@appended_identities = Hash.new { |h, k| h[k] = [] }
|
|
152
|
+
# Role => [actor ids] this sequence's own successful
|
|
153
|
+
# `Governance::RoleAssignment.Assign` steps granted — what the
|
|
154
|
+
# `actor_known` caller shape draws from (adversary.rb).
|
|
155
|
+
@granted = Hash.new { |h, k| h[k] = [] }
|
|
156
|
+
@precedence_caller = nil
|
|
157
|
+
@exercised = Set.new
|
|
158
|
+
@event_count = 0
|
|
159
|
+
@prefix = prefix
|
|
160
|
+
@favor = Array(favor)
|
|
161
|
+
@own_favor = @favor
|
|
162
|
+
@coverage = []
|
|
163
|
+
@verbs = []
|
|
164
|
+
@attempt = 0
|
|
99
165
|
end
|
|
100
166
|
|
|
101
167
|
def call
|
|
102
168
|
# Real leftover data from ordinary use (bin/console, whatever) lives
|
|
103
169
|
# under the example's data/ — a generator that boots against it
|
|
104
170
|
# starts from state its own known_ids tracking doesn't know about.
|
|
105
|
-
# IsolatedBoot resets that
|
|
171
|
+
# IsolatedBoot resets that and rebinds persistence to Memory, since
|
|
106
172
|
# a Postgres-bound domain's real store lives outside the copied
|
|
107
173
|
# directory entirely and `rm_rf`ing data/ alone cannot reach it —
|
|
108
174
|
# see isolated_boot.rb's own header.
|
|
109
175
|
IsolatedBoot.call(@domain_path, adapter: @adapter) do |copy|
|
|
110
176
|
runtime = Hecks.boot(copy)
|
|
111
177
|
catalog = build_catalog(runtime)
|
|
112
|
-
|
|
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
|
|
113
189
|
end
|
|
114
190
|
end
|
|
115
191
|
|
|
116
192
|
private
|
|
117
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
|
+
|
|
118
221
|
def attempt_step(runtime, catalog)
|
|
222
|
+
index = @attempt
|
|
223
|
+
@attempt += 1
|
|
119
224
|
entry = pick(catalog)
|
|
120
225
|
return nil unless entry
|
|
121
226
|
|
|
122
227
|
@exercised << entry[:verb]
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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(" | ")
|
|
127
248
|
end
|
|
128
249
|
end
|
|
129
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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# What each query verb the differential comparison skipped was hiding.
|
|
4
|
+
#
|
|
5
|
+
# `Differential.manifest_partition` drops a named query or read model
|
|
6
|
+
# from both sides of the Ruby/Rust comparison only when the binary's own
|
|
7
|
+
# manifest.json declares it `generated: false`. The drop is correct, but
|
|
8
|
+
# a sweep that dropped every `offset`/`cursor` ask would still read as
|
|
9
|
+
# "agreed across all steps". `bin/qa_sweep` therefore logs one Check per
|
|
10
|
+
# sweep naming every skipped verb and the construct family that caused
|
|
11
|
+
# it, and marks it Surprised when a family is outside
|
|
12
|
+
# `QualityControlDials::STRUCTURAL_REFUSAL_BOUNDARY`.
|
|
13
|
+
#
|
|
14
|
+
# The construct comes straight from the manifest entry, written by the
|
|
15
|
+
# generator branch that made the skip (`Projector::SkipReason`). This
|
|
16
|
+
# module used to guess it from the Ruby declaration's `to_h` keys, which
|
|
17
|
+
# was coarser than codegen's own decision and could drift from it.
|
|
18
|
+
module StructuralSkips
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# One entry per skipped verb: `{ verb:, constructs: [...] }`, sorted by
|
|
22
|
+
# verb so the printed observation is stable across seeds and runs. A
|
|
23
|
+
# verb the manifest doesn't declare answers `unknown`, which no
|
|
24
|
+
# boundary admits.
|
|
25
|
+
def attribute(gaps, verbs)
|
|
26
|
+
verbs.sort.map do |verb|
|
|
27
|
+
entry = gaps.not_generated(verb)
|
|
28
|
+
{ verb: verb, constructs: entry ? [entry.fetch("construct")] : %w[unknown] }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Every skipped verb whose constructs are not all inside `boundary`.
|
|
33
|
+
def outside_boundary(attributed, boundary)
|
|
34
|
+
admitted = boundary.map(&:to_s)
|
|
35
|
+
attributed.select { |entry| entry[:constructs].empty? || (entry[:constructs] - admitted).any? }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# How hard one sweep fuzzes, as a function of `Target.clean_streak`
|
|
4
|
+
# (qa/bluebook/quality_control.bluebook — that attribute's own comment
|
|
5
|
+
# says what the streak means). The sibling of `RotationPriority`: the
|
|
6
|
+
# ledger holds the number, `QualityControlDials::WIDENING_TIERS` holds
|
|
7
|
+
# the policy, and this module is where the two meet — a pure function
|
|
8
|
+
# of both, nothing else read.
|
|
9
|
+
#
|
|
10
|
+
# Why this is not in `bin/qa_sweep` any more. It was — `WIDENING_TIERS`
|
|
11
|
+
# and `widen_for_streak` lived at the top of that script, which meant
|
|
12
|
+
# policy data lived in a script (unlike every other dial, which lives
|
|
13
|
+
# in the bluebook a human edits and reviews) and duplicated itself as
|
|
14
|
+
# prose in SKILL.md. Now the table is a dial, this is the one reader,
|
|
15
|
+
# and `bin/qa_sweep` calls it the way it already calls
|
|
16
|
+
# `RotationPriority.pick`.
|
|
17
|
+
#
|
|
18
|
+
# **Pure, deliberately** — the same discipline `RotationPriority` keeps:
|
|
19
|
+
# same streak in, same `[seeds, steps]` out, every time, which is what
|
|
20
|
+
# lets a human predict what a given sweep is about to do before it
|
|
21
|
+
# runs one. `tiers:` defaults to the dial but is a plain argument, so
|
|
22
|
+
# a unit spec can pass its own table with no ledger boot at all and a
|
|
23
|
+
# `bin/qa_sweep` run against a ledger that declares no dials (an
|
|
24
|
+
# isolated spec's own fixture) can fall back the same way it already
|
|
25
|
+
# does for `ADVERSARIAL_FRACTION`.
|
|
26
|
+
module SweepDepth
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# The fallback table, for a boot with no `QualityControlDials` at all
|
|
30
|
+
# — the same shape and the same three rows the dial ships with, so a
|
|
31
|
+
# dial-less ledger fuzzes exactly as a dialled one does by default.
|
|
32
|
+
# Never read when the dial exists; `bin/qa_sweep` passes the dial in.
|
|
33
|
+
DEFAULT_TIERS = [
|
|
34
|
+
{ upto: 4, seeds: 10, steps: 25 },
|
|
35
|
+
{ upto: 19, seeds: 25, steps: 50 },
|
|
36
|
+
{ upto: Float::INFINITY, seeds: 50, steps: 100 }
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# `[seeds, steps]` for one streak. The first row whose `upto` the
|
|
40
|
+
# streak does not exceed wins — rows are read in order, so the table
|
|
41
|
+
# must be ascending, and the last row's `Float::INFINITY` is what
|
|
42
|
+
# makes it the ceiling rather than a gap.
|
|
43
|
+
def for_streak(streak, tiers: DEFAULT_TIERS)
|
|
44
|
+
raise ArgumentError, "streak must not be negative" if streak.negative?
|
|
45
|
+
|
|
46
|
+
tier = tiers.find { |row| streak <= row[:upto] }
|
|
47
|
+
raise ArgumentError, "no tier covers a streak of #{streak} — the last row must be upto: Float::INFINITY" unless tier
|
|
48
|
+
|
|
49
|
+
[tier[:seeds], tier[:steps]]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|