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.
Files changed (370) hide show
  1. phase_loop_runtime/__init__.py +20 -0
  2. phase_loop_runtime/_contract_docs/phase-loop/extraction-readiness.md +90 -0
  3. phase_loop_runtime/_contract_docs/phase-loop/granular-execution-policy.md +137 -0
  4. phase_loop_runtime/_contract_docs/phase-loop/harness-capability-matrix.md +510 -0
  5. phase_loop_runtime/_contract_docs/phase-loop/harness-skill-matrix.md +125 -0
  6. phase_loop_runtime/_contract_docs/phase-loop/harness-substrate-manifest.md +232 -0
  7. phase_loop_runtime/_contract_docs/phase-loop/pi-loop-control.md +247 -0
  8. phase_loop_runtime/_contract_docs/phase-loop/protocol.md +2759 -0
  9. phase_loop_runtime/_contract_docs/phase-loop/runtime-boundary.md +289 -0
  10. phase_loop_runtime/_contract_docs/phase-loop/spec-discovery-roots.md +47 -0
  11. phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-governed-pipeline.md +42 -0
  12. phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-portal-projection.md +34 -0
  13. phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-regenesis.md +29 -0
  14. phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-report.md +32 -0
  15. phase_loop_runtime/_contract_docs/runtime/verification-evidence-contract.md +52 -0
  16. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/commands.txt +2 -0
  17. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/manifest.json +16 -0
  18. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/mixed.json +5 -0
  19. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-provider-timeout/manifest.json +16 -0
  20. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-provider-timeout/timeout-note.txt +2 -0
  21. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-redacted-summary/manifest.json +16 -0
  22. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-redacted-summary/redacted-summary.json +6 -0
  23. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-small-sample/manifest.json +16 -0
  24. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-small-sample/sample.json +7 -0
  25. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-threshold-edge/edge.txt +2 -0
  26. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-threshold-edge/manifest.json +16 -0
  27. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/manifest.json +16 -0
  28. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-a.txt +1 -0
  29. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-b.txt +1 -0
  30. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-c.txt +1 -0
  31. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-missing-references/claimed-results.json +5 -0
  32. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-missing-references/manifest.json +16 -0
  33. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-subtle-low-variance/manifest.json +16 -0
  34. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-subtle-low-variance/metrics.json +9 -0
  35. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-templated-prose/findings.md +10 -0
  36. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-templated-prose/manifest.json +16 -0
  37. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/manifest.json +16 -0
  38. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/scores.json +4 -0
  39. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/summary.txt +1 -0
  40. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/bundle-check.json +8 -0
  41. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/manifest.json +16 -0
  42. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/operator-note.txt +2 -0
  43. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/events-sample.jsonl +3 -0
  44. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/manifest.json +16 -0
  45. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/receipt.txt +1 -0
  46. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/evidence.json +13 -0
  47. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/manifest.json +16 -0
  48. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/summary.txt +2 -0
  49. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/manifest.json +16 -0
  50. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/matrix.json +9 -0
  51. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/notes.md +4 -0
  52. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/console-summary.txt +2 -0
  53. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/manifest.json +16 -0
  54. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/screenshot-standin.ppm +5 -0
  55. phase_loop_runtime/adoption_bundle.py +273 -0
  56. phase_loop_runtime/advisor_board/CONTRACTS.md +257 -0
  57. phase_loop_runtime/advisor_board/__init__.py +288 -0
  58. phase_loop_runtime/advisor_board/backing.py +137 -0
  59. phase_loop_runtime/advisor_board/backing_omnigent.py +498 -0
  60. phase_loop_runtime/advisor_board/config.py +225 -0
  61. phase_loop_runtime/advisor_board/events.py +115 -0
  62. phase_loop_runtime/advisor_board/fixtures/advisor-boards.example.toml +59 -0
  63. phase_loop_runtime/advisor_board/fixtures.py +91 -0
  64. phase_loop_runtime/advisor_board/harness_mapping.py +125 -0
  65. phase_loop_runtime/advisor_board/matrix.py +146 -0
  66. phase_loop_runtime/advisor_board/observability.py +481 -0
  67. phase_loop_runtime/advisor_board/presets.py +188 -0
  68. phase_loop_runtime/advisor_board/registries.py +309 -0
  69. phase_loop_runtime/advisor_board/resolver.py +253 -0
  70. phase_loop_runtime/advisor_board/schema.py +269 -0
  71. phase_loop_runtime/advisor_board/standin.py +166 -0
  72. phase_loop_runtime/advisor_board/validation.py +159 -0
  73. phase_loop_runtime/agent_runtime_provider.py +676 -0
  74. phase_loop_runtime/baml_modular.py +656 -0
  75. phase_loop_runtime/baml_src/dotfiles_adoption_manifest.baml +37 -0
  76. phase_loop_runtime/baml_src/dotfiles_c4_document.baml +14 -0
  77. phase_loop_runtime/baml_src/dotfiles_plan_manifest.baml +50 -0
  78. phase_loop_runtime/baml_src/dotfiles_runtime_projection.baml +16 -0
  79. phase_loop_runtime/baml_src/dotfiles_task_catalog.baml +20 -0
  80. phase_loop_runtime/baml_src/emit_phase_closeout.baml +86 -0
  81. phase_loop_runtime/baml_src/evaluate_suspected_fake_evidence.baml +36 -0
  82. phase_loop_runtime/baml_src/verification_evidence.baml +40 -0
  83. phase_loop_runtime/broker.py +181 -0
  84. phase_loop_runtime/build_bundle.py +294 -0
  85. phase_loop_runtime/capability_registry.py +835 -0
  86. phase_loop_runtime/classifier.py +87 -0
  87. phase_loop_runtime/claude_agent_view.py +625 -0
  88. phase_loop_runtime/claude_channel_sidecar.py +1086 -0
  89. phase_loop_runtime/cli.py +2599 -0
  90. phase_loop_runtime/closeout.py +865 -0
  91. phase_loop_runtime/closeout_classifier.py +120 -0
  92. phase_loop_runtime/closeout_evidence_audit.py +112 -0
  93. phase_loop_runtime/closeout_validation.py +144 -0
  94. phase_loop_runtime/closeout_validators.py +203 -0
  95. phase_loop_runtime/consiliency_gates.py +374 -0
  96. phase_loop_runtime/consiliency_ingest.py +399 -0
  97. phase_loop_runtime/consiliency_layout.py +176 -0
  98. phase_loop_runtime/consiliency_scaffold.py +305 -0
  99. phase_loop_runtime/cross_repo_channel.py +331 -0
  100. phase_loop_runtime/discovery.py +2368 -0
  101. phase_loop_runtime/dispatch_lock.py +167 -0
  102. phase_loop_runtime/doc_delta_validator.py +98 -0
  103. phase_loop_runtime/docs_audit.py +285 -0
  104. phase_loop_runtime/docs_freshness.py +468 -0
  105. phase_loop_runtime/docs_surfaces.py +150 -0
  106. phase_loop_runtime/dotfiles_profile_plugin.py +96 -0
  107. phase_loop_runtime/events.py +154 -0
  108. phase_loop_runtime/events_migration.py +96 -0
  109. phase_loop_runtime/evidence_audit.py +1082 -0
  110. phase_loop_runtime/evidence_audit_config.py +172 -0
  111. phase_loop_runtime/fleet_map.py +426 -0
  112. phase_loop_runtime/fleet_metrics.py +298 -0
  113. phase_loop_runtime/fleet_metrics_export.py +196 -0
  114. phase_loop_runtime/git_discipline.py +479 -0
  115. phase_loop_runtime/git_ops.py +122 -0
  116. phase_loop_runtime/git_topology.py +252 -0
  117. phase_loop_runtime/governed_bundle.py +137 -0
  118. phase_loop_runtime/governed_premerge.py +219 -0
  119. phase_loop_runtime/governed_review.py +264 -0
  120. phase_loop_runtime/handoff.py +932 -0
  121. phase_loop_runtime/handoff_path.py +14 -0
  122. phase_loop_runtime/injection.py +740 -0
  123. phase_loop_runtime/install_status.py +88 -0
  124. phase_loop_runtime/lane_scheduler.py +317 -0
  125. phase_loop_runtime/launcher.py +2760 -0
  126. phase_loop_runtime/lease_store.py +553 -0
  127. phase_loop_runtime/maintenance.py +401 -0
  128. phase_loop_runtime/migrate_handoffs.py +201 -0
  129. phase_loop_runtime/models.py +2144 -0
  130. phase_loop_runtime/observability.py +1290 -0
  131. phase_loop_runtime/panel_invoker.py +1638 -0
  132. phase_loop_runtime/phase_loop_drift_audit.py +388 -0
  133. phase_loop_runtime/phase_worktree_executor.py +433 -0
  134. phase_loop_runtime/pipeline_adapter/__init__.py +12 -0
  135. phase_loop_runtime/pipeline_adapter/branch_ops.py +274 -0
  136. phase_loop_runtime/pipeline_adapter/flag.py +55 -0
  137. phase_loop_runtime/pipeline_adapter/markers.py +13 -0
  138. phase_loop_runtime/pipeline_adapter/merge_policy.py +72 -0
  139. phase_loop_runtime/pipeline_adapter/ratification.py +100 -0
  140. phase_loop_runtime/pipeline_adapter/sibling_matcher.py +123 -0
  141. phase_loop_runtime/plan_ir.py +523 -0
  142. phase_loop_runtime/plan_manifest.py +415 -0
  143. phase_loop_runtime/planner_validation.py +176 -0
  144. phase_loop_runtime/profiles.py +462 -0
  145. phase_loop_runtime/prompts.py +365 -0
  146. phase_loop_runtime/provenance.py +165 -0
  147. phase_loop_runtime/publishing.py +339 -0
  148. phase_loop_runtime/py.typed +1 -0
  149. phase_loop_runtime/reconcile.py +1550 -0
  150. phase_loop_runtime/redaction.py +184 -0
  151. phase_loop_runtime/reflection_sync.py +293 -0
  152. phase_loop_runtime/release_guard.py +186 -0
  153. phase_loop_runtime/render.py +432 -0
  154. phase_loop_runtime/repo_validation.py +427 -0
  155. phase_loop_runtime/review_summary.py +150 -0
  156. phase_loop_runtime/roadmap_lint.py +425 -0
  157. phase_loop_runtime/route_log.py +59 -0
  158. phase_loop_runtime/route_policy/__init__.py +11 -0
  159. phase_loop_runtime/route_policy/fixtures/route_selection.golden.json +263 -0
  160. phase_loop_runtime/runner.py +8400 -0
  161. phase_loop_runtime/runtime_paths.py +225 -0
  162. phase_loop_runtime/runtime_projection.py +113 -0
  163. phase_loop_runtime/runtime_resources.py +36 -0
  164. phase_loop_runtime/schema_export.py +292 -0
  165. phase_loop_runtime/schemas/phase_loop_closeout.schema.json +366 -0
  166. phase_loop_runtime/skill_install.py +176 -0
  167. phase_loop_runtime/skill_inventory.py +662 -0
  168. phase_loop_runtime/skill_paths.py +42 -0
  169. phase_loop_runtime/skill_sources_plugin.py +37 -0
  170. phase_loop_runtime/skills_bundle/claude-advisor-board/README.md +1 -0
  171. phase_loop_runtime/skills_bundle/claude-advisor-board/SKILL.md +38 -0
  172. phase_loop_runtime/skills_bundle/claude-advisor-panel/README.md +1 -0
  173. phase_loop_runtime/skills_bundle/claude-advisor-panel/SKILL.md +38 -0
  174. phase_loop_runtime/skills_bundle/claude-execute-detailed/README.md +1 -0
  175. phase_loop_runtime/skills_bundle/claude-execute-detailed/SKILL.md +152 -0
  176. phase_loop_runtime/skills_bundle/claude-execute-phase/README.md +1 -0
  177. phase_loop_runtime/skills_bundle/claude-execute-phase/SKILL.md +745 -0
  178. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/allocate_worktree_name.sh +33 -0
  179. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/audit_lane_file_touches.py +197 -0
  180. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
  181. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
  182. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
  183. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
  184. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
  185. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/state.py +274 -0
  186. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
  187. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/team_teardown.sh +32 -0
  188. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/validate_plan_doc.py +642 -0
  189. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/verify_harness.sh +96 -0
  190. phase_loop_runtime/skills_bundle/claude-phase-loop/README.md +1 -0
  191. phase_loop_runtime/skills_bundle/claude-phase-loop/SKILL.md +115 -0
  192. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/README.md +1 -0
  193. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/SKILL.md +439 -0
  194. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/assets/review_prompt.md +19 -0
  195. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
  196. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/references/roadmap-template.md +193 -0
  197. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
  198. phase_loop_runtime/skills_bundle/claude-plan-detailed/README.md +1 -0
  199. phase_loop_runtime/skills_bundle/claude-plan-detailed/SKILL.md +334 -0
  200. phase_loop_runtime/skills_bundle/claude-plan-detailed/assets/review_prompt.md +25 -0
  201. phase_loop_runtime/skills_bundle/claude-plan-phase/README.md +1 -0
  202. phase_loop_runtime/skills_bundle/claude-plan-phase/SKILL.md +776 -0
  203. phase_loop_runtime/skills_bundle/claude-plan-phase/assets/review_prompt.md +17 -0
  204. phase_loop_runtime/skills_bundle/claude-plan-phase/scripts/validate_plan_doc.py +1092 -0
  205. phase_loop_runtime/skills_bundle/claude-run-train/README.md +1 -0
  206. phase_loop_runtime/skills_bundle/claude-run-train/SKILL.md +75 -0
  207. phase_loop_runtime/skills_bundle/claude-skill-editor/README.md +1 -0
  208. phase_loop_runtime/skills_bundle/claude-skill-editor/SKILL.md +243 -0
  209. phase_loop_runtime/skills_bundle/claude-skill-editor/assets/editor_prompt.md +72 -0
  210. phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/README.md +1 -0
  211. phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/SKILL.md +249 -0
  212. phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
  213. phase_loop_runtime/skills_bundle/claude-task-contextualizer/README.md +1 -0
  214. phase_loop_runtime/skills_bundle/claude-task-contextualizer/SKILL.md +74 -0
  215. phase_loop_runtime/skills_bundle/claude-task-contextualizer/references/task-templates.md +103 -0
  216. phase_loop_runtime/skills_bundle/codex-advisor-board/README.md +1 -0
  217. phase_loop_runtime/skills_bundle/codex-advisor-board/SKILL.md +38 -0
  218. phase_loop_runtime/skills_bundle/codex-advisor-panel/README.md +1 -0
  219. phase_loop_runtime/skills_bundle/codex-advisor-panel/SKILL.md +38 -0
  220. phase_loop_runtime/skills_bundle/codex-execute-detailed/README.md +1 -0
  221. phase_loop_runtime/skills_bundle/codex-execute-detailed/SKILL.md +154 -0
  222. phase_loop_runtime/skills_bundle/codex-execute-phase/README.md +1 -0
  223. phase_loop_runtime/skills_bundle/codex-execute-phase/SKILL.md +231 -0
  224. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/allocate_worktree_name.sh +33 -0
  225. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/audit_lane_file_touches.py +197 -0
  226. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
  227. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
  228. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
  229. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
  230. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
  231. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/state.py +274 -0
  232. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
  233. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/team_teardown.sh +32 -0
  234. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/validate_plan_doc.py +642 -0
  235. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/verify_harness.sh +96 -0
  236. phase_loop_runtime/skills_bundle/codex-phase-loop/README.md +1 -0
  237. phase_loop_runtime/skills_bundle/codex-phase-loop/SKILL.md +248 -0
  238. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/README.md +1 -0
  239. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/SKILL.md +150 -0
  240. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/assets/review_prompt.md +19 -0
  241. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
  242. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/references/roadmap-template.md +193 -0
  243. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
  244. phase_loop_runtime/skills_bundle/codex-plan-detailed/README.md +1 -0
  245. phase_loop_runtime/skills_bundle/codex-plan-detailed/SKILL.md +119 -0
  246. phase_loop_runtime/skills_bundle/codex-plan-detailed/assets/review_prompt.md +25 -0
  247. phase_loop_runtime/skills_bundle/codex-plan-phase/README.md +1 -0
  248. phase_loop_runtime/skills_bundle/codex-plan-phase/SKILL.md +307 -0
  249. phase_loop_runtime/skills_bundle/codex-plan-phase/assets/review_prompt.md +17 -0
  250. phase_loop_runtime/skills_bundle/codex-plan-phase/scripts/validate_plan_doc.py +1092 -0
  251. phase_loop_runtime/skills_bundle/codex-run-train/README.md +1 -0
  252. phase_loop_runtime/skills_bundle/codex-run-train/SKILL.md +75 -0
  253. phase_loop_runtime/skills_bundle/codex-skill-editor/README.md +1 -0
  254. phase_loop_runtime/skills_bundle/codex-skill-editor/SKILL.md +94 -0
  255. phase_loop_runtime/skills_bundle/codex-skill-editor/assets/editor_prompt.md +72 -0
  256. phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/README.md +1 -0
  257. phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/SKILL.md +114 -0
  258. phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
  259. phase_loop_runtime/skills_bundle/codex-task-contextualizer/README.md +1 -0
  260. phase_loop_runtime/skills_bundle/codex-task-contextualizer/SKILL.md +102 -0
  261. phase_loop_runtime/skills_bundle/codex-task-contextualizer/references/task-templates.md +103 -0
  262. phase_loop_runtime/skills_bundle/gemini-advisor-board/README.md +1 -0
  263. phase_loop_runtime/skills_bundle/gemini-advisor-board/SKILL.md +38 -0
  264. phase_loop_runtime/skills_bundle/gemini-advisor-panel/README.md +1 -0
  265. phase_loop_runtime/skills_bundle/gemini-advisor-panel/SKILL.md +38 -0
  266. phase_loop_runtime/skills_bundle/gemini-execute-detailed/README.md +1 -0
  267. phase_loop_runtime/skills_bundle/gemini-execute-detailed/SKILL.md +144 -0
  268. phase_loop_runtime/skills_bundle/gemini-execute-phase/README.md +1 -0
  269. phase_loop_runtime/skills_bundle/gemini-execute-phase/SKILL.md +198 -0
  270. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/allocate_worktree_name.sh +33 -0
  271. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/audit_lane_file_touches.py +197 -0
  272. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
  273. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
  274. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
  275. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
  276. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
  277. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/state.py +274 -0
  278. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
  279. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/team_teardown.sh +32 -0
  280. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/validate_plan_doc.py +642 -0
  281. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/verify_harness.sh +96 -0
  282. phase_loop_runtime/skills_bundle/gemini-phase-loop/README.md +1 -0
  283. phase_loop_runtime/skills_bundle/gemini-phase-loop/SKILL.md +111 -0
  284. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/README.md +1 -0
  285. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/SKILL.md +147 -0
  286. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/assets/review_prompt.md +19 -0
  287. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
  288. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/references/roadmap-template.md +193 -0
  289. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
  290. phase_loop_runtime/skills_bundle/gemini-plan-detailed/README.md +1 -0
  291. phase_loop_runtime/skills_bundle/gemini-plan-detailed/SKILL.md +119 -0
  292. phase_loop_runtime/skills_bundle/gemini-plan-detailed/assets/review_prompt.md +25 -0
  293. phase_loop_runtime/skills_bundle/gemini-plan-phase/README.md +1 -0
  294. phase_loop_runtime/skills_bundle/gemini-plan-phase/SKILL.md +217 -0
  295. phase_loop_runtime/skills_bundle/gemini-plan-phase/assets/review_prompt.md +17 -0
  296. phase_loop_runtime/skills_bundle/gemini-plan-phase/scripts/validate_plan_doc.py +1092 -0
  297. phase_loop_runtime/skills_bundle/gemini-run-train/README.md +1 -0
  298. phase_loop_runtime/skills_bundle/gemini-run-train/SKILL.md +75 -0
  299. phase_loop_runtime/skills_bundle/gemini-skill-editor/README.md +1 -0
  300. phase_loop_runtime/skills_bundle/gemini-skill-editor/SKILL.md +87 -0
  301. phase_loop_runtime/skills_bundle/gemini-skill-editor/assets/editor_prompt.md +72 -0
  302. phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/README.md +1 -0
  303. phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/SKILL.md +109 -0
  304. phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
  305. phase_loop_runtime/skills_bundle/gemini-task-contextualizer/README.md +1 -0
  306. phase_loop_runtime/skills_bundle/gemini-task-contextualizer/SKILL.md +98 -0
  307. phase_loop_runtime/skills_bundle/gemini-task-contextualizer/references/task-templates.md +103 -0
  308. phase_loop_runtime/skills_bundle/opencode-advisor-board/README.md +1 -0
  309. phase_loop_runtime/skills_bundle/opencode-advisor-board/SKILL.md +38 -0
  310. phase_loop_runtime/skills_bundle/opencode-advisor-panel/README.md +1 -0
  311. phase_loop_runtime/skills_bundle/opencode-advisor-panel/SKILL.md +38 -0
  312. phase_loop_runtime/skills_bundle/opencode-execute-detailed/README.md +1 -0
  313. phase_loop_runtime/skills_bundle/opencode-execute-detailed/SKILL.md +144 -0
  314. phase_loop_runtime/skills_bundle/opencode-execute-phase/README.md +1 -0
  315. phase_loop_runtime/skills_bundle/opencode-execute-phase/SKILL.md +196 -0
  316. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/allocate_worktree_name.sh +33 -0
  317. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/audit_lane_file_touches.py +197 -0
  318. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
  319. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
  320. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
  321. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
  322. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
  323. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/state.py +274 -0
  324. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
  325. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/team_teardown.sh +32 -0
  326. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/validate_plan_doc.py +642 -0
  327. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/verify_harness.sh +96 -0
  328. phase_loop_runtime/skills_bundle/opencode-phase-loop/README.md +1 -0
  329. phase_loop_runtime/skills_bundle/opencode-phase-loop/SKILL.md +113 -0
  330. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/README.md +1 -0
  331. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/SKILL.md +147 -0
  332. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/assets/review_prompt.md +19 -0
  333. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
  334. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/references/roadmap-template.md +193 -0
  335. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
  336. phase_loop_runtime/skills_bundle/opencode-plan-detailed/README.md +1 -0
  337. phase_loop_runtime/skills_bundle/opencode-plan-detailed/SKILL.md +119 -0
  338. phase_loop_runtime/skills_bundle/opencode-plan-detailed/assets/review_prompt.md +25 -0
  339. phase_loop_runtime/skills_bundle/opencode-plan-phase/README.md +1 -0
  340. phase_loop_runtime/skills_bundle/opencode-plan-phase/SKILL.md +211 -0
  341. phase_loop_runtime/skills_bundle/opencode-plan-phase/assets/review_prompt.md +17 -0
  342. phase_loop_runtime/skills_bundle/opencode-plan-phase/scripts/validate_plan_doc.py +1092 -0
  343. phase_loop_runtime/skills_bundle/opencode-run-train/README.md +1 -0
  344. phase_loop_runtime/skills_bundle/opencode-run-train/SKILL.md +75 -0
  345. phase_loop_runtime/skills_bundle/opencode-skill-editor/README.md +1 -0
  346. phase_loop_runtime/skills_bundle/opencode-skill-editor/SKILL.md +87 -0
  347. phase_loop_runtime/skills_bundle/opencode-skill-editor/assets/editor_prompt.md +72 -0
  348. phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/README.md +1 -0
  349. phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/SKILL.md +109 -0
  350. phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
  351. phase_loop_runtime/skills_bundle/opencode-task-contextualizer/README.md +1 -0
  352. phase_loop_runtime/skills_bundle/opencode-task-contextualizer/SKILL.md +98 -0
  353. phase_loop_runtime/skills_bundle/opencode-task-contextualizer/references/task-templates.md +103 -0
  354. phase_loop_runtime/state.py +156 -0
  355. phase_loop_runtime/state_degradation.py +136 -0
  356. phase_loop_runtime/state_ops.py +676 -0
  357. phase_loop_runtime/train_ledger.py +215 -0
  358. phase_loop_runtime/train_roadmap.py +339 -0
  359. phase_loop_runtime/train_runner.py +1197 -0
  360. phase_loop_runtime/verification_evidence.py +650 -0
  361. phase_loop_runtime/verification_evidence_validator.py +75 -0
  362. phase_loop_runtime/visual_evidence_validator.py +36 -0
  363. phase_loop_runtime/worker_pool.py +123 -0
  364. phase_loop_runtime/worktree_index.py +270 -0
  365. phase_loop_runtime-0.2.0.data/data/share/phase-loop-runtime/protocol/protocol.md +2475 -0
  366. phase_loop_runtime-0.2.0.dist-info/METADATA +106 -0
  367. phase_loop_runtime-0.2.0.dist-info/RECORD +370 -0
  368. phase_loop_runtime-0.2.0.dist-info/WHEEL +5 -0
  369. phase_loop_runtime-0.2.0.dist-info/entry_points.txt +9 -0
  370. 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)