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,71 @@
|
|
|
1
|
+
require "hecks/ports/persistence/plugins/era"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Fuzzing
|
|
5
|
+
# Checks a target's real, already-configured `PostgresEra` ledger for
|
|
6
|
+
# ancestor eras still holding writes nobody has merged forward.
|
|
7
|
+
module EraBoundary
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# `kind:` on a `checked: false` result distinguishes a target with no
|
|
11
|
+
# lineage to audit (`:not_applicable`) from an audit that failed to
|
|
12
|
+
# run (`:error`) — `hecks quality_control ask run` holds the first and surfaces the
|
|
13
|
+
# second as a finding, instead of treating both as a clean audit.
|
|
14
|
+
def diverged_ancestor_writes(domain_path)
|
|
15
|
+
registry, directory = load_registry(domain_path)
|
|
16
|
+
bluebook = registry.bluebooks.values.first
|
|
17
|
+
return failed("no bluebook in #{directory}") unless bluebook
|
|
18
|
+
|
|
19
|
+
first = bluebook.aggregates.first
|
|
20
|
+
return not_applicable("#{bluebook.name} declares no aggregates") unless first
|
|
21
|
+
|
|
22
|
+
adapter_name = Hecks::Ports::Persistence::BindingPolicy.resolve(registry, bluebook.name, first).adapter
|
|
23
|
+
unless adapter_name == "PostgresEra"
|
|
24
|
+
return not_applicable("#{bluebook.name} is bound to #{adapter_name}, not PostgresEra")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
settings = registry.binding_settings(bluebook.name, Hecks::Ports::Persistence::VERB, adapter_name)
|
|
28
|
+
db = Hecks::Adapters::PostgresEra.connect_for(bluebook.name, settings)
|
|
29
|
+
begin
|
|
30
|
+
lineage = Hecks::Adapters::PostgresEra::Lineage.new(db, bluebook.name)
|
|
31
|
+
lineage.ensure_base!
|
|
32
|
+
eras = lineage.eras
|
|
33
|
+
breakdown = if eras.size > 1
|
|
34
|
+
(1...eras.last[:ordinal]).map do |ordinal|
|
|
35
|
+
{ ordinal: ordinal, diverged: lineage.diverged_count(ordinal) }
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
{ checked: true, era_count: eras.size, breakdown: breakdown, diverged_total: breakdown.sum { |b| b[:diverged] } }
|
|
41
|
+
ensure
|
|
42
|
+
db.close
|
|
43
|
+
end
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
# A refused connection or malformed `.world` settings is not the
|
|
46
|
+
# same as nothing to audit, so this reports `:error`, never the
|
|
47
|
+
# benign `:not_applicable` shape.
|
|
48
|
+
failed("#{e.class}: #{e.message}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def not_applicable(reason) = { checked: false, kind: :not_applicable, reason: reason }
|
|
52
|
+
|
|
53
|
+
def failed(reason) = { checked: false, kind: :error, reason: reason }
|
|
54
|
+
|
|
55
|
+
# Boots a fresh registry for the target domain, never the ledger's own
|
|
56
|
+
# registry (this asks about the swept target's lineage, not
|
|
57
|
+
# QualityControl's own).
|
|
58
|
+
def load_registry(domain_path)
|
|
59
|
+
loading = Hecks::Ports::Loading.bootstrap
|
|
60
|
+
directory = loading.bluebook_directory(domain_path)
|
|
61
|
+
registry = Hecks::Runtime::Registry.new(root: File.dirname(directory))
|
|
62
|
+
Hecks.with_registry(registry) do
|
|
63
|
+
loading.load_library
|
|
64
|
+
loading.load_project(loading.shared_root(nil, directory))
|
|
65
|
+
loading.load_domain(directory)
|
|
66
|
+
end
|
|
67
|
+
[registry, directory]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../corpus"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Fuzzing
|
|
6
|
+
# The declared forms an aggregate can exhibit, and the pairs it puts together.
|
|
7
|
+
#
|
|
8
|
+
# Shared by the combination-coverage spec and `hecks quality_control judge_novelty` so they
|
|
9
|
+
# cannot drift. The unit is one aggregate: forms on one head meet at dispatch, forms in one
|
|
10
|
+
# chapter do not.
|
|
11
|
+
module FormCensus
|
|
12
|
+
# A given path crossing two references has at least this many segments.
|
|
13
|
+
TWO_HOP_GIVEN_PATH_LENGTH = 3
|
|
14
|
+
|
|
15
|
+
FORMS = {
|
|
16
|
+
"composite_id" => ->(a) { (a["identified_by"] || []).size >= 2 },
|
|
17
|
+
"has_entity" => ->(a) { entities(a).any? },
|
|
18
|
+
"two_entities" => ->(a) { entities(a).size >= 2 },
|
|
19
|
+
"composite_piece" => ->(a) { entities(a).any? { |piece| (piece["identified_by"] || []).size >= 2 } },
|
|
20
|
+
"multi_emit" => ->(a) { commands(a).any? { |verb| (verb["emits"] || []).size >= 2 } },
|
|
21
|
+
"lifecycle" => ->(a) { !a["lifecycle"].nil? },
|
|
22
|
+
"piece_lifecycle" => ->(a) { entities(a).any? { |piece| !piece["lifecycle"].nil? } },
|
|
23
|
+
"has_query" => ->(a) { queries(a).any? },
|
|
24
|
+
"list_attr" => ->(a) { attributes(a).any? { |held| held["list"] } },
|
|
25
|
+
"reference_attr" => ->(a) { attributes(a).any? { |held| reference?(held) } },
|
|
26
|
+
"closed_set" => ->(a) { (a["value_objects"] || []).any? { |shape| shape["closed_set"] } },
|
|
27
|
+
"has_default" => ->(a) { attributes(a).any? { |held| !held["default"].nil? } },
|
|
28
|
+
"has_optional" => ->(a) { commands(a).any? { |verb| (verb["attributes"] || []).any? { |held| held["optional"] } } },
|
|
29
|
+
# Reference-hop forms, then `corrects` and `role`; entity commands count too.
|
|
30
|
+
"corrects" => ->(a) { every_command(a).any? { |verb| corrects?(verb) } },
|
|
31
|
+
"role_gated" => ->(a) { every_command(a).any? { |verb| !verb["role"].to_s.empty? } },
|
|
32
|
+
"two_hop_given" => ->(a) { two_hop_given?(a) },
|
|
33
|
+
"multi_hop_where" => ->(a) { multi_hop_where?(a) },
|
|
34
|
+
"revalued_reference" => ->(a) { revalued_reference?(a) }
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
def entities(aggregate) = aggregate["entities"] || []
|
|
40
|
+
|
|
41
|
+
def commands(aggregate) = aggregate["commands"] || []
|
|
42
|
+
|
|
43
|
+
# An aggregate's commands, its entities' included.
|
|
44
|
+
def every_command(aggregate) = commands(aggregate) + entities(aggregate).flat_map { |piece| commands(piece) }
|
|
45
|
+
|
|
46
|
+
def corrects?(verb) = (verb["mutations"] || []).any? { |change| change["op"].to_s == "corrects" }
|
|
47
|
+
|
|
48
|
+
def attributes(aggregate) = aggregate["attributes"] || []
|
|
49
|
+
|
|
50
|
+
def queries(aggregate) = aggregate["queries"] || []
|
|
51
|
+
|
|
52
|
+
def reference?(attribute) = attribute["type"].to_s.start_with?("Reference<")
|
|
53
|
+
|
|
54
|
+
def two_hop_given?(aggregate)
|
|
55
|
+
commands(aggregate).any? { |verb| (verb["givens"] || []).any? { |given| deep_lookup?(given["ast"]) } }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A `where` whose field crosses two `/` hops, as in `member/sponsor/standing`.
|
|
59
|
+
def multi_hop_where?(aggregate)
|
|
60
|
+
queries(aggregate).any? { |query| (query["wheres"] || []).any? { |where| where["field"].to_s.count("/") >= 2 } }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A command attribute reusing the name of a reference attribute under a non-reference type.
|
|
64
|
+
def revalued_reference?(aggregate)
|
|
65
|
+
references = attributes(aggregate).select { |held| reference?(held) }.to_set { |held| held["name"].to_s }
|
|
66
|
+
commands(aggregate).any? do |verb|
|
|
67
|
+
(verb["attributes"] || []).any? { |held| references.include?(held["name"].to_s) && !reference?(held) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def deep_lookup?(node)
|
|
72
|
+
case node
|
|
73
|
+
when Hash
|
|
74
|
+
return true if node["op"] == "lookup" && Array(node["path"]).size >= TWO_HOP_GIVEN_PATH_LENGTH
|
|
75
|
+
|
|
76
|
+
node.each_value.any? { |child| deep_lookup?(child) }
|
|
77
|
+
when Array
|
|
78
|
+
node.any? { |child| deep_lookup?(child) }
|
|
79
|
+
else
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def properties(aggregate)
|
|
85
|
+
FORMS.transform_values { |form| form.call(aggregate) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def pairs
|
|
89
|
+
FORMS.keys.combination(2).map { |pair| pair_key(*pair) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def pair_key(left, right) = [left, right].sort.join(" + ")
|
|
93
|
+
|
|
94
|
+
def covered_pairs(held)
|
|
95
|
+
held.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(name, shows), covered|
|
|
96
|
+
shows.select { |_, present| present }.keys.combination(2).each do |left, right|
|
|
97
|
+
covered[pair_key(left, right)] << name
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def aggregates_in(chapter_ir)
|
|
103
|
+
(chapter_ir["aggregates"] || []).map do |aggregate|
|
|
104
|
+
["#{chapter_ir['name']}::#{aggregate['name']}", properties(aggregate)]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def bluebook_files(domain_path)
|
|
109
|
+
Hecks::Corpus.bluebook_files(domain_path)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The census over a domain on disk, booted lightweight (no `Hecks.boot`, no database).
|
|
113
|
+
# Only the first-loaded chapter is measured.
|
|
114
|
+
#
|
|
115
|
+
# @raise [ArgumentError] if `domain_path` has no bluebook files
|
|
116
|
+
def census(domain_path)
|
|
117
|
+
root = File.expand_path("../../..", __dir__)
|
|
118
|
+
files = bluebook_files(domain_path)
|
|
119
|
+
raise ArgumentError, "#{domain_path} has no bluebook/*.bluebook (or *.bluebook) to measure" if files.nil?
|
|
120
|
+
|
|
121
|
+
registry = Hecks::Runtime::Registry.new(root: File.expand_path(domain_path))
|
|
122
|
+
Hecks.with_registry(registry) do
|
|
123
|
+
Kernel.load(File.join(root, "lib/hecks/ports/persistence.port"))
|
|
124
|
+
Kernel.load(File.join(root, "lib/hecks/ports/extraction.port"))
|
|
125
|
+
Kernel.load(File.join(root, "lib/hecks/adapters/driven/memory.adapter"))
|
|
126
|
+
Kernel.load(File.join(root, "lib/hecks/adapters/driven/prism.adapter"))
|
|
127
|
+
files.each { |file| Kernel.load(file) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
chapter_name = registry.bluebooks.keys.first
|
|
131
|
+
exported = JSON.parse(JSON.generate(Hecks::Projector::Exporter.call(registry).fetch(chapter_name)))
|
|
132
|
+
aggregates_in(exported)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "isolated_boot"
|
|
3
|
+
require_relative "sequence_generator"
|
|
4
|
+
require_relative "replay"
|
|
5
|
+
require_relative "shrinker"
|
|
6
|
+
require_relative "differential"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Fuzzing
|
|
10
|
+
# The child-process half of `hecks quality_control check_generated_domains`: checks one
|
|
11
|
+
# generated domain.
|
|
12
|
+
#
|
|
13
|
+
# One domain per process, since every generated domain is named `QaGenerated`.
|
|
14
|
+
# A domain that fails to boot is `invalid` (a generator defect), not a finding.
|
|
15
|
+
module GeneratedDomainCheck
|
|
16
|
+
DIFFERENTIAL_MODES = %i[differential self_consistency properties_in_differential].freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Checks one generated domain over up to `seeds` sequences, stopping at the first finding.
|
|
21
|
+
#
|
|
22
|
+
# With a compiled `binary` it diffs Ruby against Rust; without one it replays Ruby only.
|
|
23
|
+
# `match` (`{"mode" =>, "signature" =>}`) keeps only a finding that still matches, so
|
|
24
|
+
# domain-level shrinking can ask whether a smaller domain shows the same finding.
|
|
25
|
+
#
|
|
26
|
+
# @return [Hash] `"status"` is `"invalid"`, `"clean"` or `"found"`; a found finding is
|
|
27
|
+
# `#finding` merged with `"seeds_run"`, plus `"shrunk_steps"` if `shrink_budget` > 0
|
|
28
|
+
def run(domain_path, seeds:, steps:, adversarial:, binary: nil, differ: nil, match: nil, shrink_budget: 0)
|
|
29
|
+
error = boot_error(domain_path)
|
|
30
|
+
return { "status" => "invalid", "error" => error } if error
|
|
31
|
+
|
|
32
|
+
(1..seeds).each do |seed|
|
|
33
|
+
finding = check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
|
|
34
|
+
next unless finding
|
|
35
|
+
|
|
36
|
+
finding["shrunk_steps"] = shrink(domain_path, finding, binary, differ, shrink_budget) if shrink_budget.positive?
|
|
37
|
+
return finding.merge("status" => "found", "seeds_run" => seed)
|
|
38
|
+
end
|
|
39
|
+
{ "status" => "clean", "seeds_run" => seeds }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def boot_error(domain_path)
|
|
43
|
+
IsolatedBoot.call(domain_path) { |copy| Hecks.boot(copy, environment: nil) }
|
|
44
|
+
nil
|
|
45
|
+
rescue StandardError, ScriptError => e
|
|
46
|
+
"#{e.class}: #{e.message.lines.first&.strip}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
|
|
50
|
+
sequence = SequenceGenerator.generate(domain_path, seed: seed, steps: steps, adversarial: adversarial)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
finding(seed, :generator, [{ field: "generator_crash", detail: "#{e.class}: #{e.message}" }], [], match)
|
|
53
|
+
else
|
|
54
|
+
outcomes(domain_path, sequence, binary, differ).each do |mode, divergences|
|
|
55
|
+
found = finding(seed, mode, divergences, sequence, match)
|
|
56
|
+
return found if found
|
|
57
|
+
end
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Diffs against Rust when `binary` is given, otherwise replays Ruby only.
|
|
62
|
+
def outcomes(domain_path, sequence, binary, differ)
|
|
63
|
+
return Differential.diff(differ, domain_path, sequence, binary, modes: DIFFERENTIAL_MODES) if binary
|
|
64
|
+
|
|
65
|
+
history = Replay.call(domain_path, sequence, self_consistency: true)
|
|
66
|
+
{ ruby_only: Differential.property_divergences(history),
|
|
67
|
+
self_consistency: Differential.self_consistency_divergences(history) }
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
{ ruby_only: [{ field: "crash", detail: "#{e.class}: #{e.message}" }] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def finding(seed, mode, divergences, sequence, match)
|
|
73
|
+
return nil if divergences.empty?
|
|
74
|
+
|
|
75
|
+
signature = Shrinker.signature(divergences)
|
|
76
|
+
return nil if match && !(match["mode"] == mode.to_s && Set.new(match["signature"]).subset?(signature))
|
|
77
|
+
|
|
78
|
+
{ "seed" => seed, "mode" => mode.to_s, "signature" => signature.to_a.sort, "steps" => sequence,
|
|
79
|
+
"divergences" => JSON.parse(JSON.generate(divergences)) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The step-level half of shrinking; the parent already made the domain as small as it could.
|
|
83
|
+
def shrink(domain_path, found, binary, differ, budget)
|
|
84
|
+
original = Set.new(found["signature"])
|
|
85
|
+
mode = found["mode"].to_sym
|
|
86
|
+
Shrinker.call(found["steps"], budget: budget) do |candidate|
|
|
87
|
+
divergences = outcomes(domain_path, candidate, binary, differ)[mode] || []
|
|
88
|
+
Shrinker.reproduces?(original, divergences)
|
|
89
|
+
end.steps
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# Values deliberately the wrong shape for an attribute; the sibling of ValueGenerator.
|
|
4
|
+
#
|
|
5
|
+
# Refusals are answers whose wording is pinned byte-for-byte, so these probe the refusal
|
|
6
|
+
# sentence rather than the happy path.
|
|
7
|
+
module InvalidValueGenerator
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Each kind names a real confusion, not random noise.
|
|
11
|
+
KINDS = %i[
|
|
12
|
+
object_for_scalar
|
|
13
|
+
scalar_for_object
|
|
14
|
+
float_for_integer
|
|
15
|
+
numeral_string
|
|
16
|
+
array_for_scalar
|
|
17
|
+
boolean_for_string
|
|
18
|
+
null
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
def corrupt(attribute, aggregate, random:)
|
|
22
|
+
kind = kinds_for(attribute, aggregate).sample(random: random)
|
|
23
|
+
build(kind, attribute, aggregate, random: random)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Only the confusions that mean something for this attribute's declared type.
|
|
27
|
+
def kinds_for(attribute, aggregate)
|
|
28
|
+
value_object = aggregate.value_object(attribute.type.to_s)
|
|
29
|
+
return %i[scalar_for_object array_for_scalar null] if value_object
|
|
30
|
+
return %i[object_for_scalar float_for_integer numeral_string array_for_scalar null] if attribute.type.to_s == "Integer"
|
|
31
|
+
|
|
32
|
+
%i[object_for_scalar array_for_scalar boolean_for_string null]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build(kind, attribute, aggregate, random:)
|
|
36
|
+
case kind
|
|
37
|
+
when :object_for_scalar then { "cents" => random.rand(1..1000) }
|
|
38
|
+
when :scalar_for_object then scalar_for(attribute, aggregate, random: random)
|
|
39
|
+
when :float_for_integer then random.rand(1.0..100.0).round(2)
|
|
40
|
+
when :numeral_string then random.rand(1..1000).to_s
|
|
41
|
+
when :array_for_scalar then [random.rand(1..10), random.rand(1..10)]
|
|
42
|
+
when :boolean_for_string then random.rand(2).zero?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A bare scalar where a value object is declared.
|
|
47
|
+
# A single-field value object accepts one, so only a multi-field one makes it a refusal.
|
|
48
|
+
def scalar_for(attribute, aggregate, random:)
|
|
49
|
+
value_object = aggregate.value_object(attribute.type.to_s)
|
|
50
|
+
sole = value_object&.sole_attribute
|
|
51
|
+
return "a bare scalar" unless sole
|
|
52
|
+
|
|
53
|
+
# One field, so a scalar is legal; corrupt the field's own type instead.
|
|
54
|
+
{ sole.name.to_s => ["nested", "array"] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# An argument the command never declares, which exercises `refuse_unknown_arguments`.
|
|
58
|
+
def undeclared_argument(random:)
|
|
59
|
+
name = %w[colour flavour rank note].sample(random: random)
|
|
60
|
+
[name, %w[red loud third scribbled].sample(random: random)]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "tmpdir"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "outside_world"
|
|
5
|
+
require_relative "../runtime/adapter_lookup"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Fuzzing
|
|
9
|
+
# Rewrites a copied domain's persistence bindings before an ephemeral
|
|
10
|
+
# fuzz/replay boot; a directory copy alone does not isolate Postgres state.
|
|
11
|
+
module IsolatedBoot
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Copies `domain_path` to a tmpdir, rebinds its persistence to `adapter`, and
|
|
15
|
+
# yields the copy's root. `:postgres` is expensive: use smaller seed/step counts.
|
|
16
|
+
#
|
|
17
|
+
# The block runs with `OutsideWorld` standing in for every adapter a port or a port-answered
|
|
18
|
+
# query reaches: a fuzz or replay checks the domain's own rules, and the adapters of the
|
|
19
|
+
# chapters that drive tools would run shells and write files from a generated sequence.
|
|
20
|
+
def call(domain_path, adapter: :memory, database: nil, schema: nil, scratch: {})
|
|
21
|
+
Dir.mktmpdir("hecks-fuzz") do |tmp|
|
|
22
|
+
copy = File.join(tmp, File.basename(domain_path))
|
|
23
|
+
copy_dereferencing(domain_path, copy)
|
|
24
|
+
FileUtils.rm_rf(File.join(copy, "data"))
|
|
25
|
+
case adapter
|
|
26
|
+
when :memory then rebind_to_memory!(copy)
|
|
27
|
+
when :sqlite then rebind_to_sqlite!(copy)
|
|
28
|
+
when :postgres then rebind_to_postgres!(copy, **scratch)
|
|
29
|
+
when :postgres_era then rebind_to_postgres_era!(copy, database: database, schema: schema)
|
|
30
|
+
else raise ArgumentError,
|
|
31
|
+
"unknown fuzz adapter #{adapter.inspect} — :memory, :sqlite, :postgres, or :postgres_era"
|
|
32
|
+
end
|
|
33
|
+
Runtime::AdapterLookup.standing_in(OutsideWorld) { yield copy }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Copies `source` into `destination`, following symlinks rather than reproducing
|
|
38
|
+
# them (cp_r would copy a symlink as a symlink, dangling once the source tree is
|
|
39
|
+
# gone), and carries any vendored bluebooks the copy's hecksagons name.
|
|
40
|
+
def copy_dereferencing(source, destination)
|
|
41
|
+
copy_files(source, destination)
|
|
42
|
+
carry_vendored_bluebooks!(source, destination)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Copies every file under `source` into `destination`, following symlinks.
|
|
46
|
+
def copy_files(source, destination)
|
|
47
|
+
FileUtils.mkdir_p(destination)
|
|
48
|
+
Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).each do |path|
|
|
49
|
+
next if [".", ".."].include?(File.basename(path))
|
|
50
|
+
|
|
51
|
+
target = File.join(destination, path.delete_prefix("#{source}/"))
|
|
52
|
+
if File.directory?(path)
|
|
53
|
+
FileUtils.mkdir_p(target)
|
|
54
|
+
else
|
|
55
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
56
|
+
begin
|
|
57
|
+
FileUtils.cp(path, target)
|
|
58
|
+
rescue Errno::ENOENT
|
|
59
|
+
# A sibling can vanish between listing and copy under parallel workers;
|
|
60
|
+
# skip a file that's already gone.
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Copies vendored bluebook packages a copy's hecksagons name into the copy, since
|
|
67
|
+
# `Hecks.boot` only sees the domain directory itself, not `vendor/`.
|
|
68
|
+
def carry_vendored_bluebooks!(source, destination)
|
|
69
|
+
names = Dir.glob(File.join(destination, "**", "*.hecksagon")).flat_map do |path|
|
|
70
|
+
File.read(path).scan(/uses_embryonaut_bluebook\s*\(?\s*"([\w-]+)"/).flatten
|
|
71
|
+
end
|
|
72
|
+
names.uniq.each do |name|
|
|
73
|
+
package = File.join(File.dirname(source), "vendor", "embryonaut_bluebooks", name)
|
|
74
|
+
next unless File.directory?(package)
|
|
75
|
+
|
|
76
|
+
copy_files(package, File.join(File.dirname(destination), "vendor", "embryonaut_bluebooks", name))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Rewrites every `.hecksagon` in the copy to bind through Memory and replaces its
|
|
81
|
+
# `.world` files with a `default_adapter "Memory"` one, so the boot needs no settings.
|
|
82
|
+
def rebind_to_memory!(copy)
|
|
83
|
+
rewrite_bindings!(copy, "Memory")
|
|
84
|
+
strip_translations!(copy)
|
|
85
|
+
|
|
86
|
+
# Also replaces the settings, not just the bind: a bare fallback setting (like
|
|
87
|
+
# Postgres's `database:`) would otherwise still apply and WiringError refuses it.
|
|
88
|
+
write_default_worlds!(copy, "Memory")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Same as `rebind_to_memory!`, one adapter over — Sqlite also needs no `.world`
|
|
92
|
+
# settings, since `Sqlite#resolve_path` defaults `database` to `data/<table>.db`.
|
|
93
|
+
def rebind_to_sqlite!(copy)
|
|
94
|
+
rewrite_bindings!(copy, "SqlitePersistence")
|
|
95
|
+
strip_translations!(copy)
|
|
96
|
+
write_default_worlds!(copy, "SqlitePersistence")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Postgres has no zero-config default (Postgres.connect_for refuses without a
|
|
100
|
+
# `database` setting), so every domain name gets a fresh `.world` written for it.
|
|
101
|
+
# One shared schema, dropped and recreated per boot, not a fresh name per call —
|
|
102
|
+
# safe only because callers run one ephemeral boot at a time.
|
|
103
|
+
FUZZ_POSTGRES_DATABASE = "hecks_fuzz".freeze
|
|
104
|
+
FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
|
|
105
|
+
|
|
106
|
+
# Rewrites every `.hecksagon` in the copy to bind through Postgres, against the
|
|
107
|
+
# shared scratch schema unless the caller names its own via `scratch:`.
|
|
108
|
+
def rebind_to_postgres!(copy, database: nil, schema: nil)
|
|
109
|
+
require "pg"
|
|
110
|
+
database ||= FUZZ_POSTGRES_DATABASE
|
|
111
|
+
schema ||= FUZZ_POSTGRES_SCHEMA
|
|
112
|
+
rewrite_bindings!(copy, "Postgres")
|
|
113
|
+
strip_translations!(copy)
|
|
114
|
+
ensure_fuzz_schema!(database, schema)
|
|
115
|
+
|
|
116
|
+
# One `.world` per directory a `.hecksagon` lives in, not one at the copy's
|
|
117
|
+
# root — `Folder#load_domain` globs `*.world` non-recursively.
|
|
118
|
+
write_worlds!(copy, "hecks_fuzz_postgres.world") do |name|
|
|
119
|
+
<<~WORLD
|
|
120
|
+
Hecks.world "#{name}" do
|
|
121
|
+
default_adapter "Postgres"
|
|
122
|
+
persisted_by("Postgres") do
|
|
123
|
+
database "#{database}"
|
|
124
|
+
schema "#{schema}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
WORLD
|
|
128
|
+
end
|
|
129
|
+
write_unbound_chapter_worlds!(copy)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Creates `database` once per process (memoized) and drops/recreates `schema`
|
|
133
|
+
# inside it on every call — that's what isolates one ephemeral boot from the next.
|
|
134
|
+
def ensure_fuzz_schema!(database = FUZZ_POSTGRES_DATABASE, schema = FUZZ_POSTGRES_SCHEMA)
|
|
135
|
+
# PG::Connection only closes its socket when GC finalizes it, and a tight fuzz
|
|
136
|
+
# loop opens connections faster than GC reclaims them — without this, a run
|
|
137
|
+
# exhausts Postgres's max_connections after a few dozen ephemeral boots.
|
|
138
|
+
GC.start
|
|
139
|
+
|
|
140
|
+
@fuzz_databases_ready ||= {}
|
|
141
|
+
unless @fuzz_databases_ready[database]
|
|
142
|
+
admin = PG.connect(dbname: "postgres")
|
|
143
|
+
exists = admin.exec_params(
|
|
144
|
+
"SELECT 1 FROM pg_database WHERE datname = $1", [database]
|
|
145
|
+
).ntuples.positive?
|
|
146
|
+
admin.exec(%(CREATE DATABASE "#{database}")) unless exists
|
|
147
|
+
admin.close
|
|
148
|
+
@fuzz_databases_ready[database] = true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
db = PG.connect(dbname: database)
|
|
152
|
+
# Quiet on purpose: an ordinary DROP CASCADE NOTICEs per dropped object, which
|
|
153
|
+
# would bury hecks fuzz's own output on every boot after the first.
|
|
154
|
+
db.exec("SET client_min_messages = warning")
|
|
155
|
+
quoted = db.quote_ident(schema)
|
|
156
|
+
db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
|
|
157
|
+
db.exec("CREATE SCHEMA #{quoted}")
|
|
158
|
+
db.close
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# `:postgres_era` is the only mode that exercises era/lineage-bound SQL; plain
|
|
162
|
+
# `:postgres` never touches that machinery. Unlike `:postgres`, there's no shared
|
|
163
|
+
# scratch constant here — `database:`/`schema:` are required keyword args because
|
|
164
|
+
# the caller (`hecks quality_control ask run --persistence-parity`) owns that database's
|
|
165
|
+
# lifecycle.
|
|
166
|
+
def rebind_to_postgres_era!(copy, database:, schema:)
|
|
167
|
+
require "pg"
|
|
168
|
+
if database.to_s.empty? || schema.to_s.empty?
|
|
169
|
+
raise ArgumentError,
|
|
170
|
+
"adapter: :postgres_era requires both database: and schema: — a throwaway database/schema " \
|
|
171
|
+
"THIS CALLER creates and drops itself (see rebind_to_postgres_era!'s own header). " \
|
|
172
|
+
"There is no shared default, unlike :postgres, so the caller cannot forget to own the lifecycle."
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
rewrite_bindings!(copy, "PostgresEra")
|
|
176
|
+
ensure_postgres_era_schema!(database: database, schema: schema)
|
|
177
|
+
|
|
178
|
+
# `schema:` isolates one ephemeral boot from the next, the same job
|
|
179
|
+
# `FUZZ_POSTGRES_SCHEMA` does for `:postgres` (`connect_for` idempotently
|
|
180
|
+
# creates it; this method only needs to drop it first, in
|
|
181
|
+
# `ensure_postgres_era_schema!`).
|
|
182
|
+
#
|
|
183
|
+
# `allow_superuser true` is deliberate: PostgresEra normally refuses to boot as
|
|
184
|
+
# a superuser (its era write-fence is row-level security, which a superuser
|
|
185
|
+
# bypasses), to protect a real ledger from stale writes. This is a throwaway
|
|
186
|
+
# schema the caller creates and drops, comparing Memory against PostgresEra's
|
|
187
|
+
# SQL — the era fence isn't under test, so opting in here is safe.
|
|
188
|
+
write_worlds!(copy, "hecks_fuzz_postgres_era.world") do |name|
|
|
189
|
+
<<~WORLD
|
|
190
|
+
Hecks.world "#{name}" do
|
|
191
|
+
default_adapter "PostgresEra"
|
|
192
|
+
persisted_by("PostgresEra") do
|
|
193
|
+
database "#{database}"
|
|
194
|
+
schema "#{schema}"
|
|
195
|
+
allow_superuser true
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
WORLD
|
|
199
|
+
end
|
|
200
|
+
write_unbound_chapter_worlds!(copy)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Replaces every `.world` in the copy with one that declares only `default_adapter`,
|
|
204
|
+
# so an aggregate left unbound by its hecksagon still falls back to `adapter_name`.
|
|
205
|
+
def write_default_worlds!(copy, adapter_name)
|
|
206
|
+
write_worlds!(copy, "hecks_fuzz_default.world") do |name|
|
|
207
|
+
<<~WORLD
|
|
208
|
+
Hecks.world "#{name}" do
|
|
209
|
+
default_adapter "#{adapter_name}"
|
|
210
|
+
end
|
|
211
|
+
WORLD
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Gives every chapter with no hecksagon block (so no bind) a world of its own that
|
|
216
|
+
# falls back to Memory, since the worlds `write_worlds!` wrote require a database
|
|
217
|
+
# a chapter with no bind can't provide.
|
|
218
|
+
def write_unbound_chapter_worlds!(copy)
|
|
219
|
+
named = Dir.glob(File.join(copy, "**", "*.hecksagon")).flat_map do |path|
|
|
220
|
+
File.read(path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
221
|
+
end
|
|
222
|
+
bluebooks = Dir.glob(File.join(copy, "**", "*.bluebook")).group_by { |path| File.dirname(path) }
|
|
223
|
+
bluebooks.each do |dir, files|
|
|
224
|
+
unbound = files.flat_map { |path| File.read(path).scan(/Hecks\.bluebook[\s(]+"([^"]+)"/).flatten }
|
|
225
|
+
.uniq - named
|
|
226
|
+
named.concat(unbound)
|
|
227
|
+
next if unbound.empty?
|
|
228
|
+
|
|
229
|
+
worlds = unbound.map { |name| %(Hecks.world "#{name}" do\n default_adapter "Memory"\nend\n) }
|
|
230
|
+
File.write(File.join(dir, "hecks_fuzz_unbound.world"), worlds.join("\n"))
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Writes one `.world` per directory holding a `.hecksagon`, naming every
|
|
235
|
+
# `Hecks.hecksagon` block found there, and deletes every other `.world` in the
|
|
236
|
+
# copy. One write per directory, not per file — a domain can split hecksagon
|
|
237
|
+
# blocks (e.g. `context_map.hecksagon` beside the main one) across sibling files
|
|
238
|
+
# in the same directory, and a second `File.write` to the same path would drop
|
|
239
|
+
# the first file's names.
|
|
240
|
+
def write_worlds!(copy, world_file, &world_for)
|
|
241
|
+
names_by_dir = Hash.new { |hash, dir| hash[dir] = [] }
|
|
242
|
+
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
|
|
243
|
+
names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
244
|
+
names_by_dir[File.dirname(hecksagon_path)].concat(names)
|
|
245
|
+
end
|
|
246
|
+
names_by_dir.each do |dir, names|
|
|
247
|
+
next if names.empty?
|
|
248
|
+
|
|
249
|
+
File.write(File.join(dir, world_file), names.uniq.map(&world_for).join("\n"))
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
Dir.glob(File.join(copy, "**", "*.world")).each do |path|
|
|
253
|
+
File.delete(path) unless File.basename(path) == world_file
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Mirrors `ensure_fuzz_schema!`'s GC.start-before-connecting fix for the same
|
|
258
|
+
# max_connections exhaustion. Creates `database` if missing, but never drops
|
|
259
|
+
# it — that's the caller's job (see `rebind_to_postgres_era!`).
|
|
260
|
+
def ensure_postgres_era_schema!(database:, schema:)
|
|
261
|
+
GC.start
|
|
262
|
+
|
|
263
|
+
admin = PG.connect(dbname: "postgres")
|
|
264
|
+
exists = admin.exec_params(
|
|
265
|
+
"SELECT 1 FROM pg_database WHERE datname = $1", [database]
|
|
266
|
+
).ntuples.positive?
|
|
267
|
+
admin.exec(%(CREATE DATABASE "#{database}")) unless exists
|
|
268
|
+
admin.close
|
|
269
|
+
|
|
270
|
+
db = PG.connect(dbname: database)
|
|
271
|
+
db.exec("SET client_min_messages = warning")
|
|
272
|
+
quoted = db.quote_ident(schema)
|
|
273
|
+
db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
|
|
274
|
+
db.close
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Deletes every `translations/*.bluebook` in the copy. A compute/rekey translation
|
|
278
|
+
# edge refuses to boot under any non-lineage-capable adapter (only `PostgresEra`
|
|
279
|
+
# answers `lineage_capable? == true`), so every other mode strips it here rather
|
|
280
|
+
# than tripping that refusal. Safe to drop: an ephemeral, zero-history boot never
|
|
281
|
+
# has a pre-existing era-1 row to translate — the edge only matters at mint time,
|
|
282
|
+
# against a real database, never here.
|
|
283
|
+
def strip_translations!(copy)
|
|
284
|
+
Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Rewrites every `persisted_by` bind — aggregate-scoped or bare at the hecksagon's
|
|
288
|
+
# own root, naming its adapter as a literal or through a local variable — to name
|
|
289
|
+
# `adapter_name`, and drops every `projected_by` bind outright (unread rather than
|
|
290
|
+
# broken, since `Registry#read_repository` falls back to the authoritative repo).
|
|
291
|
+
def rewrite_bindings!(copy, adapter_name)
|
|
292
|
+
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
|
|
293
|
+
lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
|
|
294
|
+
# Horizontal space only: a newline ends the statement, and the next one must stay.
|
|
295
|
+
bind = /persisted_by[ \t]*\(?[ \t]*(?:"[^"]+"|[a-z_]\w*)[ \t]*\)?/
|
|
296
|
+
File.write(path, lines.join.gsub(bind, "persisted_by(\"#{adapter_name}\")"))
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|