hecks 2.9.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/hecks +51 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +16 -3
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
- data/lib/hecks/adapters/driven/postgres.rb +13 -4
- data/lib/hecks/adapters/driven/prism.rb +41 -8
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
- data/lib/hecks/behaviors/expectations.rb +1 -1
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bench/cli.rb +62 -0
- data/lib/hecks/bench/environment.rb +71 -0
- data/lib/hecks/bench/postgres_probe.rb +30 -0
- data/lib/hecks/bench/report.rb +76 -0
- data/lib/hecks/bench/ruby_runner.rb +93 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +101 -0
- data/lib/hecks/bench/stats.rb +46 -0
- data/lib/hecks/bench/suite.rb +98 -0
- data/lib/hecks/bench/workload.rb +89 -0
- data/lib/hecks/bench.rb +17 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
- data/lib/hecks/bluebook/chapter.rb +6 -2
- data/lib/hecks/bluebook/domain_port.rb +36 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
- data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
- data/lib/hecks/bluebook/expression/resolver.rb +3 -2
- data/lib/hecks/bluebook/hexagon.rb +13 -5
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
- data/lib/hecks/bluebook/meta_validator.rb +9 -4
- data/lib/hecks/bluebook/model_check.rb +23 -0
- data/lib/hecks/bluebook/pattern_subset.rb +38 -0
- data/lib/hecks/bluebook/query.rb +20 -5
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/canonical_json.rb +30 -0
- data/lib/hecks/chapters.rb +102 -0
- data/lib/hecks/cli/behaviors.rb +61 -0
- data/lib/hecks/cli/canonicalise.rb +25 -0
- data/lib/hecks/cli/check_engine_agreement.rb +32 -0
- data/lib/hecks/cli/check_era.rb +88 -0
- data/lib/hecks/cli/console.rb +66 -0
- data/lib/hecks/cli/deploy_template_diff.rb +40 -0
- data/lib/hecks/cli/doc_coverage.rb +32 -0
- data/lib/hecks/cli/document.rb +2 -2
- data/lib/hecks/cli/expression_projection.rb +41 -0
- data/lib/hecks/cli/follow.rb +88 -0
- data/lib/hecks/cli/history.rb +44 -0
- data/lib/hecks/cli/ir.rb +1 -1
- data/lib/hecks/cli/mcp.rb +3 -3
- data/lib/hecks/cli/model_check.rb +15 -8
- data/lib/hecks/cli/pattern_cases.rb +47 -0
- data/lib/hecks/cli/present.rb +39 -0
- data/lib/hecks/cli/project_cli.rb +224 -32
- data/lib/hecks/cli/project_diagrams.rb +4 -3
- data/lib/hecks/cli/project_glossary.rb +44 -0
- data/lib/hecks/cli/reference.rb +46 -0
- data/lib/hecks/cli/refresh_projections.rb +44 -0
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
- data/lib/hecks/cli/release.rb +70 -0
- data/lib/hecks/cli/release_gem.rb +59 -0
- data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
- data/lib/hecks/cli/rspec_shard_files.rb +66 -0
- data/lib/hecks/cli/run.rb +3 -3
- data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
- data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
- data/lib/hecks/cli/shape.rb +70 -0
- data/lib/hecks/cli/smoke_http.rb +230 -0
- data/lib/hecks/cli/smoke_test.rb +5 -3
- data/lib/hecks/cli/spec_example.rb +27 -0
- data/lib/hecks/cli/statements.rb +32 -0
- data/lib/hecks/cli/stores.rb +4 -5
- data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
- data/lib/hecks/cli.rb +201 -52
- data/lib/hecks/codemod.rb +324 -0
- data/lib/hecks/corpus.rb +423 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
- data/lib/hecks/deploy/oidc.json +82 -2
- data/lib/hecks/doc/reference.rb +351 -0
- data/lib/hecks/doors/cli_door.rb +196 -0
- data/lib/hecks/doors/cli_runner.rb +297 -0
- data/lib/hecks/{facade → doors}/command_request.rb +1 -1
- data/lib/hecks/{facade → doors}/handle.rb +2 -2
- data/lib/hecks/{facade → doors}/json_door.rb +4 -4
- data/lib/hecks/doors/launcher_options.rb +129 -0
- data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
- data/lib/hecks/doors/mcp_door_scope.rb +97 -0
- data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
- data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
- data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
- data/lib/hecks/doors.rb +61 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +1 -1
- data/lib/hecks/engine_agreement.rb +156 -0
- data/lib/hecks/facade.rb +24 -37
- data/lib/hecks/forms/app.rb +6 -6
- data/lib/hecks/forms/banking_presentation.rb +50 -0
- data/lib/hecks/forms/field_shape.rb +2 -1
- data/lib/hecks/forms/port_argument.rb +1 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
- data/lib/hecks/fuzzing/combination_miner.rb +86 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
- data/lib/hecks/fuzzing/differential.rb +156 -0
- data/lib/hecks/fuzzing/domain_generator.rb +615 -0
- data/lib/hecks/fuzzing/era_boundary.rb +71 -0
- data/lib/hecks/fuzzing/form_census.rb +136 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
- data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
- data/lib/hecks/fuzzing/outside_world.rb +45 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
- data/lib/hecks/fuzzing/properties/guards.rb +190 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
- data/lib/hecks/fuzzing/properties/querying.rb +175 -0
- data/lib/hecks/fuzzing/properties.rb +162 -0
- data/lib/hecks/fuzzing/qa_settings.rb +123 -0
- data/lib/hecks/fuzzing/replay.rb +505 -0
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
- data/lib/hecks/fuzzing/self_consistency.rb +339 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
- data/lib/hecks/fuzzing/shrinker.rb +166 -0
- data/lib/hecks/fuzzing/structural_skips.rb +33 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
- data/lib/hecks/fuzzing/value_generator.rb +216 -0
- data/lib/hecks/fuzzing.rb +23 -0
- data/lib/hecks/grammar/evolve.rb +336 -0
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
- data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
- data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
- data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
- data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
- data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
- data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
- data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
- data/lib/hecks/hecks/adapters/git.adapter +7 -0
- data/lib/hecks/hecks/adapters/git.rb +102 -0
- data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
- data/lib/hecks/hecks/adapters/host_http.rb +65 -0
- data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
- data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
- data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
- data/lib/hecks/hecks/adapters/local_files.rb +46 -0
- data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
- data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
- data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
- data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
- data/lib/hecks/hecks/adapters/shell.adapter +7 -0
- data/lib/hecks/hecks/adapters/shell.rb +49 -0
- data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
- data/lib/hecks/hecks/adapters/terminal.rb +74 -0
- data/lib/hecks/hecks/codebase.behaviors +172 -0
- data/lib/hecks/hecks/codebase.bluebook +2453 -0
- data/lib/hecks/hecks/context_map.hecksagon +13 -0
- data/lib/hecks/hecks/custodian.behaviors +179 -0
- data/lib/hecks/hecks/custodian.bluebook +1847 -0
- data/lib/hecks/hecks/deploy.behaviors +74 -0
- data/lib/hecks/hecks/deploy.world +12 -0
- data/lib/hecks/hecks/environments/memory.world +10 -0
- data/lib/hecks/hecks/governance.world +7 -0
- data/lib/hecks/hecks/hecks.behaviors +62 -0
- data/lib/hecks/hecks/hecks.bluebook +124 -0
- data/lib/hecks/hecks/hecks.hecksagon +390 -0
- data/lib/hecks/hecks/hecks.world +42 -0
- data/lib/hecks/kernel_capabilities.rb +194 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
- data/lib/hecks/language/bluebook/query.bluebook +7 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
- data/lib/hecks/mcp_stdio_guard.rb +2 -2
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
- data/lib/hecks/persistence_legacy_fixture.rb +113 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +15 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
- data/lib/hecks/ports/persistence/state_codec.rb +24 -5
- data/lib/hecks/projection_files.rb +285 -0
- data/lib/hecks/projections/bootstrap_table.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +5 -5
- data/lib/hecks/projections/deploy/lambda.rb +43 -32
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/preview/template.rb +1 -1
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/shared.rb +1 -1
- data/lib/hecks/projections/diagrams.rb +3 -3
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/parser_table.rb +4 -4
- data/lib/hecks/projections/reference.rb +2 -3
- data/lib/hecks/projections/rust_vocabulary.rb +1 -1
- data/lib/hecks/projector/cli_projector.rb +106 -37
- data/lib/hecks/projector.rb +1 -1
- data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
- data/lib/hecks/quality_control/adapters/agent.rb +58 -0
- data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
- data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
- data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
- data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
- data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
- data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
- data/lib/hecks/quality_control/cli/child.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
- data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
- data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
- data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
- data/lib/hecks/query_ir.rb +346 -0
- data/lib/hecks/query_ir_mcp.rb +198 -0
- data/lib/hecks/release/runner/ci_publisher.rb +2 -2
- data/lib/hecks/release/runner/clean_tree.rb +42 -0
- data/lib/hecks/release/runner/console.rb +6 -0
- data/lib/hecks/release/runner/gem_publisher.rb +14 -7
- data/lib/hecks/release/runner/npm_publisher.rb +6 -36
- data/lib/hecks/release/runner/preflight.rb +26 -2
- data/lib/hecks/release/runner/published.rb +8 -26
- data/lib/hecks/release/runner/tagger.rb +30 -15
- data/lib/hecks/release/runner.rb +57 -6
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/runtime/adapter_lookup.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
- data/lib/hecks/runtime/dependency_planning.rb +20 -2
- data/lib/hecks/runtime/dispatcher.rb +53 -3
- data/lib/hecks/runtime/interpreting.rb +3 -0
- data/lib/hecks/runtime/loader.rb +65 -12
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/policy_interpreter.rb +5 -2
- data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
- data/lib/hecks/runtime/query_interpreter.rb +77 -0
- data/lib/hecks/runtime/reaction_outcome.rb +89 -0
- data/lib/hecks/runtime/refusal_wording.rb +23 -1
- data/lib/hecks/runtime/registry/verification.rb +163 -4
- data/lib/hecks/runtime/registry.rb +57 -3
- data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
- data/lib/hecks/runtime/value/coercion.rb +23 -135
- data/lib/hecks/runtime/value/field_checks.rb +172 -0
- data/lib/hecks/runtime/value.rb +4 -1
- data/lib/hecks/runtime.rb +20 -6
- data/lib/hecks/rust_build/conformance.rb +158 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
- data/lib/hecks/rust_build/coverage.rb +218 -0
- data/lib/hecks/rust_build/native_build.rb +124 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
- data/lib/hecks/rust_build/project_rust.rb +332 -0
- data/lib/hecks/rust_build/wasm.rb +105 -0
- data/lib/hecks/rust_build/wasm_browser.rb +113 -0
- data/lib/hecks/rust_build.rb +144 -0
- data/lib/hecks/storehouse.rb +16 -16
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
- data/lib/hecks/three_zero/forms.yml +127 -0
- data/lib/hecks/three_zero.rb +12 -0
- data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
- data/lib/hecks/tools/comment_style.rb +1070 -0
- data/lib/hecks/tools/corpus_report.rb +87 -0
- data/lib/hecks/tools/deploy_recipe.rb +255 -0
- data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
- data/lib/hecks/tools/era_projection_backfill.rb +94 -0
- data/lib/hecks/tools/era_reattest.rb +146 -0
- data/lib/hecks/tools/evolve_run.rb +189 -0
- data/lib/hecks/tools/fuzz_sweep.rb +373 -0
- data/lib/hecks/tools/heki_compaction.rb +100 -0
- data/lib/hecks/tools/hoist_local_givens.rb +246 -0
- data/lib/hecks/tools/journal_compaction.rb +89 -0
- data/lib/hecks/tools/oidc_manifests.rb +70 -0
- data/lib/hecks/tools/query_ir_run.rb +81 -0
- data/lib/hecks/tools/regeneration_run.rb +166 -0
- data/lib/hecks/tools/rust_comment_style.rb +642 -0
- data/lib/hecks/tools/sequence_script.rb +62 -0
- data/lib/hecks/tools/tail_merge.rb +117 -0
- data/lib/hecks/tools/tenant_provisioning.rb +142 -0
- data/lib/hecks/tools/translation_audit.rb +208 -0
- data/lib/hecks/tools/translation_scaffold.rb +131 -0
- data/lib/hecks/tools.rb +86 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +32 -15
- data/qa/settings.yml +66 -0
- data/rust/.cargo/config.toml +6 -0
- data/rust/.gitignore +2 -0
- data/rust/Cargo.lock +54 -0
- data/rust/Cargo.toml +61 -0
- data/rust/build/.cargo/config.toml +3 -0
- data/rust/build/.gitignore +1 -0
- data/rust/build/Cargo.lock +7 -0
- data/rust/build/Cargo.toml +19 -0
- data/rust/build/src/build_artifact.rs +47 -0
- data/rust/build/src/cargo_sync.rs +248 -0
- data/rust/build/src/json.rs +421 -0
- data/rust/build/src/lineage_pass.rs +285 -0
- data/rust/build/src/main.rs +51 -0
- data/rust/build/src/optional_pass.rs +150 -0
- data/rust/build/src/pipeline.rs +264 -0
- data/rust/build/src/reserved_names.rs +76 -0
- data/rust/build/src/resolve.rs +124 -0
- data/rust/build/src/root.rs +25 -0
- data/rust/build/src/sidecars.rs +73 -0
- data/rust/build/src/subprocess.rs +51 -0
- data/rust/build/src/tmp.rs +31 -0
- data/rust/codegen/.cargo/config.toml +3 -0
- data/rust/codegen/.gitignore +1 -0
- data/rust/codegen/Cargo.lock +7 -0
- data/rust/codegen/Cargo.toml +20 -0
- data/rust/codegen/src/attr.rs +32 -0
- data/rust/codegen/src/bridging.rs +481 -0
- data/rust/codegen/src/commands.rs +1226 -0
- data/rust/codegen/src/constraints.rs +109 -0
- data/rust/codegen/src/dependency_planning.rs +209 -0
- data/rust/codegen/src/domain_generator.rs +1288 -0
- data/rust/codegen/src/exemplar.rs +230 -0
- data/rust/codegen/src/expr_emitter.rs +87 -0
- data/rust/codegen/src/fielded.rs +192 -0
- data/rust/codegen/src/hecks_naming.rs +33 -0
- data/rust/codegen/src/json.rs +335 -0
- data/rust/codegen/src/json_codec.rs +697 -0
- data/rust/codegen/src/literal.rs +207 -0
- data/rust/codegen/src/main.rs +290 -0
- data/rust/codegen/src/manifest.rs +227 -0
- data/rust/codegen/src/mutations.rs +784 -0
- data/rust/codegen/src/naming.rs +286 -0
- data/rust/codegen/src/ports.rs +208 -0
- data/rust/codegen/src/prelude.rs +116 -0
- data/rust/codegen/src/queries.rs +786 -0
- data/rust/codegen/src/reactions.rs +532 -0
- data/rust/codegen/src/read_models.rs +697 -0
- data/rust/codegen/src/reference_specs.rs +40 -0
- data/rust/codegen/src/registry.rs +966 -0
- data/rust/codegen/src/reserved_names.rs +76 -0
- data/rust/codegen/src/shared.rs +93 -0
- data/rust/codegen/src/skip_reason.rs +25 -0
- data/rust/codegen/src/types.rs +362 -0
- data/rust/host/.cargo/config.toml +3 -0
- data/rust/host/.gitignore +1 -0
- data/rust/host/Cargo.lock +4297 -0
- data/rust/host/Cargo.toml +61 -0
- data/rust/host/HECKS_RELEASE +1 -0
- data/rust/host/rds-ca-bundle.pem +2736 -0
- data/rust/host/src/api.rs +1596 -0
- data/rust/host/src/approval.rs +652 -0
- data/rust/host/src/auth.rs +1287 -0
- data/rust/host/src/bin/expr_harness.rs +46 -0
- data/rust/host/src/bin/lineage_harness.rs +119 -0
- data/rust/host/src/bin/mint_harness.rs +135 -0
- data/rust/host/src/checkout.rs +265 -0
- data/rust/host/src/dispatch.rs +1443 -0
- data/rust/host/src/expr_json.rs +1117 -0
- data/rust/host/src/field_hints.rs +52 -0
- data/rust/host/src/ir.rs +733 -0
- data/rust/host/src/journal.rs +874 -0
- data/rust/host/src/lambda_client.rs +378 -0
- data/rust/host/src/log.rs +147 -0
- data/rust/host/src/main.rs +474 -0
- data/rust/host/src/mint.rs +1865 -0
- data/rust/host/src/payments/keystore.rs +330 -0
- data/rust/host/src/payments/tests.rs +1400 -0
- data/rust/host/src/payments.rs +767 -0
- data/rust/host/src/presentation.rs +698 -0
- data/rust/host/src/presentation_write.rs +1431 -0
- data/rust/host/src/query_step.rs +149 -0
- data/rust/host/src/rate_limit.rs +904 -0
- data/rust/host/src/reference_transform.rs +241 -0
- data/rust/host/src/reference_validate.rs +338 -0
- data/rust/host/src/resend.rs +296 -0
- data/rust/host/src/secrets.rs +106 -0
- data/rust/host/src/server.rs +663 -0
- data/rust/host/src/storage_shape.rs +175 -0
- data/rust/host/src/test_pg.rs +40 -0
- data/rust/host/src/ui_schema.rs +1351 -0
- data/rust/host/src/wasm_runner.rs +191 -0
- data/rust/host/src/web/newsletter/cooldown.rs +126 -0
- data/rust/host/src/web/newsletter/tests.rs +104 -0
- data/rust/host/src/web/newsletter.rs +310 -0
- data/rust/host/src/web/newsletter_send.rs +357 -0
- data/rust/host/src/web/registrations.rs +1505 -0
- data/rust/host/src/web.rs +2937 -0
- data/rust/lsp/.gitignore +1 -0
- data/rust/lsp/Cargo.lock +7 -0
- data/rust/lsp/Cargo.toml +16 -0
- data/rust/lsp/README.md +133 -0
- data/rust/lsp/src/diagnostics.rs +165 -0
- data/rust/lsp/src/json.rs +385 -0
- data/rust/lsp/src/main.rs +399 -0
- data/rust/lsp/src/outline.rs +177 -0
- data/rust/lsp/src/rpc.rs +67 -0
- data/rust/parser/.cargo/config.toml +5 -0
- data/rust/parser/.gitignore +1 -0
- data/rust/parser/Cargo.lock +7 -0
- data/rust/parser/Cargo.toml +21 -0
- data/rust/parser/src/build/closed_sets.rs +27 -0
- data/rust/parser/src/build/identity.rs +276 -0
- data/rust/parser/src/build/mod.rs +12 -0
- data/rust/parser/src/build/naming.rs +141 -0
- data/rust/parser/src/build/pattern_subset.rs +161 -0
- data/rust/parser/src/build/query_derive.rs +72 -0
- data/rust/parser/src/build/query_inference.rs +269 -0
- data/rust/parser/src/build/query_options.rs +100 -0
- data/rust/parser/src/build/read_model.rs +94 -0
- data/rust/parser/src/build/references.rs +32 -0
- data/rust/parser/src/canonical.rs +114 -0
- data/rust/parser/src/diag.rs +55 -0
- data/rust/parser/src/emit.rs +953 -0
- data/rust/parser/src/expr/ast_json.rs +147 -0
- data/rust/parser/src/expr/evaluator.rs +81 -0
- data/rust/parser/src/expr/mod.rs +84 -0
- data/rust/parser/src/expr/resolver.rs +497 -0
- data/rust/parser/src/ir.rs +387 -0
- data/rust/parser/src/keywords.rs +585 -0
- data/rust/parser/src/lex.rs +662 -0
- data/rust/parser/src/main.rs +252 -0
- data/rust/parser/src/parse/aggregate.rs +612 -0
- data/rust/parser/src/parse/chapter.rs +517 -0
- data/rust/parser/src/parse/command.rs +637 -0
- data/rust/parser/src/parse/domain_port.rs +127 -0
- data/rust/parser/src/parse/entity.rs +527 -0
- data/rust/parser/src/parse/file.rs +67 -0
- data/rust/parser/src/parse/hecksagon.rs +231 -0
- data/rust/parser/src/parse/lifecycle.rs +162 -0
- data/rust/parser/src/parse/mod.rs +1262 -0
- data/rust/parser/src/parse/policy.rs +87 -0
- data/rust/parser/src/parse/process_manager.rs +305 -0
- data/rust/parser/src/parse/query.rs +100 -0
- data/rust/parser/src/parse/read_model.rs +214 -0
- data/rust/parser/src/parse/value_object.rs +250 -0
- data/rust/parser/src/ruby_value.rs +422 -0
- data/rust/project/bridging.rb +257 -0
- data/rust/project/commands.rb +877 -0
- data/rust/project/constraints.rb +86 -0
- data/rust/project/dependency_planning.rb +178 -0
- data/rust/project/domain_generator.rb +980 -0
- data/rust/project/exemplar.rb +148 -0
- data/rust/project/expr_emitter.rb +72 -0
- data/rust/project/fielded.rb +141 -0
- data/rust/project/json_codec.rb +641 -0
- data/rust/project/mutations.rb +721 -0
- data/rust/project/naming.rb +205 -0
- data/rust/project/ports.rb +65 -0
- data/rust/project/queries.rb +493 -0
- data/rust/project/reactions.rb +317 -0
- data/rust/project/read_models.rb +690 -0
- data/rust/project/reference_specs.rb +27 -0
- data/rust/project/registry.rb +396 -0
- data/rust/project/skip_reason.rb +29 -0
- data/rust/project/types.rb +225 -0
- data/rust/project/write_if_changed.rb +83 -0
- data/rust/project.rb +30 -0
- data/rust/project_rust_pipeline.rb +383 -0
- data/rust/src/exemplar/commands.rs +279 -0
- data/rust/src/exemplar/constraints.rs +18 -0
- data/rust/src/exemplar/fielded.rs +280 -0
- data/rust/src/exemplar/json.rs +277 -0
- data/rust/src/exemplar/mod.rs +17 -0
- data/rust/src/exemplar/mutations.rs +80 -0
- data/rust/src/exemplar/queries.rs +33 -0
- data/rust/src/exemplar/reactions.rs +88 -0
- data/rust/src/exemplar/read_models.rs +48 -0
- data/rust/src/exemplar/registry.rs +166 -0
- data/rust/src/exemplar/types.rs +44 -0
- data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
- data/rust/src/kernel/attribute_shapes/list.rs +18 -0
- data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
- data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
- data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
- data/rust/src/kernel/cli.rs +585 -0
- data/rust/src/kernel/dispatch.rs +1090 -0
- data/rust/src/kernel/expr.rs +458 -0
- data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
- data/rust/src/kernel/expression_operators/comparison.rs +48 -0
- data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
- data/rust/src/kernel/expression_operators/logical.rs +13 -0
- data/rust/src/kernel/expression_operators/membership.rs +18 -0
- data/rust/src/kernel/expression_operators/mod.rs +68 -0
- data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
- data/rust/src/kernel/expression_operators/positional.rs +126 -0
- data/rust/src/kernel/expression_operators/presence.rs +101 -0
- data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
- data/rust/src/kernel/expression_operators/sized.rs +35 -0
- data/rust/src/kernel/expression_operators/text.rs +79 -0
- data/rust/src/kernel/expression_operators/to_string.rs +18 -0
- data/rust/src/kernel/json.rs +779 -0
- data/rust/src/kernel/mod.rs +133 -0
- data/rust/src/kernel/named_query.rs +219 -0
- data/rust/src/kernel/orchestrate.rs +1810 -0
- data/rust/src/kernel/pattern.rs +307 -0
- data/rust/src/kernel/query_comparators.rs +206 -0
- data/rust/src/kernel/query_ordering.rs +103 -0
- data/rust/src/kernel/read_model.rs +914 -0
- data/rust/src/kernel/reference_lookup.rs +149 -0
- data/rust/src/kernel/repository.rs +468 -0
- data/rust/src/kernel/routing.rs +612 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
- data/rust/src/kernel/vocab/field_hint.rs +70 -0
- data/rust/src/kernel/vocab/mod.rs +20 -0
- data/rust/src/kernel/vocab/query_comparator.rs +65 -0
- data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
- data/rust/src/lib.rs +8 -0
- data/rust/src/main.rs +16 -0
- data/rust/web/.gitignore +1 -0
- data/rust/web/Cargo.lock +119 -0
- data/rust/web/Cargo.toml +18 -0
- data/rust/web/src/lib.rs +8 -0
- metadata +477 -12
- data/lib/hecks/facade/cli_door.rb +0 -96
- data/lib/hecks/facade/cli_runner.rb +0 -127
- data/lib/hecks/mcp_door_scope.rb +0 -95
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "open3"
|
|
6
|
+
require_relative "../../../hecks"
|
|
7
|
+
require "hecks/rust_build"
|
|
8
|
+
require_relative "../../fuzzing"
|
|
9
|
+
require_relative "../../fuzzing/domain_generator"
|
|
10
|
+
require_relative "../../fuzzing/generated_domain_check"
|
|
11
|
+
require_relative "child"
|
|
12
|
+
|
|
13
|
+
module Hecks
|
|
14
|
+
module QualityControlCli
|
|
15
|
+
# The command behind `hecks quality_control check_generated_domains`: generates domains nobody
|
|
16
|
+
# wrote and checks them the way the rotation does (`USAGE` lists the forms: generate,
|
|
17
|
+
# `--blueprint`, `--source` and `--promote`).
|
|
18
|
+
#
|
|
19
|
+
# Exit codes: 0 every valid domain clean; 2 at least one finding; 1 operational error.
|
|
20
|
+
#
|
|
21
|
+
# Every generated domain is named `QaGenerated`, so each is checked in its own child process
|
|
22
|
+
# (`--check <dir>`, prints one `QA_GENERATED_RESULT <json>` line). Findings are shrunk twice:
|
|
23
|
+
# the domain, then the steps. `--rust` builds a scratch crate under `tmp/qa-generated/rust`.
|
|
24
|
+
# `--source` adopts a bluebook written elsewhere; a `HYPOTHESIS.md` beside it rides into
|
|
25
|
+
# `NOTES.md`. It never touches the QA ledger; a domain that does not boot is `invalid`, never a
|
|
26
|
+
# finding.
|
|
27
|
+
class QaGeneratedDomains
|
|
28
|
+
EXIT_OK = 0
|
|
29
|
+
EXIT_ERROR = 1
|
|
30
|
+
EXIT_FOUND = 2
|
|
31
|
+
RESULT_MARKER = "QA_GENERATED_RESULT "
|
|
32
|
+
|
|
33
|
+
USAGE = "usage: hecks quality_control check_generated_domains [--domains N] [--start SEED] " \
|
|
34
|
+
"[--forms a,b] [--seeds K] [--steps M] [--adversarial F] [--rust] [--shrink-budget B] " \
|
|
35
|
+
"[--domain-shrink-budget D]\n " \
|
|
36
|
+
"hecks quality_control check_generated_domains --source <file.bluebook> [--source …] [--rust] …\n " \
|
|
37
|
+
"hecks quality_control check_generated_domains --promote <finding-dir> --name <stress_domain_name>"
|
|
38
|
+
|
|
39
|
+
Generator = Hecks::Fuzzing::DomainGenerator
|
|
40
|
+
|
|
41
|
+
# Generates, checks and reports.
|
|
42
|
+
#
|
|
43
|
+
# @param argv [Array<String>] the flags in the usage line
|
|
44
|
+
# @param root [String] the repository root
|
|
45
|
+
# @param env [Hash{String => String}] `QA_GENERATED_DOMAINS_PER_TICK` overrides the count of
|
|
46
|
+
# `--from-dials`
|
|
47
|
+
# @return [Integer] 0 clean, 2 a finding, 1 an operational error
|
|
48
|
+
# @raise [SystemExit] on a bad argument or a missing file
|
|
49
|
+
def self.call(argv, root:, env: ENV)
|
|
50
|
+
new(root: root, env: env).call(argv)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param root [String] the repository root
|
|
54
|
+
# @param env [Hash{String => String}] `QA_GENERATED_DOMAINS_PER_TICK` overrides the count of
|
|
55
|
+
# `--from-dials`
|
|
56
|
+
def initialize(root:, env: ENV)
|
|
57
|
+
@root = root
|
|
58
|
+
@env = env
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param argv [Array<String>] the flags in the usage line
|
|
62
|
+
# @return [Integer] the exit status
|
|
63
|
+
# @raise [SystemExit] on a bad argument or a missing file
|
|
64
|
+
def call(argv)
|
|
65
|
+
@options = parse(argv.dup)
|
|
66
|
+
return EXIT_OK if @options == :help
|
|
67
|
+
return check_one if @options[:check]
|
|
68
|
+
return promote if @options[:promote]
|
|
69
|
+
return EXIT_OK if @options[:from_dials] && !dials_on?
|
|
70
|
+
|
|
71
|
+
run
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Each flag that takes a value: the option it sets and how the value is read.
|
|
75
|
+
VALUE_FLAGS = {
|
|
76
|
+
"--domains" => [:domains, ->(v) { Integer(v) }], "--start" => [:start, ->(v) { Integer(v) }],
|
|
77
|
+
"--forms" => [:forms, ->(v) { v.split(",") }], "--seeds" => [:seeds, ->(v) { Integer(v) }],
|
|
78
|
+
"--steps" => [:steps, ->(v) { Integer(v) }], "--adversarial" => [:adversarial, ->(v) { Float(v) }],
|
|
79
|
+
"--shrink-budget" => [:shrink_budget, ->(v) { Integer(v) }],
|
|
80
|
+
"--domain-shrink-budget" => [:domain_shrink_budget, ->(v) { Integer(v) }],
|
|
81
|
+
"--check" => [:check, :itself.to_proc], "--binary" => [:binary, :itself.to_proc],
|
|
82
|
+
"--match" => [:match, ->(v) { JSON.parse(v) }], "--promote" => [:promote, :itself.to_proc],
|
|
83
|
+
"--name" => [:name, :itself.to_proc], "--blueprint" => [:blueprint, :itself.to_proc]
|
|
84
|
+
}.freeze
|
|
85
|
+
|
|
86
|
+
# Each flag that stands alone, and the option it turns on.
|
|
87
|
+
SWITCH_FLAGS = { "--rust" => :rust, "--from-dials" => :from_dials }.freeze
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def parse(argv)
|
|
92
|
+
options = { domains: 3, start: nil, forms: nil, seeds: 5, steps: 25, adversarial: 0.3, rust: false,
|
|
93
|
+
shrink_budget: 200, domain_shrink_budget: 40, check: nil, binary: nil, match: nil,
|
|
94
|
+
promote: nil, name: nil, sources: [] }
|
|
95
|
+
until argv.empty?
|
|
96
|
+
arg = argv.shift
|
|
97
|
+
if %w[-h --help].include?(arg)
|
|
98
|
+
puts USAGE
|
|
99
|
+
return :help
|
|
100
|
+
end
|
|
101
|
+
parse_flag(options, arg, argv)
|
|
102
|
+
end
|
|
103
|
+
options
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_flag(options, arg, argv)
|
|
107
|
+
if VALUE_FLAGS.key?(arg)
|
|
108
|
+
key, reader = VALUE_FLAGS.fetch(arg)
|
|
109
|
+
options[key] = reader.call(argv.shift)
|
|
110
|
+
elsif SWITCH_FLAGS.key?(arg)
|
|
111
|
+
options[SWITCH_FLAGS.fetch(arg)] = true
|
|
112
|
+
elsif arg == "--source"
|
|
113
|
+
options[:sources] << argv.shift
|
|
114
|
+
else
|
|
115
|
+
abort "#{USAGE}\nunexpected argument: #{arg.inspect}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# `--check DIR`: one domain in this process, one result line.
|
|
120
|
+
def check_one
|
|
121
|
+
differ = nil
|
|
122
|
+
if @options[:binary]
|
|
123
|
+
require File.join(@root, "spec/support/rust_conformance_helpers")
|
|
124
|
+
differ = Class.new do
|
|
125
|
+
include RustConformanceHelpers
|
|
126
|
+
|
|
127
|
+
attr_reader :structural_skips
|
|
128
|
+
|
|
129
|
+
def initialize
|
|
130
|
+
@structural_skips = Set.new
|
|
131
|
+
end
|
|
132
|
+
end.new
|
|
133
|
+
end
|
|
134
|
+
result = Hecks::Fuzzing::GeneratedDomainCheck.run(
|
|
135
|
+
@options[:check], seeds: @options[:seeds], steps: @options[:steps], adversarial: @options[:adversarial],
|
|
136
|
+
binary: @options[:binary], differ: differ, match: @options[:match],
|
|
137
|
+
shrink_budget: @options[:shrink_budget]
|
|
138
|
+
)
|
|
139
|
+
puts "#{RESULT_MARKER}#{JSON.generate(result)}"
|
|
140
|
+
EXIT_OK
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def camelize(name) = name.split("_").map(&:capitalize).join
|
|
144
|
+
|
|
145
|
+
def promote
|
|
146
|
+
name = @options[:name] or abort "#{USAGE}\n--promote needs --name"
|
|
147
|
+
unless name.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
148
|
+
abort "--name must be a lowercase identifier (it becomes a Rust module and Cargo feature)"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
source = File.join(@options[:promote], Generator::DIRECTORY, "bluebook", "#{Generator::DIRECTORY}.bluebook")
|
|
152
|
+
abort "no generated domain at #{source}" unless File.exist?(source)
|
|
153
|
+
target = File.join(@root, "qa/stress_domains", name)
|
|
154
|
+
abort "#{target} already exists — pick another --name" if File.exist?(target)
|
|
155
|
+
|
|
156
|
+
FileUtils.mkdir_p(File.join(target, "bluebook"))
|
|
157
|
+
File.write(File.join(target, "bluebook", "#{name}.bluebook"),
|
|
158
|
+
File.read(source).gsub(Generator::DOMAIN_NAME, camelize(name)))
|
|
159
|
+
File.write(File.join(target, "NOTES.md"), notes_for(name))
|
|
160
|
+
puts "promoted: qa/stress_domains/#{name}"
|
|
161
|
+
puts "next:"
|
|
162
|
+
puts " hecks quality_control judge_novelty qa/stress_domains/#{name}"
|
|
163
|
+
puts " hecks project_rust qa/stress_domains/#{name} # when the finding needs the Rust comparison"
|
|
164
|
+
# `hecks quality_control target.seed` derives targets from
|
|
165
|
+
# `Hecks::Corpus.rotation_targets`, so a hand-typed `target.identify` line is not printed:
|
|
166
|
+
# nobody ran it and the domain never
|
|
167
|
+
# entered the rotation.
|
|
168
|
+
puts " hecks quality_control target.seed # idempotent; picks this domain up from the corpus"
|
|
169
|
+
EXIT_OK
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def notes_for(name)
|
|
173
|
+
blueprint = File.join(@options[:promote], "blueprint.json")
|
|
174
|
+
finding = File.join(@options[:promote], "finding.json")
|
|
175
|
+
recorded = File.exist?(blueprint) ? JSON.parse(File.read(blueprint)) : nil
|
|
176
|
+
hypothesis = recorded&.dig("hypothesis")
|
|
177
|
+
<<~NOTES
|
|
178
|
+
# #{name}
|
|
179
|
+
|
|
180
|
+
Promoted from a `hecks quality_control check_generated_domains` finding — #{recorded&.key?('source') ? 'written by `hecks quality_control mine_combinations`\' agent' : 'generated'}, not
|
|
181
|
+
hand-written. The bluebook is the minimal form of the domain that
|
|
182
|
+
surprised; `QaGenerated` was renamed to `#{camelize(name)}` and nothing
|
|
183
|
+
else changed.
|
|
184
|
+
#{"\n## Hypothesis\n\n#{hypothesis.strip}\n" if hypothesis}
|
|
185
|
+
## Blueprint
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
#{recorded ? JSON.pretty_generate(recorded.except('source', 'hypothesis')) : '(not recorded)'}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Finding
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
#{File.exist?(finding) ? File.read(finding).strip : '(not recorded)'}
|
|
195
|
+
```
|
|
196
|
+
NOTES
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def dial(name, fallback)
|
|
200
|
+
return fallback unless defined?(::QualityControlDials) && ::QualityControlDials.const_defined?(name)
|
|
201
|
+
|
|
202
|
+
::QualityControlDials.const_get(name)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# `--from-dials` is how `hecks quality_control tick` runs this. The dials come from the text
|
|
206
|
+
# of the bluebook before `Hecks.bluebook`, so the ledger's Postgres is not needed.
|
|
207
|
+
# `QA_GENERATED_DOMAINS_PER_TICK` overrides the count for one run.
|
|
208
|
+
#
|
|
209
|
+
# @return [Boolean] false when the dials turn the step off, true once they are applied
|
|
210
|
+
def dials_on?
|
|
211
|
+
dials_path = File.join(@root, "lib/hecks/quality_control/quality_control.bluebook")
|
|
212
|
+
eval(File.read(dials_path).split(/^Hecks\.bluebook/).first, TOPLEVEL_BINDING, dials_path)
|
|
213
|
+
# rubocop:enable Security/Eval
|
|
214
|
+
per_tick = Integer(@env.fetch("QA_GENERATED_DOMAINS_PER_TICK", dial(:GENERATED_DOMAINS_PER_TICK, 0)))
|
|
215
|
+
if per_tick.zero?
|
|
216
|
+
puts "generated domains: off (QualityControlDials::GENERATED_DOMAINS_PER_TICK is 0)"
|
|
217
|
+
return false
|
|
218
|
+
end
|
|
219
|
+
@options.merge!(domains: per_tick, rust: dial(:GENERATED_DOMAINS_RUST, false),
|
|
220
|
+
seeds: dial(:GENERATED_DOMAIN_SEEDS, 5), adversarial: dial(:ADVERSARIAL_FRACTION, 0.3).to_f)
|
|
221
|
+
true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def child_check(dir, binary: nil, match: nil, shrink_budget: 0)
|
|
225
|
+
args = ["--check", dir, "--seeds", @options[:seeds].to_s, "--steps", @options[:steps].to_s,
|
|
226
|
+
"--adversarial", @options[:adversarial].to_s, "--shrink-budget", shrink_budget.to_s]
|
|
227
|
+
args += ["--binary", binary] if binary
|
|
228
|
+
args += ["--match", JSON.generate(match)] if match
|
|
229
|
+
output, status = Open3.capture2e(*Child.argv(@root, "qa_generated_domains", *args), chdir: @root)
|
|
230
|
+
line = output.lines.reverse.find { |candidate| candidate.start_with?(RESULT_MARKER) }
|
|
231
|
+
return JSON.parse(line.delete_prefix(RESULT_MARKER)) if line
|
|
232
|
+
|
|
233
|
+
{ "status" => "error",
|
|
234
|
+
"error" => "child exited #{status.exitstatus} with no result: #{output.lines.last(5).join.strip}" }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def build_rust(dir)
|
|
238
|
+
project = Hecks::RustBuild.capture("project_rust", [File.expand_path(dir, @root)],
|
|
239
|
+
env: { "HECKS_RUST_DIR" => @scratch })
|
|
240
|
+
unless project.ok?
|
|
241
|
+
return [nil, { "mode" => "rust_projection", "detail" => "#{project.out}#{project.err}".lines.last(12).join }]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
cargo, status = Open3.capture2e("cargo", "build", "--no-default-features", "--features",
|
|
245
|
+
Generator::DIRECTORY, chdir: @scratch)
|
|
246
|
+
return [nil, { "mode" => "rust_build", "detail" => cargo.lines.grep(/error/).first(12).join }] unless status.success?
|
|
247
|
+
|
|
248
|
+
binary = File.join(@scratch, "target/debug/rust-#{Generator::DIRECTORY}")
|
|
249
|
+
FileUtils.cp(File.join(@scratch, "target/debug/rust"), binary)
|
|
250
|
+
[binary, nil]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def sync_scratch!
|
|
254
|
+
FileUtils.mkdir_p(@scratch)
|
|
255
|
+
sources = %w[Cargo.toml Cargo.lock src].map { |entry| File.join(@root, "rust", entry) }
|
|
256
|
+
system("rsync", "-a", "--delete", "--exclude", "target", *sources, "#{@scratch}/") or
|
|
257
|
+
abort "rsync into #{@scratch} failed"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Builds one candidate (when --rust) and runs a child check. A build failure is its own result
|
|
261
|
+
# shape, so a rust_build finding shrinks by "does it still fail to build".
|
|
262
|
+
def evaluate(blueprint, dir_root, match: nil, shrink_budget: 0)
|
|
263
|
+
dir = Generator.write(blueprint, dir_root)
|
|
264
|
+
binary = nil
|
|
265
|
+
if @options[:rust]
|
|
266
|
+
binary, failure = build_rust(dir)
|
|
267
|
+
if failure
|
|
268
|
+
return { "status" => "clean" } unless match.nil? || match["mode"] == failure["mode"]
|
|
269
|
+
|
|
270
|
+
return { "status" => "found", "mode" => failure["mode"], "signature" => [failure["mode"]], "dir" => dir,
|
|
271
|
+
"divergences" => [{ "field" => failure["mode"], "detail" => failure["detail"] }], "steps" => [] }
|
|
272
|
+
end
|
|
273
|
+
return { "status" => "clean" } if match && match["mode"].start_with?("rust_")
|
|
274
|
+
end
|
|
275
|
+
child_check(dir, binary: binary, match: match, shrink_budget: shrink_budget)
|
|
276
|
+
.merge("binary" => binary, "dir" => dir)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def shrink_domain(blueprint, found, root)
|
|
280
|
+
match = { "mode" => found["mode"], "signature" => found["signature"] }
|
|
281
|
+
current = blueprint
|
|
282
|
+
attempts = 0
|
|
283
|
+
loop do
|
|
284
|
+
accepted = false
|
|
285
|
+
Generator.shrink_candidates(current).each do |candidate|
|
|
286
|
+
break if attempts >= @options[:domain_shrink_budget]
|
|
287
|
+
|
|
288
|
+
attempts += 1
|
|
289
|
+
next unless evaluate(candidate, File.join(root, "candidate"), match: match)["status"] == "found"
|
|
290
|
+
|
|
291
|
+
current = candidate
|
|
292
|
+
accepted = true
|
|
293
|
+
break
|
|
294
|
+
end
|
|
295
|
+
break unless accepted && attempts < @options[:domain_shrink_budget]
|
|
296
|
+
end
|
|
297
|
+
[current, attempts]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def relative(path) = path.delete_prefix("#{@root}/")
|
|
301
|
+
|
|
302
|
+
def print_finding_header(report)
|
|
303
|
+
puts
|
|
304
|
+
puts "=" * 72
|
|
305
|
+
puts "GENERATED DOMAIN FOUND SOMETHING — domain seed #{report[:seed]}, forms #{report[:forms].join(' + ')}"
|
|
306
|
+
puts "=" * 72
|
|
307
|
+
puts "domain: #{relative(report[:dir])}"
|
|
308
|
+
puts " shrunk from #{report[:size_before]} to #{report[:size_after]} removable element(s) " \
|
|
309
|
+
"in #{report[:domain_attempts]} candidate check(s)"
|
|
310
|
+
puts "blueprint: #{relative(File.join(report[:root], 'blueprint.json'))}"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def print_finding(report)
|
|
314
|
+
found = report[:final]
|
|
315
|
+
options = report[:options]
|
|
316
|
+
print_finding_header(report)
|
|
317
|
+
puts "mode: #{found['mode']}"
|
|
318
|
+
puts "signature: #{found['signature'].join(', ')}"
|
|
319
|
+
if found["seed"]
|
|
320
|
+
puts "sequence: seed #{found['seed']} of --seeds #{options[:seeds]}, #{options[:steps]} steps, " \
|
|
321
|
+
"adversarial #{options[:adversarial]}"
|
|
322
|
+
end
|
|
323
|
+
print_shrunk(report, found["steps"], found["shrunk_steps"])
|
|
324
|
+
puts "promote: hecks quality_control check_generated_domains --promote " \
|
|
325
|
+
"#{relative(report[:root])} --name <stress_domain_name>"
|
|
326
|
+
puts
|
|
327
|
+
found["divergences"].each do |divergence|
|
|
328
|
+
puts "-- #{divergence['field']} --"
|
|
329
|
+
divergence.except("field").each do |key, value|
|
|
330
|
+
puts "#{key}: #{value.is_a?(String) ? value : JSON.generate(value)}"
|
|
331
|
+
end
|
|
332
|
+
puts
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def print_shrunk(report, steps, shrunk)
|
|
337
|
+
return unless shrunk
|
|
338
|
+
|
|
339
|
+
puts "shrunk: #{steps.size} -> #{shrunk.size} step(s) — #{relative(report[:steps_file])}"
|
|
340
|
+
replay = if report[:binary]
|
|
341
|
+
"hecks check_conformance #{report[:dir]} script=#{report[:steps_file]} " \
|
|
342
|
+
"artifact=#{report[:binary]}"
|
|
343
|
+
else
|
|
344
|
+
"hecks run #{report[:dir]} #{report[:steps_file]}"
|
|
345
|
+
end
|
|
346
|
+
puts "replay: #{replay}"
|
|
347
|
+
shrunk.each_with_index do |step, index|
|
|
348
|
+
kind = %w[verb query dry_run].find { |key| step.key?(key) }
|
|
349
|
+
puts " #{index}: #{"#{kind} " unless kind == 'verb'}#{step[kind]} args: #{JSON.generate(step['args'])}"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def record_finding(blueprint, result, root)
|
|
354
|
+
minimal, attempts = shrink_domain(blueprint, result, root)
|
|
355
|
+
final_root = File.join(root, "minimal")
|
|
356
|
+
match = { "mode" => result["mode"], "signature" => result["signature"] }
|
|
357
|
+
final = evaluate(minimal, final_root, match: match, shrink_budget: @options[:shrink_budget])
|
|
358
|
+
unless final["status"] == "found"
|
|
359
|
+
minimal = blueprint
|
|
360
|
+
final = evaluate(blueprint, final_root, shrink_budget: @options[:shrink_budget])
|
|
361
|
+
end
|
|
362
|
+
File.write(File.join(final_root, "finding.json"), JSON.pretty_generate(final.except("dir", "binary")))
|
|
363
|
+
steps_file = File.join(final_root, "shrunk_steps.json")
|
|
364
|
+
note = "hecks quality_control check_generated_domains finding"
|
|
365
|
+
File.write(steps_file, JSON.pretty_generate(name: "qa_generated-shrunk", note: note,
|
|
366
|
+
steps: final.fetch("shrunk_steps") { final.fetch("steps", []) }))
|
|
367
|
+
{ forms: blueprint["forms"], root: final_root, dir: final["dir"], binary: final["binary"], final: final,
|
|
368
|
+
options: @options, steps_file: steps_file, domain_attempts: attempts,
|
|
369
|
+
size_before: Generator.removals(blueprint).size, size_after: Generator.removals(minimal).size }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def blueprints(start)
|
|
373
|
+
if @options[:blueprint]
|
|
374
|
+
saved = JSON.parse(File.read(@options[:blueprint]))
|
|
375
|
+
{ saved.fetch("seed", start) => saved }
|
|
376
|
+
elsif @options[:sources].any?
|
|
377
|
+
@options[:sources].each_with_index.to_h { |file, index| [start + index, source_blueprint(file, start + index)] }
|
|
378
|
+
else
|
|
379
|
+
(start...(start + @options[:domains])).to_h do |seed|
|
|
380
|
+
[seed, Generator.generate(seed: seed, forms: @options[:forms])]
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def source_blueprint(file, seed)
|
|
386
|
+
abort "no bluebook at #{file}" unless File.file?(file)
|
|
387
|
+
|
|
388
|
+
hypothesis = File.join(File.dirname(file), "HYPOTHESIS.md")
|
|
389
|
+
{ "seed" => seed, "forms" => ["source:#{File.basename(File.dirname(file))}"],
|
|
390
|
+
"origin" => File.expand_path(file), "source" => File.read(file),
|
|
391
|
+
"hypothesis" => (File.read(hypothesis) if File.exist?(hypothesis)),
|
|
392
|
+
"aggregates" => [], "policies" => [] }.compact
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def run
|
|
396
|
+
start = @options[:start] || (Time.now.to_i % 1_000_000)
|
|
397
|
+
# `-<pid>`: two runs started in the same second must not share domain directories.
|
|
398
|
+
run_dir = File.join(@root, "tmp/qa-generated/run-#{start}-#{Process.pid}")
|
|
399
|
+
@scratch = File.join(@root, "tmp/qa-generated/rust")
|
|
400
|
+
@options[:domains] = @options[:sources].size if @options[:sources].any?
|
|
401
|
+
sync_scratch! if @options[:rust]
|
|
402
|
+
puts "generated domains: #{@options[:domains]} starting at seed #{start}; #{@options[:seeds]} seed(s) x " \
|
|
403
|
+
"#{@options[:steps]} steps each, adversarial #{@options[:adversarial]}" \
|
|
404
|
+
"#{', against Rust' if @options[:rust]} — #{relative(run_dir)}"
|
|
405
|
+
|
|
406
|
+
counts = Hash.new(0)
|
|
407
|
+
findings = []
|
|
408
|
+
# `--blueprint FILE` re-checks one saved blueprint through the same check/shrink/report
|
|
409
|
+
# path.
|
|
410
|
+
blueprints(start).each do |domain_seed, blueprint|
|
|
411
|
+
check_blueprint(domain_seed, blueprint, run_dir, counts, findings)
|
|
412
|
+
end
|
|
413
|
+
report(counts, findings)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def check_blueprint(domain_seed, blueprint, run_dir, counts, findings)
|
|
417
|
+
root = File.join(run_dir, domain_seed.to_s)
|
|
418
|
+
result = evaluate(blueprint, root)
|
|
419
|
+
label = "domain #{domain_seed} [#{blueprint['forms'].join('+')}]"
|
|
420
|
+
label += " #{blueprint['aggregates'].size} aggregate(s)" unless blueprint["source"]
|
|
421
|
+
|
|
422
|
+
case result["status"]
|
|
423
|
+
when "clean"
|
|
424
|
+
counts[:clean] += 1
|
|
425
|
+
puts " #{label}: clean (#{result['seeds_run']} seed(s))"
|
|
426
|
+
when "invalid", "error"
|
|
427
|
+
counts[result["status"].to_sym] += 1
|
|
428
|
+
puts " #{label}: #{result['status'].upcase} — #{result['error']}"
|
|
429
|
+
else
|
|
430
|
+
counts[:found] += 1
|
|
431
|
+
puts " #{label}: FOUND SOMETHING (#{result['mode']}: #{result['signature'].join(', ')}) — " \
|
|
432
|
+
"shrinking the domain…"
|
|
433
|
+
findings << record_finding(blueprint, result, root).merge(seed: domain_seed)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def report(counts, findings)
|
|
438
|
+
puts
|
|
439
|
+
puts "generated domains: #{counts[:clean]} clean, #{counts[:found]} found something, " \
|
|
440
|
+
"#{counts[:invalid]} invalid, #{counts[:error]} errored"
|
|
441
|
+
findings.each { |finding| print_finding(finding) }
|
|
442
|
+
|
|
443
|
+
if findings.any? then EXIT_FOUND
|
|
444
|
+
elsif counts[:clean].zero? then EXIT_ERROR
|
|
445
|
+
else EXIT_OK
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "../../../hecks"
|
|
5
|
+
# The era subsystem does not load with core; the `PostgresEra` binding needs it (ADR 0033).
|
|
6
|
+
require_relative "../../ports/persistence/plugins/era"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module QualityControlCli
|
|
10
|
+
# The command behind `hecks quality_control log`: logs a bug to the QualityControl ledger,
|
|
11
|
+
# proven, numbered and triaged. It runs `--demonstration` first and refuses unless it exits
|
|
12
|
+
# non-zero.
|
|
13
|
+
#
|
|
14
|
+
# hecks quality_control log --sweep <sweep-id> --title "…" \
|
|
15
|
+
# --demonstration "bundle exec rspec spec/x_spec.rb -e 'the case'" \
|
|
16
|
+
# --symptom "…" --expectation "…" --submitter "<who>" \
|
|
17
|
+
# --triage self_contained|bigger [--tag word]… [--reproduced yes|no]
|
|
18
|
+
#
|
|
19
|
+
# `--reproduced no` skips the run for findings that will not reproduce reliably; the
|
|
20
|
+
# demonstration must still look like runnable code, not prose. Exit 0: logged, with
|
|
21
|
+
# `logged BUG#n …` as the last stdout line. Exit 1: refused, nothing written.
|
|
22
|
+
class QaLogBug
|
|
23
|
+
EXIT_OK = 0
|
|
24
|
+
|
|
25
|
+
USAGE = "usage: hecks quality_control log --sweep <sweep-id> --title \"…\" " \
|
|
26
|
+
"--demonstration \"<command that must FAIL>\" --symptom \"…\" --expectation \"…\" " \
|
|
27
|
+
"--submitter \"<who>\" --triage self_contained|bigger [--tag word]… [--reproduced yes|no]"
|
|
28
|
+
|
|
29
|
+
DISPOSITIONS = %w[self_contained bigger].freeze
|
|
30
|
+
REPRODUCED = %w[yes no].freeze
|
|
31
|
+
VALUE_FLAGS = %w[--sweep --title --demonstration --symptom --expectation --submitter --triage
|
|
32
|
+
--reproduced].freeze
|
|
33
|
+
|
|
34
|
+
# Logs the bug.
|
|
35
|
+
#
|
|
36
|
+
# @param argv [Array<String>] the flags in the usage line
|
|
37
|
+
# @param root [String] the repository root, where the demonstration runs
|
|
38
|
+
# @param env [Hash{String => String}] `QA_SWEEP_DOMAIN_DIR` points at another ledger
|
|
39
|
+
# @return [Integer] 0 once logged
|
|
40
|
+
# @raise [SystemExit] when the arguments are wrong or a rule refuses; nothing is written
|
|
41
|
+
def self.call(argv, root:, env: ENV)
|
|
42
|
+
new(root: root, env: env).call(argv)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param root [String] the repository root
|
|
46
|
+
# @param env [Hash{String => String}] `QA_SWEEP_DOMAIN_DIR` points at another ledger
|
|
47
|
+
def initialize(root:, env: ENV)
|
|
48
|
+
@root = root
|
|
49
|
+
@domain_dir = env.fetch("QA_SWEEP_DOMAIN_DIR", File.join(root, "qa/bluebook"))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param argv [Array<String>] the flags in the usage line
|
|
53
|
+
# @return [Integer] the exit status
|
|
54
|
+
# @raise [SystemExit] when the arguments are wrong or a rule refuses; nothing is written
|
|
55
|
+
def call(argv)
|
|
56
|
+
options = parse(argv.dup)
|
|
57
|
+
return EXIT_OK if options == :help
|
|
58
|
+
|
|
59
|
+
validate(options)
|
|
60
|
+
prove(options)
|
|
61
|
+
puts
|
|
62
|
+
runtime = boot_ledger
|
|
63
|
+
sweep = ::QualityControl::Sweep.find(options[:sweep])
|
|
64
|
+
unless sweep
|
|
65
|
+
abort "no such sweep: #{options[:sweep].inspect} — the sweep id is on hecks quality_control ask run's own " \
|
|
66
|
+
"FOUND SOMETHING report"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
bug = mint_and_log!(runtime, sweep, options).triage!(disposition: { value: options[:triage] })
|
|
70
|
+
puts "logged #{bug.id} (sequence #{bug.sequence.value}, #{bug.disposition.value}, " \
|
|
71
|
+
"reproduced=#{bug.reproduced.value}) against #{sweep.id}: #{options[:title]}"
|
|
72
|
+
EXIT_OK
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def parse(argv)
|
|
78
|
+
options = { tags: [], reproduced: "yes" }
|
|
79
|
+
until argv.empty?
|
|
80
|
+
arg = argv.shift
|
|
81
|
+
case arg
|
|
82
|
+
when "-h", "--help"
|
|
83
|
+
puts USAGE
|
|
84
|
+
return :help
|
|
85
|
+
when *VALUE_FLAGS
|
|
86
|
+
value = argv.shift
|
|
87
|
+
abort "#{USAGE}\n#{arg} needs a value" if value.nil? || value.empty?
|
|
88
|
+
options[arg.delete_prefix("--").to_sym] = value
|
|
89
|
+
when "--tag"
|
|
90
|
+
value = argv.shift
|
|
91
|
+
abort "#{USAGE}\n--tag needs a word" if value.nil? || value.empty?
|
|
92
|
+
options[:tags] << value
|
|
93
|
+
else
|
|
94
|
+
abort "#{USAGE}\nunexpected argument: #{arg.inspect}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
options
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def validate(options)
|
|
101
|
+
%i[sweep title demonstration symptom expectation submitter triage].each do |key|
|
|
102
|
+
abort "#{USAGE}\n--#{key} is required" unless options[key]
|
|
103
|
+
end
|
|
104
|
+
unless DISPOSITIONS.include?(options[:triage])
|
|
105
|
+
abort "#{USAGE}\n--triage must be one of #{DISPOSITIONS.join('|')}, got #{options[:triage].inspect}"
|
|
106
|
+
end
|
|
107
|
+
return if REPRODUCED.include?(options[:reproduced])
|
|
108
|
+
|
|
109
|
+
abort "#{USAGE}\n--reproduced must be one of #{REPRODUCED.join('|')}, got #{options[:reproduced].inspect}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Only used with `--reproduced no`, which never runs the string: a file path, an interpreter
|
|
113
|
+
# or shell invocation, or a `Hecks::Fuzzing::Replay` snippet counts as code; prose does not.
|
|
114
|
+
def looks_runnable?(str)
|
|
115
|
+
first = str.to_s.strip.split(/\s+/).first.to_s
|
|
116
|
+
return true if first.start_with?("/") && File.exist?(first)
|
|
117
|
+
return true if File.exist?(File.expand_path(first, @root))
|
|
118
|
+
return true if first.match?(%r{\A(bundle|bin/|\./|rspec|ruby|sh|bash|zsh|python3?|node|cargo)\b})
|
|
119
|
+
return true if str.match?(/\.(rb|sh|py|rs|js)\b/)
|
|
120
|
+
return true if str.include?("Hecks::Fuzzing::Replay")
|
|
121
|
+
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Runs through `sh -c` from the repository root; the output tail prints either way.
|
|
126
|
+
def prove(options)
|
|
127
|
+
if options[:reproduced] == "no"
|
|
128
|
+
unless looks_runnable?(options[:demonstration])
|
|
129
|
+
abort "#{USAGE}\nrefused: --demonstration doesn't look like a runnable script or command — " \
|
|
130
|
+
"#{options[:demonstration].inspect}\n" \
|
|
131
|
+
"with --reproduced no there is no pass/fail signal to check, but the demonstration must " \
|
|
132
|
+
"still be real, saved, runnable code (a file path or a shell/ruby invocation), never " \
|
|
133
|
+
"prose describing what happened. Nothing was logged."
|
|
134
|
+
end
|
|
135
|
+
puts "reproduced=no — skipping the must-fail check (no reliable pass/fail signal for this finding)"
|
|
136
|
+
puts "demonstration (best-effort, not run): #{options[:demonstration]}"
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
out, status = Open3.capture2e("sh", "-c", options[:demonstration], chdir: @root)
|
|
141
|
+
tail = out.lines.last(15).join
|
|
142
|
+
if status.success?
|
|
143
|
+
puts tail
|
|
144
|
+
abort "refused: the demonstration PASSED (exit 0) — #{options[:demonstration].inspect}\n" \
|
|
145
|
+
"a bug is logged with the failing test that proves it; a passing command proves nothing. " \
|
|
146
|
+
"Nothing was logged."
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
puts "demonstration failed as required (exit #{status.exitstatus.inspect}):"
|
|
150
|
+
puts tail
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def boot_ledger
|
|
154
|
+
Hecks.boot(@domain_dir)
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
abort "the QualityControl ledger did not boot — fix the ledger itself before logging against it " \
|
|
157
|
+
"(#{e.class}: #{e.message})"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Re-read on every attempt: concurrent minters see the same maximum, and the loser's `Log` is
|
|
161
|
+
# refused (`AlreadyExists`) so `mint_and_log!` retries from a fresh read.
|
|
162
|
+
def next_reference(runtime)
|
|
163
|
+
rows = runtime.query("QualityControl::Bug.All")
|
|
164
|
+
taken = rows.map { |row| row[:reference][:value] }
|
|
165
|
+
sequence = rows.map { |row| row[:sequence][:value] }.max.to_i + 1
|
|
166
|
+
sequence += 1 while taken.include?("BUG##{sequence}")
|
|
167
|
+
[sequence, "BUG##{sequence}"]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Retries up to three times: the reference can be taken between reading and logging.
|
|
171
|
+
def mint_and_log!(runtime, sweep, options)
|
|
172
|
+
3.times do
|
|
173
|
+
sequence, reference = next_reference(runtime)
|
|
174
|
+
begin
|
|
175
|
+
return log(sweep, options, sequence, reference)
|
|
176
|
+
rescue Hecks::Runtime::AlreadyExists
|
|
177
|
+
puts "#{reference} was taken between reading Bug.All and logging — minting again"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
abort "could not mint a free BUG# reference in three attempts — another minter is racing this one; " \
|
|
181
|
+
"run again"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def log(sweep, options, sequence, reference)
|
|
185
|
+
tags = options[:tags].empty? ? {} : { tags: options[:tags].map { |tag| { value: tag } } }
|
|
186
|
+
::QualityControl::Bug.log!(
|
|
187
|
+
sweep: sweep.id, reference: { value: reference }, sequence: { value: sequence },
|
|
188
|
+
title: { value: options[:title] }, demonstration: { value: options[:demonstration] },
|
|
189
|
+
symptom: { value: options[:symptom] }, expectation: { value: options[:expectation] },
|
|
190
|
+
submitter: { value: options[:submitter] }, reproduced: { value: options[:reproduced] }, **tags
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|