syncade 0.7.3__tar.gz → 0.7.6__tar.gz
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.
- {syncade-0.7.3/src/syncade.egg-info → syncade-0.7.6}/PKG-INFO +1 -1
- {syncade-0.7.3 → syncade-0.7.6}/pyproject.toml +1 -1
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/__init__.py +1 -1
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/metrics_mode.py +18 -14
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/update_mode.py +51 -56
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config.py +6 -1
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/metrics/aggregate.py +15 -12
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/metrics/findings.py +129 -71
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/metrics/schema.py +35 -17
- {syncade-0.7.3 → syncade-0.7.6/src/syncade.egg-info}/PKG-INFO +1 -1
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade.egg-info/SOURCES.txt +1 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_update_post_condition.py +3 -123
- syncade-0.7.6/tests/cli/test_version_probe.py +158 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/conftest.py +11 -12
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_aggregate.py +10 -2
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_consensus_report.py +38 -18
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_findings_capture.py +209 -78
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_schema.py +92 -7
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_unknown_panel.py +75 -15
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_worktree_base_isolation.py +2 -2
- {syncade-0.7.3 → syncade-0.7.6}/LICENSE +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/MANIFEST.in +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/README.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/docs/config-reference.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/docs/how-to-use.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/setup.cfg +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/__main__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/anthropic.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/base.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/fake.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/fake_common.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/openai.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/openai_parsing.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/producer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/producer_anthropic.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/producer_openai.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/adapters/registry.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/auth_check.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/auth_preflight.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/base_resolution.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/billing.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/checks_config.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/auth_gate.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/config_keys.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/config_list.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/config_menu_rows.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/config_mode.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/config_overrides.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/config_tui.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/doctor_mode.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/gc_mode.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/install_skill.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/modes.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/parser.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/parser_types.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/paths.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/preflight_paths.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/resolve.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/resume_mode.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/skill_status.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/toml_writer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/update_notice.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/cli/validate.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config_auth.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config_cold.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config_gc.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config_loader.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config_loop.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config_producer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config_retry.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config_types.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/config_update.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/diff_filter.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/dispatcher.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/doctor.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/doctor_env.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/doctor_preview.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/doctor_types.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/exit_codes.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/findings.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/findings_json.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/gc.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/gc_execute.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/gc_protection.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/gc_types.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/gc_worktrees.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/git_object_id.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/git_preconditions.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/logging.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/metrics/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/_runs_dir.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/branch_advance.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/branch_guard.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/budget.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/escalation_coverage.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/loop.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/loop_finalize.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/loop_preflight.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/loop_resume.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/loop_rmtree.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/loop_round_step.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/prior_round.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/producer_phase.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/results.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/resume.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/resume_load.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/resume_plan.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/resume_target.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/resume_types.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/reviewer_template_failure.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/round.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/round_checks.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/round_no_changes.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/round_predispatch.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/orchestrator/verdict.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/_atomic.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/_clusters.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/_findings_verdict.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/_markdown.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/_validation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/checks.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/decision_needed.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/findings_md.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/handoff.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/handoff_classify.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/last_reviewed.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/loop_manifest.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/loop_summary.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/loop_summary_text.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/producer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/reviewer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/round_manifest.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/run_init.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/run_summary.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/run_summary_next_steps.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/synth.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/persistence/test_run.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/presets.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/pricing_config.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/process.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/producer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/producer_attempt.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/producer_escalation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/producer_git.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/producer_result.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/prompts.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/prompts_loader.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/retry.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/run_inputs.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/run_status.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/selfcheck.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/skills/claude/README.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/skills/claude/SKILL.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/skills/codex/README.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/skills/codex/SKILL.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/snapshot.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/spec_audit.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/spec_audit_schema.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/spec_draft.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/spec_source.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesis.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesis_clusters.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesis_repair.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesis_schema.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesizer/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesizer/constants.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesizer/driver.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesizer/rendering.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesizer/result.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesizer/validation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/synthesizer/workspace.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/presets/balanced.toml +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/presets/cheap.toml +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/presets/thorough.toml +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/producer.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/reviewer.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/reviewer_adversarial.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/reviewer_codex.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/spec_audit.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/spec_draft.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/templates/synthesizer.md +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/test_runner.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/test_runner_classify.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/transcript.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/update_check.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/usage.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/worktree.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/worktree_env.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade/worktree_paths.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade.egg-info/dependency_links.txt +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade.egg-info/entry_points.txt +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade.egg-info/requires.txt +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/src/syncade.egg-info/top_level.txt +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/_anthropic_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/_openai_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/test_anthropic.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/test_anthropic_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/test_openai.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/test_openai_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/test_producer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/test_producer_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/adapters/test_prompt_transport.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/auth_check/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/auth_check/test_probes.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/auth_check/test_run_auth_check.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/_undo_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_basic_and_mutex.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_cli_library_divergence.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_config_keys.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_config_list.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_config_menu_rows.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_config_mode.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_config_overrides.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_config_tui.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_config_tui_pty.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_documented_exit_codes.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_documented_flags_exist.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_flags.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_gc.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_help_retention_contract.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_mode_flag_guards.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_resume_reports_hard_kill.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_run_and_config.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_run_status_cli.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_scope_openspec_draft.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_toml_writer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_undo_auto_init.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_undo_scope.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_update_mode.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_update_notice.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/cli/test_validate_before_mutate.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_auth_enforcement.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_auth_modes.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_auth_preflight.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_auth_report_scope.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_budget_config.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_cold_actors.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_config_checks.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_config_reference_drift.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_config_schema.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_gc_config.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_global_config_layer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_presets.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_retry_config.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_reviewer_config.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_reviewer_permissions.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/config/test_worktree_base_config.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/dispatcher/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/dispatcher/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/dispatcher/test_dispatch_core.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/dispatcher/test_dispatch_misc.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/dispatcher/test_dispatch_usage.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/doctor/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/doctor/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/doctor/conftest.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/doctor/test_doctor.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/doctor/test_doctor_diff_size.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/doctor/test_doctor_malformed_diff.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/doctor/test_doctor_preview.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/findings/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/findings/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/findings/test_parser_and_fields.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/findings/test_parser_resilience.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/findings/test_verdict_block_selection.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_autoprune.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_execute.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_plan.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_plan_execution_races.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_reaping_scope.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_retention_tiers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_robustness.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_self_orphan_protection.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_two_tier_retention.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/gc/test_worktree_age_bound.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/logging/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/logging/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/logging/test_logging.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/logging/test_logging_summary.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/conftest.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_actor_spend.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_billing_reconciliation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_billing_surfaces_agree.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_budget_metrics.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_cost_honesty.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_metrics_cli.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/metrics/test_metrics_mode.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/_resume_fixtures.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/conftest.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_budget.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_budget_stop_message.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_budget_warning.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_checks_and_recording.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_contracts.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_cross_round.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_cross_round_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_default_branch_guard.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_diff_size_cap.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_dispatch_record.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_empty_diff.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_escalation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_multiround.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_multiround_dirty.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_multiround_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_pipeline.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_provider_usage_limit.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_rename_boundary.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_resume.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_resume_base_oid.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_round_input_boundaries.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_run_layout.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_run_status_wiring.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_snapshot_and_base.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_synth_phase.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_synth_phase_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_synth_provenance.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_synth_validation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_template_robustness.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_termination_reason_drift.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_testleg.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_usage_limit_message.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/orchestrator/test_worktree_cleanup.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_atomic_writes.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_checks_surfacing.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_diff_size_recording.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_findings_md.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_findings_md_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_findings_md_verdict.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_handoff.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_loop_manifest_checks.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_loop_summary.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_loop_summary_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_md_safety.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_next_steps.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_producer_persist.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_reviewer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_round_manifest.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_run_init.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_run_summary.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_run_summary_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_run_summary_producer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_synth_persist.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/persistence/test_test_run.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/producer/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/producer/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/producer/test_producer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/producer/test_producer_confinement.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/producer/test_producer_retry.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/producer/test_producer_run.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/producer/test_producer_timeout.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/producer/test_producer_usage.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/prompts/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/prompts/test_load_template.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/prompts/test_misc.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/prompts/test_producer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/prompts/test_render_synthesizer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/prompts/test_synthesizer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/resume/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/resume/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/resume/test_budget_resume.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/resume/test_checks_phase_failure.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/resume/test_cli_malformed_rehydration.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/resume/test_find_resolve_plan.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/resume/test_plan_resume.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/resume/test_round_trip.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/resume/test_tree_drift_and_escalation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/skills/test_config_section.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/skills/test_install_skill.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/skills/test_install_skill_multi_harness.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/skills/test_install_skill_ownership.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/skills/test_install_skill_refuses.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/skills/test_skill_drift.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/skills/test_skill_status.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_anthropic_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_auth_check_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_codex_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_loop_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_orchestrator_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_producer_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_resume_reap_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_selfcheck_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_synthesizer_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/smoke/test_test_runner_smoke.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/snapshot/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/snapshot/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/snapshot/conftest.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/snapshot/test_snapshot.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/snapshot/test_snapshot_dirty.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/snapshot/test_snapshot_warning.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/snapshot/test_three_dot_diff.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesis/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesis/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesis/test_cluster_repair.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesis/test_clusters.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesis/test_models.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesis/test_models_distinct_keying.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesis/test_parsing.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesizer/test_provenance_fabrication.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/synthesizer/test_provenance_repair.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_adapter_registry.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_adapters_fake.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_auto_init_policy.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_base_resolution.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_check_loc_script.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_checks.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_config_loader.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_cross_model_claim.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_diff_filter.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_diff_filter_binary.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_diff_filter_quoted_paths.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_escalation_coverage.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_exit_codes.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_git_choke_point.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_git_config_isolation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_git_preconditions.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_last_reviewed.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_parse_repair_extra_keys.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_pricing.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_private_identifier_gate.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_process.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_process_teardown.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_producer_escalation.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_regression_acme.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_retry.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_run_status.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_run_status_signals.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_runner/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_runner/test_test_runner.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_runner/test_test_runner_more.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_safety_disclosures_survive_quiet.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_selfcheck.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_spec_audit.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_spec_draft.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_spec_source.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_streamed_capture.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_strip_list_semantics.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_synthesizer.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_synthesizer_passthrough.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_synthesizer_provenance.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_transcript.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_update_check.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_usage.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_usage_persistence.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_workflow_action_pins.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_worktree_env.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/test_worktree_strip_guard.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/worktree/__init__.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/worktree/_helpers.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/worktree/conftest.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/worktree/test_worktree.py +0 -0
- {syncade-0.7.3 → syncade-0.7.6}/tests/worktree/test_worktree_cleanup.py +0 -0
|
@@ -255,11 +255,21 @@ def render_report(conn: sqlite3.Connection, last_n: int | None = None) -> str:
|
|
|
255
255
|
).fetchone()
|
|
256
256
|
actor_rows = _actor_spend_rows(conn)
|
|
257
257
|
incomplete_tokens = _cost_incomplete_tokens(conn)
|
|
258
|
+
manifest_only, unknown_only = conn.execute(
|
|
259
|
+
"SELECT "
|
|
260
|
+
"COALESCE(SUM(CASE WHEN counts_source='manifest' THEN 1 ELSE 0 END), 0), "
|
|
261
|
+
"COALESCE(SUM(CASE WHEN counts_source='unknown' THEN 1 ELSE 0 END), 0) "
|
|
262
|
+
"FROM rounds "
|
|
263
|
+
"WHERE NOT (counts_source='manifest' AND panel_source='recorded' AND panel_size=0 "
|
|
264
|
+
"AND blockers=0 AND minors=0 AND nits=0 AND dismissed=0)"
|
|
265
|
+
).fetchone()
|
|
266
|
+
finding_note = " (lower bounds: incomplete round evidence)" if unknown_only else ""
|
|
258
267
|
|
|
259
268
|
lines = [
|
|
260
269
|
f"syncade metrics — {runs} runs",
|
|
261
270
|
f" ship-rate: {ship}/{runs} ({round(100 * ship / runs)}%)",
|
|
262
|
-
f" findings: {blockers} blockers, {minors} minors, {nits} nits, {dismissed} dismissed"
|
|
271
|
+
f" findings: {blockers} blockers, {minors} minors, {nits} nits, {dismissed} dismissed"
|
|
272
|
+
f"{finding_note}",
|
|
263
273
|
f" rounds: {rounds} total",
|
|
264
274
|
f" handoffs: {handoffs} decisions: {decisions}",
|
|
265
275
|
f" producer: committed={p_committed} stalled={p_stalled}"
|
|
@@ -322,7 +332,7 @@ def render_report(conn: sqlite3.Connection, last_n: int | None = None) -> str:
|
|
|
322
332
|
# OUTSIDE the `if reviewer_rows` block on purpose. Nested inside it, a corpus where EVERY
|
|
323
333
|
# panel is unknown printed no warning at all — the one corpus that most needs it, silent.
|
|
324
334
|
# panel_source='unknown' means no manifest had a `reviewers` key for that round. This is
|
|
325
|
-
# independent of
|
|
335
|
+
# independent of counts_source: a manifest can record finding counts without a reviewer
|
|
326
336
|
# list, and a no-dispatch round (no_changes_to_review) has panel_source='recorded' and
|
|
327
337
|
# panel_size=0 — explicitly empty, not unknown.
|
|
328
338
|
unknown_rounds, total_rounds = conn.execute(
|
|
@@ -334,23 +344,17 @@ def render_report(conn: sqlite3.Connection, last_n: int | None = None) -> str:
|
|
|
334
344
|
f" {unknown_rounds} of {total_rounds} rounds record no reviewer panel "
|
|
335
345
|
f"(no manifest evidence; excluded from consensus)"
|
|
336
346
|
)
|
|
337
|
-
manifest_only, unknown_only = conn.execute(
|
|
338
|
-
"SELECT "
|
|
339
|
-
"COALESCE(SUM(CASE WHEN blockers_source='manifest' THEN 1 ELSE 0 END), 0), "
|
|
340
|
-
"COALESCE(SUM(CASE WHEN blockers_source='unknown' THEN 1 ELSE 0 END), 0) "
|
|
341
|
-
"FROM rounds "
|
|
342
|
-
"WHERE NOT (panel_source='recorded' AND panel_size=0 AND blockers=0)"
|
|
343
|
-
).fetchone()
|
|
344
347
|
if manifest_only:
|
|
345
348
|
lines.append(
|
|
346
|
-
f" {manifest_only} of {total_rounds} rounds have
|
|
347
|
-
f"
|
|
349
|
+
f" {manifest_only} of {total_rounds} rounds have finding counts from manifests "
|
|
350
|
+
f"but no usable synthesizer artifact evidence; no per-finding provenance is available"
|
|
348
351
|
)
|
|
349
352
|
if unknown_only:
|
|
350
353
|
lines.append(
|
|
351
|
-
f" {unknown_only} of {total_rounds} rounds have no
|
|
352
|
-
f"(killed/interrupted before
|
|
353
|
-
f"excluded from the
|
|
354
|
+
f" {unknown_only} of {total_rounds} rounds have no finding-count evidence "
|
|
355
|
+
f"(killed/interrupted before a usable artifact or complete manifest); "
|
|
356
|
+
f"top-line findings are lower bounds and these rounds are excluded from the "
|
|
357
|
+
f"blocker curve"
|
|
354
358
|
)
|
|
355
359
|
|
|
356
360
|
consensus = consensus_stats(conn)
|
|
@@ -21,6 +21,7 @@ import site
|
|
|
21
21
|
import subprocess
|
|
22
22
|
import sys
|
|
23
23
|
from dataclasses import dataclass
|
|
24
|
+
from importlib.metadata import Distribution
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
|
|
26
27
|
import syncade
|
|
@@ -29,9 +30,6 @@ from syncade.process import SubprocessError, run_subprocess
|
|
|
29
30
|
from syncade.update_check import _parse, _text, manifest_once
|
|
30
31
|
|
|
31
32
|
_UPGRADE_TIMEOUT = 300.0
|
|
32
|
-
#: Reading one version string out of a fresh interpreter. Generous, because a cold venv
|
|
33
|
-
#: import is slower than it looks; short, because we already hold the operator's terminal.
|
|
34
|
-
_VERSION_TIMEOUT = 60.0
|
|
35
33
|
#: How far up from ``syncade/__init__.py`` an install marker can sit. A venv layout is
|
|
36
34
|
#: ``<root>/lib/python3.x/site-packages/syncade/__init__.py`` — five parents to the root; the
|
|
37
35
|
#: extra headroom costs nothing and covers layouts that nest one deeper.
|
|
@@ -59,6 +57,21 @@ def _in_user_site() -> bool:
|
|
|
59
57
|
return user_site in Path(syncade.__file__).resolve().parents
|
|
60
58
|
|
|
61
59
|
|
|
60
|
+
def _scoped_dist_infos() -> list[Path]:
|
|
61
|
+
"""The dist-info directories beside the package that supplied this process's code."""
|
|
62
|
+
try:
|
|
63
|
+
site_packages = Path(syncade.__file__).resolve().parent.parent
|
|
64
|
+
return sorted(
|
|
65
|
+
entry
|
|
66
|
+
for entry in site_packages.iterdir()
|
|
67
|
+
if entry.name.casefold().startswith("syncade-")
|
|
68
|
+
and entry.name.casefold().endswith(".dist-info")
|
|
69
|
+
and entry.is_dir()
|
|
70
|
+
)
|
|
71
|
+
except (OSError, RuntimeError, TypeError):
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
|
|
62
75
|
def _read_scoped_installer() -> str:
|
|
63
76
|
"""Read INSTALLER from the dist-info directory that BELONGS to this syncade install.
|
|
64
77
|
|
|
@@ -71,27 +84,44 @@ def _read_scoped_installer() -> str:
|
|
|
71
84
|
is empty string — the same as not found. First-match would misclassify in that state,
|
|
72
85
|
which contradicts the PROVE-or-refuse posture the rest of this module maintains.
|
|
73
86
|
"""
|
|
74
|
-
site_packages = Path(syncade.__file__).resolve().parent.parent
|
|
75
87
|
found: list[str] = []
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
and entry.is_dir()
|
|
82
|
-
):
|
|
83
|
-
try:
|
|
84
|
-
found.append((entry / "INSTALLER").read_text(encoding="utf-8").strip())
|
|
85
|
-
except OSError:
|
|
86
|
-
pass # No INSTALLER file — this dist-info cannot prove anything.
|
|
87
|
-
except OSError:
|
|
88
|
-
pass
|
|
88
|
+
for entry in _scoped_dist_infos():
|
|
89
|
+
try:
|
|
90
|
+
found.append((entry / "INSTALLER").read_text(encoding="utf-8").strip())
|
|
91
|
+
except OSError:
|
|
92
|
+
pass # No INSTALLER file — this dist-info cannot prove anything.
|
|
89
93
|
if not found:
|
|
90
94
|
return ""
|
|
91
95
|
# Contradictory entries in a broken packaging state are not proof of anything.
|
|
92
96
|
return found[0] if len(set(found)) == 1 else ""
|
|
93
97
|
|
|
94
98
|
|
|
99
|
+
def _read_scoped_version() -> str | None:
|
|
100
|
+
"""Read ``Version`` from this syncade install's one adjacent dist-info.
|
|
101
|
+
|
|
102
|
+
The package directory is already the authoritative identity: it supplied the code running
|
|
103
|
+
this function. Reading its sibling metadata after the package manager returns sees the new
|
|
104
|
+
on-disk version without resolving the name ``syncade`` through ``sys.path`` again. A missing,
|
|
105
|
+
malformed, or duplicate dist-info is ambiguous and therefore unverifiable, never first-match.
|
|
106
|
+
"""
|
|
107
|
+
candidates = _scoped_dist_infos()
|
|
108
|
+
if len(candidates) != 1:
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
if not (candidates[0] / "METADATA").is_file():
|
|
112
|
+
return None
|
|
113
|
+
metadata = Distribution.at(candidates[0]).metadata
|
|
114
|
+
names = metadata.get_all("Name") or []
|
|
115
|
+
versions = metadata.get_all("Version") or []
|
|
116
|
+
except Exception: # noqa: BLE001 — broken install metadata degrades to "unverifiable".
|
|
117
|
+
return None
|
|
118
|
+
if len(names) != 1 or names[0].strip().casefold() != "syncade":
|
|
119
|
+
return None
|
|
120
|
+
if len(versions) != 1:
|
|
121
|
+
return None
|
|
122
|
+
return versions[0].strip()
|
|
123
|
+
|
|
124
|
+
|
|
95
125
|
def _pip_or_unknown(where: Path | None) -> InstallMethod:
|
|
96
126
|
"""``pip`` when the distribution records pip as its installer, else ``unknown``.
|
|
97
127
|
|
|
@@ -239,49 +269,14 @@ def _installed_version() -> str | None:
|
|
|
239
269
|
"""The version on disk NOW, or ``None`` when it cannot be established.
|
|
240
270
|
|
|
241
271
|
It CANNOT be ``syncade.__version__``: this process imported that before the upgrade ran, so
|
|
242
|
-
it is by definition the old number.
|
|
243
|
-
|
|
244
|
-
|
|
272
|
+
it is by definition the old number. The value is read from the dist-info tree that belongs
|
|
273
|
+
to this install, so the answer follows the package on disk instead of any ambient metadata
|
|
274
|
+
discovery or subprocess import state.
|
|
245
275
|
|
|
246
276
|
``None`` means "could not verify", which is deliberately NOT the same as "did not change":
|
|
247
277
|
the caller must not turn an unreadable version into a failure report.
|
|
248
278
|
"""
|
|
249
|
-
|
|
250
|
-
result = run_subprocess(
|
|
251
|
-
# `-I` (isolated) is load-bearing, not tidiness. Without it the probe inherits
|
|
252
|
-
# PYTHONPATH and the user site dir, so a `sitecustomize` on that path runs inside
|
|
253
|
-
# the answer and can emit stdout before OR after the version print.
|
|
254
|
-
# `-I` keeps the venv's own site-packages, so `importlib.metadata` still finds
|
|
255
|
-
# the install (verified) — but it does NOT suppress `sitecustomize.py` that lives
|
|
256
|
-
# in those site-packages. An atexit hook registered there (syncade ships such a
|
|
257
|
-
# shim for worktree imports) can append a version-shaped line AFTER the real one.
|
|
258
|
-
# `os._exit(0)` bypasses all atexit handlers: the process terminates immediately
|
|
259
|
-
# after `os.write`, so nothing can append to our stdout (reproduced: a hostile
|
|
260
|
-
# sitecustomize turned "0.7.0" into "0.7.0\n99.0.0" before this fix).
|
|
261
|
-
# `os.write` is used instead of `print` because `os._exit` skips stdio flushing;
|
|
262
|
-
# an unbuffered syscall guarantees the version byte reaches the pipe.
|
|
263
|
-
[
|
|
264
|
-
sys.executable,
|
|
265
|
-
"-I",
|
|
266
|
-
"-c",
|
|
267
|
-
"import importlib.metadata as m, os; "
|
|
268
|
-
"os.write(1, (m.version('syncade') + '\\n').encode()); "
|
|
269
|
-
"os._exit(0)",
|
|
270
|
-
],
|
|
271
|
-
timeout=_VERSION_TIMEOUT,
|
|
272
|
-
)
|
|
273
|
-
except SubprocessError:
|
|
274
|
-
return None
|
|
275
|
-
if result.returncode != 0:
|
|
276
|
-
return None
|
|
277
|
-
raw = result.stdout.strip()
|
|
278
|
-
if not raw:
|
|
279
|
-
return None
|
|
280
|
-
# sitecustomize from PYTHONPATH (suppressed by -I) could emit noise before -c runs;
|
|
281
|
-
# the venv's own sitecustomize can still do so. Take the LAST line — atexit is now
|
|
282
|
-
# impossible (os._exit), so the last line is what os.write put there — and validate it
|
|
283
|
-
# parses as a version. Noise that accidentally ends in a dotted string still fails _parse.
|
|
284
|
-
candidate = raw.splitlines()[-1].strip()
|
|
279
|
+
candidate = _read_scoped_version()
|
|
285
280
|
return candidate if _parse(candidate) is not None else None
|
|
286
281
|
|
|
287
282
|
|
|
@@ -327,6 +327,11 @@ def _default_reviewers() -> list[ReviewerConfig]:
|
|
|
327
327
|
# model="claude-opus-4-6", adversarial_lens=True)
|
|
328
328
|
|
|
329
329
|
|
|
330
|
+
def _default_worktree_base() -> Path:
|
|
331
|
+
"""Resolve the worktree default when a config instance is created."""
|
|
332
|
+
return DEFAULT_WORKTREE_BASE
|
|
333
|
+
|
|
334
|
+
|
|
330
335
|
class SyncadeConfig(BaseModel):
|
|
331
336
|
"""Top-level ``.syncade/config.toml`` schema.
|
|
332
337
|
|
|
@@ -363,7 +368,7 @@ class SyncadeConfig(BaseModel):
|
|
|
363
368
|
# ``[worktree]`` block — Q5); default reproduces worktree.DEFAULT_WORKTREE_BASE.
|
|
364
369
|
# ``--worktree-base`` overrides per-invocation; threaded into the review run and doctor preview.
|
|
365
370
|
worktree_base: Path = Field(
|
|
366
|
-
|
|
371
|
+
default_factory=_default_worktree_base,
|
|
367
372
|
description=(
|
|
368
373
|
"Base directory under which each run's per-reviewer/producer/test git worktrees are "
|
|
369
374
|
f"created (default ``{DEFAULT_WORKTREE_BASE}``). Overridable per-run with "
|
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
from syncade.metrics.findings import (
|
|
19
|
+
read_finding_artifacts,
|
|
19
20
|
read_findings,
|
|
20
21
|
read_rounds,
|
|
21
22
|
)
|
|
@@ -137,6 +138,9 @@ def _load_init(run_dir: Path) -> dict:
|
|
|
137
138
|
def read_run(run_dir: Path | str) -> tuple[RunRow, list[ReviewerStatRow]] | None:
|
|
138
139
|
"""Map one run directory to a :class:`RunRow` + per-reviewer stats.
|
|
139
140
|
|
|
141
|
+
Finding totals on the returned row are provisional loop-manifest summaries. ``backfill()``
|
|
142
|
+
replaces the complete vector from normalized round evidence before persisting the row.
|
|
143
|
+
|
|
140
144
|
Returns ``None`` when ``loop-manifest.json`` is present but unparseable — a
|
|
141
145
|
*corrupt* run, which the caller skips with a warning rather than counting it as
|
|
142
146
|
a bogus UNKNOWN run. A *missing* or *parseable-but-partial* manifest is still
|
|
@@ -510,7 +514,8 @@ def backfill(conn: sqlite3.Connection, runs_root: Path | str) -> None:
|
|
|
510
514
|
|
|
511
515
|
Upserts every present run, refreshes each run's reviewer set (so a changed
|
|
512
516
|
roster drops stale rows), and prunes runs no longer in ``.syncade/runs/`` — so
|
|
513
|
-
the DB stays a faithful derived view even after ``--gc`` slims runs.
|
|
517
|
+
the DB stays a faithful derived view even after ``--gc`` slims runs. This is the final
|
|
518
|
+
authority for persisted run totals; ``read_run()`` alone returns provisional summaries.
|
|
514
519
|
"""
|
|
515
520
|
present: list[str] = []
|
|
516
521
|
for d in _candidate_run_dirs(runs_root):
|
|
@@ -522,6 +527,15 @@ def backfill(conn: sqlite3.Connection, runs_root: Path | str) -> None:
|
|
|
522
527
|
)
|
|
523
528
|
continue
|
|
524
529
|
row, stats = parsed
|
|
530
|
+
artifacts = read_finding_artifacts(d)
|
|
531
|
+
findings, provenance = read_findings(d, row.run_id, artifacts)
|
|
532
|
+
round_rows = read_rounds(d, row.run_id, artifacts)
|
|
533
|
+
row.blockers, row.minors, row.nits, row.dismissed = (
|
|
534
|
+
sum(r.blockers for r in round_rows),
|
|
535
|
+
sum(r.minors for r in round_rows),
|
|
536
|
+
sum(r.nits for r in round_rows),
|
|
537
|
+
sum(r.dismissed for r in round_rows),
|
|
538
|
+
)
|
|
525
539
|
upsert_run(conn, row)
|
|
526
540
|
conn.execute("DELETE FROM reviewer_stats WHERE run_id = ?", (row.run_id,))
|
|
527
541
|
conn.execute("DELETE FROM actor_stats WHERE run_id = ?", (row.run_id,))
|
|
@@ -533,24 +547,13 @@ def backfill(conn: sqlite3.Connection, runs_root: Path | str) -> None:
|
|
|
533
547
|
for stat in read_actor_stats(d):
|
|
534
548
|
upsert_actor_stat(conn, stat)
|
|
535
549
|
|
|
536
|
-
findings, provenance = read_findings(d, row.run_id)
|
|
537
550
|
for finding in findings:
|
|
538
551
|
upsert_finding(conn, finding)
|
|
539
552
|
for entry in provenance:
|
|
540
553
|
upsert_finding_provenance(conn, entry)
|
|
541
554
|
|
|
542
|
-
# ONE derivation, no branch. `rounds` already resolved artifacts-vs-manifest per round
|
|
543
|
-
# and recorded which it used, so the run total is a sum rather than a choice between two
|
|
544
|
-
# counts that could disagree. What stood here was a reached-set supplement plus a
|
|
545
|
-
# conditional derive-or-preserve — three rounds of patching the same question, which is
|
|
546
|
-
# what the single authority replaces.
|
|
547
|
-
round_rows = read_rounds(d, row.run_id)
|
|
548
555
|
for round_row in round_rows:
|
|
549
556
|
upsert_round(conn, round_row)
|
|
550
|
-
conn.execute(
|
|
551
|
-
"UPDATE runs SET blockers = ? WHERE run_id = ?",
|
|
552
|
-
(sum(r.blockers for r in round_rows), row.run_id),
|
|
553
|
-
)
|
|
554
557
|
|
|
555
558
|
present.append(row.run_id)
|
|
556
559
|
_prune_absent(conn, present)
|
|
@@ -11,12 +11,9 @@ physical lines against a 600 ceiling; extraction there would have breached it on
|
|
|
11
11
|
rebuild on schema drift exactly like ``runs``. The derived-view rule is intact: nothing here is
|
|
12
12
|
the only copy of anything.
|
|
13
13
|
|
|
14
|
-
**The count-agreement hazard is the reason this module
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
WILL drift, so the disagreement is measured and reported rather than assumed away — and the
|
|
18
|
-
verdict on what to do about it is the caller's, because a hard failure would make one
|
|
19
|
-
inconsistent legacy run poison a whole backfill.
|
|
14
|
+
**The count-agreement hazard is the reason this module owns the per-round authority.** Every
|
|
15
|
+
finding count travels as one vector with one recorded source; consumers sum those rows rather
|
|
16
|
+
than choosing independently between artifacts and manifests.
|
|
20
17
|
"""
|
|
21
18
|
|
|
22
19
|
from __future__ import annotations
|
|
@@ -27,6 +24,11 @@ from pathlib import Path
|
|
|
27
24
|
from syncade.metrics.schema import FindingProvenanceRow, FindingRow, RoundRow
|
|
28
25
|
|
|
29
26
|
_SYNTH_ARTIFACT = "synthesizer.parsed.json"
|
|
27
|
+
_KNOWN_EMPTY_REASONS = {"no_changes_to_review", "producer_emptied_diff"}
|
|
28
|
+
_KNOWN_FINDING_SEVERITIES = {"blocker", "minor", "nit"}
|
|
29
|
+
_CountVector = tuple[int, int, int, int]
|
|
30
|
+
_ArtifactEvidence = tuple[list[dict[str, object]], _CountVector]
|
|
31
|
+
_RoundArtifacts = dict[int, tuple[Path, _ArtifactEvidence | None]]
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
def round_dirs(run_dir: Path) -> list[tuple[int, Path]]:
|
|
@@ -54,8 +56,47 @@ def _text(value: object) -> str:
|
|
|
54
56
|
return value if isinstance(value, str) else ""
|
|
55
57
|
|
|
56
58
|
|
|
59
|
+
def _normalize_artifact(container: object) -> _ArtifactEvidence | None:
|
|
60
|
+
"""One complete finding list and its count vector, or no artifact evidence."""
|
|
61
|
+
if not isinstance(container, dict):
|
|
62
|
+
return None
|
|
63
|
+
consolidated = container.get("consolidated_findings")
|
|
64
|
+
if not isinstance(consolidated, list):
|
|
65
|
+
return None
|
|
66
|
+
normalized: list[dict[str, object]] = []
|
|
67
|
+
active = {severity: 0 for severity in _KNOWN_FINDING_SEVERITIES}
|
|
68
|
+
dismissed = 0
|
|
69
|
+
for finding in consolidated:
|
|
70
|
+
if not isinstance(finding, dict):
|
|
71
|
+
return None
|
|
72
|
+
severity = finding.get("severity")
|
|
73
|
+
is_dismissed = finding.get("dismissed")
|
|
74
|
+
if not isinstance(severity, str) or severity not in active:
|
|
75
|
+
return None
|
|
76
|
+
if type(is_dismissed) is not bool:
|
|
77
|
+
return None
|
|
78
|
+
normalized.append(finding)
|
|
79
|
+
if is_dismissed:
|
|
80
|
+
dismissed += 1
|
|
81
|
+
else:
|
|
82
|
+
active[severity] += 1
|
|
83
|
+
return normalized, (active["blocker"], active["minor"], active["nit"], dismissed)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def read_finding_artifacts(run_dir: Path | str) -> _RoundArtifacts:
|
|
87
|
+
"""Parse and count each round's count-critical artifact exactly once."""
|
|
88
|
+
artifacts: _RoundArtifacts = {}
|
|
89
|
+
for round_index, round_dir in round_dirs(Path(run_dir)):
|
|
90
|
+
try:
|
|
91
|
+
data = json.loads((round_dir / _SYNTH_ARTIFACT).read_text(encoding="utf-8"))
|
|
92
|
+
except (OSError, ValueError):
|
|
93
|
+
data = None
|
|
94
|
+
artifacts[round_index] = (round_dir, _normalize_artifact(data))
|
|
95
|
+
return artifacts
|
|
96
|
+
|
|
97
|
+
|
|
57
98
|
def read_findings(
|
|
58
|
-
run_dir: Path | str, run_id: str
|
|
99
|
+
run_dir: Path | str, run_id: str, artifacts: _RoundArtifacts
|
|
59
100
|
) -> tuple[list[FindingRow], list[FindingProvenanceRow]]:
|
|
60
101
|
"""Every consolidated finding in a run, with its provenance. Never raises.
|
|
61
102
|
|
|
@@ -71,26 +112,14 @@ def read_findings(
|
|
|
71
112
|
_loop = {}
|
|
72
113
|
_loop_rounds = _loop.get("rounds") if isinstance(_loop, dict) else None
|
|
73
114
|
|
|
74
|
-
for round_index, round_dir in
|
|
115
|
+
for round_index, (round_dir, evidence) in sorted(artifacts.items()):
|
|
75
116
|
# A reviewer absent from the round's PANEL cannot have raised a finding in it. Ghost
|
|
76
117
|
# provenance inflates consensus, and the ratio is the number the whole feature exists
|
|
77
118
|
# to report. An unrecorded panel (empty) cannot convict anyone, so it filters nothing.
|
|
78
119
|
_panel, _ = _round_panel(_loop_rounds, round_dir, round_index)
|
|
79
|
-
|
|
80
|
-
data = json.loads((round_dir / _SYNTH_ARTIFACT).read_text(encoding="utf-8"))
|
|
81
|
-
except (OSError, ValueError):
|
|
82
|
-
continue
|
|
83
|
-
if not isinstance(data, dict):
|
|
84
|
-
continue
|
|
85
|
-
consolidated = data.get("consolidated_findings")
|
|
86
|
-
if not isinstance(consolidated, list):
|
|
87
|
-
continue
|
|
88
|
-
# All-or-nothing: a partially malformed list degrades to 'manifest' in read_rounds().
|
|
89
|
-
# Persisting the valid-looking subset here would leave metrics.db with contradictory
|
|
90
|
-
# round/run counts versus findings/provenance rows — the count mismatch this whole
|
|
91
|
-
# table exists to prevent, reintroduced by the reader.
|
|
92
|
-
if not all(isinstance(f, dict) for f in consolidated):
|
|
120
|
+
if evidence is None:
|
|
93
121
|
continue
|
|
122
|
+
consolidated, _counts = evidence
|
|
94
123
|
|
|
95
124
|
for idx, finding in enumerate(consolidated):
|
|
96
125
|
findings.append(
|
|
@@ -99,10 +128,7 @@ def read_findings(
|
|
|
99
128
|
round=round_index,
|
|
100
129
|
idx=idx,
|
|
101
130
|
severity=_text(finding.get("severity")),
|
|
102
|
-
|
|
103
|
-
# would mark an ACTIVE blocker dismissed, quietly shrinking every count
|
|
104
|
-
# derived from it. Anything that is not a real boolean True is active.
|
|
105
|
-
dismissed=1 if finding.get("dismissed") is True else 0,
|
|
131
|
+
dismissed=1 if finding["dismissed"] else 0,
|
|
106
132
|
file=_text(finding.get("file")),
|
|
107
133
|
description=_text(finding.get("description")),
|
|
108
134
|
)
|
|
@@ -148,9 +174,9 @@ def _round_panel(loop_rounds: object, round_dir: Path, round_index: int) -> tupl
|
|
|
148
174
|
like ``no_changes_to_review`` records ``reviewers: []``) and ``'unknown'`` when no
|
|
149
175
|
manifest had the key and the panel composition cannot be determined.
|
|
150
176
|
|
|
151
|
-
Panel provenance is independent from
|
|
177
|
+
Panel provenance is independent from finding-count provenance: a manifest can carry
|
|
152
178
|
``active_blocker_count`` without a ``reviewers`` list, giving
|
|
153
|
-
``
|
|
179
|
+
``counts_source='manifest'`` and ``panel_source='unknown'``.
|
|
154
180
|
"""
|
|
155
181
|
names: set[str] = set()
|
|
156
182
|
recorded = False
|
|
@@ -176,15 +202,27 @@ def _round_panel(loop_rounds: object, round_dir: Path, round_index: int) -> tupl
|
|
|
176
202
|
return names, "recorded" if recorded else "unknown"
|
|
177
203
|
|
|
178
204
|
|
|
179
|
-
|
|
180
|
-
""
|
|
205
|
+
_COUNT_KEYS = (
|
|
206
|
+
"active_blocker_count",
|
|
207
|
+
"active_minor_count",
|
|
208
|
+
"active_nit_count",
|
|
209
|
+
"dismissed_count",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _synth_counts(container: object) -> _CountVector | None:
|
|
214
|
+
"""The complete synthesizer count vector from a manifest-shaped dict, if valid."""
|
|
181
215
|
synth = container.get("synthesizer") if isinstance(container, dict) else None
|
|
182
|
-
|
|
183
|
-
|
|
216
|
+
if not isinstance(synth, dict):
|
|
217
|
+
return None
|
|
218
|
+
values = tuple(synth.get(key) for key in _COUNT_KEYS)
|
|
219
|
+
if not all(type(value) is int and value >= 0 for value in values):
|
|
220
|
+
return None
|
|
221
|
+
return values
|
|
184
222
|
|
|
185
223
|
|
|
186
|
-
def
|
|
187
|
-
"""A manifest-recorded
|
|
224
|
+
def _manifest_counts(round_dir: Path, loop_rounds: object, round_index: int) -> _CountVector | None:
|
|
225
|
+
"""A complete manifest-recorded count vector for this round, or ``None``.
|
|
188
226
|
|
|
189
227
|
BOTH manifests are consulted, in that order, for the same reason the round set is a union:
|
|
190
228
|
either alone is incomplete. The round manifest exists for interrupted runs the loop manifest
|
|
@@ -196,24 +234,24 @@ def _manifest_blockers(round_dir: Path, loop_rounds: object, round_index: int) -
|
|
|
196
234
|
data = json.loads((round_dir / _ROUND_MANIFEST).read_text(encoding="utf-8"))
|
|
197
235
|
except (OSError, ValueError):
|
|
198
236
|
data = None
|
|
199
|
-
|
|
200
|
-
if
|
|
201
|
-
return
|
|
237
|
+
counts = _synth_counts(data)
|
|
238
|
+
if counts is not None:
|
|
239
|
+
return counts
|
|
202
240
|
if isinstance(loop_rounds, list) and round_index < len(loop_rounds):
|
|
203
|
-
return
|
|
241
|
+
return _synth_counts(loop_rounds[round_index])
|
|
204
242
|
return None
|
|
205
243
|
|
|
206
244
|
|
|
207
|
-
def read_rounds(run_dir: Path | str, run_id: str) -> list[RoundRow]:
|
|
245
|
+
def read_rounds(run_dir: Path | str, run_id: str, artifacts: _RoundArtifacts) -> list[RoundRow]:
|
|
208
246
|
"""One row per round that happened — THE authority, replacing three disagreeing derivations.
|
|
209
247
|
|
|
210
|
-
Precedence for the
|
|
248
|
+
Precedence for the complete finding-count vector is explicit and recorded, never inferred:
|
|
211
249
|
|
|
212
250
|
1. ``artifacts`` — the synthesizer artifact parsed and every entry was well-formed.
|
|
213
|
-
2. ``manifest`` — the artifact is missing or malformed but a
|
|
214
|
-
|
|
251
|
+
2. ``manifest`` — the artifact is missing or malformed but a manifest recorded all four
|
|
252
|
+
counts. The round ran and found something; only the evidence of WHAT is gone.
|
|
215
253
|
3. ``unknown`` — the round ran and nothing credible says what it found. Counted as a round,
|
|
216
|
-
contributing
|
|
254
|
+
contributing zeroes by convention, because inventing counts is worse.
|
|
217
255
|
|
|
218
256
|
A partially-malformed artifact degrades to ``manifest``/``unknown`` rather than silently
|
|
219
257
|
reporting the entries that happened to parse: dropping three of five findings and calling
|
|
@@ -229,7 +267,7 @@ def read_rounds(run_dir: Path | str, run_id: str) -> list[RoundRow]:
|
|
|
229
267
|
# The union of BOTH kinds of evidence, because either alone is incomplete: a directory can
|
|
230
268
|
# exist for a round the manifest never summarised (interrupted), and a manifest can prove a
|
|
231
269
|
# round ran whose directory is absent. Taking one source was the mistake this table replaces.
|
|
232
|
-
on_disk =
|
|
270
|
+
on_disk = {index: entry[0] for index, entry in artifacts.items()}
|
|
233
271
|
indices = set(on_disk)
|
|
234
272
|
if isinstance(loop_rounds, list):
|
|
235
273
|
indices |= set(range(len(loop_rounds)))
|
|
@@ -238,13 +276,31 @@ def read_rounds(run_dir: Path | str, run_id: str) -> list[RoundRow]:
|
|
|
238
276
|
for round_index in sorted(indices):
|
|
239
277
|
round_dir = on_disk.get(round_index, run_dir / f"round-{round_index}")
|
|
240
278
|
panel, panel_source = _round_panel(loop_rounds, round_dir, round_index)
|
|
241
|
-
|
|
279
|
+
known_no_dispatch = (
|
|
280
|
+
isinstance(loop_rounds, list)
|
|
281
|
+
and round_index == len(loop_rounds) - 1
|
|
282
|
+
and isinstance(loop, dict)
|
|
283
|
+
and loop.get("termination_reason") in _KNOWN_EMPTY_REASONS
|
|
284
|
+
and panel_source == "recorded"
|
|
285
|
+
and not panel
|
|
286
|
+
)
|
|
287
|
+
counts, source = _round_counts(
|
|
288
|
+
round_dir,
|
|
289
|
+
loop_rounds,
|
|
290
|
+
round_index,
|
|
291
|
+
artifacts.get(round_index, (round_dir, None))[1],
|
|
292
|
+
known_no_dispatch=known_no_dispatch,
|
|
293
|
+
)
|
|
294
|
+
blockers, minors, nits, dismissed = counts
|
|
242
295
|
rows.append(
|
|
243
296
|
RoundRow(
|
|
244
297
|
run_id=run_id,
|
|
245
298
|
round=round_index,
|
|
246
299
|
blockers=blockers,
|
|
247
|
-
|
|
300
|
+
minors=minors,
|
|
301
|
+
nits=nits,
|
|
302
|
+
dismissed=dismissed,
|
|
303
|
+
counts_source=source,
|
|
248
304
|
panel_size=len(panel),
|
|
249
305
|
panel_source=panel_source,
|
|
250
306
|
)
|
|
@@ -252,23 +308,22 @@ def read_rounds(run_dir: Path | str, run_id: str) -> list[RoundRow]:
|
|
|
252
308
|
return rows
|
|
253
309
|
|
|
254
310
|
|
|
255
|
-
def
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
return (fallback, "manifest") if fallback is not None else (0, "unknown")
|
|
311
|
+
def _round_counts(
|
|
312
|
+
round_dir: Path,
|
|
313
|
+
loop_rounds: object,
|
|
314
|
+
round_index: int,
|
|
315
|
+
artifact: _ArtifactEvidence | None,
|
|
316
|
+
*,
|
|
317
|
+
known_no_dispatch: bool,
|
|
318
|
+
) -> tuple[_CountVector, str]:
|
|
319
|
+
if artifact is not None:
|
|
320
|
+
return artifact[1], "artifacts"
|
|
321
|
+
fallback = _manifest_counts(round_dir, loop_rounds, round_index)
|
|
322
|
+
if fallback is not None:
|
|
323
|
+
return fallback, "manifest"
|
|
324
|
+
if known_no_dispatch:
|
|
325
|
+
return (0, 0, 0, 0), "manifest"
|
|
326
|
+
return (0, 0, 0, 0), "unknown"
|
|
272
327
|
|
|
273
328
|
|
|
274
329
|
def active_blockers(findings: list[FindingRow]) -> int:
|
|
@@ -297,7 +352,7 @@ def consensus_stats(conn) -> tuple[int, int] | None:
|
|
|
297
352
|
"""
|
|
298
353
|
WITH multi_rounds AS (
|
|
299
354
|
SELECT run_id, round FROM rounds
|
|
300
|
-
WHERE panel_size > 1 AND
|
|
355
|
+
WHERE panel_size > 1 AND counts_source = 'artifacts'
|
|
301
356
|
),
|
|
302
357
|
active AS (
|
|
303
358
|
SELECT f.run_id, f.round, f.idx FROM findings f
|
|
@@ -329,30 +384,33 @@ def blocker_curve(conn) -> list[tuple[int, int, int]]:
|
|
|
329
384
|
convergence; it is SURVIVORSHIP, since fewer runs reach each round. Returning both forces a
|
|
330
385
|
renderer to show density, which this repo published wrongly once already.
|
|
331
386
|
|
|
332
|
-
``
|
|
387
|
+
``counts_source='unknown'`` rounds are EXCLUDED. Their blocker count is 0 by convention
|
|
333
388
|
(no evidence either way), not by measurement — including them fabricates zeroes in the curve
|
|
334
389
|
for killed/interrupted runs that left no manifest or artifact. Manifest-backed rounds ARE
|
|
335
390
|
included: they have a measured count, just no per-finding provenance.
|
|
336
391
|
"""
|
|
337
392
|
rows = conn.execute(
|
|
338
393
|
"SELECT round, COALESCE(SUM(blockers), 0), COUNT(*) FROM rounds "
|
|
339
|
-
"WHERE
|
|
394
|
+
"WHERE counts_source != 'unknown' AND NOT (panel_source = 'recorded' "
|
|
395
|
+
"AND panel_size = 0 AND blockers = 0 AND minors = 0 AND nits = 0 AND dismissed = 0) "
|
|
340
396
|
"GROUP BY round ORDER BY round"
|
|
341
397
|
).fetchall()
|
|
342
398
|
return [(int(r), int(n), int(d)) for r, n, d in rows]
|
|
343
399
|
|
|
344
400
|
|
|
345
401
|
def incomplete_rounds(conn) -> int:
|
|
346
|
-
"""Rounds whose
|
|
402
|
+
"""Rounds whose finding counts are not artifact-backed — reported, never silently zeroed.
|
|
347
403
|
|
|
348
404
|
Excludes deliberate no-dispatch rounds (panel explicitly recorded as empty with no blockers):
|
|
349
405
|
those have no synthesizer by design, not because one is missing. The signature is
|
|
350
|
-
``panel_source='recorded' AND panel_size=0
|
|
351
|
-
explicitly recorded zero reviewers and zero
|
|
406
|
+
``panel_source='recorded' AND panel_size=0`` plus an all-zero vector — a round where a
|
|
407
|
+
manifest explicitly recorded zero reviewers and zero findings, such as
|
|
408
|
+
``no_changes_to_review``.
|
|
352
409
|
"""
|
|
353
410
|
return int(
|
|
354
411
|
conn.execute(
|
|
355
|
-
"SELECT COUNT(*) FROM rounds WHERE
|
|
356
|
-
"AND NOT (
|
|
412
|
+
"SELECT COUNT(*) FROM rounds WHERE counts_source != 'artifacts' "
|
|
413
|
+
"AND NOT (counts_source = 'manifest' AND panel_source = 'recorded' AND panel_size = 0 "
|
|
414
|
+
"AND blockers = 0 AND minors = 0 AND nits = 0 AND dismissed = 0)"
|
|
357
415
|
).fetchone()[0]
|
|
358
416
|
)
|