hecks 2.9.0 → 3.0.1
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/exe/hecks +51 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +16 -3
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
- data/lib/hecks/adapters/driven/postgres.rb +13 -4
- data/lib/hecks/adapters/driven/prism.rb +41 -8
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
- data/lib/hecks/behaviors/expectations.rb +1 -1
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bench/cli.rb +62 -0
- data/lib/hecks/bench/environment.rb +71 -0
- data/lib/hecks/bench/postgres_probe.rb +30 -0
- data/lib/hecks/bench/report.rb +76 -0
- data/lib/hecks/bench/ruby_runner.rb +93 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +101 -0
- data/lib/hecks/bench/stats.rb +46 -0
- data/lib/hecks/bench/suite.rb +98 -0
- data/lib/hecks/bench/workload.rb +89 -0
- data/lib/hecks/bench.rb +17 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
- data/lib/hecks/bluebook/chapter.rb +6 -2
- data/lib/hecks/bluebook/domain_port.rb +36 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
- data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
- data/lib/hecks/bluebook/expression/resolver.rb +3 -2
- data/lib/hecks/bluebook/hexagon.rb +13 -5
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
- data/lib/hecks/bluebook/meta_validator.rb +9 -4
- data/lib/hecks/bluebook/model_check.rb +23 -0
- data/lib/hecks/bluebook/pattern_subset.rb +38 -0
- data/lib/hecks/bluebook/query.rb +20 -5
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/canonical_json.rb +30 -0
- data/lib/hecks/chapters.rb +102 -0
- data/lib/hecks/cli/behaviors.rb +61 -0
- data/lib/hecks/cli/canonicalise.rb +25 -0
- data/lib/hecks/cli/check_engine_agreement.rb +32 -0
- data/lib/hecks/cli/check_era.rb +88 -0
- data/lib/hecks/cli/console.rb +66 -0
- data/lib/hecks/cli/deploy_template_diff.rb +40 -0
- data/lib/hecks/cli/doc_coverage.rb +32 -0
- data/lib/hecks/cli/document.rb +2 -2
- data/lib/hecks/cli/expression_projection.rb +41 -0
- data/lib/hecks/cli/follow.rb +88 -0
- data/lib/hecks/cli/history.rb +44 -0
- data/lib/hecks/cli/ir.rb +1 -1
- data/lib/hecks/cli/mcp.rb +3 -3
- data/lib/hecks/cli/model_check.rb +15 -8
- data/lib/hecks/cli/pattern_cases.rb +47 -0
- data/lib/hecks/cli/present.rb +39 -0
- data/lib/hecks/cli/project_cli.rb +224 -32
- data/lib/hecks/cli/project_diagrams.rb +4 -3
- data/lib/hecks/cli/project_glossary.rb +44 -0
- data/lib/hecks/cli/reference.rb +46 -0
- data/lib/hecks/cli/refresh_projections.rb +44 -0
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
- data/lib/hecks/cli/release.rb +70 -0
- data/lib/hecks/cli/release_gem.rb +59 -0
- data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
- data/lib/hecks/cli/rspec_shard_files.rb +66 -0
- data/lib/hecks/cli/run.rb +3 -3
- data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
- data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
- data/lib/hecks/cli/shape.rb +70 -0
- data/lib/hecks/cli/smoke_http.rb +230 -0
- data/lib/hecks/cli/smoke_test.rb +5 -3
- data/lib/hecks/cli/spec_example.rb +27 -0
- data/lib/hecks/cli/statements.rb +32 -0
- data/lib/hecks/cli/stores.rb +4 -5
- data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
- data/lib/hecks/cli.rb +201 -52
- data/lib/hecks/codemod.rb +324 -0
- data/lib/hecks/corpus.rb +423 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
- data/lib/hecks/deploy/oidc.json +82 -2
- data/lib/hecks/doc/reference.rb +351 -0
- data/lib/hecks/doors/cli_door.rb +196 -0
- data/lib/hecks/doors/cli_runner.rb +297 -0
- data/lib/hecks/{facade → doors}/command_request.rb +1 -1
- data/lib/hecks/{facade → doors}/handle.rb +2 -2
- data/lib/hecks/{facade → doors}/json_door.rb +4 -4
- data/lib/hecks/doors/launcher_options.rb +129 -0
- data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
- data/lib/hecks/doors/mcp_door_scope.rb +97 -0
- data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
- data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
- data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
- data/lib/hecks/doors.rb +61 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +1 -1
- data/lib/hecks/engine_agreement.rb +156 -0
- data/lib/hecks/facade.rb +24 -37
- data/lib/hecks/forms/app.rb +6 -6
- data/lib/hecks/forms/banking_presentation.rb +50 -0
- data/lib/hecks/forms/field_shape.rb +2 -1
- data/lib/hecks/forms/port_argument.rb +1 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
- data/lib/hecks/fuzzing/combination_miner.rb +86 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
- data/lib/hecks/fuzzing/differential.rb +156 -0
- data/lib/hecks/fuzzing/domain_generator.rb +615 -0
- data/lib/hecks/fuzzing/era_boundary.rb +71 -0
- data/lib/hecks/fuzzing/form_census.rb +136 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
- data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
- data/lib/hecks/fuzzing/outside_world.rb +45 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
- data/lib/hecks/fuzzing/properties/guards.rb +190 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
- data/lib/hecks/fuzzing/properties/querying.rb +175 -0
- data/lib/hecks/fuzzing/properties.rb +162 -0
- data/lib/hecks/fuzzing/qa_settings.rb +123 -0
- data/lib/hecks/fuzzing/replay.rb +505 -0
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
- data/lib/hecks/fuzzing/self_consistency.rb +339 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
- data/lib/hecks/fuzzing/shrinker.rb +166 -0
- data/lib/hecks/fuzzing/structural_skips.rb +33 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
- data/lib/hecks/fuzzing/value_generator.rb +216 -0
- data/lib/hecks/fuzzing.rb +23 -0
- data/lib/hecks/grammar/evolve.rb +336 -0
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
- data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
- data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
- data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
- data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
- data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
- data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
- data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
- data/lib/hecks/hecks/adapters/git.adapter +7 -0
- data/lib/hecks/hecks/adapters/git.rb +102 -0
- data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
- data/lib/hecks/hecks/adapters/host_http.rb +65 -0
- data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
- data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
- data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
- data/lib/hecks/hecks/adapters/local_files.rb +46 -0
- data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
- data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
- data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
- data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
- data/lib/hecks/hecks/adapters/shell.adapter +7 -0
- data/lib/hecks/hecks/adapters/shell.rb +49 -0
- data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
- data/lib/hecks/hecks/adapters/terminal.rb +74 -0
- data/lib/hecks/hecks/codebase.behaviors +172 -0
- data/lib/hecks/hecks/codebase.bluebook +2453 -0
- data/lib/hecks/hecks/context_map.hecksagon +13 -0
- data/lib/hecks/hecks/custodian.behaviors +179 -0
- data/lib/hecks/hecks/custodian.bluebook +1847 -0
- data/lib/hecks/hecks/deploy.behaviors +74 -0
- data/lib/hecks/hecks/deploy.world +12 -0
- data/lib/hecks/hecks/environments/memory.world +10 -0
- data/lib/hecks/hecks/governance.world +7 -0
- data/lib/hecks/hecks/hecks.behaviors +62 -0
- data/lib/hecks/hecks/hecks.bluebook +124 -0
- data/lib/hecks/hecks/hecks.hecksagon +390 -0
- data/lib/hecks/hecks/hecks.world +42 -0
- data/lib/hecks/kernel_capabilities.rb +194 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
- data/lib/hecks/language/bluebook/query.bluebook +7 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
- data/lib/hecks/mcp_stdio_guard.rb +2 -2
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
- data/lib/hecks/persistence_legacy_fixture.rb +113 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +15 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
- data/lib/hecks/ports/persistence/state_codec.rb +24 -5
- data/lib/hecks/projection_files.rb +285 -0
- data/lib/hecks/projections/bootstrap_table.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +5 -5
- data/lib/hecks/projections/deploy/lambda.rb +43 -32
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/preview/template.rb +1 -1
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/shared.rb +1 -1
- data/lib/hecks/projections/diagrams.rb +3 -3
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/parser_table.rb +4 -4
- data/lib/hecks/projections/reference.rb +2 -3
- data/lib/hecks/projections/rust_vocabulary.rb +1 -1
- data/lib/hecks/projector/cli_projector.rb +106 -37
- data/lib/hecks/projector.rb +1 -1
- data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
- data/lib/hecks/quality_control/adapters/agent.rb +58 -0
- data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
- data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
- data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
- data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
- data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
- data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
- data/lib/hecks/quality_control/cli/child.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
- data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
- data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
- data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
- data/lib/hecks/query_ir.rb +346 -0
- data/lib/hecks/query_ir_mcp.rb +198 -0
- data/lib/hecks/release/runner/ci_publisher.rb +2 -2
- data/lib/hecks/release/runner/clean_tree.rb +42 -0
- data/lib/hecks/release/runner/console.rb +6 -0
- data/lib/hecks/release/runner/gem_publisher.rb +14 -7
- data/lib/hecks/release/runner/npm_publisher.rb +6 -36
- data/lib/hecks/release/runner/preflight.rb +26 -2
- data/lib/hecks/release/runner/published.rb +8 -26
- data/lib/hecks/release/runner/tagger.rb +30 -15
- data/lib/hecks/release/runner.rb +57 -6
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/runtime/adapter_lookup.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
- data/lib/hecks/runtime/dependency_planning.rb +20 -2
- data/lib/hecks/runtime/dispatcher.rb +53 -3
- data/lib/hecks/runtime/interpreting.rb +3 -0
- data/lib/hecks/runtime/loader.rb +65 -12
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/policy_interpreter.rb +5 -2
- data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
- data/lib/hecks/runtime/query_interpreter.rb +77 -0
- data/lib/hecks/runtime/reaction_outcome.rb +89 -0
- data/lib/hecks/runtime/refusal_wording.rb +23 -1
- data/lib/hecks/runtime/registry/verification.rb +163 -4
- data/lib/hecks/runtime/registry.rb +57 -3
- data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
- data/lib/hecks/runtime/value/coercion.rb +23 -135
- data/lib/hecks/runtime/value/field_checks.rb +172 -0
- data/lib/hecks/runtime/value.rb +4 -1
- data/lib/hecks/runtime.rb +20 -6
- data/lib/hecks/rust_build/conformance.rb +158 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
- data/lib/hecks/rust_build/coverage.rb +218 -0
- data/lib/hecks/rust_build/native_build.rb +124 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
- data/lib/hecks/rust_build/project_rust.rb +332 -0
- data/lib/hecks/rust_build/wasm.rb +105 -0
- data/lib/hecks/rust_build/wasm_browser.rb +113 -0
- data/lib/hecks/rust_build.rb +144 -0
- data/lib/hecks/storehouse.rb +16 -16
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
- data/lib/hecks/three_zero/forms.yml +127 -0
- data/lib/hecks/three_zero.rb +12 -0
- data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
- data/lib/hecks/tools/comment_style.rb +1070 -0
- data/lib/hecks/tools/corpus_report.rb +87 -0
- data/lib/hecks/tools/deploy_recipe.rb +255 -0
- data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
- data/lib/hecks/tools/era_projection_backfill.rb +94 -0
- data/lib/hecks/tools/era_reattest.rb +146 -0
- data/lib/hecks/tools/evolve_run.rb +189 -0
- data/lib/hecks/tools/fuzz_sweep.rb +373 -0
- data/lib/hecks/tools/heki_compaction.rb +100 -0
- data/lib/hecks/tools/hoist_local_givens.rb +246 -0
- data/lib/hecks/tools/journal_compaction.rb +89 -0
- data/lib/hecks/tools/oidc_manifests.rb +70 -0
- data/lib/hecks/tools/query_ir_run.rb +81 -0
- data/lib/hecks/tools/regeneration_run.rb +166 -0
- data/lib/hecks/tools/rust_comment_style.rb +642 -0
- data/lib/hecks/tools/sequence_script.rb +62 -0
- data/lib/hecks/tools/tail_merge.rb +117 -0
- data/lib/hecks/tools/tenant_provisioning.rb +142 -0
- data/lib/hecks/tools/translation_audit.rb +208 -0
- data/lib/hecks/tools/translation_scaffold.rb +131 -0
- data/lib/hecks/tools.rb +86 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +32 -15
- data/qa/settings.yml +66 -0
- data/rust/.cargo/config.toml +6 -0
- data/rust/.gitignore +2 -0
- data/rust/Cargo.lock +54 -0
- data/rust/Cargo.toml +61 -0
- data/rust/build/.cargo/config.toml +3 -0
- data/rust/build/.gitignore +1 -0
- data/rust/build/Cargo.lock +7 -0
- data/rust/build/Cargo.toml +19 -0
- data/rust/build/src/build_artifact.rs +47 -0
- data/rust/build/src/cargo_sync.rs +248 -0
- data/rust/build/src/json.rs +421 -0
- data/rust/build/src/lineage_pass.rs +285 -0
- data/rust/build/src/main.rs +51 -0
- data/rust/build/src/optional_pass.rs +150 -0
- data/rust/build/src/pipeline.rs +264 -0
- data/rust/build/src/reserved_names.rs +76 -0
- data/rust/build/src/resolve.rs +124 -0
- data/rust/build/src/root.rs +25 -0
- data/rust/build/src/sidecars.rs +73 -0
- data/rust/build/src/subprocess.rs +51 -0
- data/rust/build/src/tmp.rs +31 -0
- data/rust/codegen/.cargo/config.toml +3 -0
- data/rust/codegen/.gitignore +1 -0
- data/rust/codegen/Cargo.lock +7 -0
- data/rust/codegen/Cargo.toml +20 -0
- data/rust/codegen/src/attr.rs +32 -0
- data/rust/codegen/src/bridging.rs +481 -0
- data/rust/codegen/src/commands.rs +1226 -0
- data/rust/codegen/src/constraints.rs +109 -0
- data/rust/codegen/src/dependency_planning.rs +209 -0
- data/rust/codegen/src/domain_generator.rs +1288 -0
- data/rust/codegen/src/exemplar.rs +230 -0
- data/rust/codegen/src/expr_emitter.rs +87 -0
- data/rust/codegen/src/fielded.rs +192 -0
- data/rust/codegen/src/hecks_naming.rs +33 -0
- data/rust/codegen/src/json.rs +335 -0
- data/rust/codegen/src/json_codec.rs +697 -0
- data/rust/codegen/src/literal.rs +207 -0
- data/rust/codegen/src/main.rs +290 -0
- data/rust/codegen/src/manifest.rs +227 -0
- data/rust/codegen/src/mutations.rs +784 -0
- data/rust/codegen/src/naming.rs +286 -0
- data/rust/codegen/src/ports.rs +208 -0
- data/rust/codegen/src/prelude.rs +116 -0
- data/rust/codegen/src/queries.rs +786 -0
- data/rust/codegen/src/reactions.rs +532 -0
- data/rust/codegen/src/read_models.rs +697 -0
- data/rust/codegen/src/reference_specs.rs +40 -0
- data/rust/codegen/src/registry.rs +966 -0
- data/rust/codegen/src/reserved_names.rs +76 -0
- data/rust/codegen/src/shared.rs +93 -0
- data/rust/codegen/src/skip_reason.rs +25 -0
- data/rust/codegen/src/types.rs +362 -0
- data/rust/host/.cargo/config.toml +3 -0
- data/rust/host/.gitignore +1 -0
- data/rust/host/Cargo.lock +4297 -0
- data/rust/host/Cargo.toml +61 -0
- data/rust/host/HECKS_RELEASE +1 -0
- data/rust/host/rds-ca-bundle.pem +2736 -0
- data/rust/host/src/api.rs +1596 -0
- data/rust/host/src/approval.rs +652 -0
- data/rust/host/src/auth.rs +1287 -0
- data/rust/host/src/bin/expr_harness.rs +46 -0
- data/rust/host/src/bin/lineage_harness.rs +119 -0
- data/rust/host/src/bin/mint_harness.rs +135 -0
- data/rust/host/src/checkout.rs +265 -0
- data/rust/host/src/dispatch.rs +1443 -0
- data/rust/host/src/expr_json.rs +1117 -0
- data/rust/host/src/field_hints.rs +52 -0
- data/rust/host/src/ir.rs +733 -0
- data/rust/host/src/journal.rs +874 -0
- data/rust/host/src/lambda_client.rs +378 -0
- data/rust/host/src/log.rs +147 -0
- data/rust/host/src/main.rs +474 -0
- data/rust/host/src/mint.rs +1865 -0
- data/rust/host/src/payments/keystore.rs +330 -0
- data/rust/host/src/payments/tests.rs +1400 -0
- data/rust/host/src/payments.rs +767 -0
- data/rust/host/src/presentation.rs +698 -0
- data/rust/host/src/presentation_write.rs +1431 -0
- data/rust/host/src/query_step.rs +149 -0
- data/rust/host/src/rate_limit.rs +904 -0
- data/rust/host/src/reference_transform.rs +241 -0
- data/rust/host/src/reference_validate.rs +338 -0
- data/rust/host/src/resend.rs +296 -0
- data/rust/host/src/secrets.rs +106 -0
- data/rust/host/src/server.rs +663 -0
- data/rust/host/src/storage_shape.rs +175 -0
- data/rust/host/src/test_pg.rs +40 -0
- data/rust/host/src/ui_schema.rs +1351 -0
- data/rust/host/src/wasm_runner.rs +191 -0
- data/rust/host/src/web/newsletter/cooldown.rs +126 -0
- data/rust/host/src/web/newsletter/tests.rs +104 -0
- data/rust/host/src/web/newsletter.rs +310 -0
- data/rust/host/src/web/newsletter_send.rs +357 -0
- data/rust/host/src/web/registrations.rs +1505 -0
- data/rust/host/src/web.rs +2937 -0
- data/rust/lsp/.gitignore +1 -0
- data/rust/lsp/Cargo.lock +7 -0
- data/rust/lsp/Cargo.toml +16 -0
- data/rust/lsp/README.md +133 -0
- data/rust/lsp/src/diagnostics.rs +165 -0
- data/rust/lsp/src/json.rs +385 -0
- data/rust/lsp/src/main.rs +399 -0
- data/rust/lsp/src/outline.rs +177 -0
- data/rust/lsp/src/rpc.rs +67 -0
- data/rust/parser/.cargo/config.toml +5 -0
- data/rust/parser/.gitignore +1 -0
- data/rust/parser/Cargo.lock +7 -0
- data/rust/parser/Cargo.toml +21 -0
- data/rust/parser/src/build/closed_sets.rs +27 -0
- data/rust/parser/src/build/identity.rs +276 -0
- data/rust/parser/src/build/mod.rs +12 -0
- data/rust/parser/src/build/naming.rs +141 -0
- data/rust/parser/src/build/pattern_subset.rs +161 -0
- data/rust/parser/src/build/query_derive.rs +72 -0
- data/rust/parser/src/build/query_inference.rs +269 -0
- data/rust/parser/src/build/query_options.rs +100 -0
- data/rust/parser/src/build/read_model.rs +94 -0
- data/rust/parser/src/build/references.rs +32 -0
- data/rust/parser/src/canonical.rs +114 -0
- data/rust/parser/src/diag.rs +55 -0
- data/rust/parser/src/emit.rs +953 -0
- data/rust/parser/src/expr/ast_json.rs +147 -0
- data/rust/parser/src/expr/evaluator.rs +81 -0
- data/rust/parser/src/expr/mod.rs +84 -0
- data/rust/parser/src/expr/resolver.rs +497 -0
- data/rust/parser/src/ir.rs +387 -0
- data/rust/parser/src/keywords.rs +585 -0
- data/rust/parser/src/lex.rs +662 -0
- data/rust/parser/src/main.rs +252 -0
- data/rust/parser/src/parse/aggregate.rs +612 -0
- data/rust/parser/src/parse/chapter.rs +517 -0
- data/rust/parser/src/parse/command.rs +637 -0
- data/rust/parser/src/parse/domain_port.rs +127 -0
- data/rust/parser/src/parse/entity.rs +527 -0
- data/rust/parser/src/parse/file.rs +67 -0
- data/rust/parser/src/parse/hecksagon.rs +231 -0
- data/rust/parser/src/parse/lifecycle.rs +162 -0
- data/rust/parser/src/parse/mod.rs +1262 -0
- data/rust/parser/src/parse/policy.rs +87 -0
- data/rust/parser/src/parse/process_manager.rs +305 -0
- data/rust/parser/src/parse/query.rs +100 -0
- data/rust/parser/src/parse/read_model.rs +214 -0
- data/rust/parser/src/parse/value_object.rs +250 -0
- data/rust/parser/src/ruby_value.rs +422 -0
- data/rust/project/bridging.rb +257 -0
- data/rust/project/commands.rb +877 -0
- data/rust/project/constraints.rb +86 -0
- data/rust/project/dependency_planning.rb +178 -0
- data/rust/project/domain_generator.rb +980 -0
- data/rust/project/exemplar.rb +148 -0
- data/rust/project/expr_emitter.rb +72 -0
- data/rust/project/fielded.rb +141 -0
- data/rust/project/json_codec.rb +641 -0
- data/rust/project/mutations.rb +721 -0
- data/rust/project/naming.rb +205 -0
- data/rust/project/ports.rb +65 -0
- data/rust/project/queries.rb +493 -0
- data/rust/project/reactions.rb +317 -0
- data/rust/project/read_models.rb +690 -0
- data/rust/project/reference_specs.rb +27 -0
- data/rust/project/registry.rb +396 -0
- data/rust/project/skip_reason.rb +29 -0
- data/rust/project/types.rb +225 -0
- data/rust/project/write_if_changed.rb +83 -0
- data/rust/project.rb +30 -0
- data/rust/project_rust_pipeline.rb +383 -0
- data/rust/src/exemplar/commands.rs +279 -0
- data/rust/src/exemplar/constraints.rs +18 -0
- data/rust/src/exemplar/fielded.rs +280 -0
- data/rust/src/exemplar/json.rs +277 -0
- data/rust/src/exemplar/mod.rs +17 -0
- data/rust/src/exemplar/mutations.rs +80 -0
- data/rust/src/exemplar/queries.rs +33 -0
- data/rust/src/exemplar/reactions.rs +88 -0
- data/rust/src/exemplar/read_models.rs +48 -0
- data/rust/src/exemplar/registry.rs +166 -0
- data/rust/src/exemplar/types.rs +44 -0
- data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
- data/rust/src/kernel/attribute_shapes/list.rs +18 -0
- data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
- data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
- data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
- data/rust/src/kernel/cli.rs +585 -0
- data/rust/src/kernel/dispatch.rs +1090 -0
- data/rust/src/kernel/expr.rs +458 -0
- data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
- data/rust/src/kernel/expression_operators/comparison.rs +48 -0
- data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
- data/rust/src/kernel/expression_operators/logical.rs +13 -0
- data/rust/src/kernel/expression_operators/membership.rs +18 -0
- data/rust/src/kernel/expression_operators/mod.rs +68 -0
- data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
- data/rust/src/kernel/expression_operators/positional.rs +126 -0
- data/rust/src/kernel/expression_operators/presence.rs +101 -0
- data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
- data/rust/src/kernel/expression_operators/sized.rs +35 -0
- data/rust/src/kernel/expression_operators/text.rs +79 -0
- data/rust/src/kernel/expression_operators/to_string.rs +18 -0
- data/rust/src/kernel/json.rs +779 -0
- data/rust/src/kernel/mod.rs +133 -0
- data/rust/src/kernel/named_query.rs +219 -0
- data/rust/src/kernel/orchestrate.rs +1810 -0
- data/rust/src/kernel/pattern.rs +307 -0
- data/rust/src/kernel/query_comparators.rs +206 -0
- data/rust/src/kernel/query_ordering.rs +103 -0
- data/rust/src/kernel/read_model.rs +914 -0
- data/rust/src/kernel/reference_lookup.rs +149 -0
- data/rust/src/kernel/repository.rs +468 -0
- data/rust/src/kernel/routing.rs +612 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
- data/rust/src/kernel/vocab/field_hint.rs +70 -0
- data/rust/src/kernel/vocab/mod.rs +20 -0
- data/rust/src/kernel/vocab/query_comparator.rs +65 -0
- data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
- data/rust/src/lib.rs +8 -0
- data/rust/src/main.rs +16 -0
- data/rust/web/.gitignore +1 -0
- data/rust/web/Cargo.lock +119 -0
- data/rust/web/Cargo.toml +18 -0
- data/rust/web/src/lib.rs +8 -0
- metadata +477 -12
- data/lib/hecks/facade/cli_door.rb +0 -96
- data/lib/hecks/facade/cli_runner.rb +0 -127
- data/lib/hecks/mcp_door_scope.rb +0 -95
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# Fields dropped before two histories are compared, declared once per shape they ride on.
|
|
4
|
+
# A literal except-list naming any of them under lib/hecks/fuzzing fails its spec.
|
|
5
|
+
module Nondeterministic
|
|
6
|
+
FIELDS = {
|
|
7
|
+
query_row: {
|
|
8
|
+
instances_at: "a full state snapshot taken for the query oracle's own use " \
|
|
9
|
+
"(Properties::Querying); the same state is already compared as `instances`, " \
|
|
10
|
+
"so keeping it would re-report one instances divergence under every query step"
|
|
11
|
+
}.freeze,
|
|
12
|
+
outbox_row: {
|
|
13
|
+
event_uid: "`Runtime::Outbox::Fanout#rows_for`'s own SecureRandom.uuid, minted fresh per " \
|
|
14
|
+
"enqueue and kept off `Event#to_h` — relay bookkeeping, never something the " \
|
|
15
|
+
"domain produced, so two otherwise-identical replays carry two different uuids",
|
|
16
|
+
delivery_id: "`\"\#{event_uid}/\#{consumer}\"` — inherits event_uid's per-enqueue uuid"
|
|
17
|
+
}.freeze,
|
|
18
|
+
event: {
|
|
19
|
+
occurred_at: "a wall-clock read; never reproducible byte-for-byte between two independent " \
|
|
20
|
+
"runs (Replay's own projected `:events` leave it off for the same reason)"
|
|
21
|
+
}.freeze,
|
|
22
|
+
history: {
|
|
23
|
+
bluebook: "a live IR object (the first-loaded bluebook) handed to properties — an object " \
|
|
24
|
+
"identity, not a value, so two boots never compare equal on it",
|
|
25
|
+
bluebooks: "the full live IR map, keyed by domain — object identities, same as `bluebook`"
|
|
26
|
+
}.freeze
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# The field names declared for one comparison group.
|
|
32
|
+
#
|
|
33
|
+
# @raise [KeyError] if `group` names no declared group
|
|
34
|
+
def names(group)
|
|
35
|
+
FIELDS.fetch(group).keys
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Every declared name, across every group.
|
|
39
|
+
def all_names
|
|
40
|
+
FIELDS.values.flat_map(&:keys).uniq
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `hash` without `group`'s fields; expects symbol keys.
|
|
44
|
+
#
|
|
45
|
+
# @raise [KeyError] if `group` names no declared group
|
|
46
|
+
def strip(hash, group)
|
|
47
|
+
hash.except(*names(group))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../runtime/errors"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Fuzzing
|
|
7
|
+
# What a replay asks in place of a port's adapter: an object that refuses every question.
|
|
8
|
+
#
|
|
9
|
+
# A domain's adapters reach outside the process, and the chapters of this repository that
|
|
10
|
+
# drive tools (the shell, git, child processes, the file system) would run them for real from
|
|
11
|
+
# a generated sequence. A replay checks the domain's own rules, so the adapter never runs:
|
|
12
|
+
# the operation is refused, which a port operation records as its `refuses` event and a query
|
|
13
|
+
# answered by a port raises like any domain refusal.
|
|
14
|
+
#
|
|
15
|
+
# The refusal is a `GivenNotMet` for the runtime's sake but not one a `given` declares, and no
|
|
16
|
+
# property that reads guard descriptions quotes it.
|
|
17
|
+
class OutsideWorld
|
|
18
|
+
# The refusal every question to a stand-in ends in.
|
|
19
|
+
class Refused < Runtime::GivenNotMet; end
|
|
20
|
+
|
|
21
|
+
# @param port_name [String] the port whose adapter this stands in for
|
|
22
|
+
# @param asked [String] what was being asked, worded into the refusal
|
|
23
|
+
# @return [OutsideWorld] the stand-in
|
|
24
|
+
def self.call(port_name, asked)
|
|
25
|
+
new(port_name, asked)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param port_name [String] the port whose adapter this stands in for
|
|
29
|
+
# @param asked [String] what was being asked, worded into the refusal
|
|
30
|
+
def initialize(port_name, asked)
|
|
31
|
+
@port_name = port_name
|
|
32
|
+
@asked = asked
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Every method a port could declare exists here, and refuses.
|
|
36
|
+
def respond_to_missing?(_name, _include_private = false) = true
|
|
37
|
+
|
|
38
|
+
# @raise [Refused] always
|
|
39
|
+
def method_missing(name, *, **)
|
|
40
|
+
raise Refused, "#{@asked} was not asked of the #{@port_name} port's adapter: a replay runs " \
|
|
41
|
+
"no adapter (#{name})"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "replay"
|
|
3
|
+
require_relative "nondeterministic"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Fuzzing
|
|
7
|
+
# Differential check of one Ruby engine against two persistences, `left:` and `right:`.
|
|
8
|
+
# Defaults to Memory vs a disposable `PostgresEra` database, which needs `database:`/`schema:`.
|
|
9
|
+
module PersistenceParity
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Replays `steps` on both adapters and lists where the results differ.
|
|
13
|
+
#
|
|
14
|
+
# Compares the same six fields as `hecks quality_control ask run`'s Ruby-vs-Rust mode. Both
|
|
15
|
+
# results are JSON-round-tripped so plain data compares equal on both sides.
|
|
16
|
+
#
|
|
17
|
+
# @param left [Symbol] an `IsolatedBoot` adapter: `:memory`, `:sqlite`, `:postgres`,
|
|
18
|
+
# `:postgres_era`
|
|
19
|
+
# @param right [Symbol] the second adapter, same set as `left`
|
|
20
|
+
# @param database [String, nil] required when either side is `:postgres_era`
|
|
21
|
+
# @param schema [String, nil] required when either side is `:postgres_era`
|
|
22
|
+
# @return [Array<Hash>] entries `{field: String, left => Object, right => Object}`,
|
|
23
|
+
# keyed by the adapter symbols; empty if both sides agree
|
|
24
|
+
def diff(domain_path, steps, left: :memory, right: :postgres_era, database: nil, schema: nil)
|
|
25
|
+
left_result = Replay.call(domain_path, steps, adapter: left, database: database, schema: schema)
|
|
26
|
+
right_result = Replay.call(domain_path, steps, adapter: right, database: database, schema: schema)
|
|
27
|
+
|
|
28
|
+
divergences = []
|
|
29
|
+
divergences.concat(diff_instances(left_result, right_result, left, right))
|
|
30
|
+
divergences.concat(diff_events(left_result, right_result, left, right))
|
|
31
|
+
divergences.concat(diff_refusals(left_result, right_result, left, right))
|
|
32
|
+
divergences.concat(diff_queries(left_result, right_result, left, right))
|
|
33
|
+
divergences.concat(diff_sagas(left_result, right_result, left, right))
|
|
34
|
+
divergences.concat(diff_reactions(left_result, right_result, left, right))
|
|
35
|
+
divergences
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Round-trips `value` through JSON so both sides compare as plain data.
|
|
39
|
+
def as_json(value) = JSON.parse(JSON.generate(value))
|
|
40
|
+
|
|
41
|
+
# Compares both sides' stored instances.
|
|
42
|
+
def diff_instances(left_result, right_result, left, right)
|
|
43
|
+
l = as_json(left_result[:instances])
|
|
44
|
+
r = as_json(right_result[:instances])
|
|
45
|
+
return [] if l == r
|
|
46
|
+
|
|
47
|
+
[{ field: "instances", left => l, right => r }]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Compares both sides' emitted events.
|
|
51
|
+
def diff_events(left_result, right_result, left, right)
|
|
52
|
+
l = as_json(left_result[:events])
|
|
53
|
+
r = as_json(right_result[:events])
|
|
54
|
+
return [] if l == r
|
|
55
|
+
|
|
56
|
+
[{ field: "events", left => l, right => r }]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Compares both sides' refusals, with `verb`/`kind` as strings.
|
|
60
|
+
def diff_refusals(left_result, right_result, left, right)
|
|
61
|
+
normalize = lambda do |refusals|
|
|
62
|
+
refusals.map { |r| { "verb" => r[:verb].to_s, "kind" => r[:kind].to_s, "error" => r[:error] } }
|
|
63
|
+
end
|
|
64
|
+
l = normalize.call(left_result[:refusals])
|
|
65
|
+
r = normalize.call(right_result[:refusals])
|
|
66
|
+
return [] if l == r
|
|
67
|
+
|
|
68
|
+
[{ field: "refusals", left => l, right => r }]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Compares both sides' query answers, minus the `query_row` fields.
|
|
72
|
+
#
|
|
73
|
+
# The `Nondeterministic` `query_row` group is dropped; instances are already compared.
|
|
74
|
+
def diff_queries(left_result, right_result, left, right)
|
|
75
|
+
strip = ->(rows) { rows.map { |row| Nondeterministic.strip(row, :query_row) } }
|
|
76
|
+
l = as_json(strip.call(left_result[:queries]))
|
|
77
|
+
r = as_json(strip.call(right_result[:queries]))
|
|
78
|
+
return [] if l == r
|
|
79
|
+
|
|
80
|
+
[{ field: "queries", left => l, right => r }]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Compares both sides' saga logs.
|
|
84
|
+
def diff_sagas(left_result, right_result, left, right)
|
|
85
|
+
l = as_json(left_result[:sagas])
|
|
86
|
+
r = as_json(right_result[:sagas])
|
|
87
|
+
return [] if l == r
|
|
88
|
+
|
|
89
|
+
[{ field: "sagas", left => l, right => r }]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Compares both sides' reaction logs.
|
|
93
|
+
def diff_reactions(left_result, right_result, left, right)
|
|
94
|
+
l = as_json(left_result[:reactions])
|
|
95
|
+
r = as_json(right_result[:reactions])
|
|
96
|
+
return [] if l == r
|
|
97
|
+
|
|
98
|
+
[{ field: "reactions", left => l, right => r }]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module Properties
|
|
4
|
+
# Property: every `corrects` mutation points at an earlier event in the same history.
|
|
5
|
+
module Corrections
|
|
6
|
+
# Every event emitted by a command with a `corrects` mutation, aggregate-level or
|
|
7
|
+
# nested in entities, needs an earlier event with the corrected name and same id.
|
|
8
|
+
#
|
|
9
|
+
# `history[:events]` is in occurrence order, so "earlier" is array position.
|
|
10
|
+
# Entity-level `corrects` is checked by neither dispatch nor build, so only this
|
|
11
|
+
# property catches it going wrong.
|
|
12
|
+
#
|
|
13
|
+
# @param history [Hash] a replayed history as returned by `Replay.call`
|
|
14
|
+
# @return [true, String] true if every event emitted by a `corrects`-bearing
|
|
15
|
+
# command has a matching, strictly earlier corrected event in the same
|
|
16
|
+
# history; otherwise a message listing every unmatched correction
|
|
17
|
+
def corrections_reference_an_emitted_event(history)
|
|
18
|
+
violations = []
|
|
19
|
+
|
|
20
|
+
(history[:bluebooks] || {}).each do |domain, bluebook|
|
|
21
|
+
bluebook.aggregates.each do |aggregate|
|
|
22
|
+
aggregate_key = "#{domain}::#{aggregate.hecks_name}"
|
|
23
|
+
|
|
24
|
+
each_command_including_entities(aggregate) do |command|
|
|
25
|
+
corrects_mutations = command.mutations.select { |mutation| mutation.op == :corrects }
|
|
26
|
+
next if corrects_mutations.empty?
|
|
27
|
+
|
|
28
|
+
corrects_mutations.each do |mutation|
|
|
29
|
+
corrected_event = mutation.target.to_s
|
|
30
|
+
|
|
31
|
+
command.emits.each do |produced_event_name|
|
|
32
|
+
violations.concat(unmatched_corrections(history[:events], aggregate_key,
|
|
33
|
+
produced_event_name.to_s, corrected_event,
|
|
34
|
+
command.hecks_name))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
violations.empty? || violations.uniq.join("; ")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Finds every occurrence of `produced_event_name`, on `aggregate_key`, with no
|
|
45
|
+
# matching `corrected_event` for the same id appearing earlier in `events`.
|
|
46
|
+
#
|
|
47
|
+
# @param events [Array<Hash>] `history[:events]`, in occurrence order
|
|
48
|
+
# @param aggregate_key [String] `"domain::AggregateName"` the events belong to
|
|
49
|
+
# @param produced_event_name [String] name of the event a `corrects` mutation's
|
|
50
|
+
# command emits
|
|
51
|
+
# @param corrected_event [String] name of the event the mutation claims to correct
|
|
52
|
+
# @param command_name [String] the command's own `hecks_name`, for the message
|
|
53
|
+
# @return [Array<String>] one message per occurrence with no matching earlier
|
|
54
|
+
# corrected event; empty when every occurrence is matched
|
|
55
|
+
def unmatched_corrections(events, aggregate_key, produced_event_name, corrected_event, command_name)
|
|
56
|
+
own_events = events.each_with_index.select do |event, _index|
|
|
57
|
+
event[:name] == produced_event_name && event[:aggregate] == aggregate_key
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
own_events.filter_map do |event, index|
|
|
61
|
+
preceding = events.first(index)
|
|
62
|
+
next if preceding.any? do |earlier|
|
|
63
|
+
earlier[:name] == corrected_event && earlier[:aggregate] == aggregate_key &&
|
|
64
|
+
earlier[:id].to_s == event[:id].to_s
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
"#{command_name} (#{aggregate_key}##{event[:id]}) emitted #{produced_event_name}, claiming to " \
|
|
68
|
+
"correct #{corrected_event}, but no #{corrected_event} for the same aggregate/id appears " \
|
|
69
|
+
"earlier in this history"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Yields every command on `owner` and, recursively, on its entities (ADR 0026).
|
|
74
|
+
#
|
|
75
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the aggregate or entity
|
|
76
|
+
# whose own commands, and whose entities' commands, to walk
|
|
77
|
+
# @yield [command] once per declared command, aggregate-level or nested
|
|
78
|
+
# @yieldparam command [Bluebook::Command] a command declared on `owner` or one
|
|
79
|
+
# of its entities
|
|
80
|
+
# @return [void]
|
|
81
|
+
def each_command_including_entities(owner, &block)
|
|
82
|
+
owner.commands.each(&block)
|
|
83
|
+
owner.entities.each { |entity| each_command_including_entities(entity, &block) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module Properties
|
|
4
|
+
# Property: a dry-run step leaves the event count and every instance unchanged.
|
|
5
|
+
# Entries without before/after snapshots (hand-built histories) are skipped.
|
|
6
|
+
module DryRuns
|
|
7
|
+
# Checks that every dry-run step left neither the event count nor any
|
|
8
|
+
# instance's state changed.
|
|
9
|
+
#
|
|
10
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
11
|
+
# @return [true, String] true if every dry-run trace shows no change; otherwise
|
|
12
|
+
# a semicolon-joined message naming each offending dry run
|
|
13
|
+
def dry_runs_leave_no_trace(history)
|
|
14
|
+
offenders = Array(history[:dry_run_traces]).filter_map do |entry|
|
|
15
|
+
before = entry[:before]
|
|
16
|
+
after = entry[:after]
|
|
17
|
+
next unless before && after
|
|
18
|
+
|
|
19
|
+
traces = []
|
|
20
|
+
traces << "events #{before[:events]} -> #{after[:events]}" unless before[:events] == after[:events]
|
|
21
|
+
traces << "instances changed" unless before[:instances] == after[:instances]
|
|
22
|
+
next if traces.empty?
|
|
23
|
+
|
|
24
|
+
"dry run of #{entry[:verb]} (ok: #{entry[:ok]}) left a trace: #{traces.join(', ')}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
offenders.empty? || offenders.join("; ")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Properties: saga/policy dispatch args match their `with_spec`, and command
|
|
32
|
+
# mutations match an independent recomputation.
|
|
33
|
+
module DispatchAndMutations
|
|
34
|
+
# Checks saga and policy dispatch args against an independent re-derivation of
|
|
35
|
+
# `with_spec`. Re-deriving, not calling the interpreter again, keeps the check
|
|
36
|
+
# from agreeing with itself. Args are compared as captured live, because a
|
|
37
|
+
# saga's memory keeps changing across a run.
|
|
38
|
+
#
|
|
39
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
40
|
+
# @return [true, String] true if every saga and policy dispatch's own bound args
|
|
41
|
+
# match an independent re-derivation of their `with_spec`; otherwise a
|
|
42
|
+
# semicolon-joined message naming each offending dispatch
|
|
43
|
+
def dispatch_binding_fidelity(history)
|
|
44
|
+
saga_offenders = history.fetch(:saga_dispatches, []).filter_map do |entry|
|
|
45
|
+
expected = resolve_dispatch_binding(entry)
|
|
46
|
+
next if expected == entry[:args]
|
|
47
|
+
|
|
48
|
+
"#{entry[:process_manager]}##{entry[:instance]} dispatching #{entry[:dispatch]} on #{entry[:on]} — " \
|
|
49
|
+
"bound #{entry[:args].inspect}, but independently re-deriving with_spec's own resolution gives " \
|
|
50
|
+
"#{expected.inspect}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
policy_offenders = history.fetch(:policy_dispatches, []).filter_map do |entry|
|
|
54
|
+
expected = resolve_trigger_binding(entry)
|
|
55
|
+
next if expected == entry[:args]
|
|
56
|
+
|
|
57
|
+
"#{entry[:policy]} on #{entry[:on]} — bound #{entry[:args].inspect}, but independently re-deriving " \
|
|
58
|
+
"with_spec's own resolution gives #{expected.inspect}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
offenders = saga_offenders + policy_offenders
|
|
62
|
+
offenders.empty? || offenders.join("; ")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Re-derives SagaInterpreter#dispatch_args: a literal, the correlation key,
|
|
66
|
+
# the triggering event's payload, or else the saga's carried memory.
|
|
67
|
+
def resolve_dispatch_binding(entry)
|
|
68
|
+
entry[:with_spec].to_h do |key, value|
|
|
69
|
+
resolved = if !value.is_a?(Symbol) then value
|
|
70
|
+
elsif value == entry[:correlation_head] then entry[:instance]
|
|
71
|
+
elsif entry[:event_payload].key?(value) then entry[:event_payload][value]
|
|
72
|
+
else entry[:memory][value]
|
|
73
|
+
end
|
|
74
|
+
[key.to_sym, Runtime::Value.materialize(resolved)]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Re-derives PolicyInterpreter#trigger_args: a policy has no correlation or
|
|
79
|
+
# memory, so the trigger payload is the only source.
|
|
80
|
+
def resolve_trigger_binding(entry)
|
|
81
|
+
entry[:with_spec].to_h do |key, value|
|
|
82
|
+
resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
|
|
83
|
+
[key.to_sym, Runtime::Value.materialize(resolved)]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Ops with an independent recomputation here. `:increment` and `:decrement` are
|
|
88
|
+
# left out: their value-object and overflow handling is a much larger reproduction.
|
|
89
|
+
#
|
|
90
|
+
# Only the entity-scoped applier (`EntityElement#apply_to_element`) is
|
|
91
|
+
# reproduced, since replay captures mutation traces for entity-owned commands only.
|
|
92
|
+
# `:set` needs its own recomputation because the self-consistency checks share the
|
|
93
|
+
# same `step_apply_mutations` call and cannot see a wrong `:set` result.
|
|
94
|
+
#
|
|
95
|
+
# `:unrecomputable` (never a finding) marks a step whose fuzzer-malformed args do
|
|
96
|
+
# not fit the op's contract.
|
|
97
|
+
RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp set].freeze
|
|
98
|
+
|
|
99
|
+
# Checks that every recomputable mutation an entity-owned command
|
|
100
|
+
# applied landed on the same after-state an independent
|
|
101
|
+
# recomputation of the same rule produces.
|
|
102
|
+
#
|
|
103
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
104
|
+
# @return [true, String] true if every recomputable mutation's after-state
|
|
105
|
+
# matches an independent recomputation; otherwise a semicolon-joined message
|
|
106
|
+
# naming each offending mutation
|
|
107
|
+
def mutations_match_recompute(history)
|
|
108
|
+
bluebooks = history.fetch(:bluebooks)
|
|
109
|
+
|
|
110
|
+
offenders = history.fetch(:mutation_traces, []).flat_map do |entry|
|
|
111
|
+
next [] unless entry[:after]
|
|
112
|
+
|
|
113
|
+
command = command_for_verb(bluebooks, entry[:verb])
|
|
114
|
+
next [] unless command
|
|
115
|
+
|
|
116
|
+
aggregate = aggregate_for_verb(bluebooks, entry[:verb])
|
|
117
|
+
next [] unless aggregate
|
|
118
|
+
|
|
119
|
+
owner = owner_for_verb(bluebooks, entry[:verb]) || aggregate
|
|
120
|
+
|
|
121
|
+
command.mutations.select { |m| RECOMPUTABLE_MUTATION_OPS.include?(m.op) }.filter_map do |mutation|
|
|
122
|
+
expected = recompute_mutation(mutation, entry[:before][mutation.target], entry[:args], entry[:before],
|
|
123
|
+
aggregate, command, owner)
|
|
124
|
+
next if expected == :unrecomputable
|
|
125
|
+
|
|
126
|
+
actual = entry[:after][mutation.target]
|
|
127
|
+
next if symbolize_deep(expected) == symbolize_deep(actual)
|
|
128
|
+
|
|
129
|
+
"#{entry[:verb]} — #{mutation.op} on #{mutation.target} — recomputing independently gives " \
|
|
130
|
+
"#{expected.inspect}, but the real dispatch left #{actual.inspect}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
offenders.empty? || offenders.join("; ")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Root aggregate for `verb`, derived from the verb alone because hand-built
|
|
138
|
+
# entries may lack `:domain`/`:aggregate`. Value-object types resolve against
|
|
139
|
+
# the root's namespace only.
|
|
140
|
+
def aggregate_for_verb(bluebooks, verb)
|
|
141
|
+
domain_name, aggregate_name, = Naming.split_verb(verb)
|
|
142
|
+
return nil unless domain_name
|
|
143
|
+
|
|
144
|
+
bluebooks[domain_name]&.aggregate(aggregate_name)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The construct declaring `mutation.target`: the root aggregate, or the owning
|
|
148
|
+
# entity for a dot-shaped command. Attribute lookups must ask that construct,
|
|
149
|
+
# as `EntityElement#locate_chain` does.
|
|
150
|
+
def owner_for_verb(bluebooks, verb)
|
|
151
|
+
domain_name, aggregate_name, command_path = Naming.split_verb(verb)
|
|
152
|
+
return nil unless command_path
|
|
153
|
+
|
|
154
|
+
aggregate = bluebooks[domain_name]&.aggregate(aggregate_name)
|
|
155
|
+
return nil unless aggregate
|
|
156
|
+
return aggregate unless command_path.include?(".")
|
|
157
|
+
|
|
158
|
+
entity_name, = command_path.split(".", 2)
|
|
159
|
+
aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Dispatches to the recompute rule for `mutation.op`.
|
|
163
|
+
def recompute_mutation(mutation, current, args, before_scope, aggregate, command, owner = aggregate)
|
|
164
|
+
case mutation.op
|
|
165
|
+
when :append
|
|
166
|
+
recompute_append(current, mutation.source, before_scope, args, aggregate, command, owner, mutation.target)
|
|
167
|
+
when :remove then recompute_remove(current, mutation.source, args)
|
|
168
|
+
when :multiply then recompute_multiply(current, resolve_mutation_source(mutation.source, args))
|
|
169
|
+
when :clamp then recompute_clamp(current, mutation.source)
|
|
170
|
+
when :set then recompute_set(mutation.source, args, aggregate, owner, mutation.target)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Re-derives the `:set` branch of `EntityElement#apply_to_element`. The
|
|
175
|
+
# attribute comes from `owner`, not the root aggregate, which would leave a
|
|
176
|
+
# nested entity's value uncoerced and disagree with the real after-state.
|
|
177
|
+
# Any raise means the raw material was wrong, so the result is `:unrecomputable`.
|
|
178
|
+
def recompute_set(source, args, aggregate, owner, target)
|
|
179
|
+
raw = resolve_mutation_source(source, args)
|
|
180
|
+
attribute = owner&.attribute(target)
|
|
181
|
+
coerced = attribute ? Runtime::Value.for_attribute(aggregate, attribute, raw) : raw
|
|
182
|
+
Runtime::Value.materialize(coerced)
|
|
183
|
+
rescue StandardError
|
|
184
|
+
:unrecomputable
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Re-derives `EntityElement#appended_to_element`: each field resolves from a
|
|
188
|
+
# caller arg (coerced as `Interpreting#coerce_declared_arguments` does) or from
|
|
189
|
+
# the entity's current field (already materialized), then the element is appended.
|
|
190
|
+
# A nested-entity element also gets its declared defaults.
|
|
191
|
+
def recompute_append(current, source_map, before_scope, args, aggregate, command, owner = aggregate, target = nil)
|
|
192
|
+
fields = source_map.transform_values do |source|
|
|
193
|
+
resolve_mutation_append_field(source, before_scope, args, aggregate, command)
|
|
194
|
+
end
|
|
195
|
+
fill_recompute_declared_defaults(aggregate, owner, target, fields)
|
|
196
|
+
Array(current) + [symbolize_deep(fields)]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Fills defaults for a nested-entity element via `Instance.default_for`, which is
|
|
200
|
+
# independently tested. No-op unless `target` names an entity under `owner`
|
|
201
|
+
# (`owner.entities`, not `aggregate.entities`).
|
|
202
|
+
#
|
|
203
|
+
# A value-object element type (the more common shape: `list_of` a plain composite,
|
|
204
|
+
# not a nested entity) defaults the other way real dispatch does — through
|
|
205
|
+
# `Runtime::Value.apply_defaults`, the exact primitive `EntityElement#appended_to_
|
|
206
|
+
# element`'s own `Value.build` call uses. Without this branch, a caller-omitted,
|
|
207
|
+
# VO-declared `default:` field is left out of the recomputed element entirely,
|
|
208
|
+
# disagreeing with real dispatch's fully-defaulted one.
|
|
209
|
+
def fill_recompute_declared_defaults(aggregate, owner, target, fields)
|
|
210
|
+
return fields unless target
|
|
211
|
+
|
|
212
|
+
element_type = owner&.attribute(target)&.type
|
|
213
|
+
return fields unless element_type
|
|
214
|
+
|
|
215
|
+
entity = owner.entities.find { |piece| piece.hecks_name == element_type.to_s }
|
|
216
|
+
if entity
|
|
217
|
+
entity.attributes.each do |attribute|
|
|
218
|
+
next if fields.key?(attribute.name)
|
|
219
|
+
|
|
220
|
+
fields[attribute.name] = attribute.list? ? [] : Runtime::Instance.default_for(aggregate, attribute)
|
|
221
|
+
end
|
|
222
|
+
return fields
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
value_object = aggregate.value_object(element_type)
|
|
226
|
+
return fields unless value_object
|
|
227
|
+
|
|
228
|
+
Runtime::Value.apply_defaults(value_object, fields)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Resolves one appended field's value from its declared source.
|
|
232
|
+
def resolve_mutation_append_field(source, before_scope, args, aggregate, command)
|
|
233
|
+
return source unless source.is_a?(Symbol)
|
|
234
|
+
return before_scope[source] unless args.key?(source)
|
|
235
|
+
|
|
236
|
+
coerce_recompute_append_arg(aggregate, command, source, args[source])
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Coerces a raw arg only when `source` is a declared attribute of the command,
|
|
240
|
+
# as a real dispatch does. Materialized so the comparison is plain data on both sides.
|
|
241
|
+
def coerce_recompute_append_arg(aggregate, command, source, raw)
|
|
242
|
+
attribute = command.attribute(source)
|
|
243
|
+
return raw unless attribute
|
|
244
|
+
|
|
245
|
+
Runtime::Value.materialize(Runtime::Value.for_attribute(aggregate, attribute, raw, argument: true))
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Re-derives `MutationApplier#removed`: removes every value-equal element.
|
|
249
|
+
# `Value.materialize` first: once `args` arrives normalized (replay.rb's
|
|
250
|
+
# `build_mutation_trace`), a composite `remove:` source is already a built
|
|
251
|
+
# `Runtime::Value` with its own declared defaults filled — comparing it
|
|
252
|
+
# unmaterialized against `current`'s plain Hashes would never match, wrongly
|
|
253
|
+
# keeping an element real dispatch correctly removed.
|
|
254
|
+
def recompute_remove(current, source, args)
|
|
255
|
+
target = symbolize_deep(Runtime::Value.materialize(resolve_mutation_source(source, args)))
|
|
256
|
+
Array(current).reject { |element| symbolize_deep(element) == target }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Re-derives `CommandRules::Arithmetic#multiply` on plain data: a Hash with one
|
|
260
|
+
# numeric field scales that field, a bare Numeric scales itself. A nil
|
|
261
|
+
# `current` is treated as 0.
|
|
262
|
+
def recompute_multiply(current, amount)
|
|
263
|
+
return :unrecomputable unless amount.is_a?(Numeric)
|
|
264
|
+
|
|
265
|
+
current ||= 0
|
|
266
|
+
if current.is_a?(Hash)
|
|
267
|
+
field = current.keys.find { |f| current[f].is_a?(Numeric) }
|
|
268
|
+
return :unrecomputable unless field
|
|
269
|
+
|
|
270
|
+
current.merge(field => current[field] * amount)
|
|
271
|
+
elsif current.is_a?(Numeric)
|
|
272
|
+
current * amount
|
|
273
|
+
else
|
|
274
|
+
:unrecomputable
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Re-derives `CommandRules::Arithmetic#clamp` like #recompute_multiply. The
|
|
279
|
+
# source is always a literal `[min, max]`, never an argument reference.
|
|
280
|
+
def recompute_clamp(current, bounds)
|
|
281
|
+
return :unrecomputable unless bounds.is_a?(Array) && bounds.size == 2
|
|
282
|
+
|
|
283
|
+
min, max = bounds
|
|
284
|
+
current ||= 0
|
|
285
|
+
if current.is_a?(Hash)
|
|
286
|
+
field = current.keys.find { |f| current[f].is_a?(Numeric) }
|
|
287
|
+
return :unrecomputable unless field
|
|
288
|
+
|
|
289
|
+
current.merge(field => current[field].clamp(min, max))
|
|
290
|
+
elsif current.is_a?(Numeric)
|
|
291
|
+
current.clamp(min, max)
|
|
292
|
+
else
|
|
293
|
+
:unrecomputable
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# A mutation source is an argument name (Symbol) or a literal.
|
|
298
|
+
def resolve_mutation_source(source, args)
|
|
299
|
+
source.is_a?(Symbol) ? args[source] : source
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Symbolizes hash keys recursively. Generated args carry string keys while
|
|
303
|
+
# materialized trace state carries symbols, and `Hash#==` would otherwise differ.
|
|
304
|
+
def symbolize_deep(value)
|
|
305
|
+
case value
|
|
306
|
+
when Hash then value.to_h { |key, val| [key.to_sym, symbolize_deep(val)] }
|
|
307
|
+
when Array then value.map { |val| symbolize_deep(val) }
|
|
308
|
+
else value
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|