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,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "open3"
|
|
6
|
+
require_relative "../../../hecks"
|
|
7
|
+
require_relative "../../fuzzing/combination_miner"
|
|
8
|
+
require_relative "../../fuzzing/domain_generator"
|
|
9
|
+
require_relative "../adapters/agent"
|
|
10
|
+
require_relative "child"
|
|
11
|
+
require_relative "../../cache_dir"
|
|
12
|
+
require_relative "../../hecks/adapters/codebase/tree"
|
|
13
|
+
|
|
14
|
+
module Hecks
|
|
15
|
+
module QualityControlCli
|
|
16
|
+
# The command behind `hecks quality_control mine_combinations`: asks an agent to mine the
|
|
17
|
+
# adversarial corpus for new domain combinations, then checks them. It is opt-in and never run
|
|
18
|
+
# by `hecks quality_control tick` (an agent call costs money and answers differently each run).
|
|
19
|
+
# `--brief` prints the agent's prompt and stops; `--from <dir>` re-checks an earlier run;
|
|
20
|
+
# `--against <domain>` narrows the census; `--agent "cmd"` reads the prompt on stdin.
|
|
21
|
+
#
|
|
22
|
+
# The agent defaults to `claude -p` (`QA_MINER_AGENT` overrides); candidates that boot are
|
|
23
|
+
# checked by `hecks quality_control check_generated_domains --source`, whose report is this
|
|
24
|
+
# command's report. Exit 0: all clean. 2: a finding (see `.claude/skills/hecks_qa/SKILL.md`). 1:
|
|
25
|
+
# an operational
|
|
26
|
+
# error.
|
|
27
|
+
class QaMineCombinations
|
|
28
|
+
EXIT_OK = 0
|
|
29
|
+
EXIT_ERROR = 1
|
|
30
|
+
RESULT_MARKER = "QA_GENERATED_RESULT "
|
|
31
|
+
|
|
32
|
+
USAGE = "usage: hecks quality_control mine_combinations [--candidates N] [--rust] [--seeds K] " \
|
|
33
|
+
"[--steps M] " \
|
|
34
|
+
"[--adversarial F] [--repair-rounds R] [--agent CMD] [--from <candidates-dir>] " \
|
|
35
|
+
"[--against <domain> …] [--brief]"
|
|
36
|
+
|
|
37
|
+
Miner = Hecks::Fuzzing::CombinationMiner
|
|
38
|
+
|
|
39
|
+
# Mines and checks the candidates.
|
|
40
|
+
#
|
|
41
|
+
# @param argv [Array<String>] the flags in the usage line
|
|
42
|
+
# @param root [String] the repository root
|
|
43
|
+
# @return [Integer] 0 all clean, 2 a finding, 1 an operational error
|
|
44
|
+
# @raise [SystemExit] on a bad argument, an agent that fails or writes nothing, or no
|
|
45
|
+
# candidate that boots
|
|
46
|
+
def self.call(argv, root:)
|
|
47
|
+
new(root: root).call(argv)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param root [String] the repository root
|
|
51
|
+
def initialize(root:)
|
|
52
|
+
@root = root
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param argv [Array<String>] the flags in the usage line
|
|
56
|
+
# @return [Integer] the exit status
|
|
57
|
+
# @raise [SystemExit] on a bad argument, an agent that fails or writes nothing, or no
|
|
58
|
+
# candidate that boots
|
|
59
|
+
def call(argv)
|
|
60
|
+
@options = parse(argv.dup)
|
|
61
|
+
return EXIT_OK if @options == :help
|
|
62
|
+
|
|
63
|
+
@run_dir = File.join(runs_root, "run-#{Time.now.strftime('%Y%m%d-%H%M%S')}-#{Process.pid}")
|
|
64
|
+
@out_dir = @options[:from] || File.join(@run_dir, "candidates")
|
|
65
|
+
@agent_log = File.join(@run_dir, "agent.log")
|
|
66
|
+
@agent = Hecks::Adapters::Agent.new
|
|
67
|
+
@command = @agent.command_for(@options[:agent])
|
|
68
|
+
corpus = @options[:against].empty? ? Miner.corpus_paths(@root) : @options[:against]
|
|
69
|
+
@brief = Miner.brief(corpus, root: @root, bug_titles: Miner.recent_bug_titles(@root))
|
|
70
|
+
prompt = Miner.prompt(@root, @brief, count: @options[:candidates], out_dir: @out_dir)
|
|
71
|
+
if @options[:brief]
|
|
72
|
+
puts prompt
|
|
73
|
+
return EXIT_OK
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
mine(prompt)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Each flag that takes a value: the option it sets and how the value is read.
|
|
80
|
+
VALUE_FLAGS = {
|
|
81
|
+
"--candidates" => [:candidates, ->(v) { Integer(v) }], "--seeds" => [:seeds, ->(v) { Integer(v) }],
|
|
82
|
+
"--steps" => [:steps, ->(v) { Integer(v) }], "--adversarial" => [:adversarial, ->(v) { Float(v) }],
|
|
83
|
+
"--repair-rounds" => [:repair_rounds, ->(v) { Integer(v) }], "--agent" => [:agent, :itself.to_proc],
|
|
84
|
+
"--from" => [:from, ->(v) { File.expand_path(v) }]
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def parse(argv)
|
|
90
|
+
options = { candidates: 3, rust: false, seeds: 5, steps: 25, adversarial: 0.3, repair_rounds: 1,
|
|
91
|
+
agent: nil, from: nil, brief: false, against: [] }
|
|
92
|
+
until argv.empty?
|
|
93
|
+
arg = argv.shift
|
|
94
|
+
if %w[-h --help].include?(arg)
|
|
95
|
+
puts USAGE
|
|
96
|
+
return :help
|
|
97
|
+
end
|
|
98
|
+
parse_flag(options, arg, argv)
|
|
99
|
+
end
|
|
100
|
+
options
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_flag(options, arg, argv)
|
|
104
|
+
case arg
|
|
105
|
+
when *VALUE_FLAGS.keys
|
|
106
|
+
key, reader = VALUE_FLAGS.fetch(arg)
|
|
107
|
+
options[key] = reader.call(argv.shift)
|
|
108
|
+
when "--rust" then options[:rust] = true
|
|
109
|
+
when "--brief" then options[:brief] = true
|
|
110
|
+
when "--against" then options[:against] << File.expand_path(argv.shift)
|
|
111
|
+
else abort "#{USAGE}\nunexpected argument: #{arg.inspect}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def relative(path) = path.delete_prefix("#{@root}/")
|
|
116
|
+
|
|
117
|
+
# Runs are kept under the checkout's `tmp/`; an installed gem, or a root that cannot be
|
|
118
|
+
# written, keeps them under the cache root instead.
|
|
119
|
+
def runs_root
|
|
120
|
+
checkout = Hecks::Adapters::Codebase::Tree.new(root: @root).checkout? && File.writable?(@root)
|
|
121
|
+
checkout ? File.join(@root, "tmp/qa-mined") : Hecks::CacheDir.path("qa-mined")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Returns nil when the agent finished, else why it did not.
|
|
125
|
+
def ask_agent(prompt)
|
|
126
|
+
@agent.ask(prompt: prompt, command: @command, chdir: @root, log: @agent_log)
|
|
127
|
+
nil
|
|
128
|
+
rescue Hecks::Adapters::Agent::Failed => e
|
|
129
|
+
e.message
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def boot_error(candidate, boot_root)
|
|
133
|
+
dir = Hecks::Fuzzing::DomainGenerator.write(
|
|
134
|
+
{ "source" => File.read(candidate[:bluebook]), "aggregates" => [], "policies" => [] },
|
|
135
|
+
File.join(boot_root, candidate[:slug])
|
|
136
|
+
)
|
|
137
|
+
output, = Open3.capture2e(*Child.argv(@root, "qa_generated_domains", "--check", dir, "--seeds", "0"),
|
|
138
|
+
chdir: @root)
|
|
139
|
+
line = output.lines.reverse.find { |candidate_line| candidate_line.start_with?(RESULT_MARKER) }
|
|
140
|
+
return "boot check produced no result: #{output.lines.last(5).join.strip}" unless line
|
|
141
|
+
|
|
142
|
+
result = JSON.parse(line.delete_prefix(RESULT_MARKER))
|
|
143
|
+
result["status"] == "invalid" ? result["error"] : nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def mine(prompt)
|
|
147
|
+
FileUtils.mkdir_p(@out_dir)
|
|
148
|
+
FileUtils.mkdir_p(@run_dir)
|
|
149
|
+
puts "combination miner: #{@brief['corpus'].size} corpus domain(s), " \
|
|
150
|
+
"#{@brief['unmet_pairs'].size} unmet pair(s) — #{relative(@run_dir)}"
|
|
151
|
+
unless @options[:from]
|
|
152
|
+
puts "asking the agent for #{@options[:candidates]} candidate(s) (#{@command.first}; " \
|
|
153
|
+
"log #{relative(@agent_log)})…"
|
|
154
|
+
failure = ask_agent(prompt)
|
|
155
|
+
abort "combination miner: #{failure}" if failure
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
candidates = Miner.candidates(@out_dir)
|
|
159
|
+
abort "combination miner: the agent wrote no candidates under #{relative(@out_dir)}" if candidates.empty?
|
|
160
|
+
|
|
161
|
+
invalid, repaired = boot_and_repair(candidates)
|
|
162
|
+
report(candidates, invalid, repaired)
|
|
163
|
+
valid = candidates.reject { |candidate| invalid.key?(candidate[:slug]) }
|
|
164
|
+
abort "combination miner: no candidate booted — nothing to check" if valid.empty?
|
|
165
|
+
|
|
166
|
+
puts
|
|
167
|
+
check(valid)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @return [Array(Hash, Array<String>)] the slug-to-error map of candidates that never booted,
|
|
171
|
+
# and the slugs a repair round fixed
|
|
172
|
+
def boot_and_repair(candidates)
|
|
173
|
+
boot_root = File.join(@run_dir, "boot")
|
|
174
|
+
failures = candidates.filter_map { |candidate| (error = boot_error(candidate, boot_root)) && [candidate, error] }
|
|
175
|
+
repaired = []
|
|
176
|
+
@options[:repair_rounds].times do |round|
|
|
177
|
+
break if failures.empty? || @options[:from]
|
|
178
|
+
|
|
179
|
+
puts "repair round #{round + 1}: #{failures.size} candidate(s) did not boot — back to the agent…"
|
|
180
|
+
failure = ask_agent(Miner.repair_prompt(failures, out_dir: @out_dir))
|
|
181
|
+
abort "combination miner: #{failure}" if failure
|
|
182
|
+
|
|
183
|
+
still = failures.filter_map { |candidate, _| (error = boot_error(candidate, boot_root)) && [candidate, error] }
|
|
184
|
+
repaired.concat(failures.map { |candidate, _| candidate[:slug] } - still.map { |candidate, _| candidate[:slug] })
|
|
185
|
+
failures = still
|
|
186
|
+
end
|
|
187
|
+
[failures.to_h { |candidate, error| [candidate[:slug], error] }, repaired]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def report(candidates, invalid, repaired)
|
|
191
|
+
puts
|
|
192
|
+
candidates.each do |candidate|
|
|
193
|
+
if invalid.key?(candidate[:slug])
|
|
194
|
+
puts " #{candidate[:slug]}: INVALID — #{invalid[candidate[:slug]]}"
|
|
195
|
+
next
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
pairs = begin
|
|
199
|
+
Miner.new_pairs(candidate[:dir], @brief["covered"])
|
|
200
|
+
rescue StandardError, ScriptError => e
|
|
201
|
+
["(census failed: #{e.class})"]
|
|
202
|
+
end
|
|
203
|
+
puts " #{candidate[:slug]}: boots#{' (repaired)' if repaired.include?(candidate[:slug])}; " \
|
|
204
|
+
"new pair(s): #{pairs.empty? ? 'none' : pairs.join(', ')}"
|
|
205
|
+
first = candidate[:hypothesis].to_s.lines.map(&:strip).find { |line| !line.empty? && !line.start_with?("#") }
|
|
206
|
+
puts " #{first}" if first
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def check(valid)
|
|
211
|
+
args = ["--start", (Time.now.to_i % 1_000_000).to_s, "--seeds", @options[:seeds].to_s,
|
|
212
|
+
"--steps", @options[:steps].to_s, "--adversarial", @options[:adversarial].to_s]
|
|
213
|
+
args << "--rust" if @options[:rust]
|
|
214
|
+
args.concat(valid.flat_map { |candidate| ["--source", candidate[:bluebook]] })
|
|
215
|
+
_, status = Process.wait2(Process.spawn(*Child.argv(@root, "qa_generated_domains", *args), chdir: @root))
|
|
216
|
+
status.exitstatus || EXIT_ERROR
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../hecks"
|
|
4
|
+
# The era subsystem does not load with core (ADR 0033).
|
|
5
|
+
require_relative "../../ports/persistence/plugins/era"
|
|
6
|
+
require_relative "../adapters/git_pr"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module QualityControlCli
|
|
10
|
+
# The command behind `hecks quality_control patch.open`: opens a PR and records it in the
|
|
11
|
+
# QualityControl ledger in one step. It is the only door into `QualityControl::Patch.Open` and
|
|
12
|
+
# `QualityControl::Improvement.Open`.
|
|
13
|
+
#
|
|
14
|
+
# It refuses (exit 1, nothing opened or recorded) unless the ledger's own `Open` command would
|
|
15
|
+
# take the record and the `GitPr` adapter agrees. The command's `given`s are the rules, asked
|
|
16
|
+
# as a dry run before anything is opened: the branch starts with the practice's prefix; with
|
|
17
|
+
# `--bug`, the bug is `fixed`; with `--angle`, it is `investigating`. The adapter answers the
|
|
18
|
+
# rest: the tree is clean, `PR_CAP_PER_DAY` (0 = uncapped) is unmet since local midnight, and
|
|
19
|
+
# the bug's commit is an ancestor of `HEAD`. It is idempotent. `QA_REPO_DIR` picks the checkout
|
|
20
|
+
# `git` runs in, so a spec can use a throwaway repo and a fake `gh`.
|
|
21
|
+
class QaOpenPr
|
|
22
|
+
EXIT_OK = 0
|
|
23
|
+
|
|
24
|
+
USAGE = "usage: hecks quality_control patch.open --bug BUG#n --title \"…\" [--body \"…\"]\n " \
|
|
25
|
+
"hecks quality_control improvement.open --improvement [--angle ANGLE-n] --title \"…\" [--body \"…\"]"
|
|
26
|
+
|
|
27
|
+
SHA_PATTERN = /\A[0-9a-fA-F]{7,40}\z/
|
|
28
|
+
|
|
29
|
+
# Stands in for the commit of a bug that is not fixed yet, so its dry run reaches the `given`.
|
|
30
|
+
PLACEHOLDER_COMMIT = "0000000"
|
|
31
|
+
|
|
32
|
+
# Opens and records the PR.
|
|
33
|
+
#
|
|
34
|
+
# @param argv [Array<String>] `--bug`, `--improvement`, `--angle`, `--title` and `--body`
|
|
35
|
+
# @param root [String] the repository root
|
|
36
|
+
# @param env [Hash{String => String}] `QA_SWEEP_DOMAIN_DIR` and `QA_REPO_DIR`
|
|
37
|
+
# @return [Integer] 0 once opened and recorded
|
|
38
|
+
# @raise [SystemExit] with `refused: ...` when a rule refuses, or the arguments are wrong
|
|
39
|
+
def self.call(argv, root:, env: ENV)
|
|
40
|
+
new(root: root, env: env).call(argv)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param root [String] the repository root
|
|
44
|
+
# @param env [Hash{String => String}] `QA_SWEEP_DOMAIN_DIR` and `QA_REPO_DIR`
|
|
45
|
+
def initialize(root:, env: ENV)
|
|
46
|
+
@domain_dir = env.fetch("QA_SWEEP_DOMAIN_DIR", File.join(root, "qa/bluebook"))
|
|
47
|
+
@repo_dir = env.fetch("QA_REPO_DIR", root)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param argv [Array<String>] the command line's arguments
|
|
51
|
+
# @return [Integer] the exit status
|
|
52
|
+
# @raise [SystemExit] with `refused: ...` when a rule refuses, or the arguments are wrong
|
|
53
|
+
def call(argv)
|
|
54
|
+
options = parse(argv.dup)
|
|
55
|
+
return EXIT_OK if options == :help
|
|
56
|
+
|
|
57
|
+
@options = options
|
|
58
|
+
@git_pr = Hecks::Adapters::GitPr.new(repo_dir: @repo_dir)
|
|
59
|
+
@branch = refusing { @git_pr.branch }
|
|
60
|
+
refusing { @git_pr.assert_clean_tree! }
|
|
61
|
+
@runtime = boot_ledger
|
|
62
|
+
find_citations
|
|
63
|
+
check_rules
|
|
64
|
+
view = open_pull_request
|
|
65
|
+
record(view)
|
|
66
|
+
merge_when_green(view[:number]) if dial(:AUTO_MERGE, false)
|
|
67
|
+
puts "#{view[:url]} (#{view[:headRefName]} @ #{view[:headRefOid].to_s[0, 7]}) — #{view[:title]}"
|
|
68
|
+
EXIT_OK
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def parse(argv)
|
|
74
|
+
options = {}
|
|
75
|
+
until argv.empty?
|
|
76
|
+
arg = argv.shift
|
|
77
|
+
case arg
|
|
78
|
+
when "-h", "--help"
|
|
79
|
+
puts USAGE
|
|
80
|
+
return :help
|
|
81
|
+
when "--improvement"
|
|
82
|
+
options[:improvement] = true
|
|
83
|
+
when "--bug", "--angle", "--title", "--body"
|
|
84
|
+
value = argv.shift
|
|
85
|
+
abort "#{USAGE}\n#{arg} needs a value" if value.nil? || value.empty?
|
|
86
|
+
options[arg.delete_prefix("--").to_sym] = value
|
|
87
|
+
else
|
|
88
|
+
abort "#{USAGE}\nunexpected argument: #{arg.inspect}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
validate(options)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate(options)
|
|
95
|
+
abort "#{USAGE}\n--title is required" unless options[:title]
|
|
96
|
+
abort "#{USAGE}\ngive --bug BUG#n OR --improvement, not both" if options[:bug] && options[:improvement]
|
|
97
|
+
abort "#{USAGE}\ngive --bug BUG#n or --improvement" unless options.key?(:bug) || options.key?(:improvement)
|
|
98
|
+
abort "#{USAGE}\n--angle only means something with --improvement" if options[:angle] && !options[:improvement]
|
|
99
|
+
|
|
100
|
+
options
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Every rule the adapter enforces refuses the same way: exit 1, nothing opened or recorded.
|
|
104
|
+
def refusing
|
|
105
|
+
yield
|
|
106
|
+
rescue Hecks::Adapters::GitPr::Refusal, Hecks::Adapters::GitPr::CommandFailed => e
|
|
107
|
+
abort "refused: #{e.message}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def boot_ledger
|
|
111
|
+
Hecks.boot(@domain_dir)
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
abort "the QualityControl ledger did not boot — fix the ledger itself before opening a PR against it " \
|
|
114
|
+
"(#{e.class}: #{e.message})"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def query(name, **args) = @runtime.query("QualityControl::#{name}", **args)
|
|
118
|
+
|
|
119
|
+
# Falls back to a dial-less fixture ledger's values: uncapped, non-draft, no auto-merge.
|
|
120
|
+
def dial(name, fallback)
|
|
121
|
+
return fallback unless defined?(::QualityControlDials) && ::QualityControlDials.const_defined?(name)
|
|
122
|
+
|
|
123
|
+
::QualityControlDials.const_get(name)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def find_citations
|
|
127
|
+
@bug = @angle = nil
|
|
128
|
+
if @options[:bug]
|
|
129
|
+
@bug = ::QualityControl::Bug.find(@options[:bug])
|
|
130
|
+
abort "refused: no such bug #{@options[:bug].inspect}" unless @bug
|
|
131
|
+
end
|
|
132
|
+
return unless @options[:angle]
|
|
133
|
+
|
|
134
|
+
@angle = ::QualityControl::Angle.find(@options[:angle])
|
|
135
|
+
abort "refused: no such angle #{@options[:angle].inspect}" unless @angle
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# What a refused `given` says to do next, by the rule's own wording; nothing for a rule it
|
|
139
|
+
# does not know.
|
|
140
|
+
def hint_for(rule)
|
|
141
|
+
case rule
|
|
142
|
+
when "the bug is fixed"
|
|
143
|
+
" (#{@bug.id} is #{@bug.status.inspect} — dispatch investigate and fix first " \
|
|
144
|
+
"(hecks run qa/bluebook fix id=#{@bug.id} reference.value=#{@bug.id} commit.value=<sha>))"
|
|
145
|
+
when "the angle is under investigation"
|
|
146
|
+
" (#{@angle.id} is #{@angle.status.inspect} — dispatch angle.investigate first, so the lead " \
|
|
147
|
+
"reads as picked up before something is built from it)"
|
|
148
|
+
when "the branch is one this practice recognises as its own"
|
|
149
|
+
" (the branch is #{@branch.inspect}; a PR this practice opens is on a branch it can recognise " \
|
|
150
|
+
"as its own, QualityControlDials::BRANCH_PREFIX)"
|
|
151
|
+
else ""
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Asks the ledger whether `verb` would take these facts, without recording anything. The
|
|
156
|
+
# number is one no record holds yet, so the dry run reaches the `given`s rather than
|
|
157
|
+
# `AlreadyExists`.
|
|
158
|
+
def refuse_unless_ledger_takes(verb, **facts)
|
|
159
|
+
@runtime.dry_run?(verb, **facts)
|
|
160
|
+
rescue Hecks::Runtime::GivenNotMet => e
|
|
161
|
+
hint = hint_for(e.message.split(" — ", 2).last)
|
|
162
|
+
abort "refused: #{e.message}#{hint} — a dry run of #{verb}, so nothing was opened or recorded"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def next_number(aggregate)
|
|
166
|
+
query("#{aggregate}.All").map { |row| row[:number][:value] }.max.to_i + 1
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def check_rules
|
|
170
|
+
recorded = { url: { value: "https://github.com/pull/0" }, title: { value: @options[:title] },
|
|
171
|
+
branch: { value: @branch }, now: { value: Time.now.to_i } }
|
|
172
|
+
if @bug
|
|
173
|
+
commit = @bug.commit.to_h[:value].to_s
|
|
174
|
+
refuse_unless_ledger_takes("QualityControl::Patch.Open",
|
|
175
|
+
bug: @bug.id, number: { value: next_number("Patch") },
|
|
176
|
+
commit: { value: commit.match?(SHA_PATTERN) ? commit : PLACEHOLDER_COMMIT },
|
|
177
|
+
**recorded)
|
|
178
|
+
else
|
|
179
|
+
refuse_unless_ledger_takes("QualityControl::Improvement.Open",
|
|
180
|
+
**(@angle ? { angle: @angle.id } : {}),
|
|
181
|
+
number: { value: next_number("Improvement") }, **recorded)
|
|
182
|
+
end
|
|
183
|
+
check_daily_cap
|
|
184
|
+
check_ancestry
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def check_daily_cap
|
|
188
|
+
cap = dial(:PR_CAP_PER_DAY, 0)
|
|
189
|
+
return unless cap.positive?
|
|
190
|
+
|
|
191
|
+
today = Time.now
|
|
192
|
+
midnight = Time.new(today.year, today.month, today.day).to_i
|
|
193
|
+
opened_today = query("Patch.OpenedSince", since: { value: midnight }).size +
|
|
194
|
+
query("Improvement.OpenedSince", since: { value: midnight }).size
|
|
195
|
+
refusing { @git_pr.assert_under_daily_cap!(opened_today: opened_today, cap: cap) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def check_ancestry
|
|
199
|
+
return unless @bug
|
|
200
|
+
|
|
201
|
+
commit = @bug.commit.to_h[:value].to_s
|
|
202
|
+
abort "refused: #{@bug.id} carries no commit" unless commit.match?(SHA_PATTERN)
|
|
203
|
+
|
|
204
|
+
refusing do
|
|
205
|
+
@git_pr.assert_ancestor!(commit: commit, owner: @bug.id)
|
|
206
|
+
@git_pr.assert_pushed!(branch: @branch, commit: commit, owner: @bug.id)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# @return [Hash] `gh`'s view of the PR, opened here when the branch had none
|
|
211
|
+
def open_pull_request
|
|
212
|
+
view = refusing { @git_pr.open_pull_request(@branch) }
|
|
213
|
+
if view
|
|
214
|
+
puts "PR ##{view[:number]} already open for #{@branch} — not re-creating"
|
|
215
|
+
else
|
|
216
|
+
create_pull_request
|
|
217
|
+
view = refusing { @git_pr.open_pull_request(@branch) }
|
|
218
|
+
abort "gh pr create returned, but gh pr view #{@branch} shows no open PR — record nothing" unless view
|
|
219
|
+
end
|
|
220
|
+
sha = view[:headRefOid].to_s
|
|
221
|
+
unless sha.match?(SHA_PATTERN)
|
|
222
|
+
abort "refused: gh reports head #{sha.inspect} for ##{view[:number]}, which does not look like a sha"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
check_pr_head(sha)
|
|
226
|
+
view
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# The PR must carry the fix commit, whether this run opened it or an earlier one did.
|
|
230
|
+
def check_pr_head(sha)
|
|
231
|
+
return unless @bug
|
|
232
|
+
|
|
233
|
+
refusing { @git_pr.assert_pr_head!(head: sha, commit: @bug.commit.to_h[:value].to_s, owner: @bug.id) }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def create_pull_request
|
|
237
|
+
# The body names the ledger record so the PR and the ledger point at each other.
|
|
238
|
+
cited = if @bug then "Fixes #{@bug.id}."
|
|
239
|
+
elsif @angle then "Builds #{@angle.id}."
|
|
240
|
+
else "Deliberate work, no Bug or Angle behind it."
|
|
241
|
+
end
|
|
242
|
+
body = @options[:body] ||
|
|
243
|
+
"#{cited}\n\nOpened by hecks quality_control patch.open and recorded in the QualityControl ledger."
|
|
244
|
+
begin
|
|
245
|
+
@git_pr.create_pull_request(branch: @branch, title: @options[:title], body: body,
|
|
246
|
+
draft: dial(:DRAFT_ONLY, false))
|
|
247
|
+
rescue Hecks::Adapters::GitPr::CommandFailed => e
|
|
248
|
+
abort e.message
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def record(view)
|
|
253
|
+
number = view[:number]
|
|
254
|
+
sha = view[:headRefOid].to_s
|
|
255
|
+
now = Time.now.to_i
|
|
256
|
+
if @bug
|
|
257
|
+
record_patch(view, number, sha, now)
|
|
258
|
+
else
|
|
259
|
+
record_improvement(view, number, sha, now)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def record_patch(view, number, sha, now)
|
|
264
|
+
if query("Patch.All").map { |row| row[:number][:value] }.include?(number)
|
|
265
|
+
puts "Patch ##{number} already recorded — not re-recording"
|
|
266
|
+
return
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
::QualityControl::Patch.open!(
|
|
270
|
+
bug: @bug.id, number: { value: number }, url: { value: view[:url] },
|
|
271
|
+
branch: { value: view[:headRefName] }, commit: { value: sha }, title: { value: view[:title] },
|
|
272
|
+
now: { value: now }
|
|
273
|
+
)
|
|
274
|
+
puts "recorded Patch ##{number} for #{@bug.id} at #{sha[0, 7]}"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def record_improvement(view, number, sha, now)
|
|
278
|
+
existing = ::QualityControl::Improvement.find(number)
|
|
279
|
+
if existing && existing.status != "opened"
|
|
280
|
+
puts "Improvement ##{number} already recorded (#{existing.status}) — not re-recording"
|
|
281
|
+
return
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# A run that stopped between `open!` and `land!` leaves the record `opened`: finish it.
|
|
285
|
+
existing || ::QualityControl::Improvement.open!(
|
|
286
|
+
**(@angle ? { angle: @angle.id } : {}),
|
|
287
|
+
number: { value: number }, url: { value: view[:url] },
|
|
288
|
+
branch: { value: view[:headRefName] }, title: { value: view[:title] },
|
|
289
|
+
now: { value: now }
|
|
290
|
+
)
|
|
291
|
+
::QualityControl::Improvement.find(number).land!(number: { value: number }, commit: { value: sha })
|
|
292
|
+
puts "recorded Improvement ##{number}#{" for #{@angle.id}" if @angle}, landed at #{sha[0, 7]}"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def merge_when_green(number)
|
|
296
|
+
begin
|
|
297
|
+
@git_pr.merge_when_green(number)
|
|
298
|
+
rescue Hecks::Adapters::GitPr::CommandFailed => e
|
|
299
|
+
abort "recorded, but #{e.message}"
|
|
300
|
+
end
|
|
301
|
+
puts "auto-merge queued for ##{number} (QualityControlDials::AUTO_MERGE)"
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../hecks"
|
|
4
|
+
# The era subsystem does not load with core; a `PostgresEra` domain needs it (ADR 0033).
|
|
5
|
+
require_relative "../../ports/persistence/plugins/era"
|
|
6
|
+
require_relative "../../adapters/driven/heki"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module QualityControlCli
|
|
10
|
+
# The command behind `hecks quality_control migrate_ledger_from_heki`: copies each aggregate's
|
|
11
|
+
# current state from a Heki data directory into the repository that the domain's
|
|
12
|
+
# `.hecksagon`/`.world` files bind today (for example `PostgresEra`). It is domain-agnostic.
|
|
13
|
+
#
|
|
14
|
+
# migrate_ledger_from_heki <domain_dir> <heki_data_dir> [aggregate_name ...] # dry run
|
|
15
|
+
# migrate_ledger_from_heki <domain_dir> <heki_data_dir> [aggregate_name ...] --force # apply
|
|
16
|
+
#
|
|
17
|
+
# One `save` per id from `Heki#all`, not a journal replay: state is kept byte-for-byte, the
|
|
18
|
+
# per-append history is not. The Heki side is only read. An id the destination holds under a
|
|
19
|
+
# different state is always refused, so a second run is idempotent. Only aggregates with a
|
|
20
|
+
# `<storage_name>.heki` file in the Heki directory are considered; trailing names (`hecks_name`
|
|
21
|
+
# or `storage_name`) narrow further.
|
|
22
|
+
class QaPostgresMigrate
|
|
23
|
+
USAGE = "usage: hecks quality_control migrate_ledger_from_heki <domain_dir> <heki_data_dir> " \
|
|
24
|
+
"[aggregate_name ...] [--force]"
|
|
25
|
+
|
|
26
|
+
# Migrates, or reports what a migration would do.
|
|
27
|
+
#
|
|
28
|
+
# @param argv [Array<String>] the domain directory, the Heki directory, aggregate names, and
|
|
29
|
+
# `--force` to apply
|
|
30
|
+
# @param out [IO] where the report goes
|
|
31
|
+
# @param err [IO] where a refusal goes
|
|
32
|
+
# @return [Integer] 0 when nothing conflicts, 1 for a usage error or a conflict
|
|
33
|
+
def self.call(argv, out: $stdout, err: $stderr)
|
|
34
|
+
new(out: out, err: err).call(argv)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param out [IO] where the report goes
|
|
38
|
+
# @param err [IO] where a refusal goes
|
|
39
|
+
def initialize(out: $stdout, err: $stderr)
|
|
40
|
+
@out = out
|
|
41
|
+
@err = err
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param argv [Array<String>] the domain directory, the Heki directory, aggregate names, and
|
|
45
|
+
# `--force` to apply
|
|
46
|
+
# @return [Integer] 0 when nothing conflicts, 1 for a usage error or a conflict
|
|
47
|
+
def call(argv)
|
|
48
|
+
argv = argv.dup
|
|
49
|
+
force = argv.delete("--force")
|
|
50
|
+
domain_dir = argv.shift
|
|
51
|
+
return usage("no domain directory given") if domain_dir.nil?
|
|
52
|
+
return usage("no such domain directory #{domain_dir.inspect}") unless Dir.exist?(domain_dir)
|
|
53
|
+
|
|
54
|
+
heki_dir = argv.shift
|
|
55
|
+
return usage("no Heki data directory given") if heki_dir.nil?
|
|
56
|
+
return usage("no such Heki data directory #{heki_dir.inspect}") unless Dir.exist?(heki_dir)
|
|
57
|
+
|
|
58
|
+
heki_dir = File.expand_path(heki_dir)
|
|
59
|
+
registry = Hecks.boot(domain_dir).registry
|
|
60
|
+
candidates = candidates_in(registry, heki_dir, argv)
|
|
61
|
+
if candidates.empty?
|
|
62
|
+
@out.puts "hecks quality_control migrate_ledger_from_heki: no aggregate matched " \
|
|
63
|
+
"#{argv.empty? ? '(any)' : argv.inspect} with a " \
|
|
64
|
+
"corresponding .heki file under #{heki_dir}"
|
|
65
|
+
return 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
migrated, skipped, conflicts = copy(candidates, registry, heki_dir, force)
|
|
69
|
+
report(force, migrated, skipped, conflicts)
|
|
70
|
+
conflicts.empty? ? 0 : 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Deep comparison independent of key type and hash order; only array order is meaningful.
|
|
74
|
+
# `spec/qa_postgres_migration_spec.rb` pins this for conflict detection.
|
|
75
|
+
#
|
|
76
|
+
# @param value [Object] a state, or any part of one
|
|
77
|
+
# @return [Object] the same with every hash keyed by string and sorted
|
|
78
|
+
def self.canonical(value)
|
|
79
|
+
case value
|
|
80
|
+
when Hash
|
|
81
|
+
value.each_with_object({}) { |(k, v), h| h[k.to_s] = canonical(v) }.sort.to_h
|
|
82
|
+
when Array
|
|
83
|
+
value.map { |v| canonical(v) }
|
|
84
|
+
else
|
|
85
|
+
value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def usage(message)
|
|
92
|
+
@err.puts "hecks quality_control migrate_ledger_from_heki: #{message}"
|
|
93
|
+
@err.puts USAGE
|
|
94
|
+
1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A candidate is an aggregate whose own `.heki` file exists, whatever it is bound to now.
|
|
98
|
+
def candidates_in(registry, heki_dir, wanted)
|
|
99
|
+
registry.bluebooks.each_with_object([]) do |(domain_name, bluebook), all|
|
|
100
|
+
bluebook.aggregates.each do |aggregate|
|
|
101
|
+
next unless wanted.empty? || wanted.include?(aggregate.hecks_name) || wanted.include?(aggregate.storage_name)
|
|
102
|
+
|
|
103
|
+
heki_path = File.join(heki_dir, "#{aggregate.storage_name}.heki")
|
|
104
|
+
next unless File.exist?(heki_path)
|
|
105
|
+
|
|
106
|
+
all << { domain_name: domain_name, aggregate: aggregate, heki_path: heki_path }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def copy(candidates, registry, heki_dir, force)
|
|
112
|
+
migrated = []
|
|
113
|
+
skipped = []
|
|
114
|
+
conflicts = []
|
|
115
|
+
candidates.each do |candidate|
|
|
116
|
+
aggregate = candidate[:aggregate]
|
|
117
|
+
# Guarded like a factory-built repository so a Heki store decodes through the state codec.
|
|
118
|
+
source = Hecks::Ports::Persistence::CodecBoundary.guard!(
|
|
119
|
+
Hecks::Adapters::Heki.new(aggregate: aggregate, settings: { dir: heki_dir }, root: nil)
|
|
120
|
+
)
|
|
121
|
+
dest = registry.repository(candidate[:domain_name], aggregate)
|
|
122
|
+
source.all.each do |instance|
|
|
123
|
+
name = "#{aggregate.storage_name}/#{instance.id}"
|
|
124
|
+
existing = dest.find(instance.id)
|
|
125
|
+
if existing.nil?
|
|
126
|
+
dest.save(instance) if force
|
|
127
|
+
migrated << name
|
|
128
|
+
elsif self.class.canonical(existing.state) == self.class.canonical(instance.state)
|
|
129
|
+
skipped << name
|
|
130
|
+
else
|
|
131
|
+
conflicts << name
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
[migrated, skipped, conflicts]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def report(force, migrated, skipped, conflicts)
|
|
139
|
+
label = force ? "MIGRATED" : "WOULD MIGRATE"
|
|
140
|
+
migrated.each { |id| @out.puts "#{label} #{id}" }
|
|
141
|
+
skipped.each { |id| @out.puts "SKIP #{id}: destination already holds the identical state" }
|
|
142
|
+
conflicts.each do |id|
|
|
143
|
+
@err.puts "REFUSED #{id}: the destination already holds a DIFFERENT state for this id — " \
|
|
144
|
+
"not overwritten, --force or not. Resolve by hand (compare the two states, decide " \
|
|
145
|
+
"which is authoritative) before migrating this id again."
|
|
146
|
+
end
|
|
147
|
+
@out.puts "" unless migrated.empty? && skipped.empty? && conflicts.empty?
|
|
148
|
+
@out.puts "#{force ? 'migrated' : 'would migrate'} #{migrated.size}, skipped #{skipped.size} " \
|
|
149
|
+
"(already caught up), refused #{conflicts.size} (conflicting data)"
|
|
150
|
+
@out.puts "re-run with --force to apply" unless force || migrated.empty?
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|