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,2224 @@
|
|
|
1
|
+
# Quality control: every check writes its expectation down before it looks at the answer.
|
|
2
|
+
# A bug is logged with the failing test that proves it, or it is not logged.
|
|
3
|
+
|
|
4
|
+
# Tunable dials for the QA loop: values in qa/settings.yml, reasoning here.
|
|
5
|
+
# `require`, not `require_relative`: isolated fuzz boots load a copy of this file from a tmpdir.
|
|
6
|
+
require "hecks/fuzzing/qa_settings"
|
|
7
|
+
|
|
8
|
+
unless defined?(QualityControlDials)
|
|
9
|
+
module QualityControlDials
|
|
10
|
+
# `DEFAULT_PATH` resolves from QaSettings' own location: this directory is copied for isolated
|
|
11
|
+
# boots. HECKS_QA_SETTINGS_PATH lets a subprocess test read a different settings file.
|
|
12
|
+
settings = Hecks::Fuzzing::QaSettings.load(ENV.fetch("HECKS_QA_SETTINGS_PATH", Hecks::Fuzzing::QaSettings::DEFAULT_PATH))
|
|
13
|
+
|
|
14
|
+
# Minimum gap in seconds between one tick's report and the next dispatch; 0 is immediately.
|
|
15
|
+
# Read by the `/loop hecks_qa` caller, not by any script.
|
|
16
|
+
CADENCE_SECONDS = settings.cadence_seconds
|
|
17
|
+
|
|
18
|
+
# Most PRs (Patch plus Improvement) `hecks quality_control patch.open` opens per calendar day; 0 is uncapped.
|
|
19
|
+
PR_CAP_PER_DAY = settings.pr_cap_per_day
|
|
20
|
+
|
|
21
|
+
# Fuzz depth by `Target.clean_streak`, in three discrete tiers, read by
|
|
22
|
+
# `SweepDepth.for_streak`. The last row (`upto: Float::INFINITY`) is the ceiling.
|
|
23
|
+
WIDENING_TIERS = settings.widening_tiers
|
|
24
|
+
|
|
25
|
+
# How many `hecks quality_control ask run <target>` children `--all` keeps running at once.
|
|
26
|
+
SWEEP_MAX_PARALLEL = settings.sweep_max_parallel
|
|
27
|
+
|
|
28
|
+
# How long the `/loop hecks_qa` caller waits for a tick's report before assuming it stalled.
|
|
29
|
+
LIVENESS_FALLBACK_SECONDS = settings.liveness_fallback_seconds
|
|
30
|
+
|
|
31
|
+
# The identity the loop runs as. `WaivedBy` repeats this literal (invariants read literals,
|
|
32
|
+
# not constants); `spec/quality_control_spec.rb` pins the two equal.
|
|
33
|
+
AUTOMATED_ENGINEER = "qa_sweep"
|
|
34
|
+
|
|
35
|
+
# How `hecks quality_control patch.open` opens a PR: not a draft, queued for auto-merge, on a branch that starts
|
|
36
|
+
# with BRANCH_PREFIX.
|
|
37
|
+
DRAFT_ONLY = settings.draft_only
|
|
38
|
+
AUTO_MERGE = settings.auto_merge
|
|
39
|
+
BRANCH_PREFIX = settings.branch_prefix
|
|
40
|
+
|
|
41
|
+
# Fraction (0.0..1.0) of generated command steps mutated adversarially; 0 turns the layer off.
|
|
42
|
+
# Higher finds refusal-ordering splits sooner but reaches deep state less often.
|
|
43
|
+
ADVERSARIAL_FRACTION = settings.adversarial_fraction
|
|
44
|
+
|
|
45
|
+
# Coverage-guided generation across one sweep's seeds (`Hecks::Fuzzing::CoverageCampaign`).
|
|
46
|
+
GUIDED_GENERATION = settings.guided_generation
|
|
47
|
+
|
|
48
|
+
# Fraction (0.0..1.0) of seeds that start from a prefix of an earlier seed's new coverage.
|
|
49
|
+
# Off by default: measured, it trades breadth for depth (banking reached 1.41x the deep-state
|
|
50
|
+
# tuples, chess lost 0.12x of all tuples), while favoring rare verbs alone never lost.
|
|
51
|
+
CORPUS_SPLICE_PROBABILITY = settings.corpus_splice_probability
|
|
52
|
+
|
|
53
|
+
# How many of the least-hit verbs the next seed weights up; 0 turns the lever off.
|
|
54
|
+
FAVOR_RARE_VERBS = settings.favor_rare_verbs
|
|
55
|
+
|
|
56
|
+
# Whether `hecks quality_control ask run` also runs the self-consistency pass (cold rehydration, replay
|
|
57
|
+
# idempotency, value-object JSON round trip) on the same generated sequence.
|
|
58
|
+
SELF_CONSISTENCY_CHECKS = settings.self_consistency_checks
|
|
59
|
+
|
|
60
|
+
# Candidate replays `hecks quality_control ask run` may spend shrinking one surprised check; 0 turns shrinking
|
|
61
|
+
# off. `concurrency` is never shrunk: a race does not reproduce on demand.
|
|
62
|
+
SHRINK_BUDGET = settings.shrink_budget
|
|
63
|
+
|
|
64
|
+
# Seconds of staleness that one point of yield score is worth in `RotationPriority.pick`.
|
|
65
|
+
# Zero would make yield invisible, so pick a number that lets it matter.
|
|
66
|
+
YIELD_WEIGHT_SECONDS = settings.yield_weight_seconds
|
|
67
|
+
|
|
68
|
+
# Percent of the prior yield score kept at each release, so recent findings dominate.
|
|
69
|
+
# Applied by the caller: the mutation language cannot express "decay, then add" in `Release`.
|
|
70
|
+
YIELD_DECAY_PERCENT = settings.yield_decay_percent
|
|
71
|
+
|
|
72
|
+
# Once a waiting target is this stale, `RotationPriority.pick` takes it whatever its yield
|
|
73
|
+
# score, so a quiet target cannot starve.
|
|
74
|
+
ROTATION_STALE_FLOOR_SECONDS = settings.rotation_stale_floor_seconds
|
|
75
|
+
|
|
76
|
+
# Ceiling on `--seeds` for `--persistence-parity`, which pays for real Postgres per dispatch.
|
|
77
|
+
# `hecks quality_control ask run` clamps down, never up, and prints a note.
|
|
78
|
+
PERSISTENCE_PARITY_SEED_CAP = settings.persistence_parity_seed_cap
|
|
79
|
+
|
|
80
|
+
# Ceiling on `--seeds` for the concurrency mode, which needs two disposable schemas per seed.
|
|
81
|
+
CONCURRENCY_SEED_CAP = settings.concurrency_seed_cap
|
|
82
|
+
|
|
83
|
+
# The two adapters each adapter-parity mode compares, by mode name; a new pairing is a new
|
|
84
|
+
# entry. `adapter_parity_sqlite` needs no `database:`/`schema:` because sqlite has no
|
|
85
|
+
# disposable database.
|
|
86
|
+
ADAPTER_PARITY_PAIRS = settings.adapter_parity_pairs
|
|
87
|
+
|
|
88
|
+
# Which comparison modes `hecks quality_control ask run` may run: the enabled ones intersected with what
|
|
89
|
+
# `TargetCapabilities.infer(path)` finds in the target. `--modes a,b,c` overrides for one run.
|
|
90
|
+
#
|
|
91
|
+
# `wasm_front` and `adapter_parity_postgres` are off because nothing runs them; the others are
|
|
92
|
+
# built and off because their I/O cost across the rotation is unmeasured. `hecks quality_control ask run`
|
|
93
|
+
# refuses to start when a mode outside `TargetCapabilities::RUNNABLE_MODES` is enabled.
|
|
94
|
+
MODES = settings.modes
|
|
95
|
+
|
|
96
|
+
# How often a generated role-gated command step carries a drawn caller (`role:`/`actor_id:`);
|
|
97
|
+
# 0 turns the draw off.
|
|
98
|
+
ROLE_DRAW_PROBABILITY = settings.role_draw_probability
|
|
99
|
+
|
|
100
|
+
# Fraction of generated command steps offered as a dry run instead of a real dispatch; 0 is
|
|
101
|
+
# off. Kept low: a dry run creates no state for later steps to act on.
|
|
102
|
+
DRY_RUN_FRACTION = settings.dry_run_fraction
|
|
103
|
+
|
|
104
|
+
# How many generated domains one `hecks quality_control tick` checks after the rotation sweep; 0 skips the
|
|
105
|
+
# step.
|
|
106
|
+
GENERATED_DOMAINS_PER_TICK = settings.generated_domains_per_tick
|
|
107
|
+
|
|
108
|
+
# Whether generated domains are also compared against a compiled Rust binary built in a scratch
|
|
109
|
+
# copy.
|
|
110
|
+
GENERATED_DOMAINS_RUST = settings.generated_domains_rust
|
|
111
|
+
|
|
112
|
+
# Generated sequences run per generated domain.
|
|
113
|
+
GENERATED_DOMAIN_SEEDS = settings.generated_domain_seeds
|
|
114
|
+
|
|
115
|
+
# Construct families a skipped query verb may belong to. A skip inside the list is logged Held,
|
|
116
|
+
# one outside it Surprised. Shrink-only: spec/fuzzing/rust_gap_manifest_spec.rb fails when a
|
|
117
|
+
# family here is declared by no committed manifest.
|
|
118
|
+
STRUCTURAL_REFUSAL_BOUNDARY = settings.structural_refusal_boundary
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
Hecks.bluebook "QualityControl" do
|
|
123
|
+
vision "A user should never be made to feel stupid by something that took what they said and quietly did otherwise. Every check writes down what it expected before it looks, every bug carries the test that proves it, and nothing reaches the outside world that nobody first tried to fix."
|
|
124
|
+
|
|
125
|
+
core
|
|
126
|
+
|
|
127
|
+
# The rotation and its lock: `Claim` is a lifecycle edge, so when two agents race for a chapter
|
|
128
|
+
# one transition wins and the other is refused.
|
|
129
|
+
aggregate "Target" do
|
|
130
|
+
description "A chapter that can be pressure-tested, and whose turn it is — the rotation, and the claim that stops two agents doing the same work twice."
|
|
131
|
+
|
|
132
|
+
identified_by :reference
|
|
133
|
+
|
|
134
|
+
attribute :reference, TargetReference
|
|
135
|
+
attribute :path, TargetPath
|
|
136
|
+
attribute :held_by, Engineer
|
|
137
|
+
# A number, not a timestamp type: the predicate sublanguage compares only numbers.
|
|
138
|
+
attribute :claimed_at, Instant
|
|
139
|
+
attribute :window, StaleAfter
|
|
140
|
+
attribute :last_swept, SweptIn
|
|
141
|
+
# Recency-decayed count of how often this chapter was worth sweeping; see `YieldScore`.
|
|
142
|
+
attribute :yield_score, YieldScore
|
|
143
|
+
# Consecutive `Release`s that came back clean; `hecks quality_control ask run` reads it to choose sweep depth.
|
|
144
|
+
# Zero means no streak yet, shared on purpose by a never-swept target and a just-surprised one.
|
|
145
|
+
attribute :clean_streak, CleanStreak
|
|
146
|
+
# Comma-joined mode names recorded at release from what the runner inferred. Stored so
|
|
147
|
+
# `EligibleFor` can answer why a mode never ran; inference at sweep time still decides.
|
|
148
|
+
attribute :capabilities, Capabilities
|
|
149
|
+
attribute :reason, TargetReason
|
|
150
|
+
|
|
151
|
+
value_object "TargetReference" do
|
|
152
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
153
|
+
invariant("a target is referenced") { !value.to_s.empty? }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Required: a name can be disputed, a path can be opened.
|
|
157
|
+
value_object "TargetPath" do
|
|
158
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
159
|
+
invariant("a target says where it is") { !value.to_s.empty? }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
value_object "Engineer" do
|
|
163
|
+
attribute :value, String, default: "nobody"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
value_object "Instant" do
|
|
167
|
+
attribute :value, Integer, default: 0
|
|
168
|
+
invariant("an instant is not before the epoch") { !value.negative? }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A number, not a string: "never" sorts after "SW-9" and inverts the rotation. Zero means
|
|
172
|
+
# never.
|
|
173
|
+
value_object "SweptIn" do
|
|
174
|
+
attribute :value, Integer, default: 0
|
|
175
|
+
invariant("a sweep time is not before the epoch") { !value.negative? }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# A count, not a clock. `default: 0` keeps rows written before this field hydrating.
|
|
179
|
+
value_object "CleanStreak" do
|
|
180
|
+
attribute :value, Integer, default: 0
|
|
181
|
+
invariant("a streak is not negative") { !value.negative? }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
value_object "TargetReason" do
|
|
185
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
186
|
+
invariant("shelving a target says why") { !value.to_s.empty? }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Comma-joined in one string so `EligibleFor` can test it with `contains`.
|
|
190
|
+
# `default: ""` keeps rows written before this field booting.
|
|
191
|
+
value_object "Capabilities" do
|
|
192
|
+
attribute :value, String, default: ""
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# One mode name, as `EligibleFor`'s argument; a query attribute, never stored.
|
|
196
|
+
value_object "Capability" do
|
|
197
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
198
|
+
invariant("a mode is named") { !value.to_s.empty? }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Recency-weighted, not lifetime: `RotationPriority.next_yield_score` decays it at each
|
|
202
|
+
# `Release` by `QualityControlDials::YIELD_DECAY_PERCENT`.
|
|
203
|
+
value_object "YieldScore" do
|
|
204
|
+
attribute :value, Integer, default: 0
|
|
205
|
+
invariant("a yield score is not negative") { !value.negative? }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# `suspended` is "surprised, waiting for a person": stale-claim arithmetic never reaches it, so
|
|
209
|
+
# a chapter with an unresolved surprise is not re-fuzzed until a `Release` puts it back.
|
|
210
|
+
lifecycle :status, default: "waiting" do
|
|
211
|
+
transition "Claim" => "held", from: ["waiting", "held"]
|
|
212
|
+
transition "Release" => "waiting", from: ["held", "suspended"]
|
|
213
|
+
transition "Suspend" => "suspended", from: "held"
|
|
214
|
+
transition "Shelve" => "shelved", from: ["waiting", "held"]
|
|
215
|
+
transition "Restore" => "waiting", from: "shelved"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
command "Identify" do
|
|
219
|
+
role "QA engineer"
|
|
220
|
+
goal "Write down that a chapter exists which nobody has swept"
|
|
221
|
+
|
|
222
|
+
attribute :reference, TargetReference
|
|
223
|
+
attribute :path, TargetPath
|
|
224
|
+
|
|
225
|
+
emits "TargetIdentified"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# `from: ["waiting", "held"]` is deliberate: a held target is claimable once its claim is
|
|
229
|
+
# stale. The sublanguage has `+` and no `-`, so `claimed_at + window <= now` stands for `now -
|
|
230
|
+
# claimed_at >= window`. `now` arrives as an argument (Clock port) because a predicate cannot
|
|
231
|
+
# read the time.
|
|
232
|
+
command "Claim" do
|
|
233
|
+
role "QA engineer"
|
|
234
|
+
goal "Take the next chapter in the rotation, or one whose holder has gone quiet"
|
|
235
|
+
|
|
236
|
+
# The window lives on the record, not in an argument: a caller-supplied window would let any
|
|
237
|
+
# agent take a live claim by asking with a window of one second.
|
|
238
|
+
reference_to Target
|
|
239
|
+
attribute :held_by, Engineer
|
|
240
|
+
attribute :now, Instant
|
|
241
|
+
|
|
242
|
+
given("a live claim is not taken from the agent holding it") do
|
|
243
|
+
status != "held" || claimed_at.value + window.value <= now.value
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
sets :held_by
|
|
247
|
+
sets :claimed_at, to: :now
|
|
248
|
+
|
|
249
|
+
emits "TargetClaimed"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
value_object "StaleAfter" do
|
|
253
|
+
attribute :value, Integer, default: 900
|
|
254
|
+
invariant("a staleness window is positive") { value.positive? }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
command "Release" do
|
|
258
|
+
role "QA engineer"
|
|
259
|
+
goal "Hand a chapter back to the rotation, stamped with when, what this pass was worth, and how the clean streak moves"
|
|
260
|
+
|
|
261
|
+
# `now` is the argument and `last_swept` the field, so the door can fill the clock.
|
|
262
|
+
#
|
|
263
|
+
# `next_streak` is the caller's to compute (`clean_streak + 1` if every check held and none
|
|
264
|
+
# was ever surprised, else 0): `Target` cannot read `Sweep` state and no policy payload can
|
|
265
|
+
# carry it. Required, so an omitted streak can neither blank the counter nor guess at it.
|
|
266
|
+
reference_to Target
|
|
267
|
+
attribute :now, Instant
|
|
268
|
+
attribute :next_streak, CleanStreak
|
|
269
|
+
# Required: an in-process dispatch does not fill defaults for absent arguments, so an omitted
|
|
270
|
+
# list refuses rather than blanks. `capabilities.value=""` is an honest value.
|
|
271
|
+
attribute :capabilities, Capabilities
|
|
272
|
+
|
|
273
|
+
# Required, not defaulted to zero: a silent zero would erase what the pass found. The caller
|
|
274
|
+
# computes `RotationPriority.next_yield_score`; decay cannot be a second mutation here
|
|
275
|
+
# because build refuses a field written twice in one command.
|
|
276
|
+
attribute :yield_score, YieldScore
|
|
277
|
+
|
|
278
|
+
sets :last_swept, to: :now
|
|
279
|
+
sets :yield_score
|
|
280
|
+
sets :clean_streak, to: :next_streak
|
|
281
|
+
sets :capabilities
|
|
282
|
+
|
|
283
|
+
emits "TargetReleased"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Dispatched by `SuspendOnSurprise` as `System`; `reason` is the literal that policy supplies.
|
|
287
|
+
command "Suspend" do
|
|
288
|
+
role "System"
|
|
289
|
+
goal "Take a chapter out of the rotation because a check surprised, until a person releases it"
|
|
290
|
+
|
|
291
|
+
reference_to Target
|
|
292
|
+
attribute :reason, TargetReason
|
|
293
|
+
|
|
294
|
+
sets :reason
|
|
295
|
+
|
|
296
|
+
emits "TargetSuspended"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
command "Shelve" do
|
|
300
|
+
role "QA engineer"
|
|
301
|
+
goal "Decide a chapter is not worth the practice's time, and say why"
|
|
302
|
+
|
|
303
|
+
reference_to Target
|
|
304
|
+
attribute :reason, TargetReason
|
|
305
|
+
|
|
306
|
+
sets :reason
|
|
307
|
+
|
|
308
|
+
emits "TargetShelved"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# `reason` is required so the record's `reason` reads as the last decision in either direction.
|
|
312
|
+
command "Restore" do
|
|
313
|
+
role "QA engineer"
|
|
314
|
+
goal "Put a shelved chapter back in the rotation, and say what changed"
|
|
315
|
+
|
|
316
|
+
reference_to Target
|
|
317
|
+
attribute :reason, TargetReason
|
|
318
|
+
|
|
319
|
+
sets :reason
|
|
320
|
+
|
|
321
|
+
emits "TargetRestored"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Not a lifecycle move, and reachable whatever the chapter's status: a recorded path can go
|
|
325
|
+
# stale (a worktree removed, a project relocated) at any point in the rotation, and fixing it
|
|
326
|
+
# should not have to wait on whatever status a stale claim or a pending sweep happens to leave
|
|
327
|
+
# the chapter in.
|
|
328
|
+
command "Relocate" do
|
|
329
|
+
role "QA engineer"
|
|
330
|
+
goal "Correct where a chapter's own domain actually lives, once its recorded path stops resolving"
|
|
331
|
+
|
|
332
|
+
reference_to Target
|
|
333
|
+
attribute :path, TargetPath
|
|
334
|
+
|
|
335
|
+
sets :path
|
|
336
|
+
|
|
337
|
+
emits "TargetRelocated"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Ordered by `last_swept` alone: `order_by` sorts one stored field, and the yield-weighted
|
|
341
|
+
# blend needs `now`. `RotationPriority.pick` weights these rows in Ruby.
|
|
342
|
+
query "Rotation" do
|
|
343
|
+
description "Whose turn it is — waiting chapters, least recently swept first. The one reason this aggregate exists. Raw ordering only — see Hecks::Fuzzing::RotationPriority for the yield-weighted pick built on top of these same rows."
|
|
344
|
+
where(status: "waiting")
|
|
345
|
+
order_by :last_swept
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
query "Untouched" do
|
|
349
|
+
description "Never swept at all — zero is the epoch and means nobody has looked. The gap no count of checks can show you, because an unswept chapter leaves no rows anywhere."
|
|
350
|
+
where(status: "waiting")
|
|
351
|
+
where(:"last_swept.value" => 0)
|
|
352
|
+
order_by :reference
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
query "Held" do
|
|
356
|
+
description "Claimed right now, and by whom. Read it before wondering why the rotation looks short — a claim older than the window is free for the taking."
|
|
357
|
+
where(status: "held")
|
|
358
|
+
order_by :claimed_at
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
query "Shelved" do
|
|
362
|
+
description "Deliberately not swept, each with its reason. Re-read before anybody claims a coverage number."
|
|
363
|
+
where(status: "shelved")
|
|
364
|
+
order_by :reference
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
query "Suspended" do
|
|
368
|
+
description "Chapters a check surprised, waiting for a person — each with the reason the policy wrote. Nothing automatic touches these; hecks quality_control ask run <target> --release --notes is how one comes back."
|
|
369
|
+
where(status: "suspended")
|
|
370
|
+
order_by :reference
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Answers from the ledger which waiting chapters a mode could run against.
|
|
374
|
+
query "EligibleFor" do
|
|
375
|
+
description "Waiting chapters whose recorded capabilities include one mode — e.g. mode=postgres_era for the persistence-parity axis. The ledger's own answer to which chapters a mode can reach; inference at sweep time is still what decides."
|
|
376
|
+
attribute :mode, Capability
|
|
377
|
+
where(:"capabilities.value" => { contains: :mode })
|
|
378
|
+
where(status: "waiting")
|
|
379
|
+
order_by :last_swept
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
query "All" do
|
|
383
|
+
description "Every chapter ever written down — the inventory a runner walking the repository checks its findings against."
|
|
384
|
+
order_by :reference
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# The commands that enrol, generate and judge chapters. TargetTools answers each by running the
|
|
388
|
+
# script's own command as a child process (lib/hecks/quality_control/adapters/target_tools.rb) and
|
|
389
|
+
# handing back what it printed; none reads or writes this aggregate's records, so each is a query.
|
|
390
|
+
# `arguments` is the command's own flags as one string, such as `--domains 3 --rust`.
|
|
391
|
+
value_object "Words" do
|
|
392
|
+
attribute :value, String, pattern: '[^\n\r]'
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
value_object "DomainPath" do
|
|
396
|
+
attribute :value, String, pattern: '[^\t\n\r]'
|
|
397
|
+
|
|
398
|
+
invariant("a domain is named by its path") { !value.to_s.empty? }
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# An answer that is one document of text, exactly as the tool that produced it wrote it.
|
|
402
|
+
value_object "Document" do
|
|
403
|
+
attribute :text, String
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
query "Seed" do
|
|
407
|
+
description "Identify every corpus domain as a rotation target, printing each one's inferred capabilities; one already on file is skipped."
|
|
408
|
+
returns Document
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
query "CheckGeneratedDomains" do
|
|
412
|
+
description "Generate domains nobody wrote and check them the way the rotation does: a domain that does not boot is invalid, never a finding."
|
|
413
|
+
attribute :arguments, Words, optional: true
|
|
414
|
+
returns Document
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
query "MineCombinations" do
|
|
418
|
+
description "Ask an agent to mine the adversarial corpus for new domain combinations, then check the ones that boot; opt-in, since an agent call costs money."
|
|
419
|
+
attribute :arguments, Words, optional: true
|
|
420
|
+
returns Document
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
query "JudgeNovelty" do
|
|
424
|
+
description "Whether a new stress domain puts two forms together on one aggregate that no existing target does: the gate before it becomes a target."
|
|
425
|
+
attribute :domain, DomainPath
|
|
426
|
+
attribute :arguments, Words, optional: true
|
|
427
|
+
returns Document
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
query "DiscoverExternalDomains" do
|
|
431
|
+
description "Sibling-repo domains that already depend on the hecks gem but are not enrolled yet; it reports and never enrols."
|
|
432
|
+
attribute :arguments, Words, optional: true
|
|
433
|
+
returns Document
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
aggregate "Sweep" do
|
|
438
|
+
description "One agent's pass over one chapter — every check it made, what each expected, and what actually came back."
|
|
439
|
+
|
|
440
|
+
reference_to Target
|
|
441
|
+
|
|
442
|
+
identified_by :reference
|
|
443
|
+
|
|
444
|
+
attribute :reference, SweepReference
|
|
445
|
+
attribute :engineer, Engineer
|
|
446
|
+
attribute :made, CheckCount
|
|
447
|
+
attribute :notes, SweepNotes
|
|
448
|
+
# A counter, not a flag (a sweep can bend several gates) or a list (`where` cannot test
|
|
449
|
+
# emptiness).
|
|
450
|
+
attribute :waived, WaiverCount
|
|
451
|
+
# The signature beside the count. No default, since a default would be a forged signature;
|
|
452
|
+
# `optional: true` because a sweep that waived nothing has no signer.
|
|
453
|
+
attribute :waived_by, WaivedBy, optional: true
|
|
454
|
+
attribute :checks, list_of(Check)
|
|
455
|
+
|
|
456
|
+
value_object "SweepReference" do
|
|
457
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
458
|
+
invariant("a sweep is referenced") { !value.to_s.empty? }
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
value_object "Engineer" do
|
|
462
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
463
|
+
invariant("a sweep says who ran it") { !value.to_s.empty? }
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
value_object "CheckCount" do
|
|
467
|
+
attribute :value, Integer, default: 0
|
|
468
|
+
invariant("a check count is not negative") { !value.negative? }
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
value_object "WaiverCount" do
|
|
472
|
+
attribute :value, Integer, default: 0
|
|
473
|
+
invariant("a waiver count is not negative") { !value.negative? }
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
value_object "SweepNotes" do
|
|
477
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
478
|
+
# The number is in the sentence because the sentence is the refusal message.
|
|
479
|
+
invariant("a sweep says what it learned, in at least 40 characters") { value.to_s.size >= 40 }
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
value_object "WaiverReason" do
|
|
483
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
484
|
+
invariant("a waived gate says why") { !value.to_s.empty? }
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# The literal "qa_sweep" is `QualityControlDials::AUTOMATED_ENGINEER`; invariants read
|
|
488
|
+
# literals, so `spec/quality_control_spec.rb` pins the two equal. "nobody" is refused: unsigned
|
|
489
|
+
# is anonymous.
|
|
490
|
+
value_object "WaivedBy" do
|
|
491
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
492
|
+
invariant("a gate is waived by a person, never by the loop") { value != "qa_sweep" }
|
|
493
|
+
invariant("a waiver is signed") { value != "nobody" }
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Restated on a surprise so the event carries it: an event holds only its dispatched arguments,
|
|
497
|
+
# and `SuspendOnSurprise` needs a `Target` to address.
|
|
498
|
+
value_object "TargetRef" do
|
|
499
|
+
attribute :value, String
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
value_object "CheckSequence" do
|
|
503
|
+
attribute :value, Integer
|
|
504
|
+
invariant("a check sequence is positive") { value.positive? }
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Plain text, no pattern: a test file is newlines by construction and tool output is whatever
|
|
508
|
+
# the tool said.
|
|
509
|
+
value_object "Listing" do
|
|
510
|
+
attribute :value, String
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
value_object "Subject" do
|
|
514
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
515
|
+
invariant("a check names what it put to the system") { !value.to_s.empty? }
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Written before the answer exists: an expectation fitted to the answer cannot be surprised.
|
|
519
|
+
value_object "Expectation" do
|
|
520
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
521
|
+
invariant("a check says what it expected") { !value.to_s.empty? }
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
value_object "Observation" do
|
|
525
|
+
attribute :value, String
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# One comparison. An entity: a check has its own identity, verbs and lifecycle, and only makes
|
|
529
|
+
# sense inside the sweep that made it. `Surprising` answers "every surprising check, any
|
|
530
|
+
# sweep".
|
|
531
|
+
entity "Check" do
|
|
532
|
+
description "One dispatch made against a written-down expectation, and what actually came back."
|
|
533
|
+
|
|
534
|
+
identified_by :sequence
|
|
535
|
+
|
|
536
|
+
attribute :sequence, CheckSequence
|
|
537
|
+
attribute :subject, Subject
|
|
538
|
+
attribute :expectation, Expectation
|
|
539
|
+
attribute :observation, Observation, optional: true
|
|
540
|
+
# Sticky: `outcome` is the current verdict and `Remake` resets it, but `Release`'s
|
|
541
|
+
# `next_streak` needs "did anything ever surprise this sweep", which a remake must not erase.
|
|
542
|
+
# `one_of` rather than a Boolean, which this chapter has no precedent for.
|
|
543
|
+
attribute :ever_surprised, one_of("no", "yes"), default: { value: "no" }
|
|
544
|
+
|
|
545
|
+
# `surprising` is not `failed`: it covers the crash and the quiet divergence without
|
|
546
|
+
# prejudging which.
|
|
547
|
+
lifecycle :outcome, default: "made" do
|
|
548
|
+
transition "Held" => "held", from: "made"
|
|
549
|
+
transition "Surprised" => "surprising", from: "made"
|
|
550
|
+
transition "Unsettled" => "unsettled", from: "made"
|
|
551
|
+
transition "Remake" => "made", from: ["held", "surprising", "unsettled"]
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
command "Held" do
|
|
555
|
+
role "QA engineer"
|
|
556
|
+
goal "Record that the system did what the chapter promised"
|
|
557
|
+
|
|
558
|
+
attribute :sequence, CheckSequence
|
|
559
|
+
attribute :observation, Observation
|
|
560
|
+
|
|
561
|
+
sets :observation
|
|
562
|
+
|
|
563
|
+
emits "CheckHeld"
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
command "Surprised" do
|
|
567
|
+
role "QA engineer"
|
|
568
|
+
goal "Record that the system did something other than what was written down"
|
|
569
|
+
|
|
570
|
+
attribute :sequence, CheckSequence
|
|
571
|
+
attribute :observation, Observation
|
|
572
|
+
# Restated so `CheckSurprised` carries a `Target` id for `SuspendOnSurprise`; see
|
|
573
|
+
# `TargetRef`. Required: a caller that omits it is refused (`AbsentArgument`) rather than
|
|
574
|
+
# suspending nothing.
|
|
575
|
+
attribute :target, TargetRef
|
|
576
|
+
|
|
577
|
+
sets :observation
|
|
578
|
+
# A literal, set here and nowhere else; see `ever_surprised`.
|
|
579
|
+
sets :ever_surprised, to: "yes"
|
|
580
|
+
|
|
581
|
+
emits "CheckSurprised"
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
command "Unsettled" do
|
|
585
|
+
role "QA engineer"
|
|
586
|
+
goal "Record a check that ran and settled nothing"
|
|
587
|
+
|
|
588
|
+
attribute :sequence, CheckSequence
|
|
589
|
+
attribute :observation, Observation
|
|
590
|
+
|
|
591
|
+
sets :observation
|
|
592
|
+
|
|
593
|
+
emits "CheckUnsettled"
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
command "Remake" do
|
|
597
|
+
role "QA engineer"
|
|
598
|
+
goal "Put a settled check back, against a system that has since changed"
|
|
599
|
+
|
|
600
|
+
attribute :sequence, CheckSequence
|
|
601
|
+
|
|
602
|
+
emits "CheckRemade"
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
query "Surprising" do
|
|
606
|
+
description "Every check the system surprised, across every sweep — the raw material a bug is written from, and the only list here that is supposed to be interesting."
|
|
607
|
+
where(outcome: "surprising")
|
|
608
|
+
order_by :sequence
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
query "Unsettled" do
|
|
612
|
+
description "Made and never ruled on, plus the runs that settled nothing. A sweep concluded with rows here concluded early."
|
|
613
|
+
where(outcome: { in: "made,unsettled" })
|
|
614
|
+
order_by :sequence
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
query "ForSubject" do
|
|
618
|
+
description "Everything ever put to one verb, across every sweep. Count the rows and that is its coverage."
|
|
619
|
+
attribute :subject, Subject
|
|
620
|
+
where(subject: :subject)
|
|
621
|
+
order_by :sequence
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
lifecycle :status, default: "sweeping" do
|
|
626
|
+
transition "Check" => "sweeping", from: "sweeping"
|
|
627
|
+
transition "Waive" => "sweeping", from: "sweeping"
|
|
628
|
+
transition "Conclude" => "concluded", from: "sweeping"
|
|
629
|
+
transition "Abandon" => "abandoned", from: "sweeping"
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
command "Open" do
|
|
633
|
+
role "QA engineer"
|
|
634
|
+
goal "Begin a pass over one chapter"
|
|
635
|
+
|
|
636
|
+
reference_to Target
|
|
637
|
+
attribute :reference, SweepReference
|
|
638
|
+
attribute :engineer, Engineer
|
|
639
|
+
|
|
640
|
+
emits "SweepOpened"
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
command "Check" do
|
|
644
|
+
role "QA engineer"
|
|
645
|
+
goal "Put one thing to the system, saying what it should do before looking"
|
|
646
|
+
|
|
647
|
+
reference_to Sweep
|
|
648
|
+
attribute :subject, Subject
|
|
649
|
+
attribute :expectation, Expectation
|
|
650
|
+
|
|
651
|
+
# `sequence` is absent because `MutationApplier#entity_element` mints it as `size + 1`.
|
|
652
|
+
# `observation` is absent on purpose: a check is written down when made, carrying only its
|
|
653
|
+
# expectation, so an expectation cannot be fitted to an answer after the fact.
|
|
654
|
+
sets :checks, append: { subject: :subject, expectation: :expectation }
|
|
655
|
+
sets :made, increment: { value: 1 }
|
|
656
|
+
|
|
657
|
+
ensures("checking never loses a check") { made.value == old.made.value + 1 }
|
|
658
|
+
|
|
659
|
+
emits "CheckMade"
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# Waiving is countable: `Sweep.Waived` can be read afterwards, so going round a gate has a
|
|
663
|
+
# price.
|
|
664
|
+
command "Waive" do
|
|
665
|
+
role "QA engineer"
|
|
666
|
+
goal "Go round a gate, on the record, signed by a person"
|
|
667
|
+
|
|
668
|
+
reference_to Sweep
|
|
669
|
+
attribute :reason, WaiverReason
|
|
670
|
+
# `WaivedBy` refuses "qa_sweep" and "nobody", so only a person can sign.
|
|
671
|
+
attribute :waived_by, WaivedBy
|
|
672
|
+
|
|
673
|
+
sets :waived, increment: { value: 1 }
|
|
674
|
+
sets :waived_by
|
|
675
|
+
|
|
676
|
+
ensures("a waiver is never silent") { waived.value == old.waived.value + 1 }
|
|
677
|
+
|
|
678
|
+
emits "GateWaived"
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
command "Conclude" do
|
|
682
|
+
role "QA engineer"
|
|
683
|
+
goal "End a pass and say what it taught"
|
|
684
|
+
|
|
685
|
+
reference_to Sweep
|
|
686
|
+
attribute :notes, SweepNotes
|
|
687
|
+
|
|
688
|
+
# A sweep that checked nothing tested nothing; the `waived` clause makes this a gate, not a
|
|
689
|
+
# wall.
|
|
690
|
+
given("a sweep that checked nothing tested nothing") { made.value.positive? || waived.value.positive? }
|
|
691
|
+
|
|
692
|
+
sets :notes
|
|
693
|
+
|
|
694
|
+
emits "SweepConcluded"
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
command "Abandon" do
|
|
698
|
+
role "QA engineer"
|
|
699
|
+
goal "End a pass whose output should not be trusted"
|
|
700
|
+
|
|
701
|
+
reference_to Sweep
|
|
702
|
+
emits "SweepAbandoned"
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
query "Sweeping" do
|
|
706
|
+
description "Passes live right now — one per agent, and the reason a target shows as held."
|
|
707
|
+
where(status: "sweeping")
|
|
708
|
+
order_by :reference
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
query "Waived" do
|
|
712
|
+
description "Sweeps that went round a gate. Read as a fraction of all of them: a practice whose waiver rate is climbing is not moving faster, it is agreeing with itself less."
|
|
713
|
+
where(:"waived.value" => { gt: 0 })
|
|
714
|
+
order_by :reference
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
query "All" do
|
|
718
|
+
description "Every pass ever made — what a runner mints the next reference from."
|
|
719
|
+
order_by :reference
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
# The commands that work a whole pass. SweepTools answers each by running the script's own command
|
|
723
|
+
# as a child process (lib/hecks/quality_control/adapters/sweep_tools.rb) and handing back what it
|
|
724
|
+
# printed: a run that found something is an answer, an operational error a refusal with its
|
|
725
|
+
# report. They log their own checks through the commands above; none of these queries reads this
|
|
726
|
+
# aggregate's records. `arguments` is the command's own flags as one string, such as `--all`.
|
|
727
|
+
value_object "Words" do
|
|
728
|
+
attribute :value, String, pattern: '[^\n\r]'
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
value_object "DomainPath" do
|
|
732
|
+
attribute :value, String, pattern: '[^\t\n\r]'
|
|
733
|
+
|
|
734
|
+
invariant("a domain is named by its path") { !value.to_s.empty? }
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
value_object "DataPath" do
|
|
738
|
+
attribute :value, String, pattern: '[^\t\n\r]'
|
|
739
|
+
|
|
740
|
+
invariant("a data directory is named by its path") { !value.to_s.empty? }
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
value_object "DatabaseName" do
|
|
744
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
745
|
+
|
|
746
|
+
invariant("a database is named") { !value.to_s.empty? }
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
value_object "RoleName" do
|
|
750
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
751
|
+
|
|
752
|
+
invariant("a role is named") { !value.to_s.empty? }
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
value_object "SchemaName" do
|
|
756
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
757
|
+
|
|
758
|
+
invariant("a schema is named") { !value.to_s.empty? }
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
value_object "StepVerb" do
|
|
762
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
763
|
+
|
|
764
|
+
invariant("a step names its verb") { !value.to_s.empty? }
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# A step's arguments as JSON text, which is how the racer's command line carries them.
|
|
768
|
+
value_object "StepArguments" do
|
|
769
|
+
attribute :value, String
|
|
770
|
+
|
|
771
|
+
invariant("a step carries its arguments, `{}` for none") { !value.to_s.empty? }
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# An answer that is one document of text, exactly as the tool that produced it wrote it.
|
|
775
|
+
value_object "Document" do
|
|
776
|
+
attribute :text, String
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
query "Tick" do
|
|
780
|
+
description "One QA tick: a clean tree, a rebase on origin/main, the PR check, a sweep of the whole rotation and the generated domains, then one report. It never logs a bug, releases a hold or opens a PR."
|
|
781
|
+
returns Document
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
query "Run" do
|
|
785
|
+
description "One claim, sweep, conclude and release cycle against a target (the rotation's pick when none is named), or every waiting target with `--all`; the first surprised check ends it with a report."
|
|
786
|
+
attribute :target, TargetRef, optional: true
|
|
787
|
+
attribute :arguments, Words, optional: true
|
|
788
|
+
returns Document
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
query "MigrateLedgerFromHeki" do
|
|
792
|
+
description "Copy each aggregate's current state from a Heki data directory into the repository the domain's bindings name today; a dry run unless `--force` is in the arguments, and an id held under a different state is always refused."
|
|
793
|
+
attribute :domain, DomainPath
|
|
794
|
+
attribute :data, DataPath
|
|
795
|
+
attribute :arguments, Words, optional: true
|
|
796
|
+
returns Document
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
query "CreateLedgerRole" do
|
|
800
|
+
description "Create the ordinary Postgres role the ledger connects as and make it own the database; idempotent, and refuses a superuser or BYPASSRLS role, which the era write-fence cannot bind."
|
|
801
|
+
attribute :database, DatabaseName
|
|
802
|
+
attribute :role, RoleName, optional: true
|
|
803
|
+
returns Document
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# Runs only as a child process a concurrency sweep starts, two at a time, so it is not something a
|
|
807
|
+
# person types.
|
|
808
|
+
query "Race" do
|
|
809
|
+
description "One racer of a concurrent dispatch: boot a domain against a schema and dispatch one step; the answer is `succeeded`, `refused` or `crashed:<class>: <message>`."
|
|
810
|
+
attribute :domain, DomainPath
|
|
811
|
+
attribute :database, DatabaseName
|
|
812
|
+
attribute :schema, SchemaName
|
|
813
|
+
attribute :verb, StepVerb
|
|
814
|
+
attribute :step_arguments, StepArguments
|
|
815
|
+
returns Document
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
# One failing test, one bug, and the test comes first: `Log` requires the demonstration, so a bug
|
|
820
|
+
# that cannot be shown cannot be written down. Hence no `found` state and no `Failure` record.
|
|
821
|
+
aggregate "Bug" do
|
|
822
|
+
description "One thing now known to be wrong, the test that proves it, and how far the fix got."
|
|
823
|
+
|
|
824
|
+
reference_to Sweep
|
|
825
|
+
|
|
826
|
+
identified_by :reference
|
|
827
|
+
|
|
828
|
+
attribute :reference, BugReference
|
|
829
|
+
attribute :sequence, BugSequence
|
|
830
|
+
attribute :title, BugTitle
|
|
831
|
+
attribute :demonstration, Demonstration
|
|
832
|
+
attribute :symptom, Symptom
|
|
833
|
+
attribute :expectation, Expectation
|
|
834
|
+
# Who reported it. Distinct from `held_by` (who is working on it now); a submitter never
|
|
835
|
+
# changes.
|
|
836
|
+
attribute :submitter, Submitter
|
|
837
|
+
attribute :order, BugOrder
|
|
838
|
+
attribute :tags, list_of(Tag)
|
|
839
|
+
attribute :site, BugSite
|
|
840
|
+
attribute :cause, BugCause
|
|
841
|
+
attribute :commit, CommitRef
|
|
842
|
+
attribute :verification, Verification
|
|
843
|
+
attribute :reason, BugReason
|
|
844
|
+
attribute :next_step, NextStep
|
|
845
|
+
attribute :waived, WaiverCount
|
|
846
|
+
# The same signature as `Sweep.waived_by`; absent until a person waives.
|
|
847
|
+
attribute :waived_by, WaivedBy, optional: true
|
|
848
|
+
# A short memorable handle such as `replace-entries-duplicate-identity`. Optional with no
|
|
849
|
+
# default: only a person reading an old bug can write a good slug.
|
|
850
|
+
attribute :name, BugName, optional: true
|
|
851
|
+
# Whether the bug is fixable on the spot or bigger; `Triage` is the only mover.
|
|
852
|
+
# `default:` keeps rows already on file hydrating as `undecided`.
|
|
853
|
+
attribute :disposition, one_of("undecided", "self_contained", "bigger"), default: { value: "undecided" }
|
|
854
|
+
# `default: yes` is true of every bug on file, since `Log` always required a failing
|
|
855
|
+
# demonstration. `no` is the escape hatch for a real but flaky finding (`hecks quality_control log
|
|
856
|
+
# --reproduced no`).
|
|
857
|
+
attribute :reproduced, one_of("yes", "no"), default: { value: "yes" }
|
|
858
|
+
# Bugs are claimed like chapters so two agents do not fix the same one. A `given` rather than a
|
|
859
|
+
# lifecycle edge: holding a bug is orthogonal to how far the fix got.
|
|
860
|
+
attribute :held_by, Engineer
|
|
861
|
+
attribute :claimed_at, Instant
|
|
862
|
+
attribute :window, StaleAfter
|
|
863
|
+
|
|
864
|
+
value_object "BugReference" do
|
|
865
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
866
|
+
invariant("a bug is referenced") { !value.to_s.empty? }
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
value_object "BugSequence" do
|
|
870
|
+
attribute :value, Integer
|
|
871
|
+
invariant("a bug sequence is positive") { value.positive? }
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Prefixed on purpose: `Doors::RubyDoor.install` puts every aggregate name on Object, so a
|
|
875
|
+
# value object sharing a word with any aggregate resolves to that class once the other domain
|
|
876
|
+
# boots.
|
|
877
|
+
value_object "BugTitle" do
|
|
878
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
879
|
+
invariant("a bug is titled") { !value.to_s.empty? }
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
# Required at `Log`, which is what makes the one absolute rule absolute.
|
|
883
|
+
value_object "Demonstration" do
|
|
884
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
885
|
+
invariant("a bug is proven by a test somebody can re-run") { !value.to_s.empty? }
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
value_object "Symptom" do
|
|
889
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
890
|
+
invariant("a bug says what actually happened") { !value.to_s.empty? }
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
value_object "Expectation" do
|
|
894
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
895
|
+
invariant("a bug says what should have happened") { !value.to_s.empty? }
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
value_object "BugSite" do
|
|
899
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
900
|
+
invariant("an investigation names where the bug lives") { !value.to_s.empty? }
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
value_object "BugCause" do
|
|
904
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
905
|
+
invariant("an investigation says why the code is wrong") { !value.to_s.empty? }
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
# 7 to 40 hex: a fix that lives only in a working tree is not a fix.
|
|
909
|
+
value_object "CommitRef" do
|
|
910
|
+
attribute :value, String, pattern: '^[0-9a-fA-F]{7,40}$'
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# What was actually run, e.g. "rspec --order random, 1335 examples, 0 failures, seed 12345":
|
|
914
|
+
# a count and a seed are checkable, "tests pass" is not.
|
|
915
|
+
value_object "Verification" do
|
|
916
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
917
|
+
invariant("a verification says what was actually run") { !value.to_s.empty? }
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
value_object "BugReason" do
|
|
921
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
922
|
+
invariant("stopping on a bug says why") { !value.to_s.empty? }
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# Who decides and what would unblock it, so "deferred" does not become "forgotten".
|
|
926
|
+
value_object "NextStep" do
|
|
927
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
928
|
+
invariant("stopping on a bug says what would move it") { !value.to_s.empty? }
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
value_object "WaiverCount" do
|
|
932
|
+
attribute :value, Integer, default: 0
|
|
933
|
+
invariant("a waiver count is not negative") { !value.negative? }
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
value_object "WaiverReason" do
|
|
937
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
938
|
+
invariant("a waived gate says why") { !value.to_s.empty? }
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
# Redeclared, as elsewhere in this file; the "qa_sweep" literal is pinned to AUTOMATED_ENGINEER
|
|
942
|
+
# by spec.
|
|
943
|
+
value_object "WaivedBy" do
|
|
944
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
945
|
+
invariant("a gate is waived by a person, never by the loop") { value != "qa_sweep" }
|
|
946
|
+
invariant("a waiver is signed") { value != "nobody" }
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
# Kebab-case, at least two words: one word is too close to a `Tag` to tell apart at a glance.
|
|
950
|
+
# The anchored pattern is the whole contract.
|
|
951
|
+
value_object "BugName" do
|
|
952
|
+
attribute :value, String, pattern: '^[a-z0-9]+(-[a-z0-9]+)+$'
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
value_object "SweepRef" do
|
|
956
|
+
attribute :value, String
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
value_object "Engineer" do
|
|
960
|
+
attribute :value, String, default: "nobody"
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
value_object "Instant" do
|
|
964
|
+
attribute :value, Integer, default: 0
|
|
965
|
+
invariant("an instant is not before the epoch") { !value.negative? }
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
value_object "StaleAfter" do
|
|
969
|
+
attribute :value, Integer, default: 900
|
|
970
|
+
invariant("a staleness window is positive") { value.positive? }
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
# Not an `Engineer`: that type defaults to "nobody" (an unclaimed bug is held by nobody), which
|
|
974
|
+
# would let a report omit its submitter. This type has no default and refuses "nobody".
|
|
975
|
+
value_object "Submitter" do
|
|
976
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
977
|
+
|
|
978
|
+
invariant("a bug is reported by somebody") { value != "nobody" }
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
# An order is judgement, kept apart from discovery order (`sequence`). Lower is sooner; the
|
|
982
|
+
# default is not zero so an unranked bug sits behind every ranked one.
|
|
983
|
+
value_object "BugOrder" do
|
|
984
|
+
attribute :value, Integer, default: 1_000
|
|
985
|
+
|
|
986
|
+
invariant("an order is a position, not a direction") { !value.negative? }
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
value_object "Tag" do
|
|
990
|
+
attribute :value, String, pattern: "^[a-z0-9][a-z0-9_-]*$"
|
|
991
|
+
|
|
992
|
+
invariant("a tag is a word, not a sentence") { !value.empty? }
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
# A bug is born proven; "somebody thinks something is wrong" is a check, on the sweep.
|
|
996
|
+
lifecycle :status, default: "logged" do
|
|
997
|
+
transition "Investigate" => "investigating", from: "logged"
|
|
998
|
+
transition "Fix" => "fixed", from: "investigating"
|
|
999
|
+
transition "Verify" => "verified", from: "fixed"
|
|
1000
|
+
transition "Pause" => "paused", from: ["logged", "investigating"]
|
|
1001
|
+
# `withdrawn` is for a report whose test proved something other than what it claimed.
|
|
1002
|
+
transition "Withdraw" => "withdrawn", from: ["logged", "investigating", "paused"]
|
|
1003
|
+
transition "Regress" => "investigating", from: ["fixed", "verified"]
|
|
1004
|
+
transition "Revisit" => "investigating", from: "paused"
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
command "Log" do
|
|
1008
|
+
role "QA engineer"
|
|
1009
|
+
goal "Write down something that is wrong, with the test that proves it"
|
|
1010
|
+
|
|
1011
|
+
reference_to Sweep
|
|
1012
|
+
attribute :reference, BugReference
|
|
1013
|
+
attribute :sequence, BugSequence
|
|
1014
|
+
attribute :title, BugTitle
|
|
1015
|
+
attribute :demonstration, Demonstration
|
|
1016
|
+
attribute :symptom, Symptom
|
|
1017
|
+
attribute :expectation, Expectation
|
|
1018
|
+
# Required like the demonstration: one is the evidence, the other is who produced it.
|
|
1019
|
+
attribute :submitter, Submitter
|
|
1020
|
+
attribute :tags, list_of(Tag), optional: true
|
|
1021
|
+
# Optional; `Name` covers bugs logged without one. No `sets`: on a creation command an
|
|
1022
|
+
# argument sharing the aggregate field's name lands there directly.
|
|
1023
|
+
attribute :name, BugName, optional: true
|
|
1024
|
+
# Optional; omitted, the aggregate's default `yes` applies. `hecks quality_control log --reproduced no`
|
|
1025
|
+
# uses it to skip its must-fail check for a finding with no reliable pass/fail signal.
|
|
1026
|
+
attribute :reproduced, one_of("yes", "no"), optional: true
|
|
1027
|
+
|
|
1028
|
+
emits "BugLogged"
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
# Tagging and ranking are not lifecycle moves: they are knowable at any status and change
|
|
1032
|
+
# nothing about what may happen next.
|
|
1033
|
+
command "Rank" do
|
|
1034
|
+
role "QA engineer"
|
|
1035
|
+
goal "Say what should be dealt with first"
|
|
1036
|
+
|
|
1037
|
+
reference_to Bug
|
|
1038
|
+
attribute :order, BugOrder
|
|
1039
|
+
|
|
1040
|
+
sets :order
|
|
1041
|
+
|
|
1042
|
+
emits "BugRanked"
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
# Tags are a set on the bug, not an entity: a tag has no identity or lifecycle. `Tag` replaces
|
|
1046
|
+
# the whole set, so removing one needs no second verb and tagging twice is not two tags.
|
|
1047
|
+
command "Tag" do
|
|
1048
|
+
role "QA engineer"
|
|
1049
|
+
goal "Say what kind of thing this bug is, so it can be found with its kind later"
|
|
1050
|
+
|
|
1051
|
+
reference_to Bug
|
|
1052
|
+
attribute :tags, list_of(Tag)
|
|
1053
|
+
|
|
1054
|
+
sets :tags
|
|
1055
|
+
|
|
1056
|
+
emits "BugTagged"
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
# Not a lifecycle move (the `Rank` shape); `undecided` is only ever the birth value.
|
|
1060
|
+
# `hecks quality_control log --triage` dispatches this right after `Log`.
|
|
1061
|
+
command "Triage" do
|
|
1062
|
+
role "QA engineer"
|
|
1063
|
+
goal "Write down whether this can be fixed on the spot or needs a bigger judgment"
|
|
1064
|
+
|
|
1065
|
+
reference_to Bug
|
|
1066
|
+
attribute :disposition, one_of("self_contained", "bigger")
|
|
1067
|
+
|
|
1068
|
+
# The `given` is the fence: an argument named like the record's own `one_of` field coerces
|
|
1069
|
+
# against the wider set, so the narrower inline set did not refuse "undecided". Predicates
|
|
1070
|
+
# read args first.
|
|
1071
|
+
given("a triage decides — self_contained or bigger, never undecided") { disposition.value != "undecided" }
|
|
1072
|
+
|
|
1073
|
+
sets :disposition
|
|
1074
|
+
|
|
1075
|
+
emits "BugTriaged"
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
# Not a lifecycle move and callable from any status, so an already-logged bug can be named
|
|
1079
|
+
# later.
|
|
1080
|
+
command "Name" do
|
|
1081
|
+
role "QA engineer"
|
|
1082
|
+
goal "Give a bug a short, memorable slug — at log time or any point after"
|
|
1083
|
+
|
|
1084
|
+
reference_to Bug
|
|
1085
|
+
attribute :name, BugName
|
|
1086
|
+
|
|
1087
|
+
sets :name
|
|
1088
|
+
|
|
1089
|
+
emits "BugNamed"
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
# Everything one agent ever reported, as against `InHand`, which is what it holds now.
|
|
1093
|
+
query "From" do
|
|
1094
|
+
description "Every bug one submitter has ever logged, in whatever state — what an agent found, as against what it is holding."
|
|
1095
|
+
attribute :submitter, Submitter
|
|
1096
|
+
where(submitter: :submitter)
|
|
1097
|
+
order_by :reference
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# `contains` against a bound argument, so it answers whichever tag you have.
|
|
1101
|
+
query "Tagged" do
|
|
1102
|
+
description "Every bug carrying one tag, whatever state it is in — the way to find all the flaky ones, or everything about the projector."
|
|
1103
|
+
attribute :tag, Tag
|
|
1104
|
+
where(tags: { contains: :tag })
|
|
1105
|
+
order_by :reference
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
# Same rule as `Target.Claim`: held by nobody, or stale. `+` because the sublanguage has no
|
|
1109
|
+
# `-`.
|
|
1110
|
+
command "Claim" do
|
|
1111
|
+
role "QA engineer"
|
|
1112
|
+
goal "Take the next bug off the queue, or one whose holder has gone quiet"
|
|
1113
|
+
|
|
1114
|
+
# Same reason as `Target.Claim`: the window is on the record, not the claimer's to name.
|
|
1115
|
+
reference_to Bug
|
|
1116
|
+
attribute :held_by, Engineer
|
|
1117
|
+
attribute :now, Instant
|
|
1118
|
+
|
|
1119
|
+
given("a live claim is not taken from the agent holding it") do
|
|
1120
|
+
held_by.value == "nobody" || claimed_at.value + window.value <= now.value
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
sets :held_by
|
|
1124
|
+
sets :claimed_at, to: :now
|
|
1125
|
+
|
|
1126
|
+
emits "BugClaimed"
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
command "Drop" do
|
|
1130
|
+
role "QA engineer"
|
|
1131
|
+
goal "Put a bug back on the queue without finishing it"
|
|
1132
|
+
|
|
1133
|
+
reference_to Bug
|
|
1134
|
+
attribute :held_by, Engineer
|
|
1135
|
+
|
|
1136
|
+
sets :held_by
|
|
1137
|
+
|
|
1138
|
+
emits "BugDropped"
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
command "Investigate" do
|
|
1142
|
+
role "QA engineer"
|
|
1143
|
+
goal "Name the code that is wrong and say why"
|
|
1144
|
+
|
|
1145
|
+
reference_to Bug
|
|
1146
|
+
attribute :site, BugSite
|
|
1147
|
+
attribute :cause, BugCause
|
|
1148
|
+
|
|
1149
|
+
sets :site
|
|
1150
|
+
sets :cause
|
|
1151
|
+
|
|
1152
|
+
emits "BugInvestigated"
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
# `reference` is restated so `BugFixed` carries it: an event holds only its dispatched
|
|
1156
|
+
# arguments, and `BugCiWatch`'s saga memory is a copy of that payload, so without it the saga
|
|
1157
|
+
# cannot know which bug it watches. Required, so omitting it is refused (`AbsentArgument`).
|
|
1158
|
+
command "Fix" do
|
|
1159
|
+
role "QA engineer"
|
|
1160
|
+
goal "Record the commit that makes it stop happening"
|
|
1161
|
+
|
|
1162
|
+
reference_to Bug
|
|
1163
|
+
attribute :reference, BugReference
|
|
1164
|
+
attribute :commit, CommitRef
|
|
1165
|
+
|
|
1166
|
+
sets :commit
|
|
1167
|
+
|
|
1168
|
+
emits "BugFixed"
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
command "Verify" do
|
|
1172
|
+
role "QA engineer"
|
|
1173
|
+
goal "Record that the fix was actually verified, and exactly how"
|
|
1174
|
+
|
|
1175
|
+
reference_to Bug
|
|
1176
|
+
attribute :evidence, Verification
|
|
1177
|
+
|
|
1178
|
+
sets :verification, to: :evidence
|
|
1179
|
+
|
|
1180
|
+
emits "BugVerified"
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
command "Pause" do
|
|
1184
|
+
role "QA engineer"
|
|
1185
|
+
goal "Stop on a bug that is real and too large to fix here — why, and what would move it"
|
|
1186
|
+
|
|
1187
|
+
reference_to Bug
|
|
1188
|
+
attribute :reason, BugReason
|
|
1189
|
+
attribute :next_step, NextStep
|
|
1190
|
+
|
|
1191
|
+
sets :reason
|
|
1192
|
+
sets :next_step
|
|
1193
|
+
|
|
1194
|
+
emits "BugPaused"
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
command "Withdraw" do
|
|
1198
|
+
role "QA engineer"
|
|
1199
|
+
goal "Take back a report whose test turned out to prove something else"
|
|
1200
|
+
|
|
1201
|
+
reference_to Bug
|
|
1202
|
+
attribute :reason, BugReason
|
|
1203
|
+
|
|
1204
|
+
sets :reason
|
|
1205
|
+
|
|
1206
|
+
emits "BugWithdrawn"
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
command "Regress" do
|
|
1210
|
+
role "QA engineer"
|
|
1211
|
+
goal "Put a bug back when its fix stopped holding"
|
|
1212
|
+
|
|
1213
|
+
reference_to Bug
|
|
1214
|
+
emits "BugRegressed"
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
command "Revisit" do
|
|
1218
|
+
role "QA engineer"
|
|
1219
|
+
goal "Pick a paused bug back up, investigation intact"
|
|
1220
|
+
|
|
1221
|
+
reference_to Bug
|
|
1222
|
+
emits "BugRevisited"
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
command "Waive" do
|
|
1226
|
+
role "QA engineer"
|
|
1227
|
+
goal "Go round a gate on this bug, on the record, signed by a person"
|
|
1228
|
+
|
|
1229
|
+
reference_to Bug
|
|
1230
|
+
attribute :reason, WaiverReason
|
|
1231
|
+
# Signed and checked, as in `Sweep.Waive`.
|
|
1232
|
+
attribute :waived_by, WaivedBy
|
|
1233
|
+
|
|
1234
|
+
sets :waived, increment: { value: 1 }
|
|
1235
|
+
sets :waived_by
|
|
1236
|
+
|
|
1237
|
+
emits "GateWaived"
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
query "Open" do
|
|
1241
|
+
description "Every bug still owed work — the open ledger, derived rather than maintained."
|
|
1242
|
+
where(status: { in: "logged,investigating,paused" })
|
|
1243
|
+
order_by :sequence
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
query "Untriaged" do
|
|
1247
|
+
description "Live bugs (logged or investigating) nobody has triaged as self_contained or bigger yet — the judgment still owed on each."
|
|
1248
|
+
where(status: { in: "logged,investigating" })
|
|
1249
|
+
where(:"disposition.value" => "undecided")
|
|
1250
|
+
order_by :sequence
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1253
|
+
# `fixed` counts as still owed: a landed fix does not answer the bigger judgment call; only
|
|
1254
|
+
# `Verify` or `Withdraw` resolves it.
|
|
1255
|
+
query "NeedsJudgment" do
|
|
1256
|
+
description "Open bugs triaged bigger and not yet resolved — the pile still owed a human or agent judgment call, not one anybody can just fix off the queue."
|
|
1257
|
+
where(:"disposition.value" => "bigger")
|
|
1258
|
+
where(status: { in: "logged,investigating,fixed,paused" })
|
|
1259
|
+
order_by :sequence
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
query "Queue" do
|
|
1263
|
+
description "Open bugs nobody is holding, most important first, then oldest. The queue an agent takes from when it is fixing rather than sweeping."
|
|
1264
|
+
where(status: { in: "logged,investigating" })
|
|
1265
|
+
where(:"held_by.value" => "nobody")
|
|
1266
|
+
# Order by the scalar, not the value object: sorting the whole `{value: n}` put rank 1 behind
|
|
1267
|
+
# two ranked 1000.
|
|
1268
|
+
order_by :"order.value"
|
|
1269
|
+
end
|
|
1270
|
+
|
|
1271
|
+
query "InHand" do
|
|
1272
|
+
description "Open bugs somebody is holding, and since when. A claim older than the window is free for the taking — read claimed_at before assuming somebody is on it."
|
|
1273
|
+
where(status: { in: "logged,investigating" })
|
|
1274
|
+
where(:"held_by.value" => { ne: "nobody" })
|
|
1275
|
+
order_by :claimed_at
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
query "FoundIn" do
|
|
1279
|
+
description "Everything one sweep turned up, in the order it was found."
|
|
1280
|
+
attribute :sweep_id, SweepRef
|
|
1281
|
+
where(sweep: :sweep_id)
|
|
1282
|
+
order_by :sequence
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
query "ReadyToVerify" do
|
|
1286
|
+
description "Fixed and not yet re-run — the queue a full CI pass empties."
|
|
1287
|
+
where(status: "fixed")
|
|
1288
|
+
order_by :sequence
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
query "Paused" do
|
|
1292
|
+
description "Real, unfixed, stopped on, each with what would move it. The only queue a ticket is ever raised from."
|
|
1293
|
+
where(status: "paused")
|
|
1294
|
+
order_by :sequence
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
query "Withdrawn" do
|
|
1298
|
+
description "Reports whose test proved something other than what was claimed. Read the reasons: a run of these is a practice testing the wrong thing, not a runtime behaving."
|
|
1299
|
+
where(status: "withdrawn")
|
|
1300
|
+
order_by :sequence
|
|
1301
|
+
end
|
|
1302
|
+
|
|
1303
|
+
query "Waived" do
|
|
1304
|
+
description "Bugs that went round one of their own gates — the honesty check on any claim about following the protocol."
|
|
1305
|
+
where(:"waived.value" => { gt: 0 })
|
|
1306
|
+
order_by :sequence
|
|
1307
|
+
end
|
|
1308
|
+
|
|
1309
|
+
query "All" do
|
|
1310
|
+
description "Every bug ever logged — what a runner mints the next reference from."
|
|
1311
|
+
order_by :sequence
|
|
1312
|
+
end
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
# The practice's backlog of leads: a citable reason to suspect an uninvestigated corner.
|
|
1316
|
+
# A lead, not a task: `citation` says why it is worth chasing, `resolution` what got built.
|
|
1317
|
+
aggregate "Angle" do
|
|
1318
|
+
description "One concrete, citable reason to suspect an uninvestigated corner exists — proposed, chased down, and either built into something real or set aside with a reason. The practice's own backlog of what to try next, so a future sweep mines its own history instead of guessing."
|
|
1319
|
+
|
|
1320
|
+
identified_by :reference
|
|
1321
|
+
|
|
1322
|
+
attribute :reference, AngleReference
|
|
1323
|
+
attribute :premise, AnglePremise
|
|
1324
|
+
attribute :citation, AngleCitation
|
|
1325
|
+
attribute :proposer, Proposer
|
|
1326
|
+
attribute :proposed_at, Instant
|
|
1327
|
+
attribute :resolution, AngleResolution
|
|
1328
|
+
attribute :reason, AngleReason
|
|
1329
|
+
|
|
1330
|
+
value_object "AngleReference" do
|
|
1331
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1332
|
+
invariant("an angle is referenced") { !value.to_s.empty? }
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
# The number is in the sentence because the sentence is the refusal; 60 characters trips a
|
|
1336
|
+
# stub.
|
|
1337
|
+
value_object "AnglePremise" do
|
|
1338
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1339
|
+
invariant("an angle says enough to be acted on without its proposer in the room, in at least 60 characters") { value.to_s.size >= 60 }
|
|
1340
|
+
end
|
|
1341
|
+
|
|
1342
|
+
# A Bug reference, ADR number or named gap that a reader can check for themselves.
|
|
1343
|
+
value_object "AngleCitation" do
|
|
1344
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1345
|
+
invariant("an angle cites something concrete, not a mood") { !value.to_s.empty? }
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
# Not an `Engineer`, for the reason `Submitter` is not: that default would let authorship go
|
|
1349
|
+
# unrecorded.
|
|
1350
|
+
value_object "Proposer" do
|
|
1351
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1352
|
+
invariant("an angle is proposed by somebody") { value != "nobody" }
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
value_object "Instant" do
|
|
1356
|
+
attribute :value, Integer, default: 0
|
|
1357
|
+
invariant("an instant is not before the epoch") { !value.negative? }
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
# What came of it (a Target, a PR number, a domain path); set once, at `Build`.
|
|
1361
|
+
value_object "AngleResolution" do
|
|
1362
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1363
|
+
invariant("a built angle says what actually got built") { !value.to_s.empty? }
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
# Redeclared as `Target::Shelve` does: a discard with no reason looks like a lead nobody read.
|
|
1367
|
+
value_object "AngleReason" do
|
|
1368
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1369
|
+
invariant("discarding an angle says why") { !value.to_s.empty? }
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
# `Build` is reachable only from `investigating`, so a lead cannot be built unless it was
|
|
1373
|
+
# picked up. `Discard` reads from both live states: a lead can prove a dead end before or after
|
|
1374
|
+
# it starts.
|
|
1375
|
+
lifecycle :status, default: "proposed" do
|
|
1376
|
+
transition "Investigate" => "investigating", from: "proposed"
|
|
1377
|
+
transition "Build" => "built", from: "investigating"
|
|
1378
|
+
transition "Discard" => "discarded", from: ["proposed", "investigating"]
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
command "Propose" do
|
|
1382
|
+
role "QA engineer"
|
|
1383
|
+
goal "Write down a concrete, citable reason to suspect an uninvestigated corner exists, before it is lost to a conversation"
|
|
1384
|
+
|
|
1385
|
+
attribute :reference, AngleReference
|
|
1386
|
+
attribute :premise, AnglePremise
|
|
1387
|
+
attribute :citation, AngleCitation
|
|
1388
|
+
attribute :proposer, Proposer
|
|
1389
|
+
# `now:` is filled from the clock port, as in `Target.Claim`; an explicit value wins.
|
|
1390
|
+
attribute :now, Instant
|
|
1391
|
+
|
|
1392
|
+
sets :proposed_at, to: :now
|
|
1393
|
+
|
|
1394
|
+
emits "AngleProposed"
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
command "Investigate" do
|
|
1398
|
+
role "QA engineer"
|
|
1399
|
+
goal "Pick a lead up for a real look, rather than leaving it to look picked-up by default"
|
|
1400
|
+
|
|
1401
|
+
reference_to Angle
|
|
1402
|
+
emits "AngleInvestigated"
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
command "Build" do
|
|
1406
|
+
role "QA engineer"
|
|
1407
|
+
goal "Say what actually came of chasing this lead"
|
|
1408
|
+
|
|
1409
|
+
reference_to Angle
|
|
1410
|
+
attribute :resolution, AngleResolution
|
|
1411
|
+
|
|
1412
|
+
sets :resolution
|
|
1413
|
+
|
|
1414
|
+
emits "AngleBuilt"
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1417
|
+
command "Discard" do
|
|
1418
|
+
role "QA engineer"
|
|
1419
|
+
goal "Decide a lead is not worth the practice's time, and say why"
|
|
1420
|
+
|
|
1421
|
+
reference_to Angle
|
|
1422
|
+
attribute :reason, AngleReason
|
|
1423
|
+
|
|
1424
|
+
sets :reason
|
|
1425
|
+
|
|
1426
|
+
emits "AngleDiscarded"
|
|
1427
|
+
end
|
|
1428
|
+
|
|
1429
|
+
# Ordered by the scalar `proposed_at.value`, not the value object (see `Bug.Queue`).
|
|
1430
|
+
query "Backlog" do
|
|
1431
|
+
description "Leads nobody has finished chasing yet, oldest first — read this before inventing a new angle from scratch."
|
|
1432
|
+
where(status: { in: "proposed,investigating" })
|
|
1433
|
+
order_by :"proposed_at.value"
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
# Built and discarded share one query: a discarded angle is evidence a citation was already
|
|
1437
|
+
# covered.
|
|
1438
|
+
query "Resolved" do
|
|
1439
|
+
description "Leads already acted on, one way or the other — built into something real, or discarded with a reason. Read beside Backlog for the practice's own hit rate."
|
|
1440
|
+
where(status: { in: "built,discarded" })
|
|
1441
|
+
order_by :reference
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
# The duplicate-premise check, run before `Propose` so a second session finds the first one's
|
|
1445
|
+
# row.
|
|
1446
|
+
query "Citing" do
|
|
1447
|
+
description "Every angle ever proposed on one exact citation, whatever became of it — the check before proposing another on the same fact."
|
|
1448
|
+
attribute :citation, AngleCitation
|
|
1449
|
+
where(citation: :citation)
|
|
1450
|
+
order_by :reference
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1453
|
+
query "All" do
|
|
1454
|
+
description "Every angle ever proposed — what a runner mints the next reference from."
|
|
1455
|
+
order_by :reference
|
|
1456
|
+
end
|
|
1457
|
+
|
|
1458
|
+
# An answer that is one document of text, exactly as the tool that produced it wrote it.
|
|
1459
|
+
value_object "Document" do
|
|
1460
|
+
attribute :text, String
|
|
1461
|
+
end
|
|
1462
|
+
|
|
1463
|
+
# AngleTools answers this by running the script's own command as a child process
|
|
1464
|
+
# (lib/hecks/quality_control/adapters/angle_tools.rb); it reads no record of this aggregate.
|
|
1465
|
+
query "Seed" do
|
|
1466
|
+
description "Propose the practice's starting leads that are not on file yet; a lead already chased down is proposed, investigated and built in one pass."
|
|
1467
|
+
returns Document
|
|
1468
|
+
end
|
|
1469
|
+
end
|
|
1470
|
+
|
|
1471
|
+
# Raise an issue only for a bug you could not fix yourself. `Raise` checks that the reference
|
|
1472
|
+
# resolves but not the bug's state; `RestingOnUnpaused` surfaces that half.
|
|
1473
|
+
aggregate "Ticket" do
|
|
1474
|
+
description "One issue raised in a tracker outside this repository — what it says, whether the tracker took it, and how many times we have asked."
|
|
1475
|
+
|
|
1476
|
+
reference_to Bug
|
|
1477
|
+
|
|
1478
|
+
identified_by :reference
|
|
1479
|
+
|
|
1480
|
+
attribute :reference, TicketReference
|
|
1481
|
+
attribute :repository, TicketRepository
|
|
1482
|
+
attribute :title, TicketTitle
|
|
1483
|
+
attribute :body, TicketBody
|
|
1484
|
+
attribute :pull_request, PullRequest
|
|
1485
|
+
attribute :number, IssueNumber
|
|
1486
|
+
attribute :url, IssueUrl
|
|
1487
|
+
attribute :refusal, RefusalText
|
|
1488
|
+
attribute :attempts, AttemptCount
|
|
1489
|
+
# The cap lives on the record: a policy carries only the event payload, so a `max` passed as an
|
|
1490
|
+
# argument would never reach the retry.
|
|
1491
|
+
attribute :max_attempts, MaxAttempts
|
|
1492
|
+
|
|
1493
|
+
value_object "TicketReference" do
|
|
1494
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1495
|
+
invariant("a ticket is referenced") { !value.to_s.empty? }
|
|
1496
|
+
end
|
|
1497
|
+
|
|
1498
|
+
value_object "TicketRepository" do
|
|
1499
|
+
attribute :value, String, pattern: '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$'
|
|
1500
|
+
end
|
|
1501
|
+
|
|
1502
|
+
value_object "TicketTitle" do
|
|
1503
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1504
|
+
invariant("a ticket is titled") { !value.to_s.empty? }
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1507
|
+
# The URL is on the ticket so the ledger can answer `Ticket.Proposed`. It defaults to ""
|
|
1508
|
+
# because most tickets are raised before anybody has fixed anything.
|
|
1509
|
+
value_object "PullRequest" do
|
|
1510
|
+
attribute :value, String, default: ""
|
|
1511
|
+
end
|
|
1512
|
+
|
|
1513
|
+
value_object "TicketBody" do
|
|
1514
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1515
|
+
invariant("a ticket says something") { !value.to_s.empty? }
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1518
|
+
value_object "IssueNumber" do
|
|
1519
|
+
attribute :value, Integer, default: 0
|
|
1520
|
+
invariant("an issue number is not negative") { !value.negative? }
|
|
1521
|
+
end
|
|
1522
|
+
|
|
1523
|
+
value_object "IssueUrl" do
|
|
1524
|
+
attribute :value, String
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1527
|
+
value_object "RefusalText" do
|
|
1528
|
+
attribute :value, String
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1531
|
+
# Three tries, then a human. The cap is on the record because a policy carries no state.
|
|
1532
|
+
value_object "AttemptCount" do
|
|
1533
|
+
attribute :value, Integer, default: 0
|
|
1534
|
+
invariant("an attempt count is not negative") { !value.negative? }
|
|
1535
|
+
end
|
|
1536
|
+
|
|
1537
|
+
value_object "MaxAttempts" do
|
|
1538
|
+
attribute :value, Integer, default: 3
|
|
1539
|
+
invariant("a filing is attempted at least once") { value.positive? }
|
|
1540
|
+
end
|
|
1541
|
+
|
|
1542
|
+
value_object "BugRef" do
|
|
1543
|
+
attribute :value, String
|
|
1544
|
+
end
|
|
1545
|
+
|
|
1546
|
+
# Raising and sending are two verbs: a crash between them leaves a `Raised` row, not a false
|
|
1547
|
+
# filing. `Submit` is non-creating, so `TicketSubmitted` carries `{ id: ... }`, which a
|
|
1548
|
+
# reference slot takes.
|
|
1549
|
+
lifecycle :status, default: "raised" do
|
|
1550
|
+
transition "Submit" => "submitting", from: ["raised", "refused"]
|
|
1551
|
+
transition "Filed" => "filed", from: "submitting"
|
|
1552
|
+
transition "Refused" => "refused", from: "submitting"
|
|
1553
|
+
transition "Retry" => "submitting", from: "refused"
|
|
1554
|
+
transition "Abandon" => "abandoned", from: ["raised", "submitting", "refused"]
|
|
1555
|
+
transition "Close" => "closed", from: "filed"
|
|
1556
|
+
end
|
|
1557
|
+
|
|
1558
|
+
# The port is declared in `quality_control.ports.hecksagon`, where wiring lives.
|
|
1559
|
+
|
|
1560
|
+
command "Raise" do
|
|
1561
|
+
role "QA engineer"
|
|
1562
|
+
goal "Say the outside world needs to know about a bug we could not fix"
|
|
1563
|
+
|
|
1564
|
+
reference_to Bug
|
|
1565
|
+
attribute :reference, TicketReference
|
|
1566
|
+
attribute :repository, TicketRepository
|
|
1567
|
+
attribute :title, TicketTitle
|
|
1568
|
+
attribute :body, TicketBody
|
|
1569
|
+
# `held_state` hands the whole ticket to the adapter, so this field reaches `gh` with no
|
|
1570
|
+
# extra argument.
|
|
1571
|
+
attribute :pull_request, PullRequest, optional: true
|
|
1572
|
+
|
|
1573
|
+
emits "TicketRaised"
|
|
1574
|
+
end
|
|
1575
|
+
|
|
1576
|
+
# Dispatch with `id:`, not through the facade: the facade form addresses by identity heads, so
|
|
1577
|
+
# the payload carries `reference` and the port refuses it ("File was not given id").
|
|
1578
|
+
command "Submit" do
|
|
1579
|
+
role "QA engineer"
|
|
1580
|
+
goal "Send it — the last reversible moment"
|
|
1581
|
+
|
|
1582
|
+
reference_to Ticket
|
|
1583
|
+
emits "TicketSubmitted"
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
command "Filed" do
|
|
1587
|
+
role "System"
|
|
1588
|
+
goal "Record that the tracker took it, and where it landed"
|
|
1589
|
+
|
|
1590
|
+
reference_to Ticket
|
|
1591
|
+
attribute :number, IssueNumber
|
|
1592
|
+
attribute :url, IssueUrl
|
|
1593
|
+
|
|
1594
|
+
sets :number
|
|
1595
|
+
sets :url
|
|
1596
|
+
|
|
1597
|
+
emits "TicketFiled"
|
|
1598
|
+
end
|
|
1599
|
+
|
|
1600
|
+
command "Refused" do
|
|
1601
|
+
role "System"
|
|
1602
|
+
goal "Record that the tracker would not take it, and what it said"
|
|
1603
|
+
|
|
1604
|
+
reference_to Ticket
|
|
1605
|
+
attribute :refusal, RefusalText
|
|
1606
|
+
|
|
1607
|
+
sets :refusal
|
|
1608
|
+
sets :attempts, increment: { value: 1 }
|
|
1609
|
+
|
|
1610
|
+
emits "TicketFilingRefused"
|
|
1611
|
+
end
|
|
1612
|
+
|
|
1613
|
+
# A policy re-enters its target with the event payload verbatim, so `Retry` must accept
|
|
1614
|
+
# `refusal` or be refused outright. It is set rather than dropped: an argument taken and
|
|
1615
|
+
# ignored is exactly what this chapter hunts.
|
|
1616
|
+
command "Retry" do
|
|
1617
|
+
role "System"
|
|
1618
|
+
goal "Ask again, up to the limit, carrying what was refused"
|
|
1619
|
+
|
|
1620
|
+
reference_to Ticket
|
|
1621
|
+
attribute :refusal, RefusalText
|
|
1622
|
+
|
|
1623
|
+
given("a retry is still allowed") { attempts.value < max_attempts.value }
|
|
1624
|
+
|
|
1625
|
+
sets :refusal
|
|
1626
|
+
|
|
1627
|
+
emits "TicketRetried"
|
|
1628
|
+
end
|
|
1629
|
+
|
|
1630
|
+
command "Abandon" do
|
|
1631
|
+
role "QA engineer"
|
|
1632
|
+
goal "Stop asking, and put it in front of a person"
|
|
1633
|
+
|
|
1634
|
+
reference_to Ticket
|
|
1635
|
+
emits "TicketFilingAbandoned"
|
|
1636
|
+
end
|
|
1637
|
+
|
|
1638
|
+
command "Close" do
|
|
1639
|
+
role "QA engineer"
|
|
1640
|
+
goal "Record that the issue is done with"
|
|
1641
|
+
|
|
1642
|
+
reference_to Ticket
|
|
1643
|
+
emits "TicketClosed"
|
|
1644
|
+
end
|
|
1645
|
+
|
|
1646
|
+
query "Proposed" do
|
|
1647
|
+
description "Tickets that already carry a pull request — a fix exists, so more work filed against them is duplicated effort."
|
|
1648
|
+
where(:"pull_request.value" => { ne: "" })
|
|
1649
|
+
order_by :reference
|
|
1650
|
+
end
|
|
1651
|
+
|
|
1652
|
+
query "Raised" do
|
|
1653
|
+
description "Composed and not sent. Nothing will happen to these until somebody submits one."
|
|
1654
|
+
where(status: "raised")
|
|
1655
|
+
order_by :reference
|
|
1656
|
+
end
|
|
1657
|
+
|
|
1658
|
+
query "Submitting" do
|
|
1659
|
+
description "Sent and not yet answered. A row that sticks here is a call whose outcome nobody recorded — look at the tracker before asking again, or a retry files a duplicate."
|
|
1660
|
+
where(status: "submitting")
|
|
1661
|
+
order_by :reference
|
|
1662
|
+
end
|
|
1663
|
+
|
|
1664
|
+
query "Filed" do
|
|
1665
|
+
description "Open issues this practice is responsible for, out in the world."
|
|
1666
|
+
where(status: "filed")
|
|
1667
|
+
order_by :number
|
|
1668
|
+
end
|
|
1669
|
+
|
|
1670
|
+
query "Refused" do
|
|
1671
|
+
description "The tracker said no, and what it said. Retried automatically until the cap."
|
|
1672
|
+
where(status: "refused")
|
|
1673
|
+
order_by :reference
|
|
1674
|
+
end
|
|
1675
|
+
|
|
1676
|
+
query "Abandoned" do
|
|
1677
|
+
description "Asked three times, refused three times. Every row belongs in the report, because nothing automatic will touch it again."
|
|
1678
|
+
where(status: "abandoned")
|
|
1679
|
+
order_by :reference
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
query "ForBug" do
|
|
1683
|
+
description "Every ticket ever raised about one bug — the duplicate check, before a second is raised."
|
|
1684
|
+
attribute :bug_id, BugRef
|
|
1685
|
+
where(bug: :bug_id)
|
|
1686
|
+
order_by :reference
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
# A dotted where hops the reference because this cannot be a `given`: `bug.status` does not
|
|
1690
|
+
# resolve inside a predicate. The rule is surfaced, not enforced.
|
|
1691
|
+
query "RestingOnUnpaused" do
|
|
1692
|
+
description "Tickets about a bug nobody has stopped on. Raise an issue only for something you could not fix yourself — this is the list where that gets skipped."
|
|
1693
|
+
where(status: { in: "raised,filed" })
|
|
1694
|
+
where(:"bug/status" => { in: "logged,investigating" })
|
|
1695
|
+
order_by :reference
|
|
1696
|
+
end
|
|
1697
|
+
|
|
1698
|
+
query "RestingOnFixed" do
|
|
1699
|
+
description "Tickets about a bug this repository went on to fix itself. Each one owes the outside world a Close."
|
|
1700
|
+
where(status: "filed")
|
|
1701
|
+
where(:"bug/status" => { in: "fixed,verified" })
|
|
1702
|
+
order_by :number
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
query "All" do
|
|
1706
|
+
description "Every ticket ever raised."
|
|
1707
|
+
order_by :reference
|
|
1708
|
+
end
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
# A PR opened for a Bug's own fix, recorded at open so `hecks quality_control check_pull_requests` looks it up by number.
|
|
1712
|
+
# Not named `PullRequest`: `Ticket::PullRequest` exists and aggregate names install on Object.
|
|
1713
|
+
aggregate "Patch" do
|
|
1714
|
+
description "One pull request opened for a Bug's own fix — number, branch, and the exact commit that makes checking its CI status a lookup instead of a guess."
|
|
1715
|
+
|
|
1716
|
+
reference_to Bug
|
|
1717
|
+
|
|
1718
|
+
identified_by :number
|
|
1719
|
+
|
|
1720
|
+
attribute :number, PatchNumber
|
|
1721
|
+
attribute :url, PatchUrl
|
|
1722
|
+
attribute :branch, PatchBranch
|
|
1723
|
+
attribute :commit, CommitRef
|
|
1724
|
+
attribute :title, PatchTitle
|
|
1725
|
+
# Makes `PR_CAP_PER_DAY` countable (`OpenedSince`). `default: 0` hydrates older rows at the
|
|
1726
|
+
# epoch, which the cap never counts.
|
|
1727
|
+
attribute :opened_at, Instant
|
|
1728
|
+
|
|
1729
|
+
value_object "Instant" do
|
|
1730
|
+
attribute :value, Integer, default: 0
|
|
1731
|
+
invariant("an instant is not before the epoch") { !value.negative? }
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
value_object "PatchNumber" do
|
|
1735
|
+
attribute :value, Integer
|
|
1736
|
+
invariant("a patch is numbered") { value.positive? }
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
value_object "PatchUrl" do
|
|
1740
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1741
|
+
invariant("a patch says where it lives") { !value.to_s.empty? }
|
|
1742
|
+
end
|
|
1743
|
+
|
|
1744
|
+
value_object "PatchBranch" do
|
|
1745
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1746
|
+
invariant("a patch says which branch it is on") { !value.to_s.empty? }
|
|
1747
|
+
end
|
|
1748
|
+
|
|
1749
|
+
# Recorded at `Open` so the commit is in hand when the PR needs checking, as `Bug.commit` is.
|
|
1750
|
+
value_object "CommitRef" do
|
|
1751
|
+
attribute :value, String, pattern: '^[0-9a-fA-F]{7,40}$'
|
|
1752
|
+
end
|
|
1753
|
+
|
|
1754
|
+
value_object "PatchTitle" do
|
|
1755
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1756
|
+
invariant("a patch is titled") { !value.to_s.empty? }
|
|
1757
|
+
end
|
|
1758
|
+
|
|
1759
|
+
value_object "BugRef" do
|
|
1760
|
+
attribute :value, String
|
|
1761
|
+
end
|
|
1762
|
+
|
|
1763
|
+
# Both endings are terminal: a merged PR does not reopen, and neither does the ledger's memory
|
|
1764
|
+
# of a closed one.
|
|
1765
|
+
lifecycle :status, default: "opened" do
|
|
1766
|
+
transition "Merge" => "merged", from: "opened"
|
|
1767
|
+
transition "Close" => "closed", from: "opened"
|
|
1768
|
+
end
|
|
1769
|
+
|
|
1770
|
+
# Dispatched only by `hecks quality_control patch.open --bug`, which reads the facts back from `gh pr view`, so
|
|
1771
|
+
# the record is written by the process that made it true. `now` is that script's clock read.
|
|
1772
|
+
# The script asks `dry_run?` of this command before it opens anything, so the two `given`s are
|
|
1773
|
+
# the rules for opening a PR at all: the branch is one this practice recognises as its own, and
|
|
1774
|
+
# the bug it fixes is fixed. The prefix repeats `BRANCH_PREFIX` as a literal, as `WaivedBy`
|
|
1775
|
+
# repeats `AUTOMATED_ENGINEER`; `spec/quality_control_spec.rb` pins the two equal.
|
|
1776
|
+
command "Open" do
|
|
1777
|
+
role "QA engineer"
|
|
1778
|
+
goal "Record that a fix has been opened as a pull request, the moment its number, branch and commit are already known"
|
|
1779
|
+
|
|
1780
|
+
reference_to Bug
|
|
1781
|
+
attribute :number, PatchNumber
|
|
1782
|
+
attribute :url, PatchUrl
|
|
1783
|
+
attribute :branch, PatchBranch
|
|
1784
|
+
attribute :commit, CommitRef
|
|
1785
|
+
attribute :title, PatchTitle
|
|
1786
|
+
attribute :now, Instant
|
|
1787
|
+
|
|
1788
|
+
given("the branch is one this practice recognises as its own") { branch.value.start_with?("qa/") }
|
|
1789
|
+
given("the bug is fixed") { bug.status == "fixed" }
|
|
1790
|
+
|
|
1791
|
+
sets :opened_at, to: :now
|
|
1792
|
+
|
|
1793
|
+
emits "PatchOpened"
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
command "Merge" do
|
|
1797
|
+
role "QA engineer"
|
|
1798
|
+
goal "Record that GitHub merged it"
|
|
1799
|
+
|
|
1800
|
+
reference_to Patch
|
|
1801
|
+
emits "PatchMerged"
|
|
1802
|
+
end
|
|
1803
|
+
|
|
1804
|
+
command "Close" do
|
|
1805
|
+
role "QA engineer"
|
|
1806
|
+
goal "Record that GitHub closed it without merging"
|
|
1807
|
+
|
|
1808
|
+
reference_to Patch
|
|
1809
|
+
emits "PatchClosed"
|
|
1810
|
+
end
|
|
1811
|
+
|
|
1812
|
+
# The worklist for `hecks quality_control check_pull_requests`: ask `gh` about each row's exact number, never search.
|
|
1813
|
+
query "Open" do
|
|
1814
|
+
description "Every PR we've opened that's still open, by number — hecks quality_control check_pull_requests's own worklist. For each of these, ask gh for exactly this number's CI status; nothing here is a guess."
|
|
1815
|
+
where(status: "opened")
|
|
1816
|
+
order_by :number
|
|
1817
|
+
end
|
|
1818
|
+
|
|
1819
|
+
query "ForBug" do
|
|
1820
|
+
description "Every patch ever opened for one bug — the duplicate check, the same shape Ticket.ForBug already gives for an issue."
|
|
1821
|
+
attribute :bug_id, BugRef
|
|
1822
|
+
where(bug: :bug_id)
|
|
1823
|
+
order_by :number
|
|
1824
|
+
end
|
|
1825
|
+
|
|
1826
|
+
# `hecks quality_control patch.open` compares this and `Improvement.OpenedSince` counts, from local midnight,
|
|
1827
|
+
# against `PR_CAP_PER_DAY`. `gte` on a bound argument, the `Account.AtOrAbove` shape.
|
|
1828
|
+
query "OpenedSince" do
|
|
1829
|
+
description "Every patch opened at or after one instant, in number order — what today's count against PR_CAP_PER_DAY is read from."
|
|
1830
|
+
attribute :since, Instant
|
|
1831
|
+
where(:"opened_at.value" => { gte: :since })
|
|
1832
|
+
order_by :number
|
|
1833
|
+
end
|
|
1834
|
+
|
|
1835
|
+
query "All" do
|
|
1836
|
+
description "Every patch ever opened — what a runner reads back to reconcile against GitHub's own list."
|
|
1837
|
+
order_by :number
|
|
1838
|
+
end
|
|
1839
|
+
end
|
|
1840
|
+
|
|
1841
|
+
# A PR for deliberate, non-bugfix work: a sibling of `Patch`, because `Bug` means "proven wrong"
|
|
1842
|
+
# and `Patch.Open` keeps its required `reference_to Bug`.
|
|
1843
|
+
# Born `opened`, with no investigating state: the work was decided before any code moved.
|
|
1844
|
+
#
|
|
1845
|
+
# `Open` and `Land` are separate so `ImprovementCiWatch` can `starts_on` the one event that is
|
|
1846
|
+
# also re-dispatched after a `Regress`; folding `commit` into `Open` would refuse a re-land
|
|
1847
|
+
# as `AlreadyExists`.
|
|
1848
|
+
aggregate "Improvement" do
|
|
1849
|
+
description "One pull request landed for deliberate, non-bugfix work — a domain-modeling addition, a tool, an aggregate this very practice needed — number, branch, and the exact commit that makes watching its CI a lookup instead of a guess, the same discipline `Patch` already keeps for a bug's own fix."
|
|
1850
|
+
|
|
1851
|
+
# Optional, set only at `Open`: not all deliberate work began as a cited `Angle`, and forcing
|
|
1852
|
+
# one would invent a lead after the fact.
|
|
1853
|
+
reference_to Angle, optional: true
|
|
1854
|
+
|
|
1855
|
+
identified_by :number
|
|
1856
|
+
|
|
1857
|
+
attribute :number, ImprovementNumber
|
|
1858
|
+
attribute :url, ImprovementUrl
|
|
1859
|
+
attribute :branch, ImprovementBranch
|
|
1860
|
+
attribute :commit, CommitRef
|
|
1861
|
+
attribute :title, ImprovementTitle
|
|
1862
|
+
# See `Patch.opened_at`: the same field, for the same cap.
|
|
1863
|
+
attribute :opened_at, Instant
|
|
1864
|
+
|
|
1865
|
+
value_object "Instant" do
|
|
1866
|
+
attribute :value, Integer, default: 0
|
|
1867
|
+
invariant("an instant is not before the epoch") { !value.negative? }
|
|
1868
|
+
end
|
|
1869
|
+
|
|
1870
|
+
value_object "ImprovementNumber" do
|
|
1871
|
+
attribute :value, Integer
|
|
1872
|
+
invariant("an improvement is numbered") { value.positive? }
|
|
1873
|
+
end
|
|
1874
|
+
|
|
1875
|
+
value_object "ImprovementUrl" do
|
|
1876
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1877
|
+
invariant("an improvement says where it lives") { !value.to_s.empty? }
|
|
1878
|
+
end
|
|
1879
|
+
|
|
1880
|
+
value_object "ImprovementBranch" do
|
|
1881
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1882
|
+
invariant("an improvement says which branch it is on") { !value.to_s.empty? }
|
|
1883
|
+
end
|
|
1884
|
+
|
|
1885
|
+
# Redeclared per aggregate. Absent until `Land`: there is nothing to watch until a commit
|
|
1886
|
+
# exists.
|
|
1887
|
+
value_object "CommitRef" do
|
|
1888
|
+
attribute :value, String, pattern: '^[0-9a-fA-F]{7,40}$'
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
value_object "ImprovementTitle" do
|
|
1892
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
1893
|
+
invariant("an improvement is titled") { !value.to_s.empty? }
|
|
1894
|
+
end
|
|
1895
|
+
|
|
1896
|
+
value_object "AngleRef" do
|
|
1897
|
+
attribute :value, String
|
|
1898
|
+
end
|
|
1899
|
+
|
|
1900
|
+
# `Merge` and `Close` are reachable from "opened" too: GitHub does not wait for a `Land`, and a
|
|
1901
|
+
# proof-only PR can be closed while still "opened" (`retire_if_settled?` hit
|
|
1902
|
+
# `LifecycleRefused`).
|
|
1903
|
+
lifecycle :status, default: "opened" do
|
|
1904
|
+
transition "Land" => "landed", from: ["opened", "needs_fix"]
|
|
1905
|
+
transition "Regress" => "needs_fix", from: "landed"
|
|
1906
|
+
transition "Merge" => "merged", from: ["opened", "landed", "needs_fix"]
|
|
1907
|
+
transition "Close" => "closed", from: ["opened", "landed", "needs_fix"]
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
# Dispatched by `hecks quality_control patch.open --improvement`, which then dispatches `Land` in the same run so
|
|
1911
|
+
# `ImprovementCiWatch` is watching before the script exits. The `given`s are `Patch.Open`'s
|
|
1912
|
+
# rules with the angle in place of the bug: a lead is picked up (`investigating`) before
|
|
1913
|
+
# something is built from it, and work with no angle behind it is admitted.
|
|
1914
|
+
command "Open" do
|
|
1915
|
+
role "QA engineer"
|
|
1916
|
+
goal "Record that a deliberate, non-bugfix piece of work has been opened as a pull request, before its own commit is known to be worth watching"
|
|
1917
|
+
|
|
1918
|
+
reference_to Angle, optional: true
|
|
1919
|
+
attribute :number, ImprovementNumber
|
|
1920
|
+
attribute :url, ImprovementUrl
|
|
1921
|
+
attribute :branch, ImprovementBranch
|
|
1922
|
+
attribute :title, ImprovementTitle
|
|
1923
|
+
attribute :now, Instant
|
|
1924
|
+
|
|
1925
|
+
given("the branch is one this practice recognises as its own") { branch.value.start_with?("qa/") }
|
|
1926
|
+
given("the angle is under investigation") { angle.unset? || angle.status == "investigating" }
|
|
1927
|
+
|
|
1928
|
+
sets :opened_at, to: :now
|
|
1929
|
+
|
|
1930
|
+
emits "ImprovementOpened"
|
|
1931
|
+
end
|
|
1932
|
+
|
|
1933
|
+
# The event `ImprovementCiWatch` starts on, and the one a re-landing fires again.
|
|
1934
|
+
# `number` is restated so the event carries it into the saga's memory (see `Bug::Fix`).
|
|
1935
|
+
command "Land" do
|
|
1936
|
+
role "QA engineer"
|
|
1937
|
+
goal "Record the commit this PR is now at, so CI can be watched for it — whether that is the commit it was opened with, or a fresh one pushed after a NeedsFix"
|
|
1938
|
+
|
|
1939
|
+
reference_to Improvement
|
|
1940
|
+
attribute :number, ImprovementNumber
|
|
1941
|
+
attribute :commit, CommitRef
|
|
1942
|
+
|
|
1943
|
+
sets :commit
|
|
1944
|
+
|
|
1945
|
+
emits "ImprovementLanded"
|
|
1946
|
+
end
|
|
1947
|
+
|
|
1948
|
+
# The same word `Bug` uses; `ImprovementCiWatch` is the only dispatcher.
|
|
1949
|
+
command "Regress" do
|
|
1950
|
+
role "QA engineer"
|
|
1951
|
+
goal "Put landed work back in front of somebody when its own commit stops holding"
|
|
1952
|
+
|
|
1953
|
+
reference_to Improvement
|
|
1954
|
+
emits "ImprovementRegressed"
|
|
1955
|
+
end
|
|
1956
|
+
|
|
1957
|
+
# Both endings are terminal and reachable from `needs_fix`: GitHub can merge or close a PR
|
|
1958
|
+
# whatever this ledger's CI watch last heard.
|
|
1959
|
+
command "Merge" do
|
|
1960
|
+
role "QA engineer"
|
|
1961
|
+
goal "Record that GitHub merged it"
|
|
1962
|
+
|
|
1963
|
+
reference_to Improvement
|
|
1964
|
+
emits "ImprovementMerged"
|
|
1965
|
+
end
|
|
1966
|
+
|
|
1967
|
+
command "Close" do
|
|
1968
|
+
role "QA engineer"
|
|
1969
|
+
goal "Record that GitHub closed it without merging"
|
|
1970
|
+
|
|
1971
|
+
reference_to Improvement
|
|
1972
|
+
emits "ImprovementClosed"
|
|
1973
|
+
end
|
|
1974
|
+
|
|
1975
|
+
query "Open" do
|
|
1976
|
+
description "Every PR we've landed for deliberate, non-bugfix work that's still open, by number — hecks quality_control check_pull_requests's own second worklist, the same shape Patch.Open already gives for a bug's own fix. For each of these, ask gh for exactly this number's CI status; nothing here is a guess."
|
|
1977
|
+
where(status: { in: "opened,landed,needs_fix" })
|
|
1978
|
+
order_by :number
|
|
1979
|
+
end
|
|
1980
|
+
|
|
1981
|
+
query "NeedsFix" do
|
|
1982
|
+
description "Landed work whose own commit came back red — what ImprovementCiWatch put back in front of somebody, and the list a human or agent clears with a fresh Land."
|
|
1983
|
+
where(status: "needs_fix")
|
|
1984
|
+
order_by :number
|
|
1985
|
+
end
|
|
1986
|
+
|
|
1987
|
+
query "ForAngle" do
|
|
1988
|
+
description "Every improvement ever landed against one angle — the duplicate check, the same shape Patch.ForBug already gives for a bug."
|
|
1989
|
+
attribute :angle_id, AngleRef
|
|
1990
|
+
where(angle: :angle_id)
|
|
1991
|
+
order_by :number
|
|
1992
|
+
end
|
|
1993
|
+
|
|
1994
|
+
# `Patch.OpenedSince`'s twin: the cap counts both kinds of PR.
|
|
1995
|
+
query "OpenedSince" do
|
|
1996
|
+
description "Every improvement opened at or after one instant, in number order — the other half of today's count against PR_CAP_PER_DAY."
|
|
1997
|
+
attribute :since, Instant
|
|
1998
|
+
where(:"opened_at.value" => { gte: :since })
|
|
1999
|
+
order_by :number
|
|
2000
|
+
end
|
|
2001
|
+
|
|
2002
|
+
query "All" do
|
|
2003
|
+
description "Every improvement ever landed — what a runner reads back to reconcile against GitHub's own list."
|
|
2004
|
+
order_by :number
|
|
2005
|
+
end
|
|
2006
|
+
end
|
|
2007
|
+
|
|
2008
|
+
# One CI run against one commit. A clearance is minted per commit, so a later commit is stale
|
|
2009
|
+
# by simply having none.
|
|
2010
|
+
aggregate "Clearance" do
|
|
2011
|
+
description "One CI run against one commit, and therefore the only answer to whether that commit is safe to ship."
|
|
2012
|
+
|
|
2013
|
+
identified_by :commit
|
|
2014
|
+
|
|
2015
|
+
attribute :commit, CommitRef
|
|
2016
|
+
attribute :summary, RunSummary
|
|
2017
|
+
|
|
2018
|
+
value_object "CommitRef" do
|
|
2019
|
+
attribute :value, String, pattern: '^[0-9a-fA-F]{7,40}$'
|
|
2020
|
+
end
|
|
2021
|
+
|
|
2022
|
+
value_object "RunSummary" do
|
|
2023
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
2024
|
+
invariant("a run says what it ran and what came back") { !value.to_s.empty? }
|
|
2025
|
+
end
|
|
2026
|
+
|
|
2027
|
+
lifecycle :status, default: "running" do
|
|
2028
|
+
transition "Passed" => "green", from: "running"
|
|
2029
|
+
transition "Failed" => "red", from: "running"
|
|
2030
|
+
end
|
|
2031
|
+
|
|
2032
|
+
command "Start" do
|
|
2033
|
+
role "QA engineer"
|
|
2034
|
+
goal "Put a commit through CI"
|
|
2035
|
+
|
|
2036
|
+
attribute :commit, CommitRef
|
|
2037
|
+
|
|
2038
|
+
emits "ClearanceStarted"
|
|
2039
|
+
end
|
|
2040
|
+
|
|
2041
|
+
command "Passed" do
|
|
2042
|
+
role "System"
|
|
2043
|
+
goal "Record that the suite went green against this commit"
|
|
2044
|
+
|
|
2045
|
+
reference_to Clearance
|
|
2046
|
+
attribute :summary, RunSummary
|
|
2047
|
+
|
|
2048
|
+
sets :summary
|
|
2049
|
+
|
|
2050
|
+
emits "ClearanceGiven"
|
|
2051
|
+
end
|
|
2052
|
+
|
|
2053
|
+
command "Failed" do
|
|
2054
|
+
role "System"
|
|
2055
|
+
goal "Record that the suite went red against this commit"
|
|
2056
|
+
|
|
2057
|
+
# `refusal`, not `summary`: a policy re-enters with the answering event's payload verbatim,
|
|
2058
|
+
# and the runtime names what a refused ask returned `refusal`.
|
|
2059
|
+
reference_to Clearance
|
|
2060
|
+
attribute :refusal, RunSummary
|
|
2061
|
+
|
|
2062
|
+
sets :summary, to: :refusal
|
|
2063
|
+
|
|
2064
|
+
emits "ClearanceRefused"
|
|
2065
|
+
end
|
|
2066
|
+
|
|
2067
|
+
# Name the commit you are about to ship; an empty answer means nothing has vouched for it.
|
|
2068
|
+
query "For" do
|
|
2069
|
+
description "Is this exact commit cleared? Ask with the sha you are about to deploy; an empty answer is a no, including when nobody has run CI on it at all."
|
|
2070
|
+
attribute :commit, CommitRef
|
|
2071
|
+
where(commit: :commit)
|
|
2072
|
+
where(status: "green")
|
|
2073
|
+
order_by :commit
|
|
2074
|
+
end
|
|
2075
|
+
|
|
2076
|
+
query "Red" do
|
|
2077
|
+
description "Commits CI refused, with what it said."
|
|
2078
|
+
where(status: "red")
|
|
2079
|
+
order_by :commit
|
|
2080
|
+
end
|
|
2081
|
+
|
|
2082
|
+
query "All" do
|
|
2083
|
+
description "Every CI run ever recorded, one per commit."
|
|
2084
|
+
order_by :commit
|
|
2085
|
+
end
|
|
2086
|
+
|
|
2087
|
+
# An answer that is one document of text, exactly as the tool that produced it wrote it.
|
|
2088
|
+
value_object "Document" do
|
|
2089
|
+
attribute :text, String
|
|
2090
|
+
end
|
|
2091
|
+
|
|
2092
|
+
# ClearanceTools answers this by running the script's own command as a child process
|
|
2093
|
+
# (lib/hecks/quality_control/adapters/clearance_tools.rb). The command starts the clearances and
|
|
2094
|
+
# lets the CI port's policies record each verdict; a newly red PR ends it with a report.
|
|
2095
|
+
query "CheckPullRequests" do
|
|
2096
|
+
description "Check CI for every PR the ledger tracks as open, a Patch or an Improvement: retire one GitHub reports merged or closed, and start a clearance for each unsettled head commit."
|
|
2097
|
+
returns Document
|
|
2098
|
+
end
|
|
2099
|
+
end
|
|
2100
|
+
|
|
2101
|
+
# Puts `Bug.Fix`'s commit and CI's `Clearance` for it back together, dispatching `Bug.Regress` on
|
|
2102
|
+
# red. Correlates by commit, which `BugFixed` carries and `Clearance` is identified by.
|
|
2103
|
+
# `ClearanceRefused` has no `ends_on`, as in banking's `ExternalSettlement`: an instance left in
|
|
2104
|
+
# "regressed" costs nothing.
|
|
2105
|
+
process_manager "BugCiWatch" do
|
|
2106
|
+
correlates_by :"commit.value"
|
|
2107
|
+
starts_on Bug::BugFixed
|
|
2108
|
+
ends_on Clearance::ClearanceGiven
|
|
2109
|
+
|
|
2110
|
+
# `bug:` is Bug's own reference key (`Naming.reference_key("Bug")`); the value is the
|
|
2111
|
+
# `reference` that `BugFixed` carries into saga memory.
|
|
2112
|
+
transition Clearance::ClearanceRefused => "regressed", from: "watching" do
|
|
2113
|
+
dispatch Bug::Regress, with: { bug: :reference }
|
|
2114
|
+
end
|
|
2115
|
+
end
|
|
2116
|
+
|
|
2117
|
+
# The `BugCiWatch` mechanism aimed at deliberate work, dispatching `Improvement::Regress`. That
|
|
2118
|
+
# lands in `needs_fix`, not an investigating state; a fresh `Improvement.Land` clears it and
|
|
2119
|
+
# starts a new watch.
|
|
2120
|
+
process_manager "ImprovementCiWatch" do
|
|
2121
|
+
correlates_by :"commit.value"
|
|
2122
|
+
starts_on Improvement::ImprovementLanded
|
|
2123
|
+
ends_on Clearance::ClearanceGiven
|
|
2124
|
+
|
|
2125
|
+
# `improvement:` is the reference key; the value is `:number`, which `ImprovementLanded`
|
|
2126
|
+
# carries.
|
|
2127
|
+
transition Clearance::ClearanceRefused => "regressed", from: "watching" do
|
|
2128
|
+
dispatch Improvement::Regress, with: { improvement: :number }
|
|
2129
|
+
end
|
|
2130
|
+
end
|
|
2131
|
+
|
|
2132
|
+
# Rootless reports: `group_by` gives each group's size, which a query's rows cannot, and they
|
|
2133
|
+
# centre on no record so they take no id.
|
|
2134
|
+
|
|
2135
|
+
# `withdrawn` beside `verified` says whether the practice finds real things or cries wolf.
|
|
2136
|
+
read_model "BugsByStatus" do
|
|
2137
|
+
description "Every bug sorted into what became of it — the tally. The share that is withdrawn, beside the share verified, is the honesty check on the whole practice."
|
|
2138
|
+
include Bug
|
|
2139
|
+
group_by :status, :reference
|
|
2140
|
+
end
|
|
2141
|
+
|
|
2142
|
+
read_model "BugsBySubmitter" do
|
|
2143
|
+
description "Every bug sorted by who reported it, then by its own reference — what each agent has actually found."
|
|
2144
|
+
include Bug
|
|
2145
|
+
group_by :submitter, :reference
|
|
2146
|
+
end
|
|
2147
|
+
|
|
2148
|
+
read_model "BugsByDisposition" do
|
|
2149
|
+
description "Every bug sorted by the call its finder made — self-contained, bigger, or not yet decided — beside its reference. The fix-rate, and the judgment still owed, at a glance."
|
|
2150
|
+
include Bug
|
|
2151
|
+
group_by :disposition, :reference
|
|
2152
|
+
end
|
|
2153
|
+
|
|
2154
|
+
read_model "TicketsByStatus" do
|
|
2155
|
+
description "Every ticket sorted by where it got to — stuck filings and abandoned ones are only visible as a count."
|
|
2156
|
+
include Ticket
|
|
2157
|
+
group_by :status, :reference
|
|
2158
|
+
end
|
|
2159
|
+
|
|
2160
|
+
read_model "ImprovementsByStatus" do
|
|
2161
|
+
description "Every improvement sorted into what became of it — landed, needing a fix, merged, or closed, at a glance."
|
|
2162
|
+
include Improvement
|
|
2163
|
+
group_by :status, :number
|
|
2164
|
+
end
|
|
2165
|
+
|
|
2166
|
+
# `Ticket.Close` is the only way a ticket leaves `filed`, and only the outside can know it
|
|
2167
|
+
# happened, so the fact arrives by whichever route reaches us first.
|
|
2168
|
+
policy "CloseWhenClosedUpstream" do
|
|
2169
|
+
on "Ticket.IssueClosedUpstream"
|
|
2170
|
+
trigger Ticket::Close
|
|
2171
|
+
end
|
|
2172
|
+
|
|
2173
|
+
# The adapter decides by answering or refusing, the only branch a policy can express.
|
|
2174
|
+
policy "ClearOnPass" do
|
|
2175
|
+
on "Clearance.SuitePassed"
|
|
2176
|
+
trigger Clearance::Passed
|
|
2177
|
+
end
|
|
2178
|
+
|
|
2179
|
+
policy "RefuseOnFail" do
|
|
2180
|
+
on "Clearance.SuiteFailed"
|
|
2181
|
+
trigger Clearance::Failed
|
|
2182
|
+
end
|
|
2183
|
+
|
|
2184
|
+
# `Raise` and the call are split so a crash leaves a `Ticket.Raised` row rather than a lie.
|
|
2185
|
+
policy "FileWhenSubmitted" do
|
|
2186
|
+
on "Ticket.TicketSubmitted"
|
|
2187
|
+
trigger Ticket::IssueTracker::File
|
|
2188
|
+
end
|
|
2189
|
+
|
|
2190
|
+
policy "RecordTheIssue" do
|
|
2191
|
+
on "Ticket.IssueFiled"
|
|
2192
|
+
trigger Ticket::Filed
|
|
2193
|
+
end
|
|
2194
|
+
|
|
2195
|
+
policy "RecordTheRefusal" do
|
|
2196
|
+
on "Ticket.IssueFilingRefused"
|
|
2197
|
+
trigger Ticket::Refused
|
|
2198
|
+
end
|
|
2199
|
+
|
|
2200
|
+
# The cap is the `given` on `Retry`, not here: a policy holds no state and would ask forever.
|
|
2201
|
+
policy "AskAgain" do
|
|
2202
|
+
on "Ticket.TicketFilingRefused"
|
|
2203
|
+
trigger Ticket::Retry
|
|
2204
|
+
end
|
|
2205
|
+
|
|
2206
|
+
policy "AskOnceMore" do
|
|
2207
|
+
on "Ticket.TicketRetried"
|
|
2208
|
+
trigger Ticket::IssueTracker::File
|
|
2209
|
+
end
|
|
2210
|
+
|
|
2211
|
+
# Nothing reacts to `TicketRaised` on purpose: a ticket waits until somebody submits it.
|
|
2212
|
+
|
|
2213
|
+
# The rotation's one automatic reaction. Releasing on `SweepConcluded` cannot work: that event
|
|
2214
|
+
# carries no target id, `next_streak` or yield score, so `hecks quality_control ask run` still releases.
|
|
2215
|
+
# `on Sweep::CheckSurprised`, not `Sweep::Check::...`: an entity command's event is stamped with
|
|
2216
|
+
# its parent aggregate. `Check.Surprised` restates `target` so the id rides in.
|
|
2217
|
+
policy "SuspendOnSurprise" do
|
|
2218
|
+
on Sweep::CheckSurprised
|
|
2219
|
+
trigger Target::Suspend, with: {
|
|
2220
|
+
target: :target,
|
|
2221
|
+
reason: { value: "a check surprised — a person releases with hecks quality_control ask run <target> --release --notes" }
|
|
2222
|
+
}
|
|
2223
|
+
end
|
|
2224
|
+
end
|