proofpath 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. {proofpath-0.2.1 → proofpath-0.3.0}/CHANGELOG.md +46 -0
  2. {proofpath-0.2.1 → proofpath-0.3.0}/PKG-INFO +53 -8
  3. {proofpath-0.2.1 → proofpath-0.3.0}/README.md +52 -7
  4. proofpath-0.3.0/docs/eval/2026-09-15-judge-live.md +95 -0
  5. {proofpath-0.2.1 → proofpath-0.3.0}/docs/superpowers/OPEN-ITEMS.md +46 -7
  6. {proofpath-0.2.1 → proofpath-0.3.0}/docs/superpowers/plans/2026-09-10-proofpath-implementation-plan.md +13 -0
  7. {proofpath-0.2.1 → proofpath-0.3.0}/docs/superpowers/specs/2026-09-10-proofpath-design.md +22 -4
  8. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/__init__.py +1 -1
  9. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/cache.py +91 -4
  10. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/cli.py +60 -6
  11. proofpath-0.3.0/src/proofpath/judge.py +827 -0
  12. proofpath-0.3.0/src/proofpath/prompts/__init__.py +1 -0
  13. proofpath-0.3.0/src/proofpath/prompts/review.md +40 -0
  14. proofpath-0.3.0/src/proofpath/prompts/summarize.md +30 -0
  15. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/report.py +190 -6
  16. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/app.py +104 -3
  17. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/runs.py +13 -0
  18. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/widgets/run_block.py +13 -0
  19. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/ui.py +35 -4
  20. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/verify.py +294 -3
  21. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_cache.py +148 -5
  22. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_check_cli.py +400 -8
  23. proofpath-0.3.0/tests/test_judge.py +959 -0
  24. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_report.py +228 -1
  25. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_sarif.py +30 -2
  26. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_tui_app.py +115 -5
  27. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_ui.py +60 -0
  28. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_verify.py +563 -3
  29. proofpath-0.2.1/src/proofpath/judge.py +0 -149
  30. proofpath-0.2.1/tests/test_judge.py +0 -106
  31. {proofpath-0.2.1 → proofpath-0.3.0}/.editorconfig +0 -0
  32. {proofpath-0.2.1 → proofpath-0.3.0}/.env.example +0 -0
  33. {proofpath-0.2.1 → proofpath-0.3.0}/.gitattributes +0 -0
  34. {proofpath-0.2.1 → proofpath-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  35. {proofpath-0.2.1 → proofpath-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  36. {proofpath-0.2.1 → proofpath-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  37. {proofpath-0.2.1 → proofpath-0.3.0}/.github/ISSUE_TEMPLATE/wrong_verdict.yml +0 -0
  38. {proofpath-0.2.1 → proofpath-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  39. {proofpath-0.2.1 → proofpath-0.3.0}/.github/workflows/ci.yml +0 -0
  40. {proofpath-0.2.1 → proofpath-0.3.0}/.github/workflows/release.yml +0 -0
  41. {proofpath-0.2.1 → proofpath-0.3.0}/.gitignore +0 -0
  42. {proofpath-0.2.1 → proofpath-0.3.0}/CLAUDE.md +0 -0
  43. {proofpath-0.2.1 → proofpath-0.3.0}/CONTRIBUTING.md +0 -0
  44. {proofpath-0.2.1 → proofpath-0.3.0}/LICENSE +0 -0
  45. {proofpath-0.2.1 → proofpath-0.3.0}/SECURITY.md +0 -0
  46. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-11-coverage.md +0 -0
  47. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-11-ghosts.md +0 -0
  48. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-11-pairing.md +0 -0
  49. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-11-scifact-dev.md +0 -0
  50. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-12-ghosts.md +0 -0
  51. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-12-pairing-author-year.md +0 -0
  52. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-12-scifact-dev.md +0 -0
  53. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-12-tiers.md +0 -0
  54. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-12-v0.1-live.md +0 -0
  55. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-15-tui-v2-live.md +0 -0
  56. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/2026-09-15-v0.2-live.md +0 -0
  57. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/tui-v2-plain.svg +0 -0
  58. {proofpath-0.2.1 → proofpath-0.3.0}/docs/eval/tui-v2-rich.svg +0 -0
  59. {proofpath-0.2.1 → proofpath-0.3.0}/docs/research/2026-09-11-free-llm-api-tiers.md +0 -0
  60. {proofpath-0.2.1 → proofpath-0.3.0}/docs/superpowers/plans/2026-09-12-phases-9-10-plan.md +0 -0
  61. {proofpath-0.2.1 → proofpath-0.3.0}/docs/superpowers/specs/2026-09-15-tui-v2-design.md +0 -0
  62. {proofpath-0.2.1 → proofpath-0.3.0}/pyproject.toml +0 -0
  63. {proofpath-0.2.1 → proofpath-0.3.0}/scripts/eval_coverage.py +0 -0
  64. {proofpath-0.2.1 → proofpath-0.3.0}/scripts/eval_ghosts.py +0 -0
  65. {proofpath-0.2.1 → proofpath-0.3.0}/scripts/eval_pairing.py +0 -0
  66. {proofpath-0.2.1 → proofpath-0.3.0}/scripts/eval_scifact.py +0 -0
  67. {proofpath-0.2.1 → proofpath-0.3.0}/scripts/zero_network_check.py +0 -0
  68. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/browser.py +0 -0
  69. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/claims.py +0 -0
  70. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/commands.py +0 -0
  71. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/config.py +0 -0
  72. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/device.py +0 -0
  73. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/document.py +0 -0
  74. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/entailment.py +0 -0
  75. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/eval/__init__.py +0 -0
  76. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/eval/metrics.py +0 -0
  77. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/eval/scifact.py +0 -0
  78. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/events.py +0 -0
  79. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/fetch.py +0 -0
  80. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/ingest.py +0 -0
  81. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/models.py +0 -0
  82. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/numerics.py +0 -0
  83. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/oa.py +0 -0
  84. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/paths.py +0 -0
  85. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/pipeline.py +0 -0
  86. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/polite.py +0 -0
  87. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/py.typed +0 -0
  88. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/resolve.py +0 -0
  89. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/retrieval.py +0 -0
  90. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/sarif.py +0 -0
  91. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/__init__.py +0 -0
  92. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/banner.py +0 -0
  93. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/commands.py +0 -0
  94. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/pet.py +0 -0
  95. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/theme.py +0 -0
  96. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/verbs.py +0 -0
  97. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/widgets/__init__.py +0 -0
  98. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/widgets/_shared.py +0 -0
  99. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/widgets/banner.py +0 -0
  100. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/widgets/finding.py +0 -0
  101. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/widgets/footer.py +0 -0
  102. {proofpath-0.2.1 → proofpath-0.3.0}/src/proofpath/tui/widgets/prompt.py +0 -0
  103. {proofpath-0.2.1 → proofpath-0.3.0}/tests/__init__.py +0 -0
  104. {proofpath-0.2.1 → proofpath-0.3.0}/tests/conftest.py +0 -0
  105. {proofpath-0.2.1 → proofpath-0.3.0}/tests/data/draft-author-year.md +0 -0
  106. {proofpath-0.2.1 → proofpath-0.3.0}/tests/data/draft-live.md +0 -0
  107. {proofpath-0.2.1 → proofpath-0.3.0}/tests/data/ghost_set.jsonl +0 -0
  108. {proofpath-0.2.1 → proofpath-0.3.0}/tests/data/pairing_author_year.jsonl +0 -0
  109. {proofpath-0.2.1 → proofpath-0.3.0}/tests/data/pairing_set.jsonl +0 -0
  110. {proofpath-0.2.1 → proofpath-0.3.0}/tests/data/sarif-schema-2.1.0.json +0 -0
  111. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fakes.py +0 -0
  112. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/crossref_work.json +0 -0
  113. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/crossref_work_no_abstract.json +0 -0
  114. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/europepmc_fulltext.xml +0 -0
  115. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/europepmc_search.json +0 -0
  116. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/europepmc_search_empty.json +0 -0
  117. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/europepmc_search_not_in_epmc.json +0 -0
  118. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/openalex_work.json +0 -0
  119. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/s2_paper.json +0 -0
  120. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/s2_paper_no_abstract.json +0 -0
  121. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/unpaywall.json +0 -0
  122. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/unpaywall_closed.json +0 -0
  123. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/oa/unpaywall_landing_only.json +0 -0
  124. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/arxiv_id_roberta.xml +0 -0
  125. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/arxiv_title_fabricated.xml +0 -0
  126. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/arxiv_title_roberta.xml +0 -0
  127. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/crossref_alphafold.json +0 -0
  128. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/crossref_fabricated.json +0 -0
  129. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/crossref_numpy.json +0 -0
  130. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/crossref_rag.json +0 -0
  131. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/crossref_retracted_wakefield.json +0 -0
  132. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/crossref_roberta.json +0 -0
  133. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/crossref_work_alphafold.json +0 -0
  134. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/crossref_work_numpy.json +0 -0
  135. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/openalex_alphafold.json +0 -0
  136. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/openalex_fabricated.json +0 -0
  137. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/openalex_rag.json +0 -0
  138. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/openalex_retracted_wakefield.json +0 -0
  139. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/openalex_roberta.json +0 -0
  140. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/openlibrary_fabricated.json +0 -0
  141. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/openlibrary_using_language.json +0 -0
  142. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/s2_alphafold.json +0 -0
  143. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/s2_fabricated.json +0 -0
  144. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/s2_rag.json +0 -0
  145. {proofpath-0.2.1 → proofpath-0.3.0}/tests/fixtures/resolve/s2_roberta.json +0 -0
  146. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_browser.py +0 -0
  147. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_cache_cli.py +0 -0
  148. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_claims.py +0 -0
  149. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_cli.py +0 -0
  150. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_commands.py +0 -0
  151. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_config.py +0 -0
  152. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_config_cli.py +0 -0
  153. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_device.py +0 -0
  154. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_document.py +0 -0
  155. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_entailment.py +0 -0
  156. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_eval_coverage.py +0 -0
  157. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_eval_pairing.py +0 -0
  158. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_eval_scifact.py +0 -0
  159. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_fetch.py +0 -0
  160. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_fetch_cli.py +0 -0
  161. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_ingest.py +0 -0
  162. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_metrics.py +0 -0
  163. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_models.py +0 -0
  164. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_numerics.py +0 -0
  165. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_oa.py +0 -0
  166. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_pipeline.py +0 -0
  167. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_polite.py +0 -0
  168. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_resolve.py +0 -0
  169. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_resolve_cli.py +0 -0
  170. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_retrieval.py +0 -0
  171. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_scifact.py +0 -0
  172. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_tui_banner.py +0 -0
  173. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_tui_commands.py +0 -0
  174. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_tui_pet.py +0 -0
  175. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_tui_rich.py +0 -0
  176. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_tui_runs.py +0 -0
  177. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_tui_theme.py +0 -0
  178. {proofpath-0.2.1 → proofpath-0.3.0}/tests/test_version.py +0 -0
  179. {proofpath-0.2.1 → proofpath-0.3.0}/uv.lock +0 -0
@@ -6,6 +6,52 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] - 2026-09-15
10
+
11
+ The judge layer: an opt-in LLM second opinion and an opt-in model-written summary.
12
+ The default run still makes zero LLM calls, and nothing the model says can change a
13
+ verdict. Live run on Groq in `docs/eval/2026-09-15-judge-live.md`.
14
+
15
+ ### Added
16
+ - **`check --judge`** (`judge.py`, `verify.py`). After the local verdicts are final, the
17
+ `low`-tier ones — never a numeric mismatch, never a claim without a quoted passage —
18
+ go to the model in batches of up to 20 items (about 7k tokens), each with its claim and
19
+ passage. The opinion (`SUPPORTED | REFUTED | NEI` plus a one-sentence rationale) is
20
+ attached beside the verdict: a `= judge (groq openai/gpt-oss-120b): …` line under a
21
+ finding it disagrees with, a `judge` column in the markdown `## Checked` table, and
22
+ `judge` fields in the JSON. The local `Verdict`, the finding kind and every state are
23
+ untouched (spec §11.1). Opinions are cached in the new `judgements` table (schema v4,
24
+ additive; wiped with the verdicts when a source's text changes), so a re-run asks
25
+ nothing, and `model_id` is untouched, so toggling `--judge` never invalidates a verdict.
26
+ - **`check --summarize`** and the TUI's **`/summarize`**: one extra call over the finished
27
+ markdown report, run after the report is final, off by default in both front-ends,
28
+ printed as `summary (model-written, groq openai/gpt-oss-120b) …` and as
29
+ `## Summary (model-written, …)` in the file. `--summarize` alone is exactly one call.
30
+ - **`JudgeClient`**: one adapter for Groq (default `openai/gpt-oss-120b`), Gemini and
31
+ Ollama over the OpenAI `chat/completions` shape; strict JSON-schema output with a
32
+ `json_object` fallback, `reasoning_effort=low` with a fallback for providers that
33
+ reject it, `Retry-After` on 429 (capped, accounted), exponential backoff on 5xx, then
34
+ `JudgeUnavailable`. The key comes from the environment or `.env`, never from config,
35
+ never appears in `repr`, errors or logs; provider bodies are never echoed. Prompts are
36
+ packaged template files (`proofpath/prompts/review.md`, `summarize.md`).
37
+ - **Cost on every surface**: the `Judging` and `Summarising` stage lines carry calls and
38
+ prompt/completion tokens; the footer counts the calls; `Report.judge_cost` and
39
+ `models["judge"]` land in the JSON.
40
+ - **An unanswered judge is reported, not hidden**: `judge unavailable after N calls
41
+ (HTTP 401 from …); local verdicts stand` in the stage line, the report header
42
+ (`judge status:` / `summary status:`), the JSON and an unsuppressed terminal line — a
43
+ `-q` or piped run cannot look like a judged-clean one. The SARIF log does not carry it.
44
+ The run never fails because of the judge.
45
+ - Gemini prints its data-use warning once per run (spec §11).
46
+
47
+ ### Changed
48
+ - `check --judge` / `--summarize` no longer exit with `arrives in v0.3`.
49
+ - Cache schema **v4** (`judgements`); a v1 file still migrates through the whole chain.
50
+ - The markdown `## Checked` table always carries a `judge` column; without `--judge` every
51
+ cell is `—`, so a v0.2 report and a v0.3 one differ by that column alone.
52
+ - `--format json` gained `summary`, `summary_model`, `judge_cost` and per-result `judge`
53
+ fields; every one of them is `null` on a default run.
54
+
9
55
  ## [0.2.1] - 2026-09-15
10
56
 
11
57
  The TUI's second look. No behaviour change: every state word, every honesty sentence,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: proofpath
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Check whether the sources behind a claim actually say what the claim says.
5
5
  Project-URL: Homepage, https://github.com/Yigtwxx/proofpath
6
6
  Project-URL: Repository, https://github.com/Yigtwxx/proofpath
@@ -239,7 +239,12 @@ runtime teardown); fixed in v0.1 — 20 of 20 piped runs exit `1`
239
239
  counted as unreachable.
240
240
  - **Coverage is not perfectly reproducible**: two runs minutes apart can read a
241
241
  different number of sources, depending on which providers answered.
242
- - The LLM judge and `--summarize` arrive in **v0.3**.
242
+ - **The judge is a second opinion, not a second verdict.** `--judge` asks the model only
243
+ about the low-tier verdicts (1 of 10 on the live draft), and its answer is printed
244
+ beside the local verdict, never in place of it. `--summarize` is one extra call over
245
+ the finished report, labelled model-written; if the provider does not answer, the
246
+ markdown report, the JSON and the terminal say so (`judge status` / `summary status`
247
+ in the header). The SARIF log does not carry it — it is a findings document.
243
248
 
244
249
  ## Speed
245
250
 
@@ -273,13 +278,53 @@ TablePlus or DBeaver — plain tables, no extension. Raw publisher text expires
273
278
  7 days; verdicts keep the passage they quote; a provider outage is never stored.
274
279
  `proofpath resolve REF` and `proofpath fetch URL|DOI` run either half on its own.
275
280
 
276
- ## Optional LLM judge — arrives in v0.3
281
+ ## Optional LLM judge (v0.3)
277
282
 
278
- Everything above runs locally. An LLM is used only at the end, as an opt-in second
279
- opinion on low-confidence verdicts it never sees a source document and cannot
280
- change a verdict. The settings exist today (`proofpath config check` proves a key
281
- works, default Groq); `check --judge` and `--summarize` are wired in v0.3. The key
282
- comes from the environment or a `.env` file, never from config, and is never printed.
283
+ Everything above runs locally, and the default run makes **zero** LLM calls. Two flags
284
+ add an LLM at the end, and only there:
285
+
286
+ ```bash
287
+ proofpath check paper.pdf --judge # a second opinion on the low-confidence verdicts
288
+ proofpath check paper.pdf --summarize # one model-written paragraph over the finished report
289
+ proofpath config check # proves the key works before you spend a run on it
290
+ ```
291
+
292
+ **What `--judge` does.** After the local verdicts are final, the verdicts the models
293
+ were least sure about — the `low` tier, never a numeric mismatch and never a claim
294
+ without a quoted passage — go to the model in batches of up to 20 (about 7k tokens),
295
+ each with its claim and the passage it was checked against. The model answers from the
296
+ passage alone, and its label and one-sentence rationale are printed **beside** the local
297
+ verdict: `= judge (groq openai/gpt-oss-120b): NEI — …`. The local verdict, the finding
298
+ kind and the report's states never change. Opinions are cached with the verdict, so a
299
+ re-run asks nothing.
300
+
301
+ **What it cannot do.** It never sees a source document, so it cannot introduce a claim
302
+ or an evidence passage of its own; it cannot turn `NEI` into `SUPPORTED`; it cannot
303
+ hide a source that could not be read. If the provider is down, rate-limited or the key
304
+ is wrong, the run finishes on the local verdicts and says so in the stage line, the
305
+ report header (`judge status:`) and the JSON — `-q` cannot hide it.
306
+
307
+ **What `--summarize` does.** One final call turns the finished markdown report into 3–5
308
+ plain sentences a reader can act on. It runs after the report is complete, its only
309
+ input is that report, it is off by default in the CLI and the TUI (`/summarize` there),
310
+ and the output is labelled `(model-written, <provider> <model>)`. `--summarize` alone
311
+ is exactly one call; with `--judge` the escalation runs first.
312
+
313
+ **Cost.** The footer counts the calls and the stage line the tokens:
314
+ `Judging … 1 of 10 verdicts reviewed, 1 call, 613 prompt · 193 completion tokens` and
315
+ `Summarising … 98 words, 1 call, 1,608 prompt · 343 completion tokens`
316
+ on the live draft ([details](docs/eval/2026-09-15-judge-live.md)). Groq's free tier
317
+ allows roughly one call a minute. Only the low-tier verdicts are sent — 1 of 10 on that
318
+ draft — and up to 20 go in one call, so a long bibliography costs a handful of calls, not
319
+ one per citation.
320
+
321
+ **Providers.** Default is Groq `openai/gpt-oss-120b` (free without a card, no training
322
+ on submitted data). `proofpath config set judge.provider gemini` switches to Gemini —
323
+ note that Google trains on free-tier prompts outside the EEA/UK/CH, and proofpath prints
324
+ that warning once per run. `judge.provider ollama` runs fully offline. Gemini and Ollama are
325
+ fixture-tested and were not exercised live in v0.3.0. All three speak
326
+ the OpenAI `chat/completions` shape. The key comes from `GROQ_API_KEY` / `GEMINI_API_KEY`
327
+ in the environment or a `.env` file, never from config, and is never printed.
283
328
 
284
329
  ## Measured
285
330
 
@@ -183,7 +183,12 @@ runtime teardown); fixed in v0.1 — 20 of 20 piped runs exit `1`
183
183
  counted as unreachable.
184
184
  - **Coverage is not perfectly reproducible**: two runs minutes apart can read a
185
185
  different number of sources, depending on which providers answered.
186
- - The LLM judge and `--summarize` arrive in **v0.3**.
186
+ - **The judge is a second opinion, not a second verdict.** `--judge` asks the model only
187
+ about the low-tier verdicts (1 of 10 on the live draft), and its answer is printed
188
+ beside the local verdict, never in place of it. `--summarize` is one extra call over
189
+ the finished report, labelled model-written; if the provider does not answer, the
190
+ markdown report, the JSON and the terminal say so (`judge status` / `summary status`
191
+ in the header). The SARIF log does not carry it — it is a findings document.
187
192
 
188
193
  ## Speed
189
194
 
@@ -217,13 +222,53 @@ TablePlus or DBeaver — plain tables, no extension. Raw publisher text expires
217
222
  7 days; verdicts keep the passage they quote; a provider outage is never stored.
218
223
  `proofpath resolve REF` and `proofpath fetch URL|DOI` run either half on its own.
219
224
 
220
- ## Optional LLM judge — arrives in v0.3
225
+ ## Optional LLM judge (v0.3)
221
226
 
222
- Everything above runs locally. An LLM is used only at the end, as an opt-in second
223
- opinion on low-confidence verdicts it never sees a source document and cannot
224
- change a verdict. The settings exist today (`proofpath config check` proves a key
225
- works, default Groq); `check --judge` and `--summarize` are wired in v0.3. The key
226
- comes from the environment or a `.env` file, never from config, and is never printed.
227
+ Everything above runs locally, and the default run makes **zero** LLM calls. Two flags
228
+ add an LLM at the end, and only there:
229
+
230
+ ```bash
231
+ proofpath check paper.pdf --judge # a second opinion on the low-confidence verdicts
232
+ proofpath check paper.pdf --summarize # one model-written paragraph over the finished report
233
+ proofpath config check # proves the key works before you spend a run on it
234
+ ```
235
+
236
+ **What `--judge` does.** After the local verdicts are final, the verdicts the models
237
+ were least sure about — the `low` tier, never a numeric mismatch and never a claim
238
+ without a quoted passage — go to the model in batches of up to 20 (about 7k tokens),
239
+ each with its claim and the passage it was checked against. The model answers from the
240
+ passage alone, and its label and one-sentence rationale are printed **beside** the local
241
+ verdict: `= judge (groq openai/gpt-oss-120b): NEI — …`. The local verdict, the finding
242
+ kind and the report's states never change. Opinions are cached with the verdict, so a
243
+ re-run asks nothing.
244
+
245
+ **What it cannot do.** It never sees a source document, so it cannot introduce a claim
246
+ or an evidence passage of its own; it cannot turn `NEI` into `SUPPORTED`; it cannot
247
+ hide a source that could not be read. If the provider is down, rate-limited or the key
248
+ is wrong, the run finishes on the local verdicts and says so in the stage line, the
249
+ report header (`judge status:`) and the JSON — `-q` cannot hide it.
250
+
251
+ **What `--summarize` does.** One final call turns the finished markdown report into 3–5
252
+ plain sentences a reader can act on. It runs after the report is complete, its only
253
+ input is that report, it is off by default in the CLI and the TUI (`/summarize` there),
254
+ and the output is labelled `(model-written, <provider> <model>)`. `--summarize` alone
255
+ is exactly one call; with `--judge` the escalation runs first.
256
+
257
+ **Cost.** The footer counts the calls and the stage line the tokens:
258
+ `Judging … 1 of 10 verdicts reviewed, 1 call, 613 prompt · 193 completion tokens` and
259
+ `Summarising … 98 words, 1 call, 1,608 prompt · 343 completion tokens`
260
+ on the live draft ([details](docs/eval/2026-09-15-judge-live.md)). Groq's free tier
261
+ allows roughly one call a minute. Only the low-tier verdicts are sent — 1 of 10 on that
262
+ draft — and up to 20 go in one call, so a long bibliography costs a handful of calls, not
263
+ one per citation.
264
+
265
+ **Providers.** Default is Groq `openai/gpt-oss-120b` (free without a card, no training
266
+ on submitted data). `proofpath config set judge.provider gemini` switches to Gemini —
267
+ note that Google trains on free-tier prompts outside the EEA/UK/CH, and proofpath prints
268
+ that warning once per run. `judge.provider ollama` runs fully offline. Gemini and Ollama are
269
+ fixture-tested and were not exercised live in v0.3.0. All three speak
270
+ the OpenAI `chat/completions` shape. The key comes from `GROQ_API_KEY` / `GEMINI_API_KEY`
271
+ in the environment or a `.env` file, never from config, and is never printed.
227
272
 
228
273
  ## Measured
229
274
 
@@ -0,0 +1,95 @@
1
+ # Judge layer live runs — 2026-09-15 (v0.3.0)
2
+
3
+ `proofpath check tests/data/draft-live.md --judge --summarize` on Groq
4
+ (`openai/gpt-oss-120b`, key from `.env`), Apple Silicon, models already downloaded.
5
+ Three runs: the first against the pre-fix build (it found the defect), then a cold run
6
+ (`--no-cache`) and a warm run against the shipped build. Stage lines are copied from
7
+ the terminal; the key never appears in any output.
8
+
9
+ ## Run 1 — pre-fix build: the summary came back empty
10
+
11
+ ```
12
+ Judging groq openai/gpt-oss-120b 1 of 10 verdicts reviewed, 1 call, 661 prompt · 211 completion tokens 0.9s
13
+ Summarising groq openai/gpt-oss-120b summary unavailable after 0 calls (no completion in the 200 response from https://api.groq.com/openai/v1/chat/completions (finish_reason=length)); local verdicts stand 1.0s
14
+ ```
15
+
16
+ `openai/gpt-oss-120b` is a reasoning model: with the planned `max_tokens=400` the
17
+ completion budget was spent on reasoning and the visible content was empty, and the
18
+ written `report.md` said nothing about the summary at all (the 9.3 reviewer had found
19
+ the same gap from the code). Both are fixed in the shipped build: requests carry
20
+ `reasoning_effort=low` (dropped on a 400 from providers that reject it), the review
21
+ budget is 4096 tokens and the summary budget 1500, and an unanswered summary is
22
+ reported on the stage line, the `summary` CLI line and a `- summary status:` header
23
+ line in the file. The 1,888-test suite was green before this run; the live run found it.
24
+
25
+ ## Run 2 — cold (`--no-cache`), shipped build
26
+
27
+ ```
28
+ Parsing text 1 pages, 7 refs 0.0s
29
+ Claims rules 7 citations, 0 unresolved 0.0s
30
+ Resolving Crossref, Semantic Scholar 6 ok, 0 amb, 1 ghost 9.3s
31
+ Retractions Retraction Watch none 2.5s
32
+ Fetching arXiv, Semantic Scholar 5 full text, 1 abstract, 0 unverified 23.8s
33
+ Verifying coreml 10 claims: 1 supported, 2 not supported, 7 NEI 44.5s
34
+ Judging groq openai/gpt-oss-120b 1 of 10 verdicts reviewed, 1 call, 613 prompt · 193 completion tokens 1.1s
35
+ Summarising groq openai/gpt-oss-120b 98 words, 1 call, 1,608 prompt · 343 completion tokens 1.0s
36
+ … written · 2 API calls · 82.1s
37
+ judge 2,221 prompt · 536 completion tokens
38
+ ```
39
+
40
+ The escalation set was one verdict — the `low`-tier `NOT SUPPORTED` on the SciPy
41
+ sentence — and the judge's opinion was printed beside it, the local verdict untouched:
42
+
43
+ ```
44
+ error[not-supported]: claim is not supported by the cited source (confidence: low)
45
+ = judge (groq openai/gpt-oss-120b): NEI — Passage states "SciPy provides fundamental
46
+ algorithms for scientific computing" but does not mention SciPy building on anything.
47
+ ```
48
+
49
+ The numeric mismatch (`high`, rule-decided) and the ghost reference were not sent, as
50
+ specified. The summary, verbatim:
51
+
52
+ > The report checked seven cited references, and it found two claims not supported and
53
+ > one ghost reference; it does not give a total number of claims that were examined. The
54
+ > most serious problem is the high‑confidence "NOT SUPPORTED" error on line 6, where the
55
+ > claim about training time for the Transformer contradicts the source. The coverage
56
+ > analysis shows that only 72 % of the material was verified against full text, 14 % was
57
+ > limited to abstracts, and the remaining 14 % could not be read, so the counts above
58
+ > apply only to the portion that was actually examined.
59
+
60
+ It repeats the coverage caveat as instructed and invents no finding. Two things it
61
+ gets slightly wrong, which is why it is labelled model-written and sits under the
62
+ computed report: it calls the line-6 numeric mismatch a "NOT SUPPORTED" error (the
63
+ report says `numeric-mismatch: claim contradicts the cited source`), and "it does not
64
+ give a total number of claims" is true of the markdown file (the stage table is not
65
+ printed there) but not of the run (`10 claims`).
66
+
67
+ ## Run 3 — warm, shipped build
68
+
69
+ ```
70
+ Resolving cache 6 ok, 0 amb, 1 ghost 0.0s
71
+ Fetching cache 5 full text, 1 abstract, 0 unverified 0.0s
72
+ Verifying coreml 10 claims: 1 supported, 2 not supported, 7 NEI, 10 cached 0.8s
73
+ Judging groq openai/gpt-oss-120b 1 of 10 verdicts reviewed, 0 calls, 0 prompt · 0 completion tokens 0.0s
74
+ Summarising groq openai/gpt-oss-120b 106 words, 1 call, 1,615 prompt · 263 completion tokens 0.9s
75
+ … written · 1 API calls · 1.8s
76
+ judge 1,615 prompt · 263 completion tokens
77
+ ```
78
+
79
+ The judgement came back from the schema-v4 `judgements` table (0 calls); the summary
80
+ is always one fresh call. A plain `check` of the same file (no flags) reports
81
+ `api_calls: 0` and `summary: null`.
82
+
83
+ ## What this does and does not show
84
+
85
+ - The judge layer works end to end on Groq's free tier: batching, strict-JSON output,
86
+ cost accounting, caching, attribution, and the report unchanged by the model.
87
+ - One document, one low-tier verdict, one provider. Nothing here measures the judge's
88
+ accuracy; the escalation band is narrow by design (OPEN-ITEMS 14.1), so a paper with
89
+ 118 citations would send a handful of verdicts, not dozens.
90
+ - Gemini and Ollama were not exercised live; they share the adapter and are covered by
91
+ the `respx` tests only.
92
+ - These runs were made with the batch cap at 7,000 prompt tokens. The whole-phase review
93
+ lowered it to 3,500 so that a full batch plus its 4,096-token answer budget stays under
94
+ Groq's 8K-per-minute tier; with one escalated item the runs above never approached
95
+ either figure, so nothing here measures the cap.
@@ -390,18 +390,15 @@ Findings from inspecting the installed base environment, and the decisions they
390
390
  | 12.6 | ~~TUI finding rows print the entry's marker twice~~ **Closed 2026-09-15 (final review fix: `strip_marker` on the label)** | `[7] [7] Marchetti, …` in the pty session: `Reference.raw` keeps its printed marker (Phase 5) and `FindingLine` prefixes the number again. The CLI diagnostic prints the entry once. Cosmetic; `strip_marker` on the label is the fix |
391
391
  | 12.7 | `loading models …` is drawn under the `Verifying` row in the TUI, above it on the CLI | the true order: `verify` emits the note inside the stage, after `StageStart`, and the TUI draws the row on `StageStart` while the CLI prints only `StageEnd`. Moving the note before `StageStart` in `verify.py` would put it outside the stage it belongs to; a `StageLine` that shows notes as its own children is the tidier fix. Cosmetic, not a `report`/`ui` one-liner |
392
392
  | 12.8 | The SARIF artifact for `check -` is `-` | `to_sarif(report, artifact=target)` takes the target as typed; a stdin run has no file, and `-` under `%SRCROOT%` is what a viewer gets. `report.document.name` (`stdin`) is no better an address; a `--artifact` override is the only honest option |
393
- | 12.9 | `api_calls` in the footer counts LLM calls only | a cold resolution of five references (33.5 s against Crossref and Semantic Scholar) still prints `0 API calls`; the counter is the judge's (Phase 9) and the wording says nothing about provider lookups. Rename or count — one line either way |
393
+ | 12.9 | `api_calls` in the footer counts LLM calls only — **restated 2026-09-15 (v0.3.0)**: it is now the judge's real counter (answers, not attempts); provider lookups are still uncounted | a cold resolution of five references (33.5 s against Crossref and Semantic Scholar) still prints `0 API calls`; the counter is the judge's (Phase 9) and the wording says nothing about provider lookups. Rename or count — one line either way |
394
394
  | 12.10 | A cached resolution reprints its notes as if current | the live draft's ghost still carries `openalex unavailable (HTTP 429, retry after 65567s)` from the day the resolution was stored. Correct (nothing was re-checked) but it reads like today's outage; a `cached:` prefix on stored notes would say so |
395
395
  | 12.11 | A provider body that is valid JSON but not an object escapes `resolve` as an error | `_json` guards against a non-JSON body (`ProviderError`), but a bare list or string parses and then fails the `.get(...)` that follows with an `AttributeError`: exit 2 instead of `UNVERIFIED (provider unavailable)`. A type check in `_json` is the fix |
396
396
  | 12.12 | `/quit` waits for an in-flight mirrored `/fetch` | a `/check` run is cancelled through its scheduler; a mirrored verb runs in `asyncio.to_thread` with no cancel hook, and the loop's shutdown joins that executor, so the app exits only when the ladder returns. Bounded by the fetch timeouts, but a browser step can take a while |
397
- | 12.13 | Phase-9 briefs are untracked | they live under the git-ignored `.superpowers/sdd/phase9/`; the tracked plan is `docs/superpowers/plans/2026-09-12-phases-9-10-plan.md` |
397
+ | 12.13 | ~~Phase-9 briefs are untracked~~ **Closed 2026-09-15**: Phase 9 shipped from the tracked `docs/superpowers/plans/2026-09-12-phases-9-10-plan.md`; the drift amendments live in the same git-ignored briefs and are summarised in §14 | — |
398
398
 
399
- ### Next session
399
+ ### Next session (as of v0.2.0 — superseded by §14)
400
400
 
401
- 1. **Phases 9–10 are deferred.** Development stops after v0.2.0 for now; when it
402
- resumes, the judge layer (v0.3) and the social provider (v0.4) are planned in
403
- `docs/superpowers/plans/2026-09-12-phases-9-10-plan.md` (briefs under
404
- `.superpowers/sdd/phase9/`), with 12.1–12.10 above as the backlog beside them.
401
+ 1. Phases 9–10 were deferred after v0.2.0 and resumed 2026-09-15; Phase 9 is §14.
405
402
 
406
403
  ## 13. TUI v2 — v0.2.1, 2026-09-15
407
404
 
@@ -427,3 +424,45 @@ Findings from inspecting the installed base environment, and the decisions they
427
424
  | 13.3 | `PLAIN` keeps the flat v0.2.0 layout the author rated 2/10 (now pure ASCII, with `= note:` rows) | By design (v2 §2, "nothing regresses where borders cannot draw"): the flat rows, the three-line pet and the `kv` footer are v0.2.0's layout with ASCII glyphs, so a legacy-conhost, `NO_COLOR` or `-q` user sees the prototype. A `PLAIN` pass — the fixed-column stage table needs no border and would fit — is the one improvement that does not touch the ASCII rule |
428
425
  | 13.4 | The exported SVGs reference a webfont | `App.save_screenshot` (Rich's exporter) emits an `@font-face` for Fira Code with `local()` first and a `cdnjs` URL second; the files are 96 KB and 66 KB, no network is needed to read them, but an offline viewer without Fira Code falls back to its own monospace and the box drawing may not join |
429
426
  | 13.5 | 12.7 still shows in the live session | `loading models …` is drawn under the `Verifying` row in both themes (true event order); the tidier fix — a `StageLine` that owns its notes — was out of T1–T5's scope |
427
+
428
+ ## 14. Phase 9 — v0.3.0, 2026-09-15
429
+
430
+ - Shipped: `JudgeClient` (9.1; Groq / Gemini / Ollama over `chat/completions`, packaged
431
+ prompts, retry/backoff, cost), `Judge` escalation with the `judgements` cache (9.2;
432
+ schema v4, `check --judge`), `--summarize` and the TUI `/summarize` (9.3). Briefs under
433
+ `.superpowers/sdd/phase9/` with their 2026-09-15 drift amendments; plan
434
+ `plans/2026-09-12-phases-9-10-plan.md`.
435
+ - Live (`docs/eval/2026-09-15-judge-live.md`): on `tests/data/draft-live.md` the judge
436
+ escalated 1 of 10 verdicts (1 call, 613 prompt · 193 completion tokens) and answered
437
+ `NEI` beside a low-tier `NOT SUPPORTED`; the summary cost 1 call (1,608 · 343); the first `--summarize` attempt came back
438
+ empty with `finish_reason=length` — `openai/gpt-oss-120b` spends its completion budget
439
+ on reasoning — which is why requests now carry `reasoning_effort=low` and larger
440
+ budgets. The v0.1 lesson held again: the suite was green, the live run found the defect.
441
+ - Reviewer-found before the tag: an empty 200 was counted as an answer (9.1); an
442
+ unavailable judge reached only the terminal `Note` (9.2); an unanswered summary was
443
+ invisible in the markdown file (9.3). All three were rule-2/6 failures and were fixed.
444
+
445
+ ### New open items
446
+
447
+ | # | Item | Note |
448
+ |---|---|---|
449
+ | 14.1 | The `NEI`-with-a-passage escalation clause is unreachable | `pipeline.aggregate` and `decide_indexed` return `Verdict(NEI, …, passage=None)`, so the escalation set is the `low` tier only (a ~0.008-wide band, 1 of 10 verdicts on the live draft). Widening it is a pipeline decision: attach the best passage to an `NEI` and let the judge see it, or narrow the clause in the spec |
450
+ | 14.2 | An agreeing opinion leaves no mark on a rendered finding | by design only a disagreement adds the `= judge:` line; agreement shows in the markdown `judge` column and in JSON. A markdown reader of a low-tier `NOT SUPPORTED` cannot tell "judge confirmed" from "judge never saw it" without the table |
451
+ | 14.3 | Judgements are matched by `(claim_hash, source_id)` | two results with identical claim text against the same source share one opinion and are asked twice in one batch. Rare; carrying the result index would settle it |
452
+ | 14.4 | `Progress` on the Judging stage counts asked items, the summary counts all results | a fully warm run emits no `Progress` and reports `1 of 10 verdicts reviewed` |
453
+ | 14.5 | `report.py` and `cache.py` import `judge` | no cycle, but `httpx` now loads behind the output and storage layers; `JudgeOpinion`/`JudgeCost` could move to `models.py` |
454
+ | 14.6 | `Cache.detail()` / `Cleared` do not count judgements | they are removed by cascade and on `clear`, just not reported |
455
+ | 14.7 | A wrong or revoked key (401/403) arrives as `JudgeUnavailable` | same type as an outage; the detail names the HTTP status, so it is reportable, but a config error and a provider outage are one exception type |
456
+ | 14.8 | ~~`--format json` carries `summary` with no inline model-written label~~ **Closed by the whole-phase review: `summary_model` sits beside it** | attribution is `models["judge"]` plus the `Summarising` stage; an inline label would change the schema |
457
+ | 14.9 | An empty 200 is not retried while a 5xx is | right for `content_filter`, pessimistic for a flaky truncation |
458
+ | 14.10 | Small test gaps | the bare-429 ladder (no `Retry-After`) is untested; `finish_reason` is echoed unbounded in the unavailable detail; the packaged-prompt test cannot fail on an editable install (the wheel listing is the evidence) |
459
+ | 14.13 | The CLI's unavailable-judge line stutters | it prints `judge judge unavailable after 0 calls (…)` because the line returns the one shared sentence verbatim while the markdown header and the sibling `summary` line strip the leading subject. Kept for v0.3.0: byte-identical wording on every surface was the point, and the same string is also printed as a `Note` where the subject is needed |
460
+ | 14.14 | The 3,500-token batch cap rests on a rough estimator | `estimate_tokens` is `len(text) // 4 + 8` and says so; 3,500 + 4,096 leaves ~400 tokens under Groq's 8K tier, so a badly estimated batch can still meet a 429 (waited out, then reported). Calibrating the estimator against the `usage` block the provider already returns would settle it |
461
+ | 14.11 | The unanswered judge is absent from the SARIF log | `_run_properties` carries `models` and `apiCalls` but no `judge status` / `summary status`, so a `--format sarif` log of a run whose provider was down reads like one with nothing to escalate. SARIF is a findings document, and the markdown, JSON and terminal all say it; still, a viewer-only workflow does not see it |
462
+ | 14.12 | Groq's free tier is metered on prompt **plus** requested answer | the whole-phase review caught `TOKEN_CAP = 7000` left over from the 1,024-token answer budget: with the 4,096-token budget of 9.3 a full batch would ask for ~11.1k against an 8K/minute tier. The cap is now 3,500. Nothing measured this live — the live escalation set was one item |
463
+
464
+ ### Next session
465
+
466
+ 1. **Phase 10** (`providers/` refactor, Bluesky/HN, Reddit/Mastodon/X, AVeriTeC) runs
467
+ from `plans/2026-09-12-phases-9-10-plan.md`; Task 10.4a (AVeriTeC loader and scorer)
468
+ is already implemented and reviewed, unstaged behind the v0.3.0 tag.
@@ -299,6 +299,19 @@ below are the original outline and stay as the scope statement.
299
299
  limit, and disabling the judge changes cost to zero without changing the report
300
300
  format. `--summarize` adds exactly one call and its absence changes nothing else.
301
301
 
302
+ **Status (2026-09-15): released as v0.3.0.** Tasks 9.1–9.3 (`.superpowers/sdd/phase9/`,
303
+ briefs in `2026-09-12-phases-9-10-plan.md`): `JudgeClient` with packaged prompts and cost
304
+ accounting; `Judge` escalation (low tier with a passage; ≤ 20 items / ~7k tokens per
305
+ call), the `judgements` cache (schema v4) and `check --judge`; `--summarize` and the TUI
306
+ `/summarize`. Gates: 118 items pack into ≤ 6 calls at the 7k cap (unit test); a judge
307
+ disabled run is byte-identical minus the judge fields with `api_calls == 0`;
308
+ `--summarize` alone is exactly one call and leaves `results`/`findings` identical
309
+ (tests). Live on Groq (`docs/eval/2026-09-15-judge-live.md`): 1 of 10 verdicts
310
+ escalated, 1 call; the first summary attempt came back empty (`finish_reason=length`)
311
+ and led to `reasoning_effort=low` plus larger budgets. The adapter table above (Ollama
312
+ default, OpenRouter) was superseded by the 2026-09-11 decision: Groq default, Gemini and
313
+ Ollama selectable.
314
+
302
315
  ---
303
316
 
304
317
  ## Phase 10 — Social provider (v0.4)
@@ -469,14 +469,28 @@ the judge enabled. With Ollama there is no wait at all.
469
469
 
470
470
  **Provider (decided 2026-09-11).** Default is Groq `openai/gpt-oss-120b`: free
471
471
  without a card, no training on submitted data, strict JSON schema output, but an
472
- 8K tokens-per-minute cap that forces batches under ~7k tokens and roughly one call
473
- per minute. Gemini 3.8 Flash is selectable but Google trains on free-tier prompts
472
+ 8K tokens-per-minute cap that forces small batches (as built: 3,500 prompt tokens plus
473
+ a 4,096-token answer budget) and roughly one call per minute. Gemini 3.8 Flash is selectable but Google trains on free-tier prompts
474
474
  outside the EEA/UK/CH, so choosing it prints a data-use warning. Ollama is the
475
475
  offline option. All three speak the OpenAI ``chat/completions`` shape, so one
476
476
  adapter covers them. The API key is read from an environment variable or a
477
477
  ``.env`` file, never written to config and never printed. Survey:
478
478
  ``docs/research/2026-09-11-free-llm-api-tiers.md``.
479
479
 
480
+ **As built (v0.3.0).** The escalation set is the `low` tier plus `NEI` verdicts that
481
+ carry a passage; a numeric mismatch (rule-decided) and any result without a passage are
482
+ never sent (rule 1 binds the judge too). `openai/gpt-oss-120b` is a reasoning model, so
483
+ requests carry `reasoning_effort=low` (dropped on a 400 from providers that reject it)
484
+ and budgets of 4096 tokens per review batch and 1500 for the summary; the first live run
485
+ with a 400-token summary budget came back empty with `finish_reason=length`. The batch
486
+ cap counts the prompt only, so it is set to 3,500 tokens: a full batch plus its answer
487
+ budget stays under the 8K-per-minute tier. The `NEI`-with-a-passage half of the
488
+ escalation rule is currently unreachable — the pipeline never attaches a passage to an
489
+ `NEI` (OPEN-ITEMS 14.1) — so in practice the judge sees the `low` tier only. A judge
490
+ that does not answer is not a §15 state — the verdicts stand — but it is reported on the
491
+ stage line, the `judge status:` / `summary status:` header lines, the JSON and an
492
+ unsuppressed terminal line; `-q` cannot hide it. The SARIF log carries findings only.
493
+
480
494
  ## 12. Cross-platform constraints
481
495
 
482
496
  | Component | Risk | Decision |
@@ -929,8 +943,12 @@ pairing at 0.940 on its hand set (`docs/eval/2026-09-12-pairing-author-year.md`)
929
943
  beyond the plan, a resolution and retraction cache (warm re-run 1.35 s,
930
944
  `docs/eval/2026-09-15-v0.2-live.md`).
931
945
 
932
- **v0.3 — judge layer.** Opt-in LLM second opinion, Ollama default, OpenRouter and
933
- Gemini adapters, batching and cost reporting.
946
+ **v0.3 — judge layer.** Opt-in LLM second opinion (Groq `openai/gpt-oss-120b` default,
947
+ Gemini and Ollama selectable), batching and cost reporting, `--summarize`.
948
+ *Shipped 2026-09-15 as v0.3.0:* `check --judge` escalates the `low`-tier verdicts with a
949
+ passage in batches of ≤ 20 / ~7k tokens and attaches the opinion beside the verdict;
950
+ judgements cached in schema v4; `--summarize` and the TUI `/summarize` as one extra call;
951
+ an unanswered judge reported on every surface but SARIF. Live run: `docs/eval/2026-09-15-judge-live.md`.
934
952
 
935
953
  **v0.4 — social provider.** Bluesky and Hacker News first, Reddit via user-supplied
936
954
  OAuth app (missing credentials are reported, never silently skipped), Mastodon
@@ -1,5 +1,5 @@
1
1
  """proofpath — check whether the sources behind a claim support the claim."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.3.0"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -1,7 +1,8 @@
1
1
  """The persistent cache: one plain SQLite file under the user cache dir.
2
2
 
3
3
  Holds fetched source text (with a 7-day TTL), sentence chunks with their
4
- embeddings, verdicts keyed ``(claim_hash, source_id, model_id)`` and the two
4
+ embeddings, verdicts keyed ``(claim_hash, source_id, model_id)``, the optional
5
+ judge's opinions beside them, and the two
5
6
  network lookups a run makes before it fetches anything -- reference resolution and
6
7
  the retraction check -- so a re-run of the same document costs nothing. No
7
8
  extension, no server: any SQLite GUI can open the file (spec sections 5.1, 12, 16).
@@ -21,12 +22,13 @@ from typing import Any
21
22
 
22
23
  import numpy as np
23
24
 
25
+ from proofpath.judge import JudgeOpinion
24
26
  from proofpath.models import Label, Passage, Tier, Verdict
25
27
  from proofpath.paths import cache_dir
26
28
  from proofpath.pipeline import CUT_DECIMALS, Thresholds
27
29
  from proofpath.resolve import Candidate, FieldMatch, ResolveResult, Retraction, State, strip_marker
28
30
 
29
- SCHEMA_VERSION = "3"
31
+ SCHEMA_VERSION = "4"
30
32
  RAW_TEXT_TTL_DAYS = 7
31
33
  # A resolution is a statement about a published record, which does not change; the
32
34
  # month is there so a reference an index had not yet ingested is looked at again.
@@ -130,7 +132,28 @@ _LOOKUPS_DDL: tuple[str, ...] = (
130
132
  )
131
133
  _LOOKUPS_SCHEMA = ";\n".join(_LOOKUPS_DDL) + ";\n"
132
134
 
133
- _SCHEMA = _BASE_SCHEMA + _CHUNKS_SCHEMA + _LOOKUPS_SCHEMA
135
+ # The optional judge's opinions, beside the verdicts and never instead of them (spec
136
+ # section 11.1). Keyed by the judge model as well, so a run with another judge asks
137
+ # again rather than reading back an opinion that model never gave. ``model_id`` is
138
+ # deliberately absent: an opinion is about the claim and the passage, not about which
139
+ # local NLI happened to escalate it, so toggling ``--judge`` cannot invalidate a
140
+ # cached verdict and re-tuning the thresholds cannot invalidate a cached opinion.
141
+ _JUDGEMENTS_DDL: tuple[str, ...] = (
142
+ """
143
+ CREATE TABLE IF NOT EXISTS judgements (
144
+ claim_hash TEXT NOT NULL,
145
+ source_id TEXT NOT NULL REFERENCES sources(source_id) ON DELETE CASCADE,
146
+ judge_model TEXT NOT NULL, -- "groq openai/gpt-oss-120b"
147
+ label TEXT NOT NULL,
148
+ rationale TEXT NOT NULL,
149
+ created_at TEXT NOT NULL,
150
+ PRIMARY KEY (claim_hash, source_id, judge_model)
151
+ )
152
+ """,
153
+ )
154
+ _JUDGEMENTS_SCHEMA = ";\n".join(_JUDGEMENTS_DDL) + ";\n"
155
+
156
+ _SCHEMA = _BASE_SCHEMA + _CHUNKS_SCHEMA + _LOOKUPS_SCHEMA + _JUDGEMENTS_SCHEMA
134
157
 
135
158
 
136
159
  def _migrate_to_v2(conn: sqlite3.Connection) -> None:
@@ -156,7 +179,19 @@ def _migrate_to_v3(conn: sqlite3.Connection) -> None:
156
179
  conn.execute(statement)
157
180
 
158
181
 
159
- # One step per schema version, oldest first; Phase 9 appends its own.
182
+ def _migrate_to_v4(conn: sqlite3.Connection) -> None:
183
+ """v3 -> v4: the ``judgements`` table.
184
+
185
+ Additive, like v3: nothing stored before v4 holds a second opinion, and the
186
+ verdicts are untouched on purpose -- their ``model_id`` says nothing about the
187
+ judge, so a file that gains this table hands back exactly the verdicts it held
188
+ before and a run with ``--judge`` costs no re-verification (spec section 11.1).
189
+ """
190
+ for statement in _JUDGEMENTS_DDL:
191
+ conn.execute(statement)
192
+
193
+
194
+ # One step per schema version, oldest first.
160
195
  #
161
196
  # Invariant, on which the self-healing in ``Cache.__init__`` rests: every step runs
162
197
  # inside the one transaction that also records the new version, so a step must issue
@@ -166,6 +201,7 @@ def _migrate_to_v3(conn: sqlite3.Connection) -> None:
166
201
  _MIGRATIONS: tuple[tuple[str, Callable[[sqlite3.Connection], None]], ...] = (
167
202
  ("2", _migrate_to_v2),
168
203
  ("3", _migrate_to_v3),
204
+ ("4", _migrate_to_v4),
169
205
  )
170
206
 
171
207
 
@@ -473,6 +509,10 @@ class Cache:
473
509
  }
474
510
  if digests - {text_sha256}:
475
511
  self._conn.execute("DELETE FROM verdicts WHERE source_id = ?", (source_id,))
512
+ # The judge quoted the same passages, so its opinions go the same way:
513
+ # a second opinion about text the source no longer serves is evidence
514
+ # nobody can check (product rule 1).
515
+ self._conn.execute("DELETE FROM judgements WHERE source_id = ?", (source_id,))
476
516
  self._conn.execute(
477
517
  "DELETE FROM chunks WHERE source_id = ? AND embed_model = ?",
478
518
  (source_id, embed_model),
@@ -553,6 +593,53 @@ class Cache:
553
593
  )
554
594
  return Verdict(Label(label), float(score), _tier(tier), passage, reason=reason)
555
595
 
596
+ # --- judgements ---------------------------------------------------------------
597
+
598
+ def put_judgement(
599
+ self,
600
+ claim_hash_: str,
601
+ source_id: str,
602
+ judge_model: str,
603
+ opinion: JudgeOpinion,
604
+ *,
605
+ now: datetime | None = None,
606
+ ) -> None:
607
+ """Store one judge opinion. Re-asking the same judge replaces what it said."""
608
+ with self._conn:
609
+ self._conn.execute(
610
+ "INSERT INTO judgements(claim_hash, source_id, judge_model, label, rationale, "
611
+ "created_at) VALUES (?, ?, ?, ?, ?, ?) "
612
+ "ON CONFLICT(claim_hash, source_id, judge_model) DO UPDATE SET "
613
+ "label = excluded.label, rationale = excluded.rationale, "
614
+ "created_at = excluded.created_at",
615
+ (
616
+ claim_hash_,
617
+ source_id,
618
+ judge_model,
619
+ opinion.label.value,
620
+ opinion.rationale,
621
+ _iso(now or _now()),
622
+ ),
623
+ )
624
+
625
+ def get_judgement(
626
+ self, claim_hash_: str, source_id: str, judge_model: str
627
+ ) -> JudgeOpinion | None:
628
+ """What this judge said about this claim and source, or ``None`` if unasked.
629
+
630
+ No TTL: an opinion is about a claim and a passage, both of which are fixed.
631
+ The passage changing is what retires it, and ``put_chunks`` does that.
632
+ """
633
+ row = self._conn.execute(
634
+ "SELECT label, rationale FROM judgements "
635
+ "WHERE claim_hash = ? AND source_id = ? AND judge_model = ?",
636
+ (claim_hash_, source_id, judge_model),
637
+ ).fetchone()
638
+ if row is None:
639
+ return None
640
+ label, rationale = row
641
+ return JudgeOpinion(label=Label(label), rationale=str(rationale), model=judge_model)
642
+
556
643
  # --- resolutions and retractions ---------------------------------------------
557
644
 
558
645
  def get_resolution(self, raw: str, *, now: datetime | None = None) -> ResolveResult | None: