phase-loop-runtime 0.2.0__py3-none-any.whl
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.
- phase_loop_runtime/__init__.py +20 -0
- phase_loop_runtime/_contract_docs/phase-loop/extraction-readiness.md +90 -0
- phase_loop_runtime/_contract_docs/phase-loop/granular-execution-policy.md +137 -0
- phase_loop_runtime/_contract_docs/phase-loop/harness-capability-matrix.md +510 -0
- phase_loop_runtime/_contract_docs/phase-loop/harness-skill-matrix.md +125 -0
- phase_loop_runtime/_contract_docs/phase-loop/harness-substrate-manifest.md +232 -0
- phase_loop_runtime/_contract_docs/phase-loop/pi-loop-control.md +247 -0
- phase_loop_runtime/_contract_docs/phase-loop/protocol.md +2759 -0
- phase_loop_runtime/_contract_docs/phase-loop/runtime-boundary.md +289 -0
- phase_loop_runtime/_contract_docs/phase-loop/spec-discovery-roots.md +47 -0
- phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-governed-pipeline.md +42 -0
- phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-portal-projection.md +34 -0
- phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-regenesis.md +29 -0
- phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-report.md +32 -0
- phase_loop_runtime/_contract_docs/runtime/verification-evidence-contract.md +52 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/commands.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/mixed.json +5 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-provider-timeout/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-provider-timeout/timeout-note.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-redacted-summary/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-redacted-summary/redacted-summary.json +6 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-small-sample/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-small-sample/sample.json +7 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-threshold-edge/edge.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-threshold-edge/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-a.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-b.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-c.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-missing-references/claimed-results.json +5 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-missing-references/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-subtle-low-variance/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-subtle-low-variance/metrics.json +9 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-templated-prose/findings.md +10 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-templated-prose/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/scores.json +4 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/summary.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/bundle-check.json +8 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/operator-note.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/events-sample.jsonl +3 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/receipt.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/evidence.json +13 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/summary.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/matrix.json +9 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/notes.md +4 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/console-summary.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/screenshot-standin.ppm +5 -0
- phase_loop_runtime/adoption_bundle.py +273 -0
- phase_loop_runtime/advisor_board/CONTRACTS.md +257 -0
- phase_loop_runtime/advisor_board/__init__.py +288 -0
- phase_loop_runtime/advisor_board/backing.py +137 -0
- phase_loop_runtime/advisor_board/backing_omnigent.py +498 -0
- phase_loop_runtime/advisor_board/config.py +225 -0
- phase_loop_runtime/advisor_board/events.py +115 -0
- phase_loop_runtime/advisor_board/fixtures/advisor-boards.example.toml +59 -0
- phase_loop_runtime/advisor_board/fixtures.py +91 -0
- phase_loop_runtime/advisor_board/harness_mapping.py +125 -0
- phase_loop_runtime/advisor_board/matrix.py +146 -0
- phase_loop_runtime/advisor_board/observability.py +481 -0
- phase_loop_runtime/advisor_board/presets.py +188 -0
- phase_loop_runtime/advisor_board/registries.py +309 -0
- phase_loop_runtime/advisor_board/resolver.py +253 -0
- phase_loop_runtime/advisor_board/schema.py +269 -0
- phase_loop_runtime/advisor_board/standin.py +166 -0
- phase_loop_runtime/advisor_board/validation.py +159 -0
- phase_loop_runtime/agent_runtime_provider.py +676 -0
- phase_loop_runtime/baml_modular.py +656 -0
- phase_loop_runtime/baml_src/dotfiles_adoption_manifest.baml +37 -0
- phase_loop_runtime/baml_src/dotfiles_c4_document.baml +14 -0
- phase_loop_runtime/baml_src/dotfiles_plan_manifest.baml +50 -0
- phase_loop_runtime/baml_src/dotfiles_runtime_projection.baml +16 -0
- phase_loop_runtime/baml_src/dotfiles_task_catalog.baml +20 -0
- phase_loop_runtime/baml_src/emit_phase_closeout.baml +86 -0
- phase_loop_runtime/baml_src/evaluate_suspected_fake_evidence.baml +36 -0
- phase_loop_runtime/baml_src/verification_evidence.baml +40 -0
- phase_loop_runtime/broker.py +181 -0
- phase_loop_runtime/build_bundle.py +294 -0
- phase_loop_runtime/capability_registry.py +835 -0
- phase_loop_runtime/classifier.py +87 -0
- phase_loop_runtime/claude_agent_view.py +625 -0
- phase_loop_runtime/claude_channel_sidecar.py +1086 -0
- phase_loop_runtime/cli.py +2599 -0
- phase_loop_runtime/closeout.py +865 -0
- phase_loop_runtime/closeout_classifier.py +120 -0
- phase_loop_runtime/closeout_evidence_audit.py +112 -0
- phase_loop_runtime/closeout_validation.py +144 -0
- phase_loop_runtime/closeout_validators.py +203 -0
- phase_loop_runtime/consiliency_gates.py +374 -0
- phase_loop_runtime/consiliency_ingest.py +399 -0
- phase_loop_runtime/consiliency_layout.py +176 -0
- phase_loop_runtime/consiliency_scaffold.py +305 -0
- phase_loop_runtime/cross_repo_channel.py +331 -0
- phase_loop_runtime/discovery.py +2368 -0
- phase_loop_runtime/dispatch_lock.py +167 -0
- phase_loop_runtime/doc_delta_validator.py +98 -0
- phase_loop_runtime/docs_audit.py +285 -0
- phase_loop_runtime/docs_freshness.py +468 -0
- phase_loop_runtime/docs_surfaces.py +150 -0
- phase_loop_runtime/dotfiles_profile_plugin.py +96 -0
- phase_loop_runtime/events.py +154 -0
- phase_loop_runtime/events_migration.py +96 -0
- phase_loop_runtime/evidence_audit.py +1082 -0
- phase_loop_runtime/evidence_audit_config.py +172 -0
- phase_loop_runtime/fleet_map.py +426 -0
- phase_loop_runtime/fleet_metrics.py +298 -0
- phase_loop_runtime/fleet_metrics_export.py +196 -0
- phase_loop_runtime/git_discipline.py +479 -0
- phase_loop_runtime/git_ops.py +122 -0
- phase_loop_runtime/git_topology.py +252 -0
- phase_loop_runtime/governed_bundle.py +137 -0
- phase_loop_runtime/governed_premerge.py +219 -0
- phase_loop_runtime/governed_review.py +264 -0
- phase_loop_runtime/handoff.py +932 -0
- phase_loop_runtime/handoff_path.py +14 -0
- phase_loop_runtime/injection.py +740 -0
- phase_loop_runtime/install_status.py +88 -0
- phase_loop_runtime/lane_scheduler.py +317 -0
- phase_loop_runtime/launcher.py +2760 -0
- phase_loop_runtime/lease_store.py +553 -0
- phase_loop_runtime/maintenance.py +401 -0
- phase_loop_runtime/migrate_handoffs.py +201 -0
- phase_loop_runtime/models.py +2144 -0
- phase_loop_runtime/observability.py +1290 -0
- phase_loop_runtime/panel_invoker.py +1638 -0
- phase_loop_runtime/phase_loop_drift_audit.py +388 -0
- phase_loop_runtime/phase_worktree_executor.py +433 -0
- phase_loop_runtime/pipeline_adapter/__init__.py +12 -0
- phase_loop_runtime/pipeline_adapter/branch_ops.py +274 -0
- phase_loop_runtime/pipeline_adapter/flag.py +55 -0
- phase_loop_runtime/pipeline_adapter/markers.py +13 -0
- phase_loop_runtime/pipeline_adapter/merge_policy.py +72 -0
- phase_loop_runtime/pipeline_adapter/ratification.py +100 -0
- phase_loop_runtime/pipeline_adapter/sibling_matcher.py +123 -0
- phase_loop_runtime/plan_ir.py +523 -0
- phase_loop_runtime/plan_manifest.py +415 -0
- phase_loop_runtime/planner_validation.py +176 -0
- phase_loop_runtime/profiles.py +462 -0
- phase_loop_runtime/prompts.py +365 -0
- phase_loop_runtime/provenance.py +165 -0
- phase_loop_runtime/publishing.py +339 -0
- phase_loop_runtime/py.typed +1 -0
- phase_loop_runtime/reconcile.py +1550 -0
- phase_loop_runtime/redaction.py +184 -0
- phase_loop_runtime/reflection_sync.py +293 -0
- phase_loop_runtime/release_guard.py +186 -0
- phase_loop_runtime/render.py +432 -0
- phase_loop_runtime/repo_validation.py +427 -0
- phase_loop_runtime/review_summary.py +150 -0
- phase_loop_runtime/roadmap_lint.py +425 -0
- phase_loop_runtime/route_log.py +59 -0
- phase_loop_runtime/route_policy/__init__.py +11 -0
- phase_loop_runtime/route_policy/fixtures/route_selection.golden.json +263 -0
- phase_loop_runtime/runner.py +8400 -0
- phase_loop_runtime/runtime_paths.py +225 -0
- phase_loop_runtime/runtime_projection.py +113 -0
- phase_loop_runtime/runtime_resources.py +36 -0
- phase_loop_runtime/schema_export.py +292 -0
- phase_loop_runtime/schemas/phase_loop_closeout.schema.json +366 -0
- phase_loop_runtime/skill_install.py +176 -0
- phase_loop_runtime/skill_inventory.py +662 -0
- phase_loop_runtime/skill_paths.py +42 -0
- phase_loop_runtime/skill_sources_plugin.py +37 -0
- phase_loop_runtime/skills_bundle/claude-advisor-board/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-advisor-board/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/claude-advisor-panel/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-advisor-panel/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/claude-execute-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-execute-detailed/SKILL.md +152 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/SKILL.md +745 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/allocate_worktree_name.sh +33 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/audit_lane_file_touches.py +197 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/state.py +274 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/team_teardown.sh +32 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/validate_plan_doc.py +642 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/verify_harness.sh +96 -0
- phase_loop_runtime/skills_bundle/claude-phase-loop/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-phase-loop/SKILL.md +115 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/SKILL.md +439 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/assets/review_prompt.md +19 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/references/roadmap-template.md +193 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
- phase_loop_runtime/skills_bundle/claude-plan-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-plan-detailed/SKILL.md +334 -0
- phase_loop_runtime/skills_bundle/claude-plan-detailed/assets/review_prompt.md +25 -0
- phase_loop_runtime/skills_bundle/claude-plan-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-plan-phase/SKILL.md +776 -0
- phase_loop_runtime/skills_bundle/claude-plan-phase/assets/review_prompt.md +17 -0
- phase_loop_runtime/skills_bundle/claude-plan-phase/scripts/validate_plan_doc.py +1092 -0
- phase_loop_runtime/skills_bundle/claude-run-train/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-run-train/SKILL.md +75 -0
- phase_loop_runtime/skills_bundle/claude-skill-editor/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-skill-editor/SKILL.md +243 -0
- phase_loop_runtime/skills_bundle/claude-skill-editor/assets/editor_prompt.md +72 -0
- phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/SKILL.md +249 -0
- phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
- phase_loop_runtime/skills_bundle/claude-task-contextualizer/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-task-contextualizer/SKILL.md +74 -0
- phase_loop_runtime/skills_bundle/claude-task-contextualizer/references/task-templates.md +103 -0
- phase_loop_runtime/skills_bundle/codex-advisor-board/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-advisor-board/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/codex-advisor-panel/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-advisor-panel/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/codex-execute-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-execute-detailed/SKILL.md +154 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/SKILL.md +231 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/allocate_worktree_name.sh +33 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/audit_lane_file_touches.py +197 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/state.py +274 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/team_teardown.sh +32 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/validate_plan_doc.py +642 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/verify_harness.sh +96 -0
- phase_loop_runtime/skills_bundle/codex-phase-loop/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-phase-loop/SKILL.md +248 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/SKILL.md +150 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/assets/review_prompt.md +19 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/references/roadmap-template.md +193 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
- phase_loop_runtime/skills_bundle/codex-plan-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-plan-detailed/SKILL.md +119 -0
- phase_loop_runtime/skills_bundle/codex-plan-detailed/assets/review_prompt.md +25 -0
- phase_loop_runtime/skills_bundle/codex-plan-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-plan-phase/SKILL.md +307 -0
- phase_loop_runtime/skills_bundle/codex-plan-phase/assets/review_prompt.md +17 -0
- phase_loop_runtime/skills_bundle/codex-plan-phase/scripts/validate_plan_doc.py +1092 -0
- phase_loop_runtime/skills_bundle/codex-run-train/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-run-train/SKILL.md +75 -0
- phase_loop_runtime/skills_bundle/codex-skill-editor/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-skill-editor/SKILL.md +94 -0
- phase_loop_runtime/skills_bundle/codex-skill-editor/assets/editor_prompt.md +72 -0
- phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/SKILL.md +114 -0
- phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
- phase_loop_runtime/skills_bundle/codex-task-contextualizer/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-task-contextualizer/SKILL.md +102 -0
- phase_loop_runtime/skills_bundle/codex-task-contextualizer/references/task-templates.md +103 -0
- phase_loop_runtime/skills_bundle/gemini-advisor-board/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-advisor-board/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/gemini-advisor-panel/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-advisor-panel/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/gemini-execute-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-execute-detailed/SKILL.md +144 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/SKILL.md +198 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/allocate_worktree_name.sh +33 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/audit_lane_file_touches.py +197 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/state.py +274 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/team_teardown.sh +32 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/validate_plan_doc.py +642 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/verify_harness.sh +96 -0
- phase_loop_runtime/skills_bundle/gemini-phase-loop/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-phase-loop/SKILL.md +111 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/SKILL.md +147 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/assets/review_prompt.md +19 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/references/roadmap-template.md +193 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
- phase_loop_runtime/skills_bundle/gemini-plan-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-plan-detailed/SKILL.md +119 -0
- phase_loop_runtime/skills_bundle/gemini-plan-detailed/assets/review_prompt.md +25 -0
- phase_loop_runtime/skills_bundle/gemini-plan-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-plan-phase/SKILL.md +217 -0
- phase_loop_runtime/skills_bundle/gemini-plan-phase/assets/review_prompt.md +17 -0
- phase_loop_runtime/skills_bundle/gemini-plan-phase/scripts/validate_plan_doc.py +1092 -0
- phase_loop_runtime/skills_bundle/gemini-run-train/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-run-train/SKILL.md +75 -0
- phase_loop_runtime/skills_bundle/gemini-skill-editor/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-skill-editor/SKILL.md +87 -0
- phase_loop_runtime/skills_bundle/gemini-skill-editor/assets/editor_prompt.md +72 -0
- phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/SKILL.md +109 -0
- phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
- phase_loop_runtime/skills_bundle/gemini-task-contextualizer/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-task-contextualizer/SKILL.md +98 -0
- phase_loop_runtime/skills_bundle/gemini-task-contextualizer/references/task-templates.md +103 -0
- phase_loop_runtime/skills_bundle/opencode-advisor-board/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-advisor-board/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/opencode-advisor-panel/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-advisor-panel/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/opencode-execute-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-execute-detailed/SKILL.md +144 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/SKILL.md +196 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/allocate_worktree_name.sh +33 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/audit_lane_file_touches.py +197 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/state.py +274 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/team_teardown.sh +32 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/validate_plan_doc.py +642 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/verify_harness.sh +96 -0
- phase_loop_runtime/skills_bundle/opencode-phase-loop/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-phase-loop/SKILL.md +113 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/SKILL.md +147 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/assets/review_prompt.md +19 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/references/roadmap-template.md +193 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
- phase_loop_runtime/skills_bundle/opencode-plan-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-plan-detailed/SKILL.md +119 -0
- phase_loop_runtime/skills_bundle/opencode-plan-detailed/assets/review_prompt.md +25 -0
- phase_loop_runtime/skills_bundle/opencode-plan-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-plan-phase/SKILL.md +211 -0
- phase_loop_runtime/skills_bundle/opencode-plan-phase/assets/review_prompt.md +17 -0
- phase_loop_runtime/skills_bundle/opencode-plan-phase/scripts/validate_plan_doc.py +1092 -0
- phase_loop_runtime/skills_bundle/opencode-run-train/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-run-train/SKILL.md +75 -0
- phase_loop_runtime/skills_bundle/opencode-skill-editor/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-skill-editor/SKILL.md +87 -0
- phase_loop_runtime/skills_bundle/opencode-skill-editor/assets/editor_prompt.md +72 -0
- phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/SKILL.md +109 -0
- phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
- phase_loop_runtime/skills_bundle/opencode-task-contextualizer/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-task-contextualizer/SKILL.md +98 -0
- phase_loop_runtime/skills_bundle/opencode-task-contextualizer/references/task-templates.md +103 -0
- phase_loop_runtime/state.py +156 -0
- phase_loop_runtime/state_degradation.py +136 -0
- phase_loop_runtime/state_ops.py +676 -0
- phase_loop_runtime/train_ledger.py +215 -0
- phase_loop_runtime/train_roadmap.py +339 -0
- phase_loop_runtime/train_runner.py +1197 -0
- phase_loop_runtime/verification_evidence.py +650 -0
- phase_loop_runtime/verification_evidence_validator.py +75 -0
- phase_loop_runtime/visual_evidence_validator.py +36 -0
- phase_loop_runtime/worker_pool.py +123 -0
- phase_loop_runtime/worktree_index.py +270 -0
- phase_loop_runtime-0.2.0.data/data/share/phase-loop-runtime/protocol/protocol.md +2475 -0
- phase_loop_runtime-0.2.0.dist-info/METADATA +106 -0
- phase_loop_runtime-0.2.0.dist-info/RECORD +370 -0
- phase_loop_runtime-0.2.0.dist-info/WHEEL +5 -0
- phase_loop_runtime-0.2.0.dist-info/entry_points.txt +9 -0
- phase_loop_runtime-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
"""Operator-callable evidence audit.
|
|
2
|
+
|
|
3
|
+
Spot-checks dirty-tree artifacts for the fake-evidence patterns surfaced
|
|
4
|
+
in the regen VISUALMATCH 2026-05-22 incident:
|
|
5
|
+
|
|
6
|
+
1. duplicate-content — multiple cited files share the same sha256
|
|
7
|
+
(e.g., "19 prototype PNGs" all having md5 8d7f1750)
|
|
8
|
+
2. uniform-numeric — numeric arrays > 3 elements where all values are
|
|
9
|
+
within epsilon (e.g., 19/19 similarity values at 0.999999)
|
|
10
|
+
3. missing-references — JSON artifacts cite path-shaped strings that
|
|
11
|
+
don't exist on disk
|
|
12
|
+
|
|
13
|
+
This is a Tier 1.5 helper: codifies the operator spot-check protocol
|
|
14
|
+
that catches evidence faking the v20 IF-gate Tier 1 validator (which
|
|
15
|
+
only matches names, not content) can't see. Full Tier 2 evidence
|
|
16
|
+
verification with runner enforcement is deferred to a future roadmap.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import math
|
|
23
|
+
import re
|
|
24
|
+
import subprocess
|
|
25
|
+
import time
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.request
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Iterable
|
|
31
|
+
|
|
32
|
+
from .baml_modular import BamlRequest, build_baml_request, parse_baml_response
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Heuristic-shaped string that might be a file path. Triggers on slashes;
|
|
36
|
+
# we filter out URLs and obviously non-path strings downstream.
|
|
37
|
+
_PATH_HINT_RE = re.compile(r"^[A-Za-z0-9_.\-/]+\.[A-Za-z0-9]{1,8}$")
|
|
38
|
+
# Skip when the "string" is actually a URL or known non-path
|
|
39
|
+
_NON_PATH_PREFIXES = ("http://", "https://", "git@", "ssh://", "file://")
|
|
40
|
+
UNCERTAIN_OPERATOR_REVIEW = "UNCERTAIN-OPERATOR-REVIEW"
|
|
41
|
+
_STRICT_REFERENCE_EXTENSIONS = {
|
|
42
|
+
".baml",
|
|
43
|
+
".csv",
|
|
44
|
+
".gif",
|
|
45
|
+
".html",
|
|
46
|
+
".jpeg",
|
|
47
|
+
".jpg",
|
|
48
|
+
".json",
|
|
49
|
+
".jsonl",
|
|
50
|
+
".log",
|
|
51
|
+
".md",
|
|
52
|
+
".pdf",
|
|
53
|
+
".png",
|
|
54
|
+
".ppm",
|
|
55
|
+
".svg",
|
|
56
|
+
".txt",
|
|
57
|
+
".webp",
|
|
58
|
+
".yaml",
|
|
59
|
+
".yml",
|
|
60
|
+
}
|
|
61
|
+
_STRICT_REFERENCE_CONTEXT_RE = re.compile(
|
|
62
|
+
r"(?:^|[._-])(?:artifact|artifacts|evidence|fixture|image|images|manifest|"
|
|
63
|
+
r"output|outputs|path|paths|ref|refs|reference|references|screenshot|screenshots|"
|
|
64
|
+
r"source|sources|trace|traces)(?:$|[._-])"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class DuplicateContentFinding:
|
|
70
|
+
sha256: str
|
|
71
|
+
paths: tuple[str, ...]
|
|
72
|
+
size_bytes: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class UniformNumericFinding:
|
|
77
|
+
json_artifact: str
|
|
78
|
+
json_pointer: str # e.g., "$.routes[*].similarity"
|
|
79
|
+
array_length: int
|
|
80
|
+
unique_values: int
|
|
81
|
+
sample_value: float
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class MissingReferenceFinding:
|
|
86
|
+
json_artifact: str
|
|
87
|
+
json_pointer: str
|
|
88
|
+
missing_path: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class LooseUniformFinding:
|
|
93
|
+
json_artifact: str
|
|
94
|
+
json_pointer: str
|
|
95
|
+
array_length: int
|
|
96
|
+
mean: float
|
|
97
|
+
stdev: float
|
|
98
|
+
coefficient_of_variation: float
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class BoilerplateFinding:
|
|
103
|
+
paths: tuple[str, ...]
|
|
104
|
+
token_overlap: float
|
|
105
|
+
shared_token_count: int
|
|
106
|
+
sample_tokens: tuple[str, ...]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class SizeDistributionFinding:
|
|
111
|
+
sibling_directory: str
|
|
112
|
+
paths: tuple[str, ...]
|
|
113
|
+
mean_size_bytes: float
|
|
114
|
+
stdev_size_bytes: float
|
|
115
|
+
coefficient_of_variation: float
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class EvidenceJudgment:
|
|
120
|
+
verdict: str
|
|
121
|
+
confidence: float
|
|
122
|
+
reasoning: str
|
|
123
|
+
specific_concerns: tuple[str, ...]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class Tier3Budget:
|
|
128
|
+
tier3_budget: int = 3
|
|
129
|
+
tier3_calls_made: int = 0
|
|
130
|
+
|
|
131
|
+
def __post_init__(self) -> None:
|
|
132
|
+
if self.tier3_budget < 0:
|
|
133
|
+
raise ValueError("tier3_budget must be non-negative")
|
|
134
|
+
if self.tier3_calls_made < 0:
|
|
135
|
+
raise ValueError("tier3_calls_made must be non-negative")
|
|
136
|
+
|
|
137
|
+
def consume(self) -> bool:
|
|
138
|
+
if self.tier3_calls_made >= self.tier3_budget:
|
|
139
|
+
return False
|
|
140
|
+
self.tier3_calls_made += 1
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class Tier3InvocationRecord:
|
|
146
|
+
finding_kind: str
|
|
147
|
+
sample_path: str
|
|
148
|
+
judgment: EvidenceJudgment
|
|
149
|
+
metadata: dict[str, Any]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class Tier3RunnerAudit:
|
|
154
|
+
tier3_budget: int
|
|
155
|
+
tier3_calls_made: int
|
|
156
|
+
invocations: tuple[Tier3InvocationRecord, ...]
|
|
157
|
+
operator_review_markers: tuple[dict[str, Any], ...]
|
|
158
|
+
warnings: tuple[str, ...]
|
|
159
|
+
blocker: dict[str, Any] | None = None
|
|
160
|
+
|
|
161
|
+
def to_json(self) -> dict[str, Any]:
|
|
162
|
+
return {
|
|
163
|
+
"tier3_budget": self.tier3_budget,
|
|
164
|
+
"tier3_calls_made": self.tier3_calls_made,
|
|
165
|
+
"operator_review_markers": list(self.operator_review_markers),
|
|
166
|
+
"warnings": list(self.warnings),
|
|
167
|
+
"invocations": [
|
|
168
|
+
{
|
|
169
|
+
"finding_kind": record.finding_kind,
|
|
170
|
+
"sample_path": record.sample_path,
|
|
171
|
+
"judgment": evidence_judgment_to_json(record.judgment),
|
|
172
|
+
"metadata": record.metadata,
|
|
173
|
+
}
|
|
174
|
+
for record in self.invocations
|
|
175
|
+
],
|
|
176
|
+
**({"blocker": self.blocker} if self.blocker else {}),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class EvidenceAuditResult:
|
|
182
|
+
repo: str
|
|
183
|
+
files_scanned: int = 0
|
|
184
|
+
json_artifacts_scanned: int = 0
|
|
185
|
+
duplicate_content: list[DuplicateContentFinding] = field(default_factory=list)
|
|
186
|
+
uniform_numeric: list[UniformNumericFinding] = field(default_factory=list)
|
|
187
|
+
missing_references: list[MissingReferenceFinding] = field(default_factory=list)
|
|
188
|
+
tier2_enabled: bool = False
|
|
189
|
+
loose_uniform: list[LooseUniformFinding] = field(default_factory=list)
|
|
190
|
+
boilerplate_text: list[BoilerplateFinding] = field(default_factory=list)
|
|
191
|
+
size_distribution: list[SizeDistributionFinding] = field(default_factory=list)
|
|
192
|
+
tier3_enabled: bool = False
|
|
193
|
+
tier3_judgments: list[EvidenceJudgment] = field(default_factory=list)
|
|
194
|
+
|
|
195
|
+
def is_clean(self) -> bool:
|
|
196
|
+
return not (
|
|
197
|
+
self.duplicate_content
|
|
198
|
+
or self.uniform_numeric
|
|
199
|
+
or self.missing_references
|
|
200
|
+
or self.loose_uniform
|
|
201
|
+
or self.boilerplate_text
|
|
202
|
+
or self.size_distribution
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def to_json(self) -> dict[str, Any]:
|
|
206
|
+
payload: dict[str, Any] = {
|
|
207
|
+
"repo": self.repo,
|
|
208
|
+
"files_scanned": self.files_scanned,
|
|
209
|
+
"json_artifacts_scanned": self.json_artifacts_scanned,
|
|
210
|
+
"is_clean": self.is_clean(),
|
|
211
|
+
"duplicate_content": [
|
|
212
|
+
{"sha256": f.sha256, "paths": list(f.paths), "size_bytes": f.size_bytes}
|
|
213
|
+
for f in self.duplicate_content
|
|
214
|
+
],
|
|
215
|
+
"uniform_numeric": [
|
|
216
|
+
{
|
|
217
|
+
"json_artifact": f.json_artifact,
|
|
218
|
+
"json_pointer": f.json_pointer,
|
|
219
|
+
"array_length": f.array_length,
|
|
220
|
+
"unique_values": f.unique_values,
|
|
221
|
+
"sample_value": f.sample_value,
|
|
222
|
+
}
|
|
223
|
+
for f in self.uniform_numeric
|
|
224
|
+
],
|
|
225
|
+
"missing_references": [
|
|
226
|
+
{"json_artifact": f.json_artifact, "json_pointer": f.json_pointer, "missing_path": f.missing_path}
|
|
227
|
+
for f in self.missing_references
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
if self.tier2_enabled:
|
|
231
|
+
payload["tier2_findings"] = {
|
|
232
|
+
"loose_uniform": [
|
|
233
|
+
{
|
|
234
|
+
"json_artifact": f.json_artifact,
|
|
235
|
+
"json_pointer": f.json_pointer,
|
|
236
|
+
"array_length": f.array_length,
|
|
237
|
+
"mean": f.mean,
|
|
238
|
+
"stdev": f.stdev,
|
|
239
|
+
"coefficient_of_variation": f.coefficient_of_variation,
|
|
240
|
+
}
|
|
241
|
+
for f in self.loose_uniform
|
|
242
|
+
],
|
|
243
|
+
"boilerplate_text": [
|
|
244
|
+
{
|
|
245
|
+
"paths": list(f.paths),
|
|
246
|
+
"token_overlap": f.token_overlap,
|
|
247
|
+
"shared_token_count": f.shared_token_count,
|
|
248
|
+
"sample_tokens": list(f.sample_tokens),
|
|
249
|
+
}
|
|
250
|
+
for f in self.boilerplate_text
|
|
251
|
+
],
|
|
252
|
+
"size_distribution": [
|
|
253
|
+
{
|
|
254
|
+
"sibling_directory": f.sibling_directory,
|
|
255
|
+
"paths": list(f.paths),
|
|
256
|
+
"mean_size_bytes": f.mean_size_bytes,
|
|
257
|
+
"stdev_size_bytes": f.stdev_size_bytes,
|
|
258
|
+
"coefficient_of_variation": f.coefficient_of_variation,
|
|
259
|
+
}
|
|
260
|
+
for f in self.size_distribution
|
|
261
|
+
],
|
|
262
|
+
}
|
|
263
|
+
if self.tier3_enabled:
|
|
264
|
+
payload["tier3_judgments"] = [
|
|
265
|
+
{
|
|
266
|
+
"verdict": judgment.verdict,
|
|
267
|
+
"confidence": judgment.confidence,
|
|
268
|
+
"reasoning": judgment.reasoning,
|
|
269
|
+
"specific_concerns": list(judgment.specific_concerns),
|
|
270
|
+
}
|
|
271
|
+
for judgment in self.tier3_judgments
|
|
272
|
+
]
|
|
273
|
+
return payload
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _git_dirty_paths(repo: Path) -> list[str]:
|
|
277
|
+
# --untracked-files=all so untracked directories are expanded to
|
|
278
|
+
# individual file entries rather than collapsed to "dir/" — otherwise
|
|
279
|
+
# we miss the actual files-in-untracked-dir case.
|
|
280
|
+
try:
|
|
281
|
+
result = subprocess.run(
|
|
282
|
+
["git", "-C", str(repo), "status", "--porcelain", "--untracked-files=all"],
|
|
283
|
+
capture_output=True,
|
|
284
|
+
text=True,
|
|
285
|
+
check=False,
|
|
286
|
+
)
|
|
287
|
+
except (OSError, subprocess.SubprocessError):
|
|
288
|
+
return []
|
|
289
|
+
if result.returncode != 0:
|
|
290
|
+
return []
|
|
291
|
+
paths: list[str] = []
|
|
292
|
+
for line in result.stdout.splitlines():
|
|
293
|
+
if len(line) < 3:
|
|
294
|
+
continue
|
|
295
|
+
# First 2 chars are status; rest is path (possibly with -> for renames)
|
|
296
|
+
path = line[3:]
|
|
297
|
+
if "->" in path:
|
|
298
|
+
path = path.split("->", 1)[1].strip()
|
|
299
|
+
paths.append(path.strip().strip('"'))
|
|
300
|
+
return paths
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _sha256_of_file(path: Path, max_bytes: int = 50 * 1024 * 1024) -> tuple[str, int] | None:
|
|
304
|
+
try:
|
|
305
|
+
size = path.stat().st_size
|
|
306
|
+
except OSError:
|
|
307
|
+
return None
|
|
308
|
+
if size > max_bytes:
|
|
309
|
+
return None
|
|
310
|
+
try:
|
|
311
|
+
with open(path, "rb") as f:
|
|
312
|
+
h = hashlib.sha256()
|
|
313
|
+
h.update(f.read())
|
|
314
|
+
return h.hexdigest(), size
|
|
315
|
+
except OSError:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def detect_duplicate_content(
|
|
320
|
+
files: Iterable[Path], min_duplicates: int = 3, min_size_bytes: int = 1
|
|
321
|
+
) -> list[DuplicateContentFinding]:
|
|
322
|
+
"""Flag when N or more files share the same sha256.
|
|
323
|
+
|
|
324
|
+
min_duplicates default of 3 is intentional — the regen incident had
|
|
325
|
+
19 identical files; a value of 2 would false-positive on legitimate
|
|
326
|
+
duplicates (e.g., template files copied verbatim). The pattern we
|
|
327
|
+
want to catch is "many supposedly-distinct artifacts all the same."
|
|
328
|
+
|
|
329
|
+
min_size_bytes default of 1 excludes empty files from dedup detection.
|
|
330
|
+
Empty marker files (e.g., .gitkeep) all share sha256
|
|
331
|
+
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 by
|
|
332
|
+
definition — surfacing this as "duplicate content" produces spurious
|
|
333
|
+
findings on full-tree scans of repos with multiple empty marker files.
|
|
334
|
+
"""
|
|
335
|
+
by_hash: dict[str, list[tuple[str, int]]] = {}
|
|
336
|
+
for p in files:
|
|
337
|
+
if not p.is_file():
|
|
338
|
+
continue
|
|
339
|
+
h = _sha256_of_file(p)
|
|
340
|
+
if h is None:
|
|
341
|
+
continue
|
|
342
|
+
sha, size = h
|
|
343
|
+
if size < min_size_bytes:
|
|
344
|
+
continue
|
|
345
|
+
by_hash.setdefault(sha, []).append((str(p), size))
|
|
346
|
+
findings: list[DuplicateContentFinding] = []
|
|
347
|
+
for sha, entries in by_hash.items():
|
|
348
|
+
if len(entries) < min_duplicates:
|
|
349
|
+
continue
|
|
350
|
+
findings.append(
|
|
351
|
+
DuplicateContentFinding(
|
|
352
|
+
sha256=sha,
|
|
353
|
+
paths=tuple(p for p, _ in entries),
|
|
354
|
+
size_bytes=entries[0][1],
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
return findings
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _walk_json(obj: Any, pointer: str = "$") -> Iterable[tuple[str, Any]]:
|
|
361
|
+
yield pointer, obj
|
|
362
|
+
if isinstance(obj, dict):
|
|
363
|
+
for k, v in obj.items():
|
|
364
|
+
yield from _walk_json(v, f"{pointer}.{k}")
|
|
365
|
+
elif isinstance(obj, list):
|
|
366
|
+
for i, v in enumerate(obj):
|
|
367
|
+
yield from _walk_json(v, f"{pointer}[{i}]")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def detect_uniform_numeric(
|
|
371
|
+
json_path: Path, min_array_length: int = 4, epsilon: float = 1e-6
|
|
372
|
+
) -> list[UniformNumericFinding]:
|
|
373
|
+
"""Flag numeric arrays where all values are within epsilon of each other.
|
|
374
|
+
|
|
375
|
+
Catches the regen pattern: 19/19 similarity scores at exactly 0.999999.
|
|
376
|
+
Real comparison output has natural variance; uniform-to-6-decimals across
|
|
377
|
+
a >=4-element array is a strong template-fill signal.
|
|
378
|
+
|
|
379
|
+
Default min_array_length=4 because legitimate 2-3 element arrays often
|
|
380
|
+
DO have identical values (e.g., [true, true] or [0, 0, 0] for a 3-axis
|
|
381
|
+
score). 4+ identical values is suspicious.
|
|
382
|
+
"""
|
|
383
|
+
try:
|
|
384
|
+
text = json_path.read_text(encoding="utf-8")
|
|
385
|
+
except OSError:
|
|
386
|
+
return []
|
|
387
|
+
try:
|
|
388
|
+
data = json.loads(text)
|
|
389
|
+
except (json.JSONDecodeError, ValueError):
|
|
390
|
+
return []
|
|
391
|
+
findings: list[UniformNumericFinding] = []
|
|
392
|
+
# Collect all numeric arrays we encounter directly
|
|
393
|
+
for pointer, value in _walk_json(data):
|
|
394
|
+
if not isinstance(value, list):
|
|
395
|
+
continue
|
|
396
|
+
numerics = [x for x in value if isinstance(x, (int, float)) and not isinstance(x, bool)]
|
|
397
|
+
if len(numerics) != len(value):
|
|
398
|
+
# mixed-type arrays don't count
|
|
399
|
+
continue
|
|
400
|
+
if len(numerics) < min_array_length:
|
|
401
|
+
continue
|
|
402
|
+
unique = {round(x / epsilon) for x in numerics}
|
|
403
|
+
if len(unique) == 1:
|
|
404
|
+
findings.append(
|
|
405
|
+
UniformNumericFinding(
|
|
406
|
+
json_artifact=str(json_path),
|
|
407
|
+
json_pointer=pointer,
|
|
408
|
+
array_length=len(numerics),
|
|
409
|
+
unique_values=1,
|
|
410
|
+
sample_value=float(numerics[0]),
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
continue
|
|
414
|
+
# Also catch the "all entries in an array of objects have identical numeric field"
|
|
415
|
+
# pattern: iterate first-level-object-array → extract each object's numeric fields →
|
|
416
|
+
# check uniformity. This catches "every entry has similarity=0.999999".
|
|
417
|
+
# Second pass: object arrays where every object shares an identical numeric field
|
|
418
|
+
for pointer, value in _walk_json(data):
|
|
419
|
+
if not isinstance(value, list) or len(value) < min_array_length:
|
|
420
|
+
continue
|
|
421
|
+
if not all(isinstance(item, dict) for item in value):
|
|
422
|
+
continue
|
|
423
|
+
# For each common numeric field, check uniformity
|
|
424
|
+
common_keys = set(value[0].keys())
|
|
425
|
+
for item in value[1:]:
|
|
426
|
+
common_keys &= set(item.keys())
|
|
427
|
+
for key in common_keys:
|
|
428
|
+
field_values = [item.get(key) for item in value]
|
|
429
|
+
numerics = [v for v in field_values if isinstance(v, (int, float)) and not isinstance(v, bool)]
|
|
430
|
+
if len(numerics) != len(field_values):
|
|
431
|
+
continue
|
|
432
|
+
unique = {round(x / epsilon) for x in numerics}
|
|
433
|
+
if len(unique) == 1:
|
|
434
|
+
findings.append(
|
|
435
|
+
UniformNumericFinding(
|
|
436
|
+
json_artifact=str(json_path),
|
|
437
|
+
json_pointer=f"{pointer}[*].{key}",
|
|
438
|
+
array_length=len(numerics),
|
|
439
|
+
unique_values=1,
|
|
440
|
+
sample_value=float(numerics[0]),
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
return findings
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _json_pointer_context(pointer: str) -> str:
|
|
447
|
+
return re.sub(r"\[\d+\]", "", pointer).lower()
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _is_strict_reference_candidate(pointer: str, value: str) -> bool:
|
|
451
|
+
if Path(value).suffix.lower() not in _STRICT_REFERENCE_EXTENSIONS:
|
|
452
|
+
return False
|
|
453
|
+
context = _json_pointer_context(pointer)
|
|
454
|
+
if "changed_files" in context:
|
|
455
|
+
return False
|
|
456
|
+
return bool(_STRICT_REFERENCE_CONTEXT_RE.search(context))
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def detect_missing_references(
|
|
460
|
+
json_path: Path, repo: Path, strict: bool = True
|
|
461
|
+
) -> list[MissingReferenceFinding]:
|
|
462
|
+
"""Flag JSON string values that look like paths but don't resolve on disk.
|
|
463
|
+
|
|
464
|
+
Catches the pattern where artifacts cite paths that were never created
|
|
465
|
+
(a planted manifest with no actual files behind it).
|
|
466
|
+
"""
|
|
467
|
+
try:
|
|
468
|
+
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
469
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
470
|
+
return []
|
|
471
|
+
findings: list[MissingReferenceFinding] = []
|
|
472
|
+
for pointer, value in _walk_json(data):
|
|
473
|
+
if not isinstance(value, str) or not value:
|
|
474
|
+
continue
|
|
475
|
+
if any(value.startswith(prefix) for prefix in _NON_PATH_PREFIXES):
|
|
476
|
+
continue
|
|
477
|
+
if not _PATH_HINT_RE.match(value):
|
|
478
|
+
continue
|
|
479
|
+
if "/" not in value:
|
|
480
|
+
continue
|
|
481
|
+
if strict and not _is_strict_reference_candidate(pointer, value):
|
|
482
|
+
continue
|
|
483
|
+
# Resolve relative to repo
|
|
484
|
+
candidate = (repo / value).resolve() if not Path(value).is_absolute() else Path(value)
|
|
485
|
+
if not candidate.exists():
|
|
486
|
+
findings.append(
|
|
487
|
+
MissingReferenceFinding(
|
|
488
|
+
json_artifact=str(json_path),
|
|
489
|
+
json_pointer=pointer,
|
|
490
|
+
missing_path=value,
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
return findings
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _population_stdev(values: list[float], mean: float) -> float:
|
|
497
|
+
return math.sqrt(sum((value - mean) ** 2 for value in values) / len(values))
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _loose_uniform_finding(
|
|
501
|
+
*,
|
|
502
|
+
json_path: Path,
|
|
503
|
+
pointer: str,
|
|
504
|
+
values: list[float],
|
|
505
|
+
stdev_threshold: float,
|
|
506
|
+
) -> LooseUniformFinding | None:
|
|
507
|
+
if len(set(values)) == 1:
|
|
508
|
+
return None
|
|
509
|
+
mean = sum(values) / len(values)
|
|
510
|
+
stdev = _population_stdev(values, mean)
|
|
511
|
+
coefficient = stdev if mean == 0 else stdev / abs(mean)
|
|
512
|
+
if coefficient >= stdev_threshold:
|
|
513
|
+
return None
|
|
514
|
+
return LooseUniformFinding(
|
|
515
|
+
json_artifact=str(json_path),
|
|
516
|
+
json_pointer=pointer,
|
|
517
|
+
array_length=len(values),
|
|
518
|
+
mean=mean,
|
|
519
|
+
stdev=stdev,
|
|
520
|
+
coefficient_of_variation=coefficient,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def detect_loose_uniform(
|
|
525
|
+
json_path: Path, min_array_length: int = 4, stdev_threshold: float = 1e-3
|
|
526
|
+
) -> list[LooseUniformFinding]:
|
|
527
|
+
"""Flag near-uniform numeric arrays without double-reporting exact uniformity."""
|
|
528
|
+
try:
|
|
529
|
+
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
530
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
531
|
+
return []
|
|
532
|
+
findings: list[LooseUniformFinding] = []
|
|
533
|
+
for pointer, value in _walk_json(data):
|
|
534
|
+
if isinstance(value, list):
|
|
535
|
+
numerics = [float(x) for x in value if isinstance(x, (int, float)) and not isinstance(x, bool)]
|
|
536
|
+
if len(numerics) == len(value) and len(numerics) >= min_array_length:
|
|
537
|
+
finding = _loose_uniform_finding(
|
|
538
|
+
json_path=json_path,
|
|
539
|
+
pointer=pointer,
|
|
540
|
+
values=numerics,
|
|
541
|
+
stdev_threshold=stdev_threshold,
|
|
542
|
+
)
|
|
543
|
+
if finding is not None:
|
|
544
|
+
findings.append(finding)
|
|
545
|
+
if len(value) < min_array_length or not all(isinstance(item, dict) for item in value):
|
|
546
|
+
continue
|
|
547
|
+
common_keys = set(value[0].keys())
|
|
548
|
+
for item in value[1:]:
|
|
549
|
+
common_keys &= set(item.keys())
|
|
550
|
+
for key in common_keys:
|
|
551
|
+
field_values = [item.get(key) for item in value]
|
|
552
|
+
numerics = [
|
|
553
|
+
float(v)
|
|
554
|
+
for v in field_values
|
|
555
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool)
|
|
556
|
+
]
|
|
557
|
+
if len(numerics) != len(field_values):
|
|
558
|
+
continue
|
|
559
|
+
finding = _loose_uniform_finding(
|
|
560
|
+
json_path=json_path,
|
|
561
|
+
pointer=f"{pointer}[*].{key}",
|
|
562
|
+
values=numerics,
|
|
563
|
+
stdev_threshold=stdev_threshold,
|
|
564
|
+
)
|
|
565
|
+
if finding is not None:
|
|
566
|
+
findings.append(finding)
|
|
567
|
+
return findings
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
_TEXT_TOKEN_RE = re.compile(r"[A-Za-z0-9_./\\:-]+")
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _text_tokens(path: Path) -> set[str] | None:
|
|
574
|
+
try:
|
|
575
|
+
text = path.read_text(encoding="utf-8")
|
|
576
|
+
except (OSError, UnicodeDecodeError):
|
|
577
|
+
return None
|
|
578
|
+
tokens: set[str] = set()
|
|
579
|
+
for raw in _TEXT_TOKEN_RE.findall(text.lower()):
|
|
580
|
+
token = raw.strip("._-:/\\")
|
|
581
|
+
if not token:
|
|
582
|
+
continue
|
|
583
|
+
if "/" in raw or "\\" in raw or _PATH_HINT_RE.match(raw):
|
|
584
|
+
continue
|
|
585
|
+
tokens.add(token)
|
|
586
|
+
return tokens
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _token_overlap(left: set[str], right: set[str]) -> float:
|
|
590
|
+
if not left or not right:
|
|
591
|
+
return 0.0
|
|
592
|
+
return len(left & right) / min(len(left), len(right))
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _text_segment_tokens(path: Path) -> list[tuple[str, set[str]]]:
|
|
596
|
+
try:
|
|
597
|
+
text = path.read_text(encoding="utf-8")
|
|
598
|
+
except (OSError, UnicodeDecodeError):
|
|
599
|
+
return []
|
|
600
|
+
segments = [segment.strip() for segment in re.split(r"\n\s*\n+", text) if segment.strip()]
|
|
601
|
+
if len(segments) < 2:
|
|
602
|
+
return []
|
|
603
|
+
entries: list[tuple[str, set[str]]] = []
|
|
604
|
+
for index, segment in enumerate(segments):
|
|
605
|
+
tokens: set[str] = set()
|
|
606
|
+
for raw in _TEXT_TOKEN_RE.findall(segment.lower()):
|
|
607
|
+
token = raw.strip("._-:/\\")
|
|
608
|
+
if not token:
|
|
609
|
+
continue
|
|
610
|
+
if "/" in raw or "\\" in raw or _PATH_HINT_RE.match(raw):
|
|
611
|
+
continue
|
|
612
|
+
tokens.add(token)
|
|
613
|
+
if len(tokens) >= 5:
|
|
614
|
+
entries.append((f"{path}#segment-{index + 1}", tokens))
|
|
615
|
+
return entries
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def detect_boilerplate_text(
|
|
619
|
+
file_group: Iterable[Path], token_overlap_threshold: float = 0.80, min_group_size: int = 3
|
|
620
|
+
) -> list[BoilerplateFinding]:
|
|
621
|
+
"""Flag groups of text files with high non-path token overlap."""
|
|
622
|
+
entries: list[tuple[Path, set[str]]] = []
|
|
623
|
+
for path in file_group:
|
|
624
|
+
if not path.is_file():
|
|
625
|
+
continue
|
|
626
|
+
tokens = _text_tokens(path)
|
|
627
|
+
if tokens:
|
|
628
|
+
entries.append((path, tokens))
|
|
629
|
+
for segment_name, segment_tokens in _text_segment_tokens(path):
|
|
630
|
+
entries.append((Path(segment_name), segment_tokens))
|
|
631
|
+
|
|
632
|
+
neighbors: dict[int, set[int]] = {i: set() for i in range(len(entries))}
|
|
633
|
+
for i, (_, left) in enumerate(entries):
|
|
634
|
+
for j in range(i + 1, len(entries)):
|
|
635
|
+
_, right = entries[j]
|
|
636
|
+
if _token_overlap(left, right) >= token_overlap_threshold:
|
|
637
|
+
neighbors[i].add(j)
|
|
638
|
+
neighbors[j].add(i)
|
|
639
|
+
|
|
640
|
+
findings: list[BoilerplateFinding] = []
|
|
641
|
+
seen: set[int] = set()
|
|
642
|
+
for start in range(len(entries)):
|
|
643
|
+
if start in seen:
|
|
644
|
+
continue
|
|
645
|
+
stack = [start]
|
|
646
|
+
component: set[int] = set()
|
|
647
|
+
while stack:
|
|
648
|
+
item = stack.pop()
|
|
649
|
+
if item in component:
|
|
650
|
+
continue
|
|
651
|
+
component.add(item)
|
|
652
|
+
stack.extend(neighbors[item] - component)
|
|
653
|
+
seen |= component
|
|
654
|
+
if len(component) < min_group_size:
|
|
655
|
+
continue
|
|
656
|
+
token_sets = [entries[i][1] for i in sorted(component)]
|
|
657
|
+
shared = set.intersection(*token_sets)
|
|
658
|
+
overlap = min(
|
|
659
|
+
_token_overlap(token_sets[i], token_sets[j])
|
|
660
|
+
for i in range(len(token_sets))
|
|
661
|
+
for j in range(i + 1, len(token_sets))
|
|
662
|
+
)
|
|
663
|
+
if overlap < token_overlap_threshold:
|
|
664
|
+
continue
|
|
665
|
+
findings.append(
|
|
666
|
+
BoilerplateFinding(
|
|
667
|
+
paths=tuple(str(entries[i][0]) for i in sorted(component)),
|
|
668
|
+
token_overlap=overlap,
|
|
669
|
+
shared_token_count=len(shared),
|
|
670
|
+
sample_tokens=tuple(sorted(shared)[:12]),
|
|
671
|
+
)
|
|
672
|
+
)
|
|
673
|
+
return findings
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def detect_size_distribution(
|
|
677
|
+
file_paths: Iterable[Path], variance_threshold: float = 0.05, min_group_size: int = 3
|
|
678
|
+
) -> list[SizeDistributionFinding]:
|
|
679
|
+
"""Flag sibling-directory groups whose byte sizes are tightly clustered."""
|
|
680
|
+
by_parent: dict[Path, list[Path]] = {}
|
|
681
|
+
for path in file_paths:
|
|
682
|
+
if path.is_file():
|
|
683
|
+
by_parent.setdefault(path.parent, []).append(path)
|
|
684
|
+
|
|
685
|
+
findings: list[SizeDistributionFinding] = []
|
|
686
|
+
for parent, paths in sorted(by_parent.items(), key=lambda item: str(item[0])):
|
|
687
|
+
if len(paths) < min_group_size:
|
|
688
|
+
continue
|
|
689
|
+
sizes = [float(path.stat().st_size) for path in paths]
|
|
690
|
+
mean = sum(sizes) / len(sizes)
|
|
691
|
+
stdev = _population_stdev(sizes, mean)
|
|
692
|
+
coefficient = 0.0 if mean == 0 else stdev / mean
|
|
693
|
+
if coefficient >= variance_threshold:
|
|
694
|
+
continue
|
|
695
|
+
findings.append(
|
|
696
|
+
SizeDistributionFinding(
|
|
697
|
+
sibling_directory=str(parent),
|
|
698
|
+
paths=tuple(str(path) for path in sorted(paths)),
|
|
699
|
+
mean_size_bytes=mean,
|
|
700
|
+
stdev_size_bytes=stdev,
|
|
701
|
+
coefficient_of_variation=coefficient,
|
|
702
|
+
)
|
|
703
|
+
)
|
|
704
|
+
return findings
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _finding_summary(finding: Any) -> str:
|
|
708
|
+
if isinstance(finding, LooseUniformFinding):
|
|
709
|
+
return json.dumps(
|
|
710
|
+
{
|
|
711
|
+
"kind": "tier2_uncertain_loose_uniform",
|
|
712
|
+
"json_artifact": finding.json_artifact,
|
|
713
|
+
"json_pointer": finding.json_pointer,
|
|
714
|
+
"array_length": finding.array_length,
|
|
715
|
+
"mean": finding.mean,
|
|
716
|
+
"stdev": finding.stdev,
|
|
717
|
+
"coefficient_of_variation": finding.coefficient_of_variation,
|
|
718
|
+
},
|
|
719
|
+
sort_keys=True,
|
|
720
|
+
)
|
|
721
|
+
return json.dumps({"kind": type(finding).__name__, "summary": str(finding)}, sort_keys=True)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _finding_kind(finding: Any) -> str:
|
|
725
|
+
if isinstance(finding, LooseUniformFinding):
|
|
726
|
+
return "tier2_uncertain_loose_uniform"
|
|
727
|
+
return type(finding).__name__
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _uncertain_fallback(reason: str) -> EvidenceJudgment:
|
|
731
|
+
redacted = re.sub(r"\s+", " ", str(reason or "unknown")).strip()[:240]
|
|
732
|
+
return EvidenceJudgment(
|
|
733
|
+
verdict="uncertain",
|
|
734
|
+
confidence=0.0,
|
|
735
|
+
reasoning=f"tier3_call_error: {redacted}",
|
|
736
|
+
specific_concerns=(redacted,),
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def evidence_judgment_to_json(judgment: EvidenceJudgment) -> dict[str, Any]:
|
|
741
|
+
return {
|
|
742
|
+
"verdict": judgment.verdict,
|
|
743
|
+
"confidence": judgment.confidence,
|
|
744
|
+
"reasoning": judgment.reasoning,
|
|
745
|
+
"specific_concerns": list(judgment.specific_concerns),
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def tier3_judgment_blocker(judgment: EvidenceJudgment, *, confidence_threshold: float) -> dict[str, Any] | None:
|
|
750
|
+
verdict = judgment.verdict.strip().lower()
|
|
751
|
+
if verdict == "uncertain":
|
|
752
|
+
return None
|
|
753
|
+
if verdict == "fake" or judgment.confidence < confidence_threshold:
|
|
754
|
+
return {
|
|
755
|
+
"human_required": False,
|
|
756
|
+
"blocker_class": "contract_bug",
|
|
757
|
+
"blocker_summary": (
|
|
758
|
+
f"Tier 3 evidence audit judged closeout evidence as {verdict!r} "
|
|
759
|
+
f"with confidence {judgment.confidence:.3f}."
|
|
760
|
+
),
|
|
761
|
+
"required_human_inputs": (),
|
|
762
|
+
"access_attempts": (),
|
|
763
|
+
"metadata": {"tier3_judgment": evidence_judgment_to_json(judgment)},
|
|
764
|
+
}
|
|
765
|
+
return None
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def build_tier3_audit_event_metadata(
|
|
769
|
+
*,
|
|
770
|
+
tier2_finding: Any,
|
|
771
|
+
sample_path: Path,
|
|
772
|
+
expected_artifact_characteristics: str,
|
|
773
|
+
judgment: EvidenceJudgment,
|
|
774
|
+
latency_ms: int,
|
|
775
|
+
token_counts: dict[str, int] | None = None,
|
|
776
|
+
estimated_cost_usd: float | None = None,
|
|
777
|
+
) -> dict[str, Any]:
|
|
778
|
+
prompt_payload = {
|
|
779
|
+
"tier2_signal_summary": _finding_summary(tier2_finding),
|
|
780
|
+
"sample_path": str(sample_path),
|
|
781
|
+
"expected_artifact_characteristics": str(expected_artifact_characteristics),
|
|
782
|
+
}
|
|
783
|
+
response_payload = evidence_judgment_to_json(judgment)
|
|
784
|
+
prompt_json = json.dumps(prompt_payload, sort_keys=True, separators=(",", ":"))
|
|
785
|
+
response_json = json.dumps(response_payload, sort_keys=True, separators=(",", ":"))
|
|
786
|
+
return {
|
|
787
|
+
"prompt_sha256": hashlib.sha256(prompt_json.encode("utf-8")).hexdigest(),
|
|
788
|
+
"response_sha256": hashlib.sha256(response_json.encode("utf-8")).hexdigest(),
|
|
789
|
+
"verdict": judgment.verdict,
|
|
790
|
+
"confidence": judgment.confidence,
|
|
791
|
+
"token_counts": token_counts,
|
|
792
|
+
"latency_ms": latency_ms,
|
|
793
|
+
"estimated_cost_usd": estimated_cost_usd,
|
|
794
|
+
"tier3_budget_marker": "tier3_calls_made",
|
|
795
|
+
"finding_kind": _finding_kind(tier2_finding),
|
|
796
|
+
"sample_path": str(sample_path),
|
|
797
|
+
"judgment": response_payload,
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def run_tier3_runner_audit(
|
|
802
|
+
repo: Path,
|
|
803
|
+
*,
|
|
804
|
+
tier3_budget: int = 3,
|
|
805
|
+
confidence_threshold: float = 0.85,
|
|
806
|
+
expected_artifact_characteristics: str = "Real evidence should contain provenance-specific, naturally varied values and references.",
|
|
807
|
+
dirty_only: bool = True,
|
|
808
|
+
) -> Tier3RunnerAudit:
|
|
809
|
+
result = run_evidence_audit(
|
|
810
|
+
repo,
|
|
811
|
+
dirty_only=dirty_only,
|
|
812
|
+
tier2_enabled=True,
|
|
813
|
+
enable_tier_3=False,
|
|
814
|
+
expected_artifact_characteristics=expected_artifact_characteristics,
|
|
815
|
+
)
|
|
816
|
+
budget = Tier3Budget(tier3_budget=tier3_budget)
|
|
817
|
+
invocations: list[Tier3InvocationRecord] = []
|
|
818
|
+
markers: list[dict[str, Any]] = []
|
|
819
|
+
warnings: list[str] = []
|
|
820
|
+
blocker: dict[str, Any] | None = None
|
|
821
|
+
if result.duplicate_content or result.uniform_numeric or result.missing_references:
|
|
822
|
+
return Tier3RunnerAudit(tier3_budget, budget.tier3_calls_made, (), (), ())
|
|
823
|
+
for finding in result.loose_uniform:
|
|
824
|
+
sample_path = _tier3_sample_path(finding)
|
|
825
|
+
if not sample_path.is_file():
|
|
826
|
+
continue
|
|
827
|
+
if not budget.consume():
|
|
828
|
+
markers.append(
|
|
829
|
+
{
|
|
830
|
+
"marker": UNCERTAIN_OPERATOR_REVIEW,
|
|
831
|
+
"finding_kind": _finding_kind(finding),
|
|
832
|
+
"sample_path": str(sample_path),
|
|
833
|
+
"tier3_budget": budget.tier3_budget,
|
|
834
|
+
"tier3_calls_made": budget.tier3_calls_made,
|
|
835
|
+
}
|
|
836
|
+
)
|
|
837
|
+
continue
|
|
838
|
+
started = time.monotonic()
|
|
839
|
+
try:
|
|
840
|
+
judgment = evaluate_suspected_fake_evidence(
|
|
841
|
+
finding,
|
|
842
|
+
sample_path,
|
|
843
|
+
expected_artifact_characteristics,
|
|
844
|
+
)
|
|
845
|
+
except Exception as exc:
|
|
846
|
+
judgment = _uncertain_fallback(str(exc))
|
|
847
|
+
latency_ms = max(0, int((time.monotonic() - started) * 1000))
|
|
848
|
+
metadata = build_tier3_audit_event_metadata(
|
|
849
|
+
tier2_finding=finding,
|
|
850
|
+
sample_path=sample_path,
|
|
851
|
+
expected_artifact_characteristics=expected_artifact_characteristics,
|
|
852
|
+
judgment=judgment,
|
|
853
|
+
latency_ms=latency_ms,
|
|
854
|
+
)
|
|
855
|
+
invocations.append(
|
|
856
|
+
Tier3InvocationRecord(
|
|
857
|
+
finding_kind=_finding_kind(finding),
|
|
858
|
+
sample_path=str(sample_path),
|
|
859
|
+
judgment=judgment,
|
|
860
|
+
metadata=metadata,
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
candidate = tier3_judgment_blocker(judgment, confidence_threshold=confidence_threshold)
|
|
864
|
+
if candidate is not None and blocker is None:
|
|
865
|
+
blocker = candidate
|
|
866
|
+
if judgment.verdict.strip().lower() == "uncertain":
|
|
867
|
+
warnings.append(judgment.reasoning)
|
|
868
|
+
return Tier3RunnerAudit(
|
|
869
|
+
tier3_budget=tier3_budget,
|
|
870
|
+
tier3_calls_made=budget.tier3_calls_made,
|
|
871
|
+
invocations=tuple(invocations),
|
|
872
|
+
operator_review_markers=tuple(markers),
|
|
873
|
+
warnings=tuple(warnings),
|
|
874
|
+
blocker=blocker,
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _read_sample_artifact(sample_path: Path, max_sample_bytes: int) -> str:
|
|
879
|
+
data = sample_path.read_bytes()[:max_sample_bytes]
|
|
880
|
+
return data.decode("utf-8", errors="replace")
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _execute_baml_request(request: BamlRequest, *, timeout_seconds: int) -> str:
|
|
884
|
+
data = json.dumps(request.body).encode("utf-8")
|
|
885
|
+
http_request = urllib.request.Request(
|
|
886
|
+
request.url,
|
|
887
|
+
data=data,
|
|
888
|
+
headers=request.headers,
|
|
889
|
+
method=request.method,
|
|
890
|
+
)
|
|
891
|
+
with urllib.request.urlopen(http_request, timeout=timeout_seconds) as response:
|
|
892
|
+
return response.read().decode("utf-8", errors="replace")
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def evaluate_suspected_fake_evidence(
|
|
896
|
+
tier2_finding: Any,
|
|
897
|
+
sample_path: Path,
|
|
898
|
+
expected: str,
|
|
899
|
+
*,
|
|
900
|
+
max_sample_bytes: int = 8192,
|
|
901
|
+
timeout_seconds: int = 30,
|
|
902
|
+
) -> EvidenceJudgment:
|
|
903
|
+
try:
|
|
904
|
+
sample = _read_sample_artifact(Path(sample_path), max_sample_bytes)
|
|
905
|
+
request = build_baml_request(
|
|
906
|
+
"EvaluateSuspectedFakeEvidence",
|
|
907
|
+
{
|
|
908
|
+
"tier2_signal_summary": _finding_summary(tier2_finding),
|
|
909
|
+
"sample_artifact_content": sample,
|
|
910
|
+
"expected_artifact_characteristics": str(expected),
|
|
911
|
+
},
|
|
912
|
+
)
|
|
913
|
+
raw_response = _execute_baml_request(request, timeout_seconds=timeout_seconds)
|
|
914
|
+
parsed = parse_baml_response("EvidenceJudgment", raw_response).payload
|
|
915
|
+
return EvidenceJudgment(
|
|
916
|
+
verdict=str(parsed["verdict"]),
|
|
917
|
+
confidence=float(parsed["confidence"]),
|
|
918
|
+
reasoning=str(parsed["reasoning"]),
|
|
919
|
+
specific_concerns=tuple(str(item) for item in parsed["specific_concerns"]),
|
|
920
|
+
)
|
|
921
|
+
except (
|
|
922
|
+
OSError,
|
|
923
|
+
TimeoutError,
|
|
924
|
+
urllib.error.URLError,
|
|
925
|
+
KeyError,
|
|
926
|
+
TypeError,
|
|
927
|
+
ValueError,
|
|
928
|
+
json.JSONDecodeError,
|
|
929
|
+
) as exc:
|
|
930
|
+
return _uncertain_fallback(str(exc))
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _tier3_sample_path(finding: LooseUniformFinding) -> Path:
|
|
934
|
+
return Path(finding.json_artifact)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def run_evidence_audit(
|
|
938
|
+
repo: Path,
|
|
939
|
+
*,
|
|
940
|
+
dirty_only: bool = True,
|
|
941
|
+
min_duplicates: int = 3,
|
|
942
|
+
uniform_epsilon: float = 1e-6,
|
|
943
|
+
uniform_min_length: int = 4,
|
|
944
|
+
tier2_enabled: bool = False,
|
|
945
|
+
loose_uniform_stdev_threshold: float = 1e-3,
|
|
946
|
+
boilerplate_token_overlap_threshold: float = 0.80,
|
|
947
|
+
boilerplate_min_group_size: int = 3,
|
|
948
|
+
size_distribution_variance_threshold: float = 0.05,
|
|
949
|
+
size_distribution_min_group_size: int = 3,
|
|
950
|
+
enable_tier_3: bool = False,
|
|
951
|
+
missing_references_strict: bool = True,
|
|
952
|
+
expected_artifact_characteristics: str = "Real evidence should contain provenance-specific, naturally varied values and references.",
|
|
953
|
+
) -> EvidenceAuditResult:
|
|
954
|
+
"""Run all three detectors against the repo's dirty (or full) tree.
|
|
955
|
+
|
|
956
|
+
dirty_only=True scopes the audit to currently-modified/untracked paths,
|
|
957
|
+
which is the typical pre-reconcile use case. dirty_only=False audits
|
|
958
|
+
every tracked file (slower; useful for forensic sweeps).
|
|
959
|
+
"""
|
|
960
|
+
repo = repo.expanduser().resolve()
|
|
961
|
+
effective_tier2_enabled = bool(tier2_enabled or enable_tier_3)
|
|
962
|
+
result = EvidenceAuditResult(repo=str(repo), tier2_enabled=effective_tier2_enabled, tier3_enabled=enable_tier_3)
|
|
963
|
+
|
|
964
|
+
if dirty_only:
|
|
965
|
+
rels = _git_dirty_paths(repo)
|
|
966
|
+
files = [repo / p for p in rels if (repo / p).is_file()]
|
|
967
|
+
else:
|
|
968
|
+
files = [p for p in repo.rglob("*") if p.is_file() and ".git" not in p.parts]
|
|
969
|
+
|
|
970
|
+
result.files_scanned = len(files)
|
|
971
|
+
|
|
972
|
+
# Duplicate-content detector
|
|
973
|
+
result.duplicate_content = detect_duplicate_content(files, min_duplicates=min_duplicates)
|
|
974
|
+
|
|
975
|
+
# Per-JSON-artifact detectors
|
|
976
|
+
json_artifacts = [p for p in files if p.suffix == ".json"]
|
|
977
|
+
result.json_artifacts_scanned = len(json_artifacts)
|
|
978
|
+
for jp in json_artifacts:
|
|
979
|
+
result.uniform_numeric.extend(
|
|
980
|
+
detect_uniform_numeric(jp, min_array_length=uniform_min_length, epsilon=uniform_epsilon)
|
|
981
|
+
)
|
|
982
|
+
result.missing_references.extend(
|
|
983
|
+
detect_missing_references(jp, repo, strict=missing_references_strict)
|
|
984
|
+
)
|
|
985
|
+
if effective_tier2_enabled:
|
|
986
|
+
result.loose_uniform.extend(
|
|
987
|
+
detect_loose_uniform(
|
|
988
|
+
jp,
|
|
989
|
+
min_array_length=uniform_min_length,
|
|
990
|
+
stdev_threshold=loose_uniform_stdev_threshold,
|
|
991
|
+
)
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
if effective_tier2_enabled:
|
|
995
|
+
result.boilerplate_text = detect_boilerplate_text(
|
|
996
|
+
files,
|
|
997
|
+
token_overlap_threshold=boilerplate_token_overlap_threshold,
|
|
998
|
+
min_group_size=boilerplate_min_group_size,
|
|
999
|
+
)
|
|
1000
|
+
result.size_distribution = detect_size_distribution(
|
|
1001
|
+
files,
|
|
1002
|
+
variance_threshold=size_distribution_variance_threshold,
|
|
1003
|
+
min_group_size=size_distribution_min_group_size,
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
if enable_tier_3 and not result.duplicate_content and not result.uniform_numeric and not result.missing_references:
|
|
1007
|
+
for finding in result.loose_uniform:
|
|
1008
|
+
sample_path = _tier3_sample_path(finding)
|
|
1009
|
+
if sample_path.is_file():
|
|
1010
|
+
result.tier3_judgments.append(
|
|
1011
|
+
evaluate_suspected_fake_evidence(
|
|
1012
|
+
finding,
|
|
1013
|
+
sample_path,
|
|
1014
|
+
expected_artifact_characteristics,
|
|
1015
|
+
)
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
return result
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def render_text(result: EvidenceAuditResult) -> str:
|
|
1022
|
+
"""Human-readable rendering for the CLI."""
|
|
1023
|
+
lines = [
|
|
1024
|
+
f"evidence-audit: {result.repo}",
|
|
1025
|
+
f" files scanned: {result.files_scanned}",
|
|
1026
|
+
f" json artifacts scanned: {result.json_artifacts_scanned}",
|
|
1027
|
+
f" duplicate-content findings: {len(result.duplicate_content)}",
|
|
1028
|
+
f" uniform-numeric findings: {len(result.uniform_numeric)}",
|
|
1029
|
+
f" missing-references findings: {len(result.missing_references)}",
|
|
1030
|
+
]
|
|
1031
|
+
if result.tier2_enabled:
|
|
1032
|
+
lines.extend(
|
|
1033
|
+
[
|
|
1034
|
+
f" tier2: loose-uniform findings: {len(result.loose_uniform)}",
|
|
1035
|
+
f" tier2: boilerplate-text findings: {len(result.boilerplate_text)}",
|
|
1036
|
+
f" tier2: size-distribution findings: {len(result.size_distribution)}",
|
|
1037
|
+
]
|
|
1038
|
+
)
|
|
1039
|
+
if result.tier3_enabled:
|
|
1040
|
+
lines.append(f" tier3: EvidenceJudgment results: {len(result.tier3_judgments)}")
|
|
1041
|
+
if result.is_clean():
|
|
1042
|
+
lines.append("")
|
|
1043
|
+
lines.append("CLEAN — no fake-evidence patterns detected.")
|
|
1044
|
+
return "\n".join(lines)
|
|
1045
|
+
lines.append("")
|
|
1046
|
+
lines.append("SUSPECT — review before reconciling:")
|
|
1047
|
+
for f in result.duplicate_content:
|
|
1048
|
+
lines.append(
|
|
1049
|
+
f" duplicate-content (sha256={f.sha256[:12]}, size={f.size_bytes}B): {len(f.paths)} files share this hash"
|
|
1050
|
+
)
|
|
1051
|
+
for p in f.paths[:5]:
|
|
1052
|
+
lines.append(f" {p}")
|
|
1053
|
+
if len(f.paths) > 5:
|
|
1054
|
+
lines.append(f" ...and {len(f.paths) - 5} more")
|
|
1055
|
+
for f in result.uniform_numeric:
|
|
1056
|
+
lines.append(
|
|
1057
|
+
f" uniform-numeric: {f.json_artifact} {f.json_pointer} — "
|
|
1058
|
+
f"{f.array_length} entries all = {f.sample_value!r}"
|
|
1059
|
+
)
|
|
1060
|
+
for f in result.missing_references:
|
|
1061
|
+
lines.append(
|
|
1062
|
+
f" missing-reference: {f.json_artifact} {f.json_pointer} -> {f.missing_path!r}"
|
|
1063
|
+
)
|
|
1064
|
+
for f in result.loose_uniform:
|
|
1065
|
+
lines.append(
|
|
1066
|
+
f" tier2: loose-uniform: {f.json_artifact} {f.json_pointer} — "
|
|
1067
|
+
f"{f.array_length} entries cv={f.coefficient_of_variation:.6g}"
|
|
1068
|
+
)
|
|
1069
|
+
for f in result.boilerplate_text:
|
|
1070
|
+
lines.append(
|
|
1071
|
+
f" tier2: boilerplate-text: {len(f.paths)} files overlap={f.token_overlap:.2f}"
|
|
1072
|
+
)
|
|
1073
|
+
for p in f.paths[:5]:
|
|
1074
|
+
lines.append(f" {p}")
|
|
1075
|
+
if len(f.paths) > 5:
|
|
1076
|
+
lines.append(f" ...and {len(f.paths) - 5} more")
|
|
1077
|
+
for f in result.size_distribution:
|
|
1078
|
+
lines.append(
|
|
1079
|
+
f" tier2: size-distribution: {f.sibling_directory} — "
|
|
1080
|
+
f"{len(f.paths)} files cv={f.coefficient_of_variation:.6g}"
|
|
1081
|
+
)
|
|
1082
|
+
return "\n".join(lines)
|