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
|
@@ -3,56 +3,57 @@ require "tmpdir"
|
|
|
3
3
|
require "tempfile"
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "isolated_boot"
|
|
6
|
+
require_relative "../naming"
|
|
6
7
|
|
|
7
8
|
module Hecks
|
|
8
9
|
module Fuzzing
|
|
9
|
-
# A
|
|
10
|
+
# **A generated sequence, raced for real** — `spec/adapters/driven/
|
|
10
11
|
# postgres_era_concurrent_dispatch_spec.rb` proves ADR 0036's own fix
|
|
11
12
|
# (a real `pg_advisory_xact_lock` serializes a PostgresEra-bound
|
|
12
|
-
# dispatch across separate OS processes) against
|
|
13
|
-
# fixture and
|
|
14
|
-
# against a `$10,000` account). This module asks the
|
|
13
|
+
# dispatch across separate OS processes) against one hand-authored
|
|
14
|
+
# fixture and one hand-picked conflicting pair (two `$6,000` Debits
|
|
15
|
+
# against a `$10,000` account). This module asks the same question —
|
|
15
16
|
# does the cross-process write lock actually serialize concurrent
|
|
16
|
-
# writers? — of an
|
|
17
|
+
# writers? — of an arbitrary generated sequence against a real target
|
|
17
18
|
# domain, so the practice's own adversarial generator gets to pick the
|
|
18
19
|
# conflict instead of a human picking it once and never again.
|
|
19
20
|
#
|
|
20
|
-
#
|
|
21
|
+
# The mechanism, generalized from that spec rather than REDERIVED: a
|
|
21
22
|
# command step partway through a generated sequence is chosen as the
|
|
22
|
-
#
|
|
23
|
+
# race step; every step before it is setup (replayed once, sequentially,
|
|
23
24
|
# to bring a fresh disposable schema to the state the race step expects
|
|
24
|
-
# to act against); the race step is then dispatched
|
|
25
|
+
# to act against); the race step is then dispatched twice — once from
|
|
25
26
|
# each of two real, separate, forked OS processes racing against that
|
|
26
|
-
#
|
|
27
|
+
# same schema, no artificial gating, whichever the scheduler favors.
|
|
27
28
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
29
|
+
# The oracle is the same pair, dispatched sequentially, not a
|
|
30
|
+
# hardcoded expectation — unlike the hand-authored spec (which can
|
|
30
31
|
# assert `%w[refused succeeded]` because it knows its own fixture's
|
|
31
32
|
# business rule), this module has no idea whether an arbitrary
|
|
32
33
|
# generated race step conflicts with itself at all. So it asks the
|
|
33
|
-
#
|
|
34
|
-
# dispatch the
|
|
34
|
+
# domain: replay the identical setup on a separate fresh schema, then
|
|
35
|
+
# dispatch the same race step twice, one after the other, in one
|
|
35
36
|
# process, with no contention at all — genuinely correct by
|
|
36
37
|
# construction, since nothing else can touch that schema while it
|
|
37
38
|
# runs. Two conflicting writes settle as {"succeeded", "refused"};
|
|
38
39
|
# two independent ones settle as {"succeeded", "succeeded"}; either
|
|
39
|
-
# way,
|
|
40
|
+
# way, that is the multiset the concurrent pair must also produce if
|
|
40
41
|
# the write lock actually serializes them — order does not matter
|
|
41
|
-
# (which real racer wins a genuine race is never controlled), the
|
|
42
|
+
# (which real racer wins a genuine race is never controlled), the set
|
|
42
43
|
# of outcomes does.
|
|
43
44
|
#
|
|
44
|
-
#
|
|
45
|
+
# What a broken lock looks like here: the concurrent pair settling as
|
|
45
46
|
# {"succeeded", "succeeded"} where the sequential oracle says
|
|
46
47
|
# {"succeeded", "refused"} — two processes each hydrated the
|
|
47
48
|
# pre-write state, neither saw the other's write, and the second
|
|
48
49
|
# commit landed as a silent lost update instead of failing its own
|
|
49
|
-
# `given`. That is the
|
|
50
|
+
# `given`. That is the exact corruption class ADR 0036 fixed for
|
|
50
51
|
# PostgresEra and `postgres_concurrent_dispatch_spec.rb` still
|
|
51
52
|
# documents, unfixed, for plain Postgres.
|
|
52
53
|
#
|
|
53
|
-
#
|
|
54
|
+
# Not a replacement for the hand-authored spec — that spec proves the
|
|
54
55
|
# mechanism once, precisely, with controlled gating so the assertion
|
|
55
|
-
# is deterministic; this module proves the
|
|
56
|
+
# is deterministic; this module proves the same mechanism holds for
|
|
56
57
|
# whatever a real domain's own generated sequences throw at it, with
|
|
57
58
|
# no gating (a genuine, ungated race), on every sweep this mode runs.
|
|
58
59
|
module ConcurrentDispatch
|
|
@@ -60,8 +61,8 @@ module Hecks
|
|
|
60
61
|
|
|
61
62
|
COMMAND_STEP = ->(step) { step["verb"] && !step["query"] && !step["dry_run"] }
|
|
62
63
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
64
|
+
# One divergence list, the same shape every other mode in this
|
|
65
|
+
# practice produces — `[]` when nothing was found (including the
|
|
65
66
|
# legitimate "this seed's generated sequence has no command step to
|
|
66
67
|
# race at all" case: a sequence of pure queries/dry-runs has nothing
|
|
67
68
|
# to concurrently dispatch, and that is not a finding).
|
|
@@ -70,14 +71,26 @@ module Hecks
|
|
|
70
71
|
# (`bin/qa_sweep`'s own `persistence_parity_database`, reused here
|
|
71
72
|
# for the identical reason: a container, not the thing that's
|
|
72
73
|
# unique per run). `race_schema:`/`reference_schema:` are two
|
|
73
|
-
#
|
|
74
|
+
# different disposable schema names this one call owns for its own
|
|
74
75
|
# duration — the caller creates neither ahead of time (both are
|
|
75
76
|
# wiped fresh by the boots below) and drops both afterward, the
|
|
76
77
|
# same lifecycle `persistence_parity_schema` already has.
|
|
77
78
|
def check(domain_path, steps, database:, race_schema:, reference_schema:)
|
|
78
79
|
normalized = steps.map { |step| step.transform_keys(&:to_s) }
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
lockable, probe_errors = lockable_verbs(domain_path, normalized, database: database, schema: reference_schema)
|
|
81
|
+
race_index = pick_race_index(normalized, lockable)
|
|
82
|
+
unless race_index
|
|
83
|
+
# `[]` here means "nothing to race", and must only ever mean that.
|
|
84
|
+
# A sequence of pure queries/dry-runs is a legitimate clean
|
|
85
|
+
# result; a probe that raised for every verb is not — that one
|
|
86
|
+
# used to produce the identical `[]` and be logged as a clean
|
|
87
|
+
# concurrency Check (see `lockable_verbs`).
|
|
88
|
+
return [] if probe_errors.empty?
|
|
89
|
+
|
|
90
|
+
return [{ field: "concurrency_unraceable",
|
|
91
|
+
detail: "no command step could be raced because the cross-process-lock probe failed: " \
|
|
92
|
+
"#{probe_errors.uniq.join('; ')}" }]
|
|
93
|
+
end
|
|
81
94
|
|
|
82
95
|
setup_steps = normalized[0...race_index]
|
|
83
96
|
race_step = normalized[race_index]
|
|
@@ -90,7 +103,7 @@ module Hecks
|
|
|
90
103
|
[{ field: "process", detail: "#{e.class}: #{e.message}" }]
|
|
91
104
|
end
|
|
92
105
|
|
|
93
|
-
#
|
|
106
|
+
# The command step CLOSEST to the middle of the sequence — not the
|
|
94
107
|
# first (racing a bare identity-creation with no setup at all is a
|
|
95
108
|
# legitimate, useful case, so index 0 is not excluded) and not
|
|
96
109
|
# chosen for any domain-specific reason: a mid-sequence step has, on
|
|
@@ -98,13 +111,95 @@ module Hecks
|
|
|
98
111
|
# actually conflicting with itself. `nil` when the generated
|
|
99
112
|
# sequence has no command step at all (every step a query or a dry
|
|
100
113
|
# run) — nothing here for this seed to race.
|
|
101
|
-
|
|
114
|
+
#
|
|
115
|
+
# `lockable_verbs` — optional, but `check` always passes one: the
|
|
116
|
+
# set of verbs whose own aggregate is actually bound to an adapter
|
|
117
|
+
# that declares `:cross_process_lock` in this domain (see
|
|
118
|
+
# `lockable_verbs` below for why this can't be assumed just because
|
|
119
|
+
# the domain binds some of its own aggregates to PostgresEra).
|
|
120
|
+
# Racing anything outside that set is not a legitimate race at
|
|
121
|
+
# all — nil when nothing eligible is left, same as the "no command
|
|
122
|
+
# step" case, never a finding of its own.
|
|
123
|
+
def pick_race_index(steps, lockable_verbs = nil)
|
|
102
124
|
command_indices = steps.each_index.select { |i| COMMAND_STEP.call(steps[i]) }
|
|
125
|
+
command_indices = command_indices.select { |i| lockable_verbs.include?(steps[i]["verb"]) } if lockable_verbs
|
|
103
126
|
return nil if command_indices.empty?
|
|
104
127
|
|
|
105
128
|
command_indices[command_indices.size / 2]
|
|
106
129
|
end
|
|
107
130
|
|
|
131
|
+
# Which of this sequence's own command verbs are even candidates to
|
|
132
|
+
# race — BUG#142 (SW-quality_control-1789768606's own first real
|
|
133
|
+
# concurrency run, seed 3): a domain that `persisted_by("PostgresEra")`
|
|
134
|
+
# binds its own aggregates is not thereby binding a framework
|
|
135
|
+
# member it merely `uses_framework`s — `hecksagon_builder.rb`'s own
|
|
136
|
+
# `uses_framework` loads only that member's shape, never its
|
|
137
|
+
# persistence (see `examples/banking/bluebook/banking.hecksagon`'s
|
|
138
|
+
# own comment: a framework member's aggregates need a sibling
|
|
139
|
+
# hecksagon, registered under that member's own name, or
|
|
140
|
+
# `Ports::Persistence::BindingPolicy.default_binding` silently
|
|
141
|
+
# gives them "Memory" — no hecksagon registered under that name at
|
|
142
|
+
# all, so `resolve`'s own `missing_binding` refusal (which only
|
|
143
|
+
# fires when a hecksagon exists for that domain and simply omits
|
|
144
|
+
# this aggregate) never gets a chance to say so).
|
|
145
|
+
# `qa/bluebook/quality_control.hecksagon` attaches `Governance` via
|
|
146
|
+
# `uses_framework` with no such sibling — `Governance::
|
|
147
|
+
# RoleAssignment`/`RoleTransition` are Memory-backed, process-
|
|
148
|
+
# local, in the real ledger, `concurrency` mode included. Racing a
|
|
149
|
+
# Memory-backed aggregate across two real OS processes can never
|
|
150
|
+
# agree with the single-process sequential oracle — each racer's
|
|
151
|
+
# own boot gets its own independent, empty store — no matter how
|
|
152
|
+
# correct any write lock is; that is a guaranteed false positive,
|
|
153
|
+
# not evidence of a broken lock.
|
|
154
|
+
#
|
|
155
|
+
# Boots the same PostgresEra-rebound copy the race itself boots
|
|
156
|
+
# (`boot_preserving_schema` — structural inspection only, nothing
|
|
157
|
+
# dispatched, so the schema it's given is left exactly as it found
|
|
158
|
+
# it) and asks each distinct command verb's own resolved repository
|
|
159
|
+
# whether it actually declares `:cross_process_lock` — the same
|
|
160
|
+
# capability `Interpreting#run_dispatch_order_with_isolation`
|
|
161
|
+
# itself keys off of to decide whether a real advisory lock is
|
|
162
|
+
# even in play for that aggregate. A verb this boot can't resolve
|
|
163
|
+
# at all (a malformed adversarial verb, say) is conservatively
|
|
164
|
+
# excluded, not raced on a guess.
|
|
165
|
+
# Answers `[lockable, probe_errors]`. The errors half exists because
|
|
166
|
+
# the probe below rescues to `false`: a verb that cannot be resolved
|
|
167
|
+
# is indistinguishable, from the outside, from one that resolves fine
|
|
168
|
+
# and simply declares no `:cross_process_lock`. If resolution broke
|
|
169
|
+
# for every verb (a renamed capability symbol, a wiring change),
|
|
170
|
+
# `lockable` came back empty, `check` returned `[]`, and the sweep
|
|
171
|
+
# logged a clean concurrency Check for a race that never happened.
|
|
172
|
+
# `check` reports that case now instead of holding it.
|
|
173
|
+
def lockable_verbs(domain_path, steps, database:, schema:)
|
|
174
|
+
verbs = steps.select { |step| COMMAND_STEP.call(step) }.map { |step| step["verb"] }.uniq
|
|
175
|
+
lockable = []
|
|
176
|
+
probe_errors = []
|
|
177
|
+
boot_preserving_schema(domain_path, database: database, schema: schema) do |copy|
|
|
178
|
+
runtime = Hecks.boot(copy)
|
|
179
|
+
verbs.each do |verb|
|
|
180
|
+
lockable << verb if verb_cross_process_lockable?(runtime, verb, probe_errors)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
[lockable, probe_errors]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def verb_cross_process_lockable?(runtime, verb, probe_errors = [])
|
|
187
|
+
domain, aggregate_name, = Naming.split_verb(verb)
|
|
188
|
+
return false unless domain && aggregate_name
|
|
189
|
+
|
|
190
|
+
aggregate = runtime.registry.bluebook(domain)&.aggregate(aggregate_name)
|
|
191
|
+
return false unless aggregate
|
|
192
|
+
|
|
193
|
+
repository = runtime.registry.repository(domain, aggregate)
|
|
194
|
+
repository.capabilities.include?(:cross_process_lock)
|
|
195
|
+
rescue StandardError => e
|
|
196
|
+
# Still `false` — a verb this boot cannot resolve is not raced on a
|
|
197
|
+
# guess — but no longer silent: `check` needs to tell "nothing here
|
|
198
|
+
# declares a cross-process lock" from "asking broke".
|
|
199
|
+
probe_errors << "#{verb}: #{e.class}: #{e.message}"
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
|
|
108
203
|
def divergences_for(race_step, reference, concurrent)
|
|
109
204
|
crashes = (reference + concurrent).select { |outcome| outcome.start_with?("crashed:") }.uniq
|
|
110
205
|
return crashes.map { |c| { field: "concurrency_crash", verb: race_step["verb"], detail: c } } if crashes.any?
|
|
@@ -117,8 +212,8 @@ module Hecks
|
|
|
117
212
|
"#{reference.sort} — the cross-process write lock did not correctly serialize this write" }]
|
|
118
213
|
end
|
|
119
214
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
215
|
+
# The oracle — one boot, one process, the setup then the race step
|
|
216
|
+
# twice in immediate succession. Nothing else ever touches this
|
|
122
217
|
# schema while this runs, so whatever the domain itself settles on
|
|
123
218
|
# is correct by construction, not asserted.
|
|
124
219
|
def reference_outcomes(domain_path, setup_steps, race_step, database:, schema:)
|
|
@@ -132,32 +227,32 @@ module Hecks
|
|
|
132
227
|
outcomes
|
|
133
228
|
end
|
|
134
229
|
|
|
135
|
-
#
|
|
230
|
+
# The race itself — setup runs once, sequentially, in this process
|
|
136
231
|
# (the same `IsolatedBoot.call` wipe-then-boot every other mode
|
|
137
|
-
# here already uses), and only
|
|
138
|
-
# Each racer boots its
|
|
139
|
-
#
|
|
232
|
+
# here already uses), and only then do the two real racers run.
|
|
233
|
+
# Each racer boots its own fresh copy of the domain against the
|
|
234
|
+
# same now-populated schema — `boot_preserving_schema`, below,
|
|
140
235
|
# deliberately skips the wipe `IsolatedBoot.call` always does, or
|
|
141
236
|
# the setup this line just wrote would be gone before either racer
|
|
142
237
|
# ever dispatched anything.
|
|
143
238
|
#
|
|
144
|
-
#
|
|
239
|
+
# Real, separate OS processes, not `Thread.new` — `postgres_era_
|
|
145
240
|
# concurrent_dispatch_spec.rb`'s own header explains why:
|
|
146
241
|
# `Runtime::AggregateLock`'s in-process registry would fully (and
|
|
147
|
-
# misleadingly) serialize two
|
|
242
|
+
# misleadingly) serialize two threads sharing one process even with
|
|
148
243
|
# the cross-process lock fix reverted. Only two genuinely separate
|
|
149
244
|
# OS processes exercise the gap this check exists to catch.
|
|
150
245
|
#
|
|
151
|
-
# `Process.spawn`,
|
|
246
|
+
# `Process.spawn`, not `Process.fork` — `bin/qa_sweep`'s own
|
|
152
247
|
# top-of-file comment on `--all` names the identical hazard this
|
|
153
|
-
# sidesteps: by the time a `concurrency` seed runs,
|
|
248
|
+
# sidesteps: by the time a `concurrency` seed runs, this process
|
|
154
249
|
# already holds the QualityControl ledger's own live PostgresEra
|
|
155
250
|
# connection (this module's own caller, `bin/qa_sweep`, booted it
|
|
156
251
|
# long before any seed ran). `Process.fork` duplicates every open
|
|
157
252
|
# file descriptor, SSL session state included — confirmed live
|
|
158
253
|
# while wiring this mode up: forking directly from here corrupted
|
|
159
|
-
# the
|
|
160
|
-
# surfacing on the
|
|
254
|
+
# the ledger's own connection the moment either racer child exited,
|
|
255
|
+
# surfacing on the next unrelated ledger write, nowhere near this
|
|
161
256
|
# method's own code. `bin/qa_concurrency_racer` is this method's own
|
|
162
257
|
# worker, one real `ruby` process per racer — read that script's own
|
|
163
258
|
# header for the rest of this reasoning.
|
|
@@ -186,14 +281,14 @@ module Hecks
|
|
|
186
281
|
end
|
|
187
282
|
end
|
|
188
283
|
|
|
189
|
-
#
|
|
284
|
+
# One step, one outcome — never raises: a declared domain refusal is
|
|
190
285
|
# "refused" (the expected, ordinary answer a `given`/invariant can
|
|
191
286
|
# give), anything else escaping is "crashed:<class>: <message>", a
|
|
192
287
|
# genuine finding this module's own caller surfaces rather than lets
|
|
193
288
|
# kill a forked racer silently.
|
|
194
289
|
def dispatch_one(runtime, step)
|
|
195
290
|
args = (step["args"] || {}).transform_keys(&:to_sym)
|
|
196
|
-
runtime.
|
|
291
|
+
runtime.dispatch_flat(step["verb"], args)
|
|
197
292
|
"succeeded"
|
|
198
293
|
rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError
|
|
199
294
|
"refused"
|
|
@@ -201,7 +296,7 @@ module Hecks
|
|
|
201
296
|
"crashed:#{e.class}: #{e.message}"
|
|
202
297
|
end
|
|
203
298
|
|
|
204
|
-
#
|
|
299
|
+
# Setup tolerates an ordinary refusal (a generated sequence's own
|
|
205
300
|
# earlier step can legitimately refuse — every other mode in this
|
|
206
301
|
# practice already replays a prefix that way) but never a crash: an
|
|
207
302
|
# unexpected exception during setup means the schema this race is
|
|
@@ -214,13 +309,13 @@ module Hecks
|
|
|
214
309
|
end
|
|
215
310
|
end
|
|
216
311
|
|
|
217
|
-
#
|
|
218
|
-
# :postgres_era, ...)`
|
|
219
|
-
#
|
|
312
|
+
# The same copy-and-rebind `IsolatedBoot.call(..., adapter:
|
|
313
|
+
# :postgres_era, ...)` does, minus the schema wipe — deliberately
|
|
314
|
+
# not reusing `IsolatedBoot.rebind_to_postgres_era!` itself, which
|
|
220
315
|
# bundles `ensure_postgres_era_schema!`'s own `DROP SCHEMA` into the
|
|
221
316
|
# same call with no way to opt out (see that method's own header:
|
|
222
317
|
# the wipe is the "zero-history guarantee every other adapter mode
|
|
223
|
-
# already gives," exactly the guarantee
|
|
318
|
+
# already gives," exactly the guarantee this caller must not have —
|
|
224
319
|
# the whole point of a race is booting against what setup already
|
|
225
320
|
# wrote). `copy_dereferencing`/`rewrite_bindings!` are the same two
|
|
226
321
|
# public steps that method itself calls first; only the `.world`
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# What one sweep has already reached, and where the next seed starts.
|
|
4
|
+
#
|
|
5
|
+
# `SequenceGenerator` steers within one sequence (an unexercised verb is
|
|
6
|
+
# weighted up — picker.rb's `steer`), but every seed of a sweep used to
|
|
7
|
+
# start from nothing and forget everything the seeds before it reached.
|
|
8
|
+
# Roughly half of the QA ledger's bugs are refusal-kind splits (BUG#2–4,
|
|
9
|
+
# 7, 8, 14–16, 19–21, 23, 27, 28, 36–38, 41, 54, 56): a particular verb,
|
|
10
|
+
# in a particular state, with a particular malformed argument, refused
|
|
11
|
+
# one way on Ruby and another on Rust. BUG#11 was found by hand because
|
|
12
|
+
# a 25-step random walk never reached `Annotate` at all. Uniform seeds
|
|
13
|
+
# reach those corners by luck; this makes the sweep remember.
|
|
14
|
+
#
|
|
15
|
+
# The coverage unit is a tuple, not a verb — `verb | step kind |
|
|
16
|
+
# lifecycle state before | adversarial mutation | outcome` (see
|
|
17
|
+
# `SequenceGenerator#coverage_tuple`). "Renew refused LifecycleRefused
|
|
18
|
+
# from free" and "Renew ok from held" are different places a runtime can
|
|
19
|
+
# be wrong, where a verb-level count calls them the same.
|
|
20
|
+
#
|
|
21
|
+
# Two levers, both expressed as plain generator arguments so a seed
|
|
22
|
+
# stays reproducible from one call:
|
|
23
|
+
#
|
|
24
|
+
# 1. Splicing (`prefix:`). A sequence that reached a new tuple joins
|
|
25
|
+
# the corpus, cut at its last new tuple. A later seed may start by
|
|
26
|
+
# re-generating a random-length prefix of a corpus entry — the same
|
|
27
|
+
# seed, favor and nested prefix, so the same steps, dispatched for
|
|
28
|
+
# real, reaching the same state — and then carries on with its own
|
|
29
|
+
# seed's randomness from there. Deep state gets reached once and
|
|
30
|
+
# explored many times, instead of re-rolled per seed.
|
|
31
|
+
# 2. Favor (`favor:`). The verbs this sweep has hit least often —
|
|
32
|
+
# including declared verbs it has never hit at all — are weighted
|
|
33
|
+
# up for the next seed.
|
|
34
|
+
#
|
|
35
|
+
# Nothing here is random at the campaign's own level beyond
|
|
36
|
+
# `Random.new(seed)` per plan, and a plan is printed with every finding
|
|
37
|
+
# (`bin/qa_sweep`'s `reproduce:` line), so no finding depends on
|
|
38
|
+
# re-running the whole campaign to get back to it.
|
|
39
|
+
class CoverageCampaign
|
|
40
|
+
Plan = Struct.new(:prefix, :favor, keyword_init: true) do
|
|
41
|
+
def generator_options = { prefix: prefix, favor: favor }
|
|
42
|
+
def spliced? = !prefix.nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
attr_reader :corpus
|
|
46
|
+
|
|
47
|
+
def initialize(splice_probability:, favor_count:, corpus_limit: 64, max_prefix_depth: 6)
|
|
48
|
+
@splice_probability = splice_probability.to_f
|
|
49
|
+
@favor_count = favor_count.to_i
|
|
50
|
+
@corpus_limit = corpus_limit
|
|
51
|
+
@max_prefix_depth = max_prefix_depth
|
|
52
|
+
@corpus = []
|
|
53
|
+
@seen = Set.new
|
|
54
|
+
@verb_hits = Hash.new(0)
|
|
55
|
+
@declared_verbs = Set.new
|
|
56
|
+
@seeds = 0
|
|
57
|
+
@spliced = 0
|
|
58
|
+
@seeds_with_new = 0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def plan(seed)
|
|
62
|
+
random = Random.new(seed)
|
|
63
|
+
prefix = nil
|
|
64
|
+
if !@corpus.empty? && random.rand < @splice_probability
|
|
65
|
+
entry = @corpus[random.rand(@corpus.size)]
|
|
66
|
+
prefix = entry[:spec].merge("steps" => random.rand(1..entry[:attempts]))
|
|
67
|
+
end
|
|
68
|
+
Plan.new(prefix: prefix, favor: rare_verbs)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# `trace` is `SequenceGenerator.trace`'s answer: `coverage` is
|
|
72
|
+
# `[[attempt_index, tuple], ...]`, `verbs` every verb the booted
|
|
73
|
+
# catalog offered.
|
|
74
|
+
def record(seed, plan, trace)
|
|
75
|
+
@seeds += 1
|
|
76
|
+
@spliced += 1 if plan.spliced?
|
|
77
|
+
@declared_verbs.merge(trace.verbs)
|
|
78
|
+
|
|
79
|
+
new_at = trace.coverage.filter_map do |index, tuple|
|
|
80
|
+
@verb_hits[tuple.split(" | ").first] += 1
|
|
81
|
+
index if @seen.add?(tuple)
|
|
82
|
+
end
|
|
83
|
+
return if new_at.empty?
|
|
84
|
+
|
|
85
|
+
@seeds_with_new += 1
|
|
86
|
+
admit(seed, plan, new_at.max + 1)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def tuples_seen = @seen.size
|
|
90
|
+
|
|
91
|
+
def summary
|
|
92
|
+
"coverage: #{@seen.size} distinct (verb, kind, state, mutation, outcome) tuple(s) over #{@seeds} seed(s); " \
|
|
93
|
+
"#{@seeds_with_new} seed(s) reached something new; #{@spliced} spliced from a corpus of #{@corpus.size}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def admit(seed, plan, attempts)
|
|
99
|
+
return if depth(plan.prefix) + 1 > @max_prefix_depth
|
|
100
|
+
|
|
101
|
+
spec = { "seed" => seed, "favor" => plan.favor }
|
|
102
|
+
spec["prefix"] = plan.prefix if plan.prefix
|
|
103
|
+
@corpus << { spec: spec, attempts: attempts }
|
|
104
|
+
@corpus.shift while @corpus.size > @corpus_limit
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def depth(prefix) = prefix.nil? ? 0 : 1 + depth(prefix["prefix"])
|
|
108
|
+
|
|
109
|
+
# Never-hit declared verbs first (hits 0), then the least-hit, ties
|
|
110
|
+
# broken by name so a plan is a pure function of what was recorded.
|
|
111
|
+
def rare_verbs
|
|
112
|
+
return [] unless @favor_count.positive?
|
|
113
|
+
|
|
114
|
+
(@declared_verbs | @verb_hits.keys).min_by(@favor_count) { |verb| [@verb_hits[verb], verb] }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open3"
|
|
3
|
+
require_relative "replay"
|
|
4
|
+
require_relative "properties"
|
|
5
|
+
require_relative "self_consistency"
|
|
6
|
+
require_relative "rust_gap_manifest"
|
|
7
|
+
require_relative "nondeterministic"
|
|
8
|
+
|
|
9
|
+
module Hecks
|
|
10
|
+
module Fuzzing
|
|
11
|
+
# The Ruby-vs-Rust comparison of one generated sequence — moved here,
|
|
12
|
+
# unchanged, out of `bin/qa_sweep`'s own `diff_ruby_vs_rust` so a second
|
|
13
|
+
# caller (`bin/qa_generated_domains --rust`, which compares domains the
|
|
14
|
+
# generator wrote rather than ones on the rotation) runs the same
|
|
15
|
+
# comparison rather than a re-derived one. `bin/qa_sweep` delegates to
|
|
16
|
+
# it; its own comments on the known-gap filtering, the wire-precision
|
|
17
|
+
# reduction and the deep copy taken before `strip_emitted_flags!` apply
|
|
18
|
+
# here word for word.
|
|
19
|
+
#
|
|
20
|
+
# `differ` is anything answering `RustConformanceHelpers`' comparison
|
|
21
|
+
# helpers (spec/support/rust_conformance_helpers.rb) plus a
|
|
22
|
+
# `structural_skips` set — duck-typed, so lib never requires spec/.
|
|
23
|
+
# Which query verbs may diverge is not the differ's call: it is read
|
|
24
|
+
# off the binary's own manifest.json (`manifest_partition`, below).
|
|
25
|
+
#
|
|
26
|
+
# Returns one divergence list per active mode — `{ differential: [...],
|
|
27
|
+
# self_consistency: [...], properties_in_differential: [...],
|
|
28
|
+
# adapter_parity_sqlite: [...] }`, keys present only for the modes in
|
|
29
|
+
# `modes:` (`differential` always). `adapter_parity_sqlite:` is the
|
|
30
|
+
# caller's own callable, because which two adapters that mode pairs is
|
|
31
|
+
# the caller's dial, not this module's.
|
|
32
|
+
module Differential
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# The one place a Ruby/Rust query divergence may be tolerated — and
|
|
36
|
+
# only for a verb `gaps` (a `RustGapManifest`) declares not generated.
|
|
37
|
+
# Rust refuses such a verb outright while Ruby answers it for real
|
|
38
|
+
# (or refuses it for its own business reason), so both sides' rows for
|
|
39
|
+
# that verb leave the comparison. Everything else stays in: a refusal
|
|
40
|
+
# the manifest doesn't account for — however its message is worded —
|
|
41
|
+
# is a real divergence.
|
|
42
|
+
#
|
|
43
|
+
# `skipped` is every tolerated verb this history actually reached (for
|
|
44
|
+
# the sweep's structural_skip_report). `stale` is a divergence per
|
|
45
|
+
# tolerated verb Rust nonetheless answered with a query row: the
|
|
46
|
+
# manifest says "not generated" but the binary disagrees, so the
|
|
47
|
+
# tolerance itself is wrong and must not silently hold.
|
|
48
|
+
def manifest_partition(gaps, ruby_refusals:, rust_refusals:, ruby_queries:, rust_queries:)
|
|
49
|
+
verb_of = ->(row) { row.key?("verb") ? row["verb"] : row["query"] }
|
|
50
|
+
declared = ->(row) { gaps.not_generated?(verb_of.call(row)) }
|
|
51
|
+
stale = rust_queries.select(&declared).map do |row|
|
|
52
|
+
{ field: "manifest", verb: row["query"],
|
|
53
|
+
detail: "#{row['query']} is declared generated: false in manifest.json " \
|
|
54
|
+
"(#{gaps.not_generated(row['query']).values_at('gap_class', 'construct').join('/')}), " \
|
|
55
|
+
"but the Rust binary answered it — regenerate with bin/project_rust" }
|
|
56
|
+
end
|
|
57
|
+
reached = (ruby_refusals + rust_refusals + ruby_queries).select(&declared)
|
|
58
|
+
{ ruby_refusals: ruby_refusals.reject(&declared), rust_refusals: rust_refusals.reject(&declared),
|
|
59
|
+
ruby_queries: ruby_queries.reject(&declared), rust_queries: rust_queries.reject(&declared),
|
|
60
|
+
skipped: reached.to_set(&verb_of), stale: stale }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def property_divergences(history)
|
|
64
|
+
Properties.check(history).reject { |_, result| result == true }
|
|
65
|
+
.map { |name, message| { field: name.to_s, detail: message } }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self_consistency_divergences(history)
|
|
69
|
+
return [] unless history[:self_consistency]
|
|
70
|
+
|
|
71
|
+
history[:self_consistency].values.flatten(1)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
75
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
76
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
77
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
78
|
+
def diff(differ, domain_path, steps, binary, modes:, adapter_parity_sqlite: nil)
|
|
79
|
+
self_consistency = modes.include?(:self_consistency)
|
|
80
|
+
ruby_result = Replay.call(domain_path, steps, self_consistency: self_consistency)
|
|
81
|
+
ruby_instances = JSON.parse(JSON.generate(ruby_result[:instances]))
|
|
82
|
+
ruby_events = JSON.parse(JSON.generate(ruby_result[:events]))
|
|
83
|
+
ruby_refusals = ruby_result[:refusals].map do |r|
|
|
84
|
+
{ "verb" => r[:verb].to_s, "kind" => r[:kind].to_s.split("::").last, "error" => r[:error] }
|
|
85
|
+
end
|
|
86
|
+
ruby_queries = JSON.parse(JSON.generate(ruby_result[:queries].map { |row| Nondeterministic.strip(row, :query_row) }))
|
|
87
|
+
ruby_sagas = JSON.parse(JSON.generate(ruby_result[:sagas]))
|
|
88
|
+
ruby_dry_runs = ruby_result[:dry_runs].map { |d| { "verb" => d[:verb].to_s, "ok" => d[:ok] } }
|
|
89
|
+
|
|
90
|
+
outcomes = {}
|
|
91
|
+
outcomes[:properties_in_differential] = property_divergences(ruby_result) if modes.include?(:properties_in_differential)
|
|
92
|
+
if modes.include?(:adapter_parity_sqlite) && adapter_parity_sqlite
|
|
93
|
+
outcomes[:adapter_parity_sqlite] = adapter_parity_sqlite.call
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
stdout, status = Open3.capture2(binary, stdin_data: JSON.generate({ "steps" => steps }))
|
|
97
|
+
unless status.success?
|
|
98
|
+
return outcomes.merge(differential: [{ field: "process",
|
|
99
|
+
detail: "rust binary exited #{status.exitstatus}: #{stdout}" }])
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
rust_output = JSON.parse(stdout)
|
|
103
|
+
rust_live_instances = JSON.parse(JSON.generate(rust_output["instances"])) if self_consistency
|
|
104
|
+
differ.strip_emitted_flags!(rust_output["instances"])
|
|
105
|
+
differ.strip_emitted_flags!(rust_output["queries"])
|
|
106
|
+
differ.strip_occurred_at!(rust_output["events"])
|
|
107
|
+
|
|
108
|
+
divergences = []
|
|
109
|
+
divergences << { field: "instances", ruby: ruby_instances, rust: rust_output["instances"] } \
|
|
110
|
+
unless rust_output["instances"] == ruby_instances
|
|
111
|
+
divergences << { field: "events", ruby: ruby_events, rust: rust_output["events"] } \
|
|
112
|
+
unless rust_output["events"] == ruby_events
|
|
113
|
+
|
|
114
|
+
kept = manifest_partition(RustGapManifest.for_binary(binary),
|
|
115
|
+
ruby_refusals: ruby_refusals, rust_refusals: rust_output["refusals"],
|
|
116
|
+
ruby_queries: ruby_queries, rust_queries: rust_output["queries"])
|
|
117
|
+
differ.structural_skips.merge(kept[:skipped])
|
|
118
|
+
divergences.concat(kept[:stale])
|
|
119
|
+
|
|
120
|
+
by_kind = ->(r) { r.slice("verb", "kind") }
|
|
121
|
+
rust_refusals = kept[:rust_refusals].map(&by_kind)
|
|
122
|
+
kept_ruby_refusals = kept[:ruby_refusals].map(&by_kind)
|
|
123
|
+
divergences << { field: "refusals", ruby: kept_ruby_refusals, rust: rust_refusals } \
|
|
124
|
+
unless rust_refusals == kept_ruby_refusals
|
|
125
|
+
|
|
126
|
+
wordless = ->(row) { differ.reduce_to_wire_precision(row.except("error", "reference_error")) }
|
|
127
|
+
rust_queries = kept[:rust_queries].map(&wordless)
|
|
128
|
+
kept_ruby_queries = kept[:ruby_queries].map(&wordless)
|
|
129
|
+
divergences << { field: "queries", ruby: kept_ruby_queries, rust: rust_queries } \
|
|
130
|
+
unless rust_queries == kept_ruby_queries
|
|
131
|
+
|
|
132
|
+
divergences << { field: "sagas", ruby: ruby_sagas, rust: rust_output["sagas"] } \
|
|
133
|
+
unless rust_output["sagas"] == ruby_sagas
|
|
134
|
+
|
|
135
|
+
cross_domain = differ.cross_domain_policy_names(rust_output)
|
|
136
|
+
kept_ruby_reactions = JSON.parse(JSON.generate(ruby_result[:reactions]))
|
|
137
|
+
.reject { |r| cross_domain.include?(r["policy"]) }
|
|
138
|
+
rust_reactions = rust_output.fetch("reactions")
|
|
139
|
+
divergences << { field: "reactions", ruby: kept_ruby_reactions, rust: rust_reactions } \
|
|
140
|
+
unless rust_reactions == kept_ruby_reactions
|
|
141
|
+
|
|
142
|
+
rust_dry_runs = Array(rust_output["dry_runs"]).map { |d| d.slice("verb", "ok") }
|
|
143
|
+
divergences << { field: "dry_runs", ruby: ruby_dry_runs, rust: rust_dry_runs } \
|
|
144
|
+
unless rust_dry_runs == ruby_dry_runs
|
|
145
|
+
outcomes[:differential] = divergences
|
|
146
|
+
|
|
147
|
+
if self_consistency
|
|
148
|
+
outcomes[:self_consistency] =
|
|
149
|
+
self_consistency_divergences(ruby_result) +
|
|
150
|
+
SelfConsistency.check_rust_rehydration(binary, differ, rust_live_instances) +
|
|
151
|
+
SelfConsistency.check_rust_idempotency(binary, differ, rust_live_instances)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
outcomes
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|