hecks 2.9.0 → 3.0.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/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 +4 -2
- 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 +7 -4
- 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 +6 -3
- 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,505 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "tmpdir"
|
|
3
|
+
require_relative "isolated_boot"
|
|
4
|
+
require_relative "self_consistency"
|
|
5
|
+
require_relative "../query_specification/common/comparators"
|
|
6
|
+
require_relative "../query_specification/common/where_clause"
|
|
7
|
+
require_relative "../query_specification/field_path"
|
|
8
|
+
require_relative "../ports/query/in_memory"
|
|
9
|
+
|
|
10
|
+
module Hecks
|
|
11
|
+
module Fuzzing
|
|
12
|
+
# Replays a step list in-process against a fresh boot, returning the
|
|
13
|
+
# observable history as data instead of JSON on stdout.
|
|
14
|
+
module Replay
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Mirrors QuerySpecification::Common::COMPARATORS, not the Rust
|
|
18
|
+
# kernel's own comparator enum, which is missing `none_in_state` and has drifted.
|
|
19
|
+
FILTER_COMPARATORS = Hecks::QuerySpecification::Common::COMPARATORS.map(&:to_s).freeze
|
|
20
|
+
|
|
21
|
+
# The two classes Admissibility#enforce_givens/#enforce_lifecycle_guard raise;
|
|
22
|
+
# any other DOMAIN_REFUSAL proves the guard itself did not fire.
|
|
23
|
+
GUARD_REFUSAL_CLASSES = [Runtime::GivenNotMet, Runtime::LifecycleRefused].freeze
|
|
24
|
+
|
|
25
|
+
# Replays +steps+ against a fresh boot of +domain_path+. The oracle snapshots taken
|
|
26
|
+
# inside the loop are timed relative to dispatch; do not reorder them.
|
|
27
|
+
#
|
|
28
|
+
# @param domain_path [String] filesystem path to the domain directory to replay
|
|
29
|
+
# @param adapter [Symbol] persistence adapter to boot the copy with
|
|
30
|
+
# @param database [String, nil] PostgresEra database name (required for :postgres_era)
|
|
31
|
+
# @param self_consistency [Boolean] also run SelfConsistency.check before returning
|
|
32
|
+
# @return [Hash] instances, events, refusals, reactions, sagas, queries, oracle traces
|
|
33
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
34
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
35
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
36
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
37
|
+
def call(domain_path, steps, adapter: :memory, database: nil, schema: nil, self_consistency: false)
|
|
38
|
+
# See isolated_boot.rb's own header: resets data/ and rebinds persistence to the
|
|
39
|
+
# chosen adapter, since a Postgres-bound domain's real store lives outside the
|
|
40
|
+
# copied directory and can't be reached by resetting data/ alone.
|
|
41
|
+
IsolatedBoot.call(domain_path, adapter: adapter, database: database, schema: schema) do |copy|
|
|
42
|
+
runtime = Hecks.boot(copy, environment: nil)
|
|
43
|
+
|
|
44
|
+
refusals = []
|
|
45
|
+
queries = []
|
|
46
|
+
dry_runs = []
|
|
47
|
+
dry_run_traces = []
|
|
48
|
+
fan_outs = []
|
|
49
|
+
guard_checks = []
|
|
50
|
+
mutation_traces = []
|
|
51
|
+
outbox_traces = []
|
|
52
|
+
|
|
53
|
+
# Every `[domain, aggregate_name]` a `for_each` policy could query, resolved
|
|
54
|
+
# once from every loaded bluebook's own fanning-out policies. Empty when no
|
|
55
|
+
# domain declares `for_each`, so the snapshot below costs nothing until one does.
|
|
56
|
+
fan_out_targets = runtime.registry.bluebooks.each_with_object({}) do |(domain, bluebook), targets|
|
|
57
|
+
bluebook.policies.select(&:fans_out?).each do |policy|
|
|
58
|
+
query_domain, aggregate_name, = policy.for_each_route(domain)
|
|
59
|
+
targets[[query_domain, aggregate_name]] ||= runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
steps.each do |step|
|
|
64
|
+
step = step.transform_keys(&:to_s)
|
|
65
|
+
args = (step["args"] || {}).transform_keys(&:to_sym)
|
|
66
|
+
|
|
67
|
+
if (question = step["query"])
|
|
68
|
+
# A `{aggregate:, field:, op:, value:}` Hash "query" step — the ad hoc
|
|
69
|
+
# filter kernel/cli.rs's own object-form step also reads, bypassing the
|
|
70
|
+
# declared bluebook query DSL. Answered via `Ports::Query::InMemory` directly.
|
|
71
|
+
if question.is_a?(Hash)
|
|
72
|
+
begin
|
|
73
|
+
rows = run_filter(runtime, question)
|
|
74
|
+
queries << { query: question, rows: rows, instances_at: snapshot_instances(runtime) }
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
refusals << { verb: filter_label(question), error: e.message, kind: refusal_kind(e) }
|
|
77
|
+
end
|
|
78
|
+
next
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Each engine runs in its own begin/rescue, never a shared one — a shared
|
|
82
|
+
# rescue would hide "one engine refused, the other didn't" (the real
|
|
83
|
+
# divergence this differential oracle exists to catch) behind a plain refusal.
|
|
84
|
+
native_rows = native_error = nil
|
|
85
|
+
begin
|
|
86
|
+
native_rows = runtime.query(question, **args)
|
|
87
|
+
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
88
|
+
native_error = e
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Read-model asks (bare domain form, no "::") have no
|
|
92
|
+
# reference twin at all — never attempted, not "attempted and
|
|
93
|
+
# agreed."
|
|
94
|
+
has_reference = question.include?("::")
|
|
95
|
+
reference_rows = reference_error = nil
|
|
96
|
+
if has_reference
|
|
97
|
+
begin
|
|
98
|
+
reference_rows = runtime.reference_query(question, **args)
|
|
99
|
+
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
100
|
+
reference_error = e
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
entry = { query: question, args: args, rows: native_rows, instances_at: snapshot_instances(runtime) }
|
|
105
|
+
entry[:error] = native_error.message if native_error
|
|
106
|
+
if has_reference
|
|
107
|
+
entry[:reference_rows] = reference_rows
|
|
108
|
+
entry[:reference_error] = reference_error.message if reference_error
|
|
109
|
+
end
|
|
110
|
+
queries << entry
|
|
111
|
+
|
|
112
|
+
refusals << { verb: question, error: native_error.message, kind: refusal_kind(native_error) } if native_error
|
|
113
|
+
next
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# `dry_run` is evaluated hypothetically and recorded, never a refusal.
|
|
117
|
+
# `dry_runs`' shape (`{verb:, ok:, error?:}`) is compared against
|
|
118
|
+
# `kernel/cli.rs`'s own answer; `dry_run_traces` is Ruby-only oracle data.
|
|
119
|
+
if (hypothetical = step["dry_run"])
|
|
120
|
+
before = { instances: snapshot_instances(runtime), events: runtime.events.size }
|
|
121
|
+
entry = { verb: hypothetical }
|
|
122
|
+
begin
|
|
123
|
+
as_step_caller(step) { runtime.dry_run?(hypothetical, **args) }
|
|
124
|
+
entry[:ok] = true
|
|
125
|
+
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
126
|
+
entry.merge!(ok: false, error: e.message)
|
|
127
|
+
end
|
|
128
|
+
after = { instances: snapshot_instances(runtime), events: runtime.events.size }
|
|
129
|
+
dry_runs << entry
|
|
130
|
+
dry_run_traces << entry.merge(before: before, after: after)
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
begin
|
|
135
|
+
# Taken before dispatch, so this step's own reactions can be sliced out
|
|
136
|
+
# after and matched against an independent recomputation of what a
|
|
137
|
+
# `for_each` policy should have fanned out over.
|
|
138
|
+
reaction_mark = runtime.reactions.size
|
|
139
|
+
|
|
140
|
+
# `saga_log_mark` slices `runtime.sagas` the same way `reaction_mark`
|
|
141
|
+
# slices `runtime.reactions`. `outbox_before_ids` is a set of delivery_ids,
|
|
142
|
+
# not a size, because `runtime.outbox.rows` concatenates several stores'
|
|
143
|
+
# own arrays, so a plain "grew from N to M" tail slice could miss or
|
|
144
|
+
# misattribute rows once more than one repository has an outbox.
|
|
145
|
+
saga_log_mark = runtime.sagas.size
|
|
146
|
+
outbox_before_ids = runtime.outbox.rows.map(&:delivery_id)
|
|
147
|
+
|
|
148
|
+
# The snapshot a `for_each` query would have seen, taken before dispatch:
|
|
149
|
+
# `deliver_for_each` runs its query synchronously inside this same
|
|
150
|
+
# dispatch, so reading the live repository after would see what the
|
|
151
|
+
# fan-out's own dispatched commands already mutated, not what it matched.
|
|
152
|
+
fan_out_snapshot = fan_out_targets.each_with_object({}) do |((fdomain, faggregate_name), aggregate), snap|
|
|
153
|
+
next unless aggregate
|
|
154
|
+
|
|
155
|
+
snap[[fdomain, faggregate_name]] =
|
|
156
|
+
runtime.registry.repository(fdomain, aggregate).all.to_h { |record| [record.id, record.state.dup] }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Same idiom as fan_out_snapshot above: the guard is replayed read-only
|
|
160
|
+
# before this step's real dispatch can mutate anything a cross-aggregate
|
|
161
|
+
# given dereferences.
|
|
162
|
+
guard_check = build_guard_check(runtime, step["verb"], args)
|
|
163
|
+
|
|
164
|
+
# Same idiom again: the entity element a mutation step's args address is
|
|
165
|
+
# snapshotted before dispatch, so it can be diffed against its post-dispatch state.
|
|
166
|
+
mutation_trace = build_mutation_trace(runtime, step["verb"], args)
|
|
167
|
+
|
|
168
|
+
# `role:`/`actor_id:` are optional per-step keys; a step with neither
|
|
169
|
+
# dispatches bare, as every existing corpus step always has. Binds the
|
|
170
|
+
# ambient caller for exactly this one dispatch, then unbinds, so
|
|
171
|
+
# back-to-back steps with different (or no) `role:` never leak into each other.
|
|
172
|
+
result = as_step_caller(step) { runtime.dispatch_flat(step["verb"], args) }
|
|
173
|
+
|
|
174
|
+
fan_outs.concat(fan_out_findings(runtime, fan_out_snapshot, result.events, runtime.reactions[reaction_mark..]))
|
|
175
|
+
|
|
176
|
+
# Every outbox row this step's own dispatch newly wrote, across every
|
|
177
|
+
# bound repository including any a reaction cascade touched, paired with
|
|
178
|
+
# the reaction/saga rows that same dispatch produced. Skipped when empty.
|
|
179
|
+
outbox_new_rows = runtime.outbox.rows.reject { |row| outbox_before_ids.include?(row.delivery_id) }
|
|
180
|
+
if outbox_new_rows.any?
|
|
181
|
+
outbox_traces << { verb: step["verb"], rows: outbox_new_rows.map(&:to_h),
|
|
182
|
+
reactions: runtime.reactions[reaction_mark..].dup,
|
|
183
|
+
sagas: runtime.sagas[saga_log_mark..].dup }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
guard_checks << guard_check.merge(actual_refused: false, actual_kind: nil) if guard_check
|
|
187
|
+
# After — only on success ; a refused step mutated nothing,
|
|
188
|
+
# so there is no "after" to compare (and #build_mutation_
|
|
189
|
+
# trace already skipped anything with no mutations to
|
|
190
|
+
# trace in the first place).
|
|
191
|
+
mutation_traces << mutation_trace.merge(after: read_mutation_after(runtime, mutation_trace)) if mutation_trace
|
|
192
|
+
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
193
|
+
# `kind:` is the raised class, not derived from the message: several
|
|
194
|
+
# refusal templates share the same wording, so only the class tells a
|
|
195
|
+
# guard refusal apart from the rest.
|
|
196
|
+
refusals << { verb: step["verb"], error: e.message, kind: refusal_kind(e) }
|
|
197
|
+
# Only a refusal raised by the guard itself counts here. A refusal from a
|
|
198
|
+
# stage before or after enforce_givens can share TypeMismatch's class, so
|
|
199
|
+
# anything outside the two guard classes is left out — inconclusive, not a pass.
|
|
200
|
+
if guard_check && GUARD_REFUSAL_CLASSES.include?(e.class)
|
|
201
|
+
guard_checks << guard_check.merge(actual_refused: true,
|
|
202
|
+
actual_kind: e.class.name)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
instances = snapshot_instances(runtime)
|
|
208
|
+
|
|
209
|
+
events = runtime.events.map do |event|
|
|
210
|
+
{ name: event.name, aggregate: event.aggregate, id: event.id, payload: event.payload }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The live process-manager store, materialised to inert data (`{ pm_name =>
|
|
214
|
+
# { correlation => { state:, memory: } } }`), captured here because the
|
|
215
|
+
# runtime goes out of scope with the boot and the Memory rebind leaves no
|
|
216
|
+
# real saga store for Properties.sagas_rehydrate_cleanly to read otherwise.
|
|
217
|
+
saga_instances = runtime.registry.saga_instances.each_with_object({}) do |(pm_name, conversations), out|
|
|
218
|
+
out[pm_name] = conversations.each_with_object({}) do |(correlation, instance), rows|
|
|
219
|
+
rows[correlation] = { state: instance[:state], memory: Runtime::Value.materialize(instance[:memory]) }
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# The booted chapter rides along so properties.rb's lifecycle/saga checks
|
|
224
|
+
# have the declared IR beside the history it produced. `bluebook:` (singular)
|
|
225
|
+
# is the first-loaded chapter, kept for properties scoped to it deliberately;
|
|
226
|
+
# `bluebooks:` (plural) is the full domain-name-keyed map, for a property that
|
|
227
|
+
# needs to resolve a verb back to its own declaring bluebook.
|
|
228
|
+
history = { instances: instances, events: events, refusals: refusals,
|
|
229
|
+
reactions: runtime.reactions, sagas: runtime.sagas, saga_instances: saga_instances,
|
|
230
|
+
queries: queries, dry_runs: dry_runs, dry_run_traces: dry_run_traces,
|
|
231
|
+
fan_outs: fan_outs, guard_checks: guard_checks,
|
|
232
|
+
mutation_traces: mutation_traces, outbox_traces: outbox_traces,
|
|
233
|
+
saga_dispatches: runtime.saga_dispatches, policy_dispatches: runtime.policy_dispatches,
|
|
234
|
+
bluebook: runtime.registry.bluebooks.values.first,
|
|
235
|
+
bluebooks: runtime.registry.bluebooks.dup }
|
|
236
|
+
|
|
237
|
+
# `runtime` is still live here — the one and only place it is — so
|
|
238
|
+
# SelfConsistency runs now, against the same registry a second boot couldn't reuse.
|
|
239
|
+
history[:self_consistency] = SelfConsistency.check(runtime, history) if self_consistency
|
|
240
|
+
|
|
241
|
+
history
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Runs the block with this step's `role:`/`actor_id:` bound as the ambient
|
|
246
|
+
# caller, if it declares one; a step with no `role:` dispatches bare.
|
|
247
|
+
def as_step_caller(step, &)
|
|
248
|
+
return yield unless step["role"]
|
|
249
|
+
|
|
250
|
+
Hecks.as_caller(role: step["role"], actor_id: step["actor_id"], &)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Every persisted record's state, keyed by `"Domain::Aggregate#id"`. Called once
|
|
254
|
+
# per query step, not only at the end, so a property recomputing "the eligible
|
|
255
|
+
# rows" reads the state as that query actually saw it, not a shared final one.
|
|
256
|
+
def snapshot_instances(runtime)
|
|
257
|
+
instances = {}
|
|
258
|
+
runtime.registry.bluebooks.each do |domain_name, bluebook|
|
|
259
|
+
bluebook.aggregates.each do |aggregate|
|
|
260
|
+
runtime.registry.repository(domain_name, aggregate).all.each do |record|
|
|
261
|
+
instances["#{domain_name}::#{aggregate.name}##{record.id}"] = record.state
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
instances
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Resolves the record (if any) this step's guard should be checked against, and
|
|
269
|
+
# replays enforce_givens then admissible_transition, in that order, to mirror
|
|
270
|
+
# the real DISPATCH_ORDER. `nil` for anything out of scope.
|
|
271
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
272
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
273
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
274
|
+
def build_guard_check(runtime, verb, args)
|
|
275
|
+
domain_name, aggregate_name, command_name = Naming.split_verb(verb)
|
|
276
|
+
return nil unless command_name && !command_name.include?(".")
|
|
277
|
+
|
|
278
|
+
aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
|
|
279
|
+
command = aggregate&.command(command_name)
|
|
280
|
+
return nil unless aggregate && command && !command.creates?
|
|
281
|
+
|
|
282
|
+
# A transition-only guard (no per-command `from:`/`given`, only an aggregate
|
|
283
|
+
# `lifecycle` block naming this command) still counts as something to check,
|
|
284
|
+
# now that `admissible_transition` is reproduced below.
|
|
285
|
+
has_transition = aggregate.lifecycle&.transitions_for(command.hecks_name)&.any?
|
|
286
|
+
return nil if command.givens.empty? && !command.from && !has_transition
|
|
287
|
+
|
|
288
|
+
reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
|
|
289
|
+
id = Runtime::Identity.of(aggregate, args) ||
|
|
290
|
+
Runtime::Identity.from(aggregate, args, :id) ||
|
|
291
|
+
(reference_key && Runtime::Identity.from(aggregate, args, reference_key))
|
|
292
|
+
return nil unless id
|
|
293
|
+
|
|
294
|
+
record = runtime.registry.repository(domain_name, aggregate).find(id)
|
|
295
|
+
return nil unless record
|
|
296
|
+
|
|
297
|
+
rules = Runtime::CommandRules.new(runtime.registry)
|
|
298
|
+
# Real dispatch normalizes args (step_normalize_args) before it ever reaches
|
|
299
|
+
# enforce_givens, coercing a bare scalar like "white" into its command's declared
|
|
300
|
+
# value-object shape. Skipping that here would hand enforce_givens the raw fuzzed
|
|
301
|
+
# shape instead, and a `given` comparing a normalized field against it would walk a
|
|
302
|
+
# path a String happens to answer (a substring lookup) rather than the refusal real
|
|
303
|
+
# dispatch raises. `send` reaches the interpreter's own private coercion (the same
|
|
304
|
+
# idiom SelfConsistency#last_advancing_event uses for `saga_correlation`) rather than
|
|
305
|
+
# reimplementing it here.
|
|
306
|
+
interpreter = Runtime::CommandInterpreter.new(runtime.registry, rules: rules)
|
|
307
|
+
normalized_args = interpreter.send(:normalize_args, aggregate, command, args)
|
|
308
|
+
recomputed_kind = begin
|
|
309
|
+
rules.enforce_givens(record.dup, command, normalized_args, domain: domain_name, declaring: aggregate)
|
|
310
|
+
|
|
311
|
+
# A second, separate DISPATCH_ORDER step: the aggregate's own `lifecycle`
|
|
312
|
+
# transition guard is a wholly different method from enforce_givens' own
|
|
313
|
+
# per-command `from:` check, called only when enforce_givens didn't already refuse.
|
|
314
|
+
# It reads only the record's own lifecycle field and the command's declared
|
|
315
|
+
# transitions, never `args`, so it needs no normalized input of its own.
|
|
316
|
+
rules.admissible_transition(aggregate, command, record.dup)
|
|
317
|
+
nil
|
|
318
|
+
rescue *GUARD_REFUSAL_CLASSES => e
|
|
319
|
+
e.class.name
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
{ verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name, id: id,
|
|
323
|
+
recomputed_refused: !recomputed_kind.nil?, recomputed_kind: recomputed_kind }
|
|
324
|
+
rescue StandardError
|
|
325
|
+
nil
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Scoped to entity-dispatched commands only, the one place `append`/`remove`/
|
|
329
|
+
# `multiply`/`clamp` act on an entity's own attributes. `nil` for anything out of
|
|
330
|
+
# scope: an aggregate-level command, no mutations, or unresolvable identity args.
|
|
331
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
332
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
333
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
334
|
+
def build_mutation_trace(runtime, verb, args)
|
|
335
|
+
domain_name, aggregate_name, command_name = Naming.split_verb(verb)
|
|
336
|
+
return nil unless command_name&.include?(".")
|
|
337
|
+
|
|
338
|
+
aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
|
|
339
|
+
return nil unless aggregate
|
|
340
|
+
|
|
341
|
+
entity_name, entity_command_name = command_name.split(".", 2)
|
|
342
|
+
entity = aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
|
|
343
|
+
command = entity&.command(entity_command_name)
|
|
344
|
+
return nil unless command&.mutations&.any?
|
|
345
|
+
|
|
346
|
+
# Same idiom as build_guard_check above: real dispatch normalizes args
|
|
347
|
+
# (step_normalize_args) before apply_mutations ever runs, coercing a declared
|
|
348
|
+
# payload attribute into its command's shape — a composite argument's own
|
|
349
|
+
# `Value.build` fills that shape's declared defaults along the way. Recomputing
|
|
350
|
+
# a mutation against the raw fuzzed args instead would disagree with real
|
|
351
|
+
# dispatch whenever a caller omitted a defaulted field, the same false-divergence
|
|
352
|
+
# shape build_guard_check guards against for the guard check.
|
|
353
|
+
args = Runtime::CommandInterpreter.new(runtime.registry, rules: Runtime::CommandRules.new(runtime.registry))
|
|
354
|
+
.send(:normalize_args, aggregate, command, args)
|
|
355
|
+
|
|
356
|
+
reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
|
|
357
|
+
parent_id = Runtime::Identity.of(aggregate, args) ||
|
|
358
|
+
Runtime::Identity.from(aggregate, args, :id) ||
|
|
359
|
+
(reference_key && Runtime::Identity.from(aggregate, args, reference_key))
|
|
360
|
+
return nil unless parent_id
|
|
361
|
+
|
|
362
|
+
record = runtime.registry.repository(domain_name, aggregate).find(parent_id)
|
|
363
|
+
return nil unless record
|
|
364
|
+
|
|
365
|
+
list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
|
|
366
|
+
return nil unless list_attr
|
|
367
|
+
|
|
368
|
+
wants = entity.identity_paths.map do |path|
|
|
369
|
+
head = path.to_s.split(".").first.to_sym
|
|
370
|
+
raw = args[head]
|
|
371
|
+
return nil if raw.nil?
|
|
372
|
+
|
|
373
|
+
[head, Runtime::Value.for_attribute(aggregate, entity.attribute(head), raw)]
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
element = Array(record.state[list_attr.name]).find { |el| wants.all? { |head, want| el[head] == want } }
|
|
377
|
+
return nil unless element
|
|
378
|
+
|
|
379
|
+
{ verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name,
|
|
380
|
+
parent_id: parent_id, list_attr: list_attr.name, element_wants: wants,
|
|
381
|
+
before: Runtime::Value.materialize(element), args: args }
|
|
382
|
+
rescue StandardError
|
|
383
|
+
nil
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Re-locates the same element by identity, not position, since a mutation could
|
|
387
|
+
# have changed the array's own length or order. `nil` if it vanished.
|
|
388
|
+
def read_mutation_after(runtime, trace)
|
|
389
|
+
aggregate = runtime.registry.bluebook(trace[:domain])&.aggregate(trace[:aggregate])
|
|
390
|
+
return nil unless aggregate
|
|
391
|
+
|
|
392
|
+
record = runtime.registry.repository(trace[:domain], aggregate).find(trace[:parent_id])
|
|
393
|
+
return nil unless record
|
|
394
|
+
|
|
395
|
+
element = Array(record.state[trace[:list_attr]]).find do |el|
|
|
396
|
+
trace[:element_wants].all? { |head, want| el[head] == want }
|
|
397
|
+
end
|
|
398
|
+
element && Runtime::Value.materialize(element)
|
|
399
|
+
rescue StandardError
|
|
400
|
+
nil
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# One finding per (event, for_each policy) pair, independent of
|
|
404
|
+
# `PolicyInterpreter#deliver_for_each`, answered via `Ports::Query::InMemory`
|
|
405
|
+
# directly rather than `QueryInterpreter`, so this stays blind to nothing the
|
|
406
|
+
# fan-out feature adds. `expected_row_ids` is `nil` when `where` did not hold.
|
|
407
|
+
def fan_out_findings(runtime, snapshot, announced, reactions_since)
|
|
408
|
+
announced.each_with_object([]) do |event, findings|
|
|
409
|
+
# `event.aggregate` is domain-qualified ("Banking::Account"); split the same
|
|
410
|
+
# two ways `PolicyInterpreter#policies_for` does.
|
|
411
|
+
domain = event.aggregate.to_s.split("::").first
|
|
412
|
+
bluebook = runtime.registry.bluebook(domain)
|
|
413
|
+
next unless bluebook
|
|
414
|
+
|
|
415
|
+
emitting = Naming.demodulise(event.aggregate)
|
|
416
|
+
|
|
417
|
+
bluebook.policies.each do |policy|
|
|
418
|
+
next unless policy.fans_out? && policy.event_name == event.name
|
|
419
|
+
next unless policy.event_qualifier.nil? || policy.event_qualifier == emitting
|
|
420
|
+
|
|
421
|
+
findings << fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Builds one fan-out finding, comparing one `for_each` policy's independently
|
|
427
|
+
# recomputed expected rows against what actually reacted for one event.
|
|
428
|
+
def fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
|
|
429
|
+
payload = event.payload.transform_keys(&:to_sym)
|
|
430
|
+
held = policy.where.to_s.empty? ||
|
|
431
|
+
Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
|
|
432
|
+
|
|
433
|
+
expected = held ? expected_fan_out_rows(runtime, snapshot, policy, domain, payload) : nil
|
|
434
|
+
|
|
435
|
+
actual = reactions_since.select { |r| r[:policy] == policy.name && r[:on] == event.name }
|
|
436
|
+
.filter_map { |r| r[:for_row] }
|
|
437
|
+
|
|
438
|
+
{ policy: policy.name, on: event.name, expected_row_ids: expected, actual_row_ids: actual }
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# `policy.for_each`'s declared query, answered against the pre-dispatch snapshot
|
|
442
|
+
# (the real fan-out's query runs before its own dispatched commands can mutate
|
|
443
|
+
# anything it would match, so this must read the same "before" state). A `Symbol`
|
|
444
|
+
# where-value binds to the triggering event's own payload; a literal is compared as declared.
|
|
445
|
+
def expected_fan_out_rows(runtime, snapshot, policy, domain, payload)
|
|
446
|
+
query_domain, aggregate_name, query_name = policy.for_each_route(domain)
|
|
447
|
+
aggregate = runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
|
|
448
|
+
query = aggregate&.query(query_name)
|
|
449
|
+
return [] unless query
|
|
450
|
+
|
|
451
|
+
rows = snapshot[[query_domain, aggregate_name]] || {}
|
|
452
|
+
matched = rows.select do |_id, state|
|
|
453
|
+
query.wheres.all? do |clause|
|
|
454
|
+
held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, clause.field))
|
|
455
|
+
Ports::Query::InMemory.holds?(clause, held, payload)
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
matched.keys.map(&:to_s).sort
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Names the outcome class a recorded refusal row should carry: an evaluation
|
|
463
|
+
# fault is `"Fault"` (matching the Rust kernel's `Refusal::Fault`), never a raw
|
|
464
|
+
# Ruby exception name.
|
|
465
|
+
def refusal_kind(error)
|
|
466
|
+
error.is_a?(Bluebook::Expression::EvaluationError) ? "Fault" : error.class.name
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Answers one ad hoc filter step, the mirror image of kernel/cli.rs's own
|
|
470
|
+
# `run_filter`, calling the same production `Ports::Query::InMemory` rather than
|
|
471
|
+
# re-deriving comparator behavior by hand. Sorted by id ascending regardless,
|
|
472
|
+
# since an ad hoc filter declares no order of its own.
|
|
473
|
+
#
|
|
474
|
+
# @raise [Bluebook::Expression::EvaluationError] if `op` or `"aggregate"` is unknown
|
|
475
|
+
def run_filter(runtime, filter)
|
|
476
|
+
aggregate_ref = filter["aggregate"].to_s
|
|
477
|
+
field = filter["field"].to_s
|
|
478
|
+
op = filter["op"].to_s
|
|
479
|
+
value = filter["value"]
|
|
480
|
+
|
|
481
|
+
# A malformed ad-hoc ask is a fault (C8.3), not a bare RuntimeError.
|
|
482
|
+
unless FILTER_COMPARATORS.include?(op)
|
|
483
|
+
raise Bluebook::Expression::EvaluationError, "unknown query comparator #{op.inspect}"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
domain_name, aggregate_name = aggregate_ref.split("::", 2)
|
|
487
|
+
aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
|
|
488
|
+
raise Bluebook::Expression::EvaluationError, "unknown aggregate #{aggregate_ref.inspect}" unless aggregate
|
|
489
|
+
|
|
490
|
+
clause = QuerySpecification::Common::WhereClause.new(field: field, op: op, value: value)
|
|
491
|
+
records = runtime.registry.repository(domain_name, aggregate).all
|
|
492
|
+
matched = records.select do |record|
|
|
493
|
+
held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(record, field))
|
|
494
|
+
Ports::Query::InMemory.holds?(clause, held, {})
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
matched.sort_by { |record| record.id.to_s }.map { |record| { id: record.id }.merge(record.state) }
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Builds the `refusals` entry's own "verb" column for a refused ad hoc filter,
|
|
501
|
+
# which carries no real verb to report.
|
|
502
|
+
def filter_label(filter) = "filter #{filter['aggregate']}.#{filter['field']} #{filter['op']}"
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# Weighted pick over `QualityControl::Target.Rotation` rows: staleness blended with yield.
|
|
4
|
+
# Pure: no clock, no queries; `now` is an argument.
|
|
5
|
+
module RotationPriority
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# The next stored `Target.yield_score`: decayed old score plus this period's surprises.
|
|
9
|
+
# Integer division, like every other count in the ledger.
|
|
10
|
+
#
|
|
11
|
+
# @param old_score [Integer] the score before this period
|
|
12
|
+
# @param surprises_this_period [Integer] Surprised checks found this period
|
|
13
|
+
# @param decay_percent [Integer] percent of `old_score` that survives
|
|
14
|
+
# @return [Integer] the next score to store
|
|
15
|
+
# @raise [ArgumentError] if either count is negative
|
|
16
|
+
def next_yield_score(old_score:, surprises_this_period:,
|
|
17
|
+
decay_percent: QualityControlDials::YIELD_DECAY_PERCENT)
|
|
18
|
+
raise ArgumentError, "old_score must not be negative" if old_score.negative?
|
|
19
|
+
raise ArgumentError, "surprises_this_period must not be negative" if surprises_this_period.negative?
|
|
20
|
+
|
|
21
|
+
((old_score * decay_percent) / 100) + surprises_this_period
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Picks the next target from `Target.Rotation` rows, or nil when there are none.
|
|
25
|
+
#
|
|
26
|
+
# A row stale past `floor_seconds` wins outright (oldest first), so no target starves.
|
|
27
|
+
# Otherwise the highest `staleness + yield_score * weight_seconds` wins.
|
|
28
|
+
#
|
|
29
|
+
# @param rows [Array<Hash>] rows with `:last_swept`, `:yield_score` as `{ value: Integer }`
|
|
30
|
+
# @param now [Integer] current time, Unix epoch seconds
|
|
31
|
+
# @param weight_seconds [Integer] seconds of staleness one yield point is worth
|
|
32
|
+
# @param floor_seconds [Integer] staleness past which a row jumps the queue
|
|
33
|
+
# @return [Hash, nil] the picked row
|
|
34
|
+
def pick(rows, now:, weight_seconds: QualityControlDials::YIELD_WEIGHT_SECONDS,
|
|
35
|
+
floor_seconds: QualityControlDials::ROTATION_STALE_FLOOR_SECONDS)
|
|
36
|
+
return nil if rows.empty?
|
|
37
|
+
|
|
38
|
+
floored = rows.select { |row| staleness(row, now) >= floor_seconds }
|
|
39
|
+
return floored.min_by { |row| row[:last_swept][:value] } unless floored.empty?
|
|
40
|
+
|
|
41
|
+
rows.max_by { |row| staleness(row, now) + (row[:yield_score][:value] * weight_seconds) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def staleness(row, now) = now - row[:last_swept][:value]
|
|
45
|
+
private_class_method :staleness
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "hecks/naming"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Fuzzing
|
|
6
|
+
# What a compiled Rust binary declares it did not generate, read from the generated
|
|
7
|
+
# `manifest.json` files rather than inferred from Rust's refusal wording.
|
|
8
|
+
#
|
|
9
|
+
# The differential fuzzer drops a query or read-model verb only when a manifest says
|
|
10
|
+
# `generated: false`; any other refusal stays in the comparison and fails it.
|
|
11
|
+
#
|
|
12
|
+
# A binary's manifests are its own `src/generated/<feature>/` plus every shared chapter
|
|
13
|
+
# (a generated directory with no `merged.rs`). A missing manifest tolerates nothing.
|
|
14
|
+
class RustGapManifest
|
|
15
|
+
# Commands change state, so dropping their refusal would make the comparison dishonest.
|
|
16
|
+
TOLERABLE_KINDS = %w[query read_model].freeze
|
|
17
|
+
PINNED_BINARY = %r{\A(?<rust_dir>.+)/target/debug/rust-(?<feature>[a-z0-9_]+)\z}
|
|
18
|
+
|
|
19
|
+
attr_reader :rust_dir, :feature, :entries
|
|
20
|
+
|
|
21
|
+
# Builds the manifest reader for the pinned conformance binary at `binary`.
|
|
22
|
+
#
|
|
23
|
+
# @param binary [String] path to a pinned binary, `<rust_dir>/target/debug/rust-<feature>`
|
|
24
|
+
# @return [Fuzzing::RustGapManifest] the manifest reader for that binary's feature
|
|
25
|
+
# @raise [ArgumentError] if `binary` does not match the pinned-binary path shape
|
|
26
|
+
def self.for_binary(binary)
|
|
27
|
+
match = PINNED_BINARY.match(File.expand_path(binary.to_s))
|
|
28
|
+
unless match
|
|
29
|
+
raise ArgumentError, "#{binary.inspect} is not a pinned conformance binary " \
|
|
30
|
+
"(<rust_dir>/target/debug/rust-<feature>) — cannot tell which manifest describes it"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
new(rust_dir: match[:rust_dir], feature: match[:feature])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Every committed manifest entry under `rust_dir`, each tagged with its generated module.
|
|
37
|
+
#
|
|
38
|
+
# @param rust_dir [String] path to the Rust project root (holds `src/generated/*/`)
|
|
39
|
+
# @return [Array<Hash>] String-keyed entries, each plus `"module" => String`
|
|
40
|
+
def self.all_entries(rust_dir)
|
|
41
|
+
Dir.glob(File.join(rust_dir, "src/generated/*/manifest.json")).flat_map do |path|
|
|
42
|
+
module_name = File.basename(File.dirname(path))
|
|
43
|
+
JSON.parse(File.read(path)).map { |entry| entry.merge("module" => module_name) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param rust_dir [String] path to the Rust project root (holds `src/generated/*/`)
|
|
48
|
+
# @param feature [String] the domain feature this manifest reader describes
|
|
49
|
+
def initialize(rust_dir:, feature:)
|
|
50
|
+
@rust_dir = rust_dir
|
|
51
|
+
@feature = feature
|
|
52
|
+
@entries = manifest_paths.flat_map { |path| JSON.parse(File.read(path)) }.freeze
|
|
53
|
+
@not_generated = index_not_generated
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The manifest entry declaring `verb` not generated, or nil. An ad hoc filter (a Hash)
|
|
57
|
+
# is never declared.
|
|
58
|
+
#
|
|
59
|
+
# @param verb [String, Object] the wire-spelled query or read-model verb
|
|
60
|
+
# @return [Hash, nil] the String-keyed manifest entry
|
|
61
|
+
def not_generated(verb)
|
|
62
|
+
return nil unless verb.is_a?(String)
|
|
63
|
+
|
|
64
|
+
@not_generated[verb]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Answers whether `verb` is declared not generated by this manifest.
|
|
68
|
+
#
|
|
69
|
+
# @param verb [String, Object] the wire-spelled query or read-model verb to look up
|
|
70
|
+
# @return [Boolean] whether the manifest declares `verb` not generated
|
|
71
|
+
def not_generated?(verb) = !not_generated(verb).nil?
|
|
72
|
+
|
|
73
|
+
# Lists every verb this manifest declares not generated.
|
|
74
|
+
#
|
|
75
|
+
# @return [Set<String>] every verb the manifest declares not generated
|
|
76
|
+
def not_generated_verbs = @not_generated.keys.to_set
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def manifest_paths
|
|
81
|
+
generated = File.join(rust_dir, "src/generated")
|
|
82
|
+
own = File.join(generated, feature, "manifest.json")
|
|
83
|
+
chapters = Dir.glob(File.join(generated, "*/manifest.json")).reject do |path|
|
|
84
|
+
dir = File.dirname(path)
|
|
85
|
+
File.basename(dir) == feature || File.exist?(File.join(dir, "merged.rs"))
|
|
86
|
+
end
|
|
87
|
+
([own] + chapters.sort).select { |path| File.exist?(path) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def index_not_generated
|
|
91
|
+
@entries.each_with_object({}) do |entry, index|
|
|
92
|
+
next unless TOLERABLE_KINDS.include?(entry["kind"]) && entry["generated"] == false
|
|
93
|
+
|
|
94
|
+
wire_verbs(entry).each { |verb| index[verb] = entry }
|
|
95
|
+
end.freeze
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# A read model's id is "Domain::Name" but it is asked as "Domain.Name" or
|
|
99
|
+
# "Domain.snake_name" (the spellings `kernel::read_model::find` accepts).
|
|
100
|
+
def wire_verbs(entry)
|
|
101
|
+
return [entry["id"]] if entry["kind"] == "query"
|
|
102
|
+
|
|
103
|
+
domain, name = entry["id"].split("::", 2)
|
|
104
|
+
["#{domain}.#{name}", "#{domain}.#{Hecks::Naming.snake(name)}"].uniq
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|