second-read 0.1.3__tar.gz → 0.1.4__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 (315) hide show
  1. second_read-0.1.4/ARCHITECTURE.md +561 -0
  2. {second_read-0.1.3 → second_read-0.1.4}/CLAUDE.md +3 -3
  3. {second_read-0.1.3 → second_read-0.1.4}/PKG-INFO +42 -13
  4. {second_read-0.1.3 → second_read-0.1.4}/README.md +41 -12
  5. {second_read-0.1.3 → second_read-0.1.4}/ROADMAP.md +4 -4
  6. second_read-0.1.4/SECURITY.md +34 -0
  7. {second_read-0.1.3 → second_read-0.1.4}/eval/README.md +2 -2
  8. second_read-0.1.4/glama.json +4 -0
  9. {second_read-0.1.3 → second_read-0.1.4}/pyproject.toml +1 -1
  10. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/prompts.py +5 -1
  11. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/research.py +10 -1
  12. {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/__init__.py +1 -1
  13. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/assemble.py +1 -1
  14. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/conflicts.py +1 -1
  15. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/reactions.py +2 -2
  16. {second_read-0.1.3 → second_read-0.1.4}/server.json +2 -2
  17. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/test_agent.py +32 -0
  18. {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_dataset.py +1 -1
  19. {second_read-0.1.3 → second_read-0.1.4}/uv.lock +1 -1
  20. second_read-0.1.3/ARCHITECTURE.md +0 -150
  21. second_read-0.1.3/DESIGN.md +0 -343
  22. second_read-0.1.3/secondread/mcp_local/README.md +0 -96
  23. {second_read-0.1.3 → second_read-0.1.4}/.env.example +0 -0
  24. {second_read-0.1.3 → second_read-0.1.4}/.github/workflows/ci.yml +0 -0
  25. {second_read-0.1.3 → second_read-0.1.4}/.github/workflows/publish.yml +0 -0
  26. {second_read-0.1.3 → second_read-0.1.4}/.gitignore +0 -0
  27. {second_read-0.1.3 → second_read-0.1.4}/.python-version +0 -0
  28. {second_read-0.1.3 → second_read-0.1.4}/LICENSE +0 -0
  29. {second_read-0.1.3 → second_read-0.1.4}/eval/__init__.py +0 -0
  30. {second_read-0.1.3 → second_read-0.1.4}/eval/__main__.py +0 -0
  31. {second_read-0.1.3 → second_read-0.1.4}/eval/baseline.py +0 -0
  32. {second_read-0.1.3 → second_read-0.1.4}/eval/cli.py +0 -0
  33. {second_read-0.1.3 → second_read-0.1.4}/eval/data/01-leicester-money.json +0 -0
  34. {second_read-0.1.3 → second_read-0.1.4}/eval/data/01-leicester-money.md +0 -0
  35. {second_read-0.1.3 → second_read-0.1.4}/eval/data/02-possession.json +0 -0
  36. {second_read-0.1.3 → second_read-0.1.4}/eval/data/02-possession.md +0 -0
  37. {second_read-0.1.3 → second_read-0.1.4}/eval/data/03-invincibles.json +0 -0
  38. {second_read-0.1.3 → second_read-0.1.4}/eval/data/03-invincibles.md +0 -0
  39. {second_read-0.1.3 → second_read-0.1.4}/eval/data/04-var.json +0 -0
  40. {second_read-0.1.3 → second_read-0.1.4}/eval/data/04-var.md +0 -0
  41. {second_read-0.1.3 → second_read-0.1.4}/eval/data/05-four-day-week.json +0 -0
  42. {second_read-0.1.3 → second_read-0.1.4}/eval/data/05-four-day-week.md +0 -0
  43. {second_read-0.1.3 → second_read-0.1.4}/eval/data/06-sqlite-production.json +0 -0
  44. {second_read-0.1.3 → second_read-0.1.4}/eval/data/06-sqlite-production.md +0 -0
  45. {second_read-0.1.3 → second_read-0.1.4}/eval/data/07-postgres-queue.json +0 -0
  46. {second_read-0.1.3 → second_read-0.1.4}/eval/data/07-postgres-queue.md +0 -0
  47. {second_read-0.1.3 → second_read-0.1.4}/eval/data/08-uuid-keys.json +0 -0
  48. {second_read-0.1.3 → second_read-0.1.4}/eval/data/08-uuid-keys.md +0 -0
  49. {second_read-0.1.3 → second_read-0.1.4}/eval/data/09-cdn-cache.json +0 -0
  50. {second_read-0.1.3 → second_read-0.1.4}/eval/data/09-cdn-cache.md +0 -0
  51. {second_read-0.1.3 → second_read-0.1.4}/eval/data/10-best-league.json +0 -0
  52. {second_read-0.1.3 → second_read-0.1.4}/eval/data/10-best-league.md +0 -0
  53. {second_read-0.1.3 → second_read-0.1.4}/eval/data/11-greece-2004.json +0 -0
  54. {second_read-0.1.3 → second_read-0.1.4}/eval/data/11-greece-2004.md +0 -0
  55. {second_read-0.1.3 → second_read-0.1.4}/eval/data/12-istanbul-momentum.json +0 -0
  56. {second_read-0.1.3 → second_read-0.1.4}/eval/data/12-istanbul-momentum.md +0 -0
  57. {second_read-0.1.3 → second_read-0.1.4}/eval/data/13-five-substitutes.json +0 -0
  58. {second_read-0.1.3 → second_read-0.1.4}/eval/data/13-five-substitutes.md +0 -0
  59. {second_read-0.1.3 → second_read-0.1.4}/eval/data/14-brazil-1970.json +0 -0
  60. {second_read-0.1.3 → second_read-0.1.4}/eval/data/14-brazil-1970.md +0 -0
  61. {second_read-0.1.3 → second_read-0.1.4}/eval/data/15-penalty-lottery.json +0 -0
  62. {second_read-0.1.3 → second_read-0.1.4}/eval/data/15-penalty-lottery.md +0 -0
  63. {second_read-0.1.3 → second_read-0.1.4}/eval/data/16-womens-game-stadiums.json +0 -0
  64. {second_read-0.1.3 → second_read-0.1.4}/eval/data/16-womens-game-stadiums.md +0 -0
  65. {second_read-0.1.3 → second_read-0.1.4}/eval/data/17-no-kubernetes.json +0 -0
  66. {second_read-0.1.3 → second_read-0.1.4}/eval/data/17-no-kubernetes.md +0 -0
  67. {second_read-0.1.3 → second_read-0.1.4}/eval/data/18-python-type-hints.json +0 -0
  68. {second_read-0.1.3 → second_read-0.1.4}/eval/data/18-python-type-hints.md +0 -0
  69. {second_read-0.1.3 → second_read-0.1.4}/eval/data/19-big-rewrite.json +0 -0
  70. {second_read-0.1.3 → second_read-0.1.4}/eval/data/19-big-rewrite.md +0 -0
  71. {second_read-0.1.3 → second_read-0.1.4}/eval/data/20-nuclear-climate.json +0 -0
  72. {second_read-0.1.3 → second_read-0.1.4}/eval/data/20-nuclear-climate.md +0 -0
  73. {second_read-0.1.3 → second_read-0.1.4}/eval/data/21-phones-in-schools.json +0 -0
  74. {second_read-0.1.3 → second_read-0.1.4}/eval/data/21-phones-in-schools.md +0 -0
  75. {second_read-0.1.3 → second_read-0.1.4}/eval/data/22-clock-changes.json +0 -0
  76. {second_read-0.1.3 → second_read-0.1.4}/eval/data/22-clock-changes.md +0 -0
  77. {second_read-0.1.3 → second_read-0.1.4}/eval/data/23-close-the-libraries.json +0 -0
  78. {second_read-0.1.3 → second_read-0.1.4}/eval/data/23-close-the-libraries.md +0 -0
  79. {second_read-0.1.3 → second_read-0.1.4}/eval/data/24-robots-not-astronauts.json +0 -0
  80. {second_read-0.1.3 → second_read-0.1.4}/eval/data/24-robots-not-astronauts.md +0 -0
  81. {second_read-0.1.3 → second_read-0.1.4}/eval/data/25-jwt-sessions.json +0 -0
  82. {second_read-0.1.3 → second_read-0.1.4}/eval/data/25-jwt-sessions.md +0 -0
  83. {second_read-0.1.3 → second_read-0.1.4}/eval/data/26-kafka-stream.json +0 -0
  84. {second_read-0.1.3 → second_read-0.1.4}/eval/data/26-kafka-stream.md +0 -0
  85. {second_read-0.1.3 → second_read-0.1.4}/eval/data/27-rate-limiter.json +0 -0
  86. {second_read-0.1.3 → second_read-0.1.4}/eval/data/27-rate-limiter.md +0 -0
  87. {second_read-0.1.3 → second_read-0.1.4}/eval/data/28-audit-log-archive.json +0 -0
  88. {second_read-0.1.3 → second_read-0.1.4}/eval/data/28-audit-log-archive.md +0 -0
  89. {second_read-0.1.3 → second_read-0.1.4}/eval/data/29-raft-config-store.json +0 -0
  90. {second_read-0.1.3 → second_read-0.1.4}/eval/data/29-raft-config-store.md +0 -0
  91. {second_read-0.1.3 → second_read-0.1.4}/eval/data/30-tls13-internal.json +0 -0
  92. {second_read-0.1.3 → second_read-0.1.4}/eval/data/30-tls13-internal.md +0 -0
  93. {second_read-0.1.3 → second_read-0.1.4}/eval/data/31-womens-champions-league.json +0 -0
  94. {second_read-0.1.3 → second_read-0.1.4}/eval/data/31-womens-champions-league.md +0 -0
  95. {second_read-0.1.3 → second_read-0.1.4}/eval/data/32-national-league-third-place.json +0 -0
  96. {second_read-0.1.3 → second_read-0.1.4}/eval/data/32-national-league-third-place.md +0 -0
  97. {second_read-0.1.3 → second_read-0.1.4}/eval/data/33-mjallby-model.json +0 -0
  98. {second_read-0.1.3 → second_read-0.1.4}/eval/data/33-mjallby-model.md +0 -0
  99. {second_read-0.1.3 → second_read-0.1.4}/eval/data/34-congestion-charge.json +0 -0
  100. {second_read-0.1.3 → second_read-0.1.4}/eval/data/34-congestion-charge.md +0 -0
  101. {second_read-0.1.3 → second_read-0.1.4}/eval/data/35-postgres-18-upgrade.json +0 -0
  102. {second_read-0.1.3 → second_read-0.1.4}/eval/data/35-postgres-18-upgrade.md +0 -0
  103. {second_read-0.1.3 → second_read-0.1.4}/eval/data/36-wsl-fourteen-teams.json +0 -0
  104. {second_read-0.1.3 → second_read-0.1.4}/eval/data/36-wsl-fourteen-teams.md +0 -0
  105. {second_read-0.1.3 → second_read-0.1.4}/eval/data/37-metlife-final.json +0 -0
  106. {second_read-0.1.3 → second_read-0.1.4}/eval/data/37-metlife-final.md +0 -0
  107. {second_read-0.1.3 → second_read-0.1.4}/eval/data/38-votes-at-sixteen.json +0 -0
  108. {second_read-0.1.3 → second_read-0.1.4}/eval/data/38-votes-at-sixteen.md +0 -0
  109. {second_read-0.1.3 → second_read-0.1.4}/eval/data/39-logical-replication-reporting.json +0 -0
  110. {second_read-0.1.3 → second_read-0.1.4}/eval/data/39-logical-replication-reporting.md +0 -0
  111. {second_read-0.1.3 → second_read-0.1.4}/eval/data/40-hpa-ingest-workers.json +0 -0
  112. {second_read-0.1.3 → second_read-0.1.4}/eval/data/40-hpa-ingest-workers.md +0 -0
  113. {second_read-0.1.3 → second_read-0.1.4}/eval/dataset.py +0 -0
  114. {second_read-0.1.3 → second_read-0.1.4}/eval/fake_pipeline.py +0 -0
  115. {second_read-0.1.3 → second_read-0.1.4}/eval/harness.py +0 -0
  116. {second_read-0.1.3 → second_read-0.1.4}/eval/items.py +0 -0
  117. {second_read-0.1.3 → second_read-0.1.4}/eval/judge.py +0 -0
  118. {second_read-0.1.3 → second_read-0.1.4}/eval/live.py +0 -0
  119. {second_read-0.1.3 → second_read-0.1.4}/eval/metrics.py +0 -0
  120. {second_read-0.1.3 → second_read-0.1.4}/eval/results.py +0 -0
  121. {second_read-0.1.3 → second_read-0.1.4}/eval/textspan.py +0 -0
  122. {second_read-0.1.3 → second_read-0.1.4}/scripts/__init__.py +0 -0
  123. {second_read-0.1.3 → second_read-0.1.4}/scripts/compare_search.py +0 -0
  124. {second_read-0.1.3 → second_read-0.1.4}/secondread/__init__.py +0 -0
  125. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/__init__.py +0 -0
  126. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/copyedit/__init__.py +0 -0
  127. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/copyedit/agent.py +0 -0
  128. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/copyedit/prompts.py +0 -0
  129. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/copyedit/text.py +0 -0
  130. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/__init__.py +0 -0
  131. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/agent.py +0 -0
  132. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/prompts.py +0 -0
  133. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/schemas.py +0 -0
  134. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/text.py +0 -0
  135. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/__init__.py +0 -0
  136. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/agent.py +0 -0
  137. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/chunks.py +0 -0
  138. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/prompts.py +0 -0
  139. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/spans.py +0 -0
  140. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/__init__.py +0 -0
  141. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/agent.py +0 -0
  142. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/schemas.py +0 -0
  143. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/sources.py +0 -0
  144. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/originality/__init__.py +0 -0
  145. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/originality/agent.py +0 -0
  146. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/originality/sentences.py +0 -0
  147. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/originality/similarity.py +0 -0
  148. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/structure/__init__.py +0 -0
  149. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/structure/agent.py +0 -0
  150. {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/structure/prompts.py +0 -0
  151. {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/__init__.py +0 -0
  152. {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/__main__.py +0 -0
  153. {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/envfile.py +0 -0
  154. {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/errors.py +0 -0
  155. {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/main.py +0 -0
  156. {second_read-0.1.3 → second_read-0.1.4}/secondread/contracts/__init__.py +0 -0
  157. {second_read-0.1.3 → second_read-0.1.4}/secondread/contracts/errors.py +0 -0
  158. {second_read-0.1.3 → second_read-0.1.4}/secondread/contracts/interfaces.py +0 -0
  159. {second_read-0.1.3 → second_read-0.1.4}/secondread/contracts/models.py +0 -0
  160. {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/__main__.py +0 -0
  161. {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/demo.py +0 -0
  162. {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/errors.py +0 -0
  163. {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/main.py +0 -0
  164. {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/progress.py +0 -0
  165. {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/server.py +0 -0
  166. {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/wiring.py +0 -0
  167. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/__init__.py +0 -0
  168. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/authwatch.py +0 -0
  169. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/classify.py +0 -0
  170. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/labels.py +0 -0
  171. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/orchestrator.py +0 -0
  172. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/planning.py +0 -0
  173. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/progress.py +0 -0
  174. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/prompts.py +0 -0
  175. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/registry.py +0 -0
  176. {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/schemas.py +0 -0
  177. {second_read-0.1.3 → second_read-0.1.4}/secondread/pipeline.py +0 -0
  178. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/__init__.py +0 -0
  179. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/__init__.py +0 -0
  180. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/extract.py +0 -0
  181. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/fetcher.py +0 -0
  182. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/guarded.py +0 -0
  183. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/ssrf.py +0 -0
  184. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/__init__.py +0 -0
  185. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/_common.py +0 -0
  186. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/anthropic_client.py +0 -0
  187. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/config.py +0 -0
  188. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/openai_client.py +0 -0
  189. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/__init__.py +0 -0
  190. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/base.py +0 -0
  191. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/brave.py +0 -0
  192. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/config.py +0 -0
  193. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/exa.py +0 -0
  194. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/retry.py +0 -0
  195. {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/tavily.py +0 -0
  196. {second_read-0.1.3 → second_read-0.1.4}/secondread/registry.py +0 -0
  197. {second_read-0.1.3 → second_read-0.1.4}/secondread/report/__init__.py +0 -0
  198. {second_read-0.1.3 → second_read-0.1.4}/secondread/report/email.py +0 -0
  199. {second_read-0.1.3 → second_read-0.1.4}/secondread/report/html.py +0 -0
  200. {second_read-0.1.3 → second_read-0.1.4}/secondread/report/markdown.py +0 -0
  201. {second_read-0.1.3 → second_read-0.1.4}/secondread/report/templates/email.html.j2 +0 -0
  202. {second_read-0.1.3 → second_read-0.1.4}/secondread/report/text.py +0 -0
  203. {second_read-0.1.3 → second_read-0.1.4}/secondread/report/view.py +0 -0
  204. {second_read-0.1.3 → second_read-0.1.4}/secondread/testing/__init__.py +0 -0
  205. {second_read-0.1.3 → second_read-0.1.4}/secondread/testing/fakes.py +0 -0
  206. {second_read-0.1.3 → second_read-0.1.4}/secondread/testing/samples.py +0 -0
  207. {second_read-0.1.3 → second_read-0.1.4}/tests/__init__.py +0 -0
  208. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/__init__.py +0 -0
  209. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/copyedit/__init__.py +0 -0
  210. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/copyedit/test_agent.py +0 -0
  211. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/copyedit/test_quotes.py +0 -0
  212. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/copyedit/test_text.py +0 -0
  213. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/devils_advocate/__init__.py +0 -0
  214. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/devils_advocate/test_agent.py +0 -0
  215. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/devils_advocate/test_text.py +0 -0
  216. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/extractor/__init__.py +0 -0
  217. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/extractor/test_agent.py +0 -0
  218. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/extractor/test_spans.py +0 -0
  219. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/extractor/test_standalone.py +0 -0
  220. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/__init__.py +0 -0
  221. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/test_precision.py +0 -0
  222. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/test_sources.py +0 -0
  223. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/test_standalone.py +0 -0
  224. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/originality/__init__.py +0 -0
  225. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/originality/test_agent.py +0 -0
  226. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/originality/test_sentences.py +0 -0
  227. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/originality/test_similarity.py +0 -0
  228. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/structure/__init__.py +0 -0
  229. {second_read-0.1.3 → second_read-0.1.4}/tests/agents/structure/test_agent.py +0 -0
  230. {second_read-0.1.3 → second_read-0.1.4}/tests/cli/__init__.py +0 -0
  231. {second_read-0.1.3 → second_read-0.1.4}/tests/cli/test_cli.py +0 -0
  232. {second_read-0.1.3 → second_read-0.1.4}/tests/cli/test_envfile.py +0 -0
  233. {second_read-0.1.3 → second_read-0.1.4}/tests/cli/test_live.py +0 -0
  234. {second_read-0.1.3 → second_read-0.1.4}/tests/cli/test_registry.py +0 -0
  235. {second_read-0.1.3 → second_read-0.1.4}/tests/conftest.py +0 -0
  236. {second_read-0.1.3 → second_read-0.1.4}/tests/contracts/__init__.py +0 -0
  237. {second_read-0.1.3 → second_read-0.1.4}/tests/contracts/test_models.py +0 -0
  238. {second_read-0.1.3 → second_read-0.1.4}/tests/eval/__init__.py +0 -0
  239. {second_read-0.1.3 → second_read-0.1.4}/tests/eval/helpers.py +0 -0
  240. {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_baseline.py +0 -0
  241. {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_harness.py +0 -0
  242. {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_judge_and_spans.py +0 -0
  243. {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_live.py +0 -0
  244. {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_metrics.py +0 -0
  245. {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/__init__.py +0 -0
  246. {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/helpers.py +0 -0
  247. {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_env_file.py +0 -0
  248. {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_progress.py +0 -0
  249. {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_server.py +0 -0
  250. {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_stdio.py +0 -0
  251. {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_wiring.py +0 -0
  252. {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/__init__.py +0 -0
  253. {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/builders.py +0 -0
  254. {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_authwatch.py +0 -0
  255. {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_conflicts.py +0 -0
  256. {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_pipeline.py +0 -0
  257. {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_planning.py +0 -0
  258. {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_progress_classify.py +0 -0
  259. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/__init__.py +0 -0
  260. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/__init__.py +0 -0
  261. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_extract.py +0 -0
  262. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_fetcher.py +0 -0
  263. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_guarded.py +0 -0
  264. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_live.py +0 -0
  265. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_ssrf.py +0 -0
  266. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/__init__.py +0 -0
  267. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_auth_error.json +0 -0
  268. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_bad_request.json +0 -0
  269. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_low_credit.json +0 -0
  270. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_overloaded.json +0 -0
  271. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_rate_limit.json +0 -0
  272. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_refusal.json +0 -0
  273. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_text.json +0 -0
  274. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_tool_use.json +0 -0
  275. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_tool_use_invalid.json +0 -0
  276. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_truncated.json +0 -0
  277. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_auth_error.json +0 -0
  278. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_insufficient_quota.json +0 -0
  279. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_json_invalid.json +0 -0
  280. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_json_schema.json +0 -0
  281. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_rate_limit.json +0 -0
  282. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_refusal.json +0 -0
  283. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_server_error.json +0 -0
  284. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_text.json +0 -0
  285. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_truncated.json +0 -0
  286. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/replay.py +0 -0
  287. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/schemas.py +0 -0
  288. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/test_anthropic.py +0 -0
  289. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/test_common.py +0 -0
  290. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/test_live.py +0 -0
  291. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/test_openai.py +0 -0
  292. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/search/__init__.py +0 -0
  293. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/search/test_adapters.py +0 -0
  294. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/search/test_config.py +0 -0
  295. {second_read-0.1.3 → second_read-0.1.4}/tests/providers/search/test_live.py +0 -0
  296. {second_read-0.1.3 → second_read-0.1.4}/tests/report/__init__.py +0 -0
  297. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/empty.html +0 -0
  298. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/empty.md +0 -0
  299. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/empty.txt +0 -0
  300. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_document.html +0 -0
  301. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_document.md +0 -0
  302. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_document.txt +0 -0
  303. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_notes.html +0 -0
  304. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_notes.md +0 -0
  305. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_notes.txt +0 -0
  306. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_without_document.html +0 -0
  307. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_without_document.md +0 -0
  308. {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_without_document.txt +0 -0
  309. {second_read-0.1.3 → second_read-0.1.4}/tests/report/test_render.py +0 -0
  310. {second_read-0.1.3 → second_read-0.1.4}/tests/report/test_snapshots.py +0 -0
  311. {second_read-0.1.3 → second_read-0.1.4}/tests/scripts/__init__.py +0 -0
  312. {second_read-0.1.3 → second_read-0.1.4}/tests/scripts/test_compare_search.py +0 -0
  313. {second_read-0.1.3 → second_read-0.1.4}/tests/test_server_json.py +0 -0
  314. {second_read-0.1.3 → second_read-0.1.4}/tests/testing/__init__.py +0 -0
  315. {second_read-0.1.3 → second_read-0.1.4}/tests/testing/test_fakes.py +0 -0
@@ -0,0 +1,561 @@
1
+ # Architecture
2
+
3
+ How Second Read is built, why it is built that way, and what each choice costs.
4
+ This describes the code as it is today. A hosted service is planned but not
5
+ built; it is described near the end. The last part of this file is the
6
+ reference for the shared types in `secondread/contracts/`.
7
+
8
+ ## What it is
9
+
10
+ A document reviewer that runs on the user's machine with the user's own API
11
+ keys. It takes a draft (text or a public URL) and returns one report: facts
12
+ checked against retrieved sources, the strongest evidence-backed counter-case,
13
+ structure and copy edits, and originality notes. It is reachable two ways:
14
+
15
+ - an MCP server over stdio with one tool, `review_document`
16
+ (`secondread/mcp_local/`), for Claude Code, Claude Desktop and other local
17
+ MCP clients;
18
+ - a command line, `secondread review` (`secondread/cli/`).
19
+
20
+ Both call the same function, `secondread.pipeline.run_review`. Nothing is
21
+ hosted, nothing is stored after a review, and no data goes to the project's
22
+ author. Document text goes to the model provider (Anthropic) and claim-derived
23
+ search queries go to the search provider (Tavily), both under the user's keys.
24
+
25
+ ## Non-goals
26
+
27
+ - **Writing for the user.** Agents critique and verify; they never draft or
28
+ rewrite the argument.
29
+ - **Granular public tools.** No `extract_claims` or `find_counterevidence`;
30
+ exposing them would hand orchestration to the client.
31
+ - **Hosted models.** The project never pays for inference; users bring their
32
+ own keys.
33
+ - **Guaranteed plagiarism detection.** The originality check is a heuristic and
34
+ is labelled as one.
35
+ - **Auto-publishing or posting.** Out of scope.
36
+
37
+ ## System overview
38
+
39
+ ```
40
+ CLI MCP client (Claude Code / Desktop / Cursor)
41
+ secondread review │ stdio
42
+ │ ▼
43
+ │ mcp_local.server (review_document)
44
+ │ validate · fetch URL · word cap
45
+ └──────────────┬───────────────────┘
46
+ ▼
47
+ pipeline.run_review ──► Orchestrator ◄── AgentRegistry
48
+ │ (built by registry.py
49
+ classify ► extract ► plan │ from environment keys)
50
+ ▼
51
+ ┌────────── parallel review (shared claim ledger snapshot) ──────────┐
52
+ │ fact-check · devil's advocate · copy edit · structure · originality│
53
+ └────────────────────────────────┬───────────────────────────────────┘
54
+ ▼
55
+ reaction round: re-check, then debate (top claims only)
56
+ ▼
57
+ conflict rules (pure functions) ► rank ► Report
58
+ ▼
59
+ markdown · HTML · plain text renderers
60
+
61
+ Providers behind three protocols: LLMClient · SearchClient · Fetcher
62
+ (Anthropic) (Tavily) (SSRF-guarded HTTP)
63
+ ```
64
+
65
+ The rule that shapes everything: **agents propose, the orchestrator and plain
66
+ code decide.** Agents return findings and ledger updates; they never mutate
67
+ shared state. Models propose verdicts, plans and quotes; code validates each
68
+ one against the retrieved evidence and the guardrails before it counts.
69
+
70
+ ## Package map
71
+
72
+ | Path | Responsibility |
73
+ | --- | --- |
74
+ | `secondread/contracts/` | Pydantic models, Protocols and errors every package codes against. Changing it touches everything. |
75
+ | `secondread/pipeline.py` | `run_review`: the single entry point both front ends call. |
76
+ | `secondread/orchestrator/` | Classification, planning, parallel dispatch, reaction round, conflict rules, report assembly, progress, key-rejection watch. |
77
+ | `secondread/agents/<name>/` | One package per agent: extractor, factcheck, devils_advocate, copyedit, structure, originality. |
78
+ | `secondread/providers/` | Adapters: `llm/` (Anthropic, OpenAI), `search/` (Tavily, Brave, Exa), `fetch/` (HTTP with SSRF protection and text extraction). |
79
+ | `secondread/registry.py` | Real wiring from environment variables. Offers Anthropic and Tavily only. |
80
+ | `secondread/report/` | One format-neutral view model, three renderers (markdown, HTML email, plain text). |
81
+ | `secondread/cli/`, `secondread/mcp_local/` | The two entry points. |
82
+ | `secondread/testing/` | Fakes for every protocol and sample data; all tests use these. |
83
+ | `eval/` | Seeded documents with answer keys, harness, baselines, metrics. |
84
+
85
+ ## How a review runs
86
+
87
+ `Orchestrator.review` runs one job end to end. Each stage reports a
88
+ `ProgressStep` that the MCP server relays as progress notifications.
89
+
90
+ 1. **Classify.** A cheap-tier call picks a profile (`opinion` or `design_doc`)
91
+ unless the caller named one; a heuristic is the fallback if the call fails.
92
+ 2. **Extract.** The claim extractor splits the document into a thesis,
93
+ supporting claims and checkable factual claims, each with an exact span and
94
+ an importance score, and a `standalone` restatement that can be researched
95
+ out of context. Long documents are chunked. This runs first because
96
+ everything else reads the ledger it fills.
97
+ 3. **Plan.** A strong-tier call proposes an execution plan: which agents run,
98
+ which claims get deep research, and each claim's budget. `validate_plan`
99
+ checks it in code (allowed agents for the profile, at most 3 research
100
+ iterations per claim, budgets summing within the job) and replaces an
101
+ invalid plan with `default_plan`. Number-heavy documents get more deep
102
+ claims (12 instead of 8).
103
+ 4. **Review in parallel.** The planned agents run concurrently against one
104
+ read-only ledger snapshot. Their results are applied afterwards in a fixed
105
+ order (`AGENT_ORDER`), so the outcome does not depend on which agent
106
+ finishes first.
107
+ 5. **React.** If the devil's advocate retrieved evidence that contradicts a
108
+ claim the fact-checker verified, that claim is re-checked once. Then, for
109
+ the top 3 claims by importance where the two still disagree, each side
110
+ answers the other's evidence once and the orchestrator rules.
111
+ 6. **Resolve.** Pure functions in `orchestrator/conflicts.py` settle what is
112
+ left (see below), then findings are ranked.
113
+ 7. **Report.** `assemble` builds a `Report`; renderers turn it into markdown
114
+ for the tool result, and HTML or plain text for email.
115
+
116
+ A job has hard caps: 400,000 tokens, 60 search calls and 600 seconds by
117
+ default. Every agent charges one shared `BudgetMeter` for each model, search
118
+ and fetch call. Hitting the time limit returns partial results with a note.
119
+ When budget runs short, lower-importance claims get a single search or are
120
+ marked `unchecked`; the job does not fail.
121
+
122
+ ### The report
123
+
124
+ `Report` is structured data, rendered to markdown (the tool result), HTML (for
125
+ email) and plain text. Sections, in order:
126
+
127
+ 1. **Verdict line:** one sentence on readiness, with counts by severity.
128
+ 2. **Must fix:** factual errors and unsupported claims, each with the quoted
129
+ span, the problem and its sources. **Could not verify** sits beneath it at
130
+ lower severity.
131
+ 3. **Strongest counter-case:** the devil's advocate's top three rebuttals, each
132
+ with sources and the claim it targets.
133
+ 4. **Should fix:** structure findings.
134
+ 5. **Polish:** copy edits, grouped.
135
+ 6. **Originality notes:** possible matches, labelled heuristic.
136
+ 7. **Appendix:** the claim ledger with verdicts and sources, then a footer with
137
+ usage and degradation notices.
138
+
139
+ ## The agents
140
+
141
+ | Agent | Does | Model tier | Notes |
142
+ | --- | --- | --- | --- |
143
+ | Extractor | Claims with spans and importance | cheap | Asks for verbatim quotes and locates them in code. |
144
+ | Fact-checker | Verdict per factual claim, with cited evidence | mid | Bounded research loop per claim; code enforces the evidence rules. |
145
+ | Devil's advocate | Strongest opposing case from retrieved counter-evidence | strong | Same bounded loop; opinion and design-doc prompt variants. |
146
+ | Copy editor | Line edits that keep the author's voice | cheap | Verbatim quote plus replacement; unlocatable edits are dropped. |
147
+ | Structure reviewer | Flow, order, missing sections, gaps | mid | One call over the whole document; long ones reduced to an outline. |
148
+ | Originality checker | Near-matches to text found online | none | No model call: sampling, search and similarity are all code. |
149
+
150
+ Default models: Claude Haiku 4.5 (cheap), Sonnet 5.5 (mid) and Opus 5.5
151
+ (strong). Each tier can be overridden with `SECONDREAD_MODEL_CHEAP`, `_MID`
152
+ and `_STRONG`.
153
+
154
+ ### The research loop
155
+
156
+ The fact-checker and the devil's advocate share one idea: a bounded loop per
157
+ claim, not a single search. Each iteration plans queries, searches, fetches the
158
+ most promising pages, pulls out the passages that match the claim, and has the
159
+ model assess each source. The loop then stops, refines the query, or follows a
160
+ link to the primary source it found. It ends when a primary source settles the
161
+ claim, when two independent sources agree, when the model says more research
162
+ will not help, when the budget runs out, or at three iterations.
163
+
164
+ The fact-checker then makes one judge call that proposes a verdict. **Code
165
+ decides whether the verdict stands.** "Verified" or "wrong" needs a primary
166
+ source, two independent sources, or one reputable fetched source that nothing
167
+ retrieved contradicts. Social and user-generated hosts never count. A claim is
168
+ verified only if every checkable part is. When sources exist but do not back
169
+ the claim, it is "unsupported" (must fix); when no adequate source was found,
170
+ it is reported as "could not verify" at lower severity, because failing to find
171
+ a source is not evidence of an error. Before giving up, a claim gets a "second
172
+ look" at pages already fetched for the document's other claims, which costs no
173
+ search calls. The evidence attached to a finding is limited to the sources the
174
+ judge says it relied on.
175
+
176
+ ### Conflict rules
177
+
178
+ Applied after the reaction round, as pure functions over the ledger that return
179
+ a new ledger plus the decisions taken (IDs and reasons only, never document
180
+ text):
181
+
182
+ - a copy edit on a span flagged wrong or unsupported is dropped, and so is one
183
+ on borrowed or quoted text;
184
+ - a rebuttal that attacks a verified claim is kept only if it targets the
185
+ interpretation, not the fact;
186
+ - findings from two agents on the same span merge into one with both reasons;
187
+ - a rebuttal with no retrieved source is downgraded to "consider";
188
+ - ranking: factual error > unsupported > strong rebuttal > structure > style >
189
+ consider.
190
+
191
+ ## Trust boundaries and security
192
+
193
+ The threat model: documents and fetched web pages are written by strangers and
194
+ may contain instructions aimed at the reviewer.
195
+
196
+ - **Untrusted text only in user messages.** Document text, claims, snippets and
197
+ pages go inside delimited blocks (random-nonce markers, or `<untrusted_*>`
198
+ tags with look-alikes neutralised) in user messages, never in system
199
+ prompts. System prompts tell the model to treat the blocks as data.
200
+ - **Models cannot act.** Agents have read-only tools: search, fetch, read. A
201
+ malicious page can waste budget but cannot send, write or change anything.
202
+ - **Code re-validates model output.** Cited URLs must be ones the loop
203
+ retrieved, quoted excerpts must occur in the retrieved text, verdicts must be
204
+ settled by the evidence, and claim spans must slice the original document.
205
+ Pages that look like they carry injected instructions are skipped.
206
+ - **SSRF.** The fetcher refuses non-http(s) schemes, credentials in URLs,
207
+ unusual ports, localhost, numeric-host tricks, and any host that resolves to a
208
+ non-public address (including IPv6 forms embedding IPv4). Every redirect hop
209
+ is re-checked, redirects and body size are capped (counted after
210
+ decompression), and the check repeats at connect time against the vetted IP
211
+ to close the DNS-rebinding gap.
212
+ - **Keys.** Read from the environment or a gitignored `.env`. Held in masked
213
+ `Secret` objects, absent from `repr`, logs and error messages. Error
214
+ messages never contain document text either.
215
+ - **Output.** Renderers escape all untrusted text and link only http(s) URLs.
216
+ - **Rejected keys surface.** `authwatch` records the first provider
217
+ `AuthError` during a job and raises it at the end, so an invalid or
218
+ out-of-credit key produces a clear error instead of an empty "ready" report.
219
+
220
+ ## Failure handling
221
+
222
+ An agent that raises or reports an error becomes a note in the report
223
+ ("fact-check unavailable"); the job still returns a report. Provider failures
224
+ retry with backoff. Structured-output failures are re-asked once, then raise
225
+ `SchemaError`. The one deliberate exception is a rejected key, which fails the
226
+ job (above). Progress reporting can never break a review: a failing progress
227
+ send is logged and skipped.
228
+
229
+ ## Design decisions and trade-offs
230
+
231
+ Each entry says what was chosen, why, and what it costs.
232
+
233
+ ### Evidence and trust
234
+
235
+ **Code decides verdicts; the model proposes them.**
236
+ Why: a model can be wrong, manipulated by a page, or simply agreeable. The
237
+ evidence rules are deterministic and testable, so "verified" always has a
238
+ source behind it. Cost: more code, and rules that can be stricter than a human
239
+ reader (a claim with a real but weak source ends up "could not verify").
240
+
241
+ **Every finding must cite retrieved evidence.**
242
+ Why: the point of the tool is checkable claims, not model opinion. Excerpts are
243
+ validated against retrieved text so a citation cannot be invented. Cost: the
244
+ tool cannot flag what it cannot find a source for, and a retrieved excerpt can
245
+ still be on-topic but not supportive (see the first roadmap item).
246
+
247
+ **"Could not verify" is separate from "unsupported".**
248
+ Why: absence of evidence is not an error, and calling it one would make false
249
+ alarms the main output. Cost: one more section in the report, and some real
250
+ errors land in the lower-severity bucket.
251
+
252
+ **Part-by-part verification.**
253
+ Why: confirming "the club chartered a plane" says nothing about "the next day".
254
+ Requiring every material part to be verified stops one confirmed detail from
255
+ carrying a whole sentence. Cost: extra judge output and more claims end up
256
+ unverified.
257
+
258
+ ### Orchestration
259
+
260
+ **Hand-written asyncio orchestration, not an agent framework.**
261
+ Why: the control flow is the product (plan, parallel review, reaction round,
262
+ rules), and a framework would hide it. Typed Pydantic models keep it legible.
263
+ Cost: the project owns retry, budgeting and progress code that a framework
264
+ would provide.
265
+
266
+ **Agents never mutate the ledger.**
267
+ Why: parallel agents sharing mutable state would race, and results would depend
268
+ on completion order. They return `LedgerUpdate`s; the orchestrator applies them
269
+ in a fixed order. Cost: a little indirection, and agents cannot see each
270
+ other's work until the reaction round.
271
+
272
+ **A shared claim ledger instead of agents passing text.**
273
+ Why: findings are tied to claim IDs and character spans, so conflicts are
274
+ detected by span overlap rather than by reading prose. Cost: everything must
275
+ carry accurate spans, which drives the quote-and-locate design below.
276
+
277
+ **Model-written plan, code-checked.**
278
+ Why: the model can see that a document is number-heavy or a design doc and skip
279
+ or deepen work; code keeps it inside the profile's allowed agents and the job
280
+ budget, and falls back to a default plan if it is invalid. Cost: one strong-tier
281
+ call per review, and plans vary between runs.
282
+
283
+ **A reaction round, capped hard.**
284
+ Why: the two research agents often disagree about the same claim, and one
285
+ re-check plus one debate exchange on the top three claims resolves most of
286
+ that. Cost: extra calls and latency. Caps (one re-check per claim, three
287
+ debates, three research iterations) keep the cost bounded.
288
+
289
+ **Conflict rules as pure functions.**
290
+ Why: they are easy to test, trace and explain, and they never touch document
291
+ text. Cost: fixed rules cannot weigh nuance a human editor would.
292
+
293
+ ### Models and cost
294
+
295
+ **Three model tiers by task.**
296
+ Why: extraction and copy edits need a cheap model; judging evidence needs a
297
+ mid one; building the opposing case and ruling on debates need a strong one.
298
+ Cost: three model settings to configure, and rebuttal quality could drop if the
299
+ strong tier is downgraded (trying a cheaper devil's advocate is on the roadmap,
300
+ to be checked against the evaluation set).
301
+
302
+ **The originality check uses no model.**
303
+ Why: sampling, exact-phrase search and similarity scoring are deterministic,
304
+ cheap and reproducible. Cost: it finds near-copies online but misses well-known
305
+ lines a model would recognise from memory. Findings are labelled heuristic.
306
+
307
+ **Structured output via tool calls, with one re-ask.**
308
+ Why: current Claude models reject a forced `tool_choice`, so the adapter
309
+ requests the tool with `auto` plus an instruction, validates against the
310
+ Pydantic model, re-asks once, then raises. Numeric fields are left unbounded in
311
+ the schema and clamped in code, so a slightly out-of-range reply degrades
312
+ instead of failing validation. Cost: occasional extra calls.
313
+
314
+ **Models quote; code locates.**
315
+ Why: models are unreliable at character offsets. The extractor, copy editor and
316
+ structure reviewer ask for verbatim quotes and find them in the document with
317
+ layered matching (exact, normalised, trimmed, elided). Quotes that cannot be
318
+ located are dropped rather than given an invented span. Cost: some valid edits
319
+ are lost when a quote does not match.
320
+
321
+ **Whole review costs real money, and the budgets are the guard.**
322
+ A review is about 150,000 to 180,000 tokens and roughly $0.60 to $1.20. Job and
323
+ per-claim caps keep a long or hostile document from running up a bill. Cost:
324
+ deep research is limited to the most important claims.
325
+
326
+ ### Product shape
327
+
328
+ **Local and bring-your-own-key, not hosted.**
329
+ Why: the project never pays for inference, stores no documents, and needs no
330
+ accounts, queue or email, and building the local engine first meant hosting
331
+ work could wait until review quality was proven. Cost: setup needs `uv` and two
332
+ API keys, and ChatGPT and claude.ai on the web cannot connect, because they
333
+ accept only remote MCP servers. The hosted design is below and on the roadmap.
334
+
335
+ **One public tool, not granular ones.**
336
+ Why: exposing `extract_claims` or `find_counterevidence` would hand
337
+ orchestration to the client. Cost: users cannot run one stage alone.
338
+
339
+ **Anthropic and Tavily only.**
340
+ Why: they are the pair a real review has run through end to end. OpenAI, Brave
341
+ and Exa adapters exist behind the same protocols but are not offered by
342
+ `registry.py` until a real review has run through each. Cost: fewer choices
343
+ today.
344
+
345
+ **One report model, three renderers.**
346
+ Why: `ReportView` decides section order, grouping, quoting and link safety
347
+ once; markdown, HTML and text only format it. Cost: a new section touches the
348
+ view and every renderer, which snapshot tests cover.
349
+
350
+ ### Quality and testing
351
+
352
+ **All tests run offline on fakes.**
353
+ Why: tests are fast, free and deterministic. `FakeLLM` keys replies by prompt
354
+ tag, so each prompt can be scripted. Tests that call real providers are marked
355
+ `live` and skipped by default. Cost: fakes can drift from real provider
356
+ behaviour, so live runs and the evaluation still matter.
357
+
358
+ **A seeded-error evaluation with answer keys.**
359
+ Why: quality needs numbers. Each document carries known defects (wrong facts,
360
+ unsupported claims, weak arguments, borrowed sentences, grammar errors,
361
+ conflicts) and the harness scores recall, precision, citation validity,
362
+ rebuttal strength, cost and latency against a single-prompt baseline. The set
363
+ also seeds multi-part claims and claims whose top search results are secondary
364
+ sources, where research loops should matter most. Cost: runs spend real money,
365
+ so it runs by hand, not on every push.
366
+
367
+ ## Planned: hosted service
368
+
369
+ Not built. This is the design the roadmap item "Remote MCP server" refers to, so
370
+ that ChatGPT and claude.ai on the web can use Second Read.
371
+
372
+ - **Shape.** A thin MCP and setup server (Streamable HTTP, OAuth 2.1, Google
373
+ sign-in) that authenticates, enqueues a job and relays status. Workers run the
374
+ same orchestrator and agents as today.
375
+ - **Response modes**, chosen from the client's declared capabilities: MCP Tasks
376
+ (a task handle with live status), progress notifications (the call stays open
377
+ and streams steps), or fire-and-forget. The report is always emailed too, so a
378
+ dropped connection loses nothing.
379
+ - **Job lifecycle:** queued, extracting, reviewing, resolving, reporting,
380
+ emailed, purged. Transient errors retry with backoff; a failed agent degrades
381
+ the report; a hard time cap emails partial results; a content hash plus user
382
+ ID de-duplicates double submits.
383
+ - **Keys.** Entered only on a setup page, validated with a cheap test call,
384
+ stored with envelope encryption (a per-user data key wrapped by a KMS master
385
+ key), decrypted only inside a worker for one job, never logged.
386
+ - **Data.** Three tables: users (Google subject and verified email), provider
387
+ keys (encrypted) and job metadata (kept 30 days, no content). Documents,
388
+ ledgers and reports are deleted after delivery. OAuth refresh tokens are
389
+ stored hashed.
390
+ - **Limits.** Per-user daily cap, per-job token and search caps, a global
391
+ circuit breaker on the search provider. Traces record metadata, never text.
392
+ - **What it needs:** hosting, a job queue, a database, a KMS, a search plan and
393
+ a transactional email provider with SPF and DKIM.
394
+ - **Trade-off.** It removes the setup friction for web clients, at the price of
395
+ operating a service that holds users' keys, which the local design avoids.
396
+
397
+ ## Known limits
398
+
399
+ - **Run-to-run variance.** Search results and model judgments vary, so two runs
400
+ on one draft can reach different verdicts.
401
+ - **Citation fit.** A retrieved excerpt can be relevant without supporting the
402
+ finding it is attached to.
403
+ - **Unsupported claims.** Specific statistics and quotations that no source
404
+ backs are caught less often than contradicted ones.
405
+ - **Sources.** Web pages only; PDFs are not read.
406
+ - **Originality.** A heuristic; famous quotations recognised from memory are
407
+ missed.
408
+ - **Latency and cost.** About two minutes and 150,000 to 180,000 tokens for a
409
+ 700-word article.
410
+
411
+ `ROADMAP.md` ranks the work to address these.
412
+
413
+ ## Shared contracts reference
414
+
415
+ The shared types every part of Second Read codes against.
416
+
417
+ ### Layout
418
+
419
+ | Path | What |
420
+ | --- | --- |
421
+ | `secondread/contracts/models.py` | Pydantic v2 data models |
422
+ | `secondread/contracts/interfaces.py` | Protocols, `ReviewContext`, `BudgetMeter` |
423
+ | `secondread/contracts/errors.py` | Exception hierarchy |
424
+ | `secondread/testing/fakes.py` | Fakes for every protocol, `make_context` |
425
+ | `secondread/testing/samples.py` | Sample documents, ledger, reports |
426
+ | `secondread/providers/{llm,search,fetch}/` | Provider adapters |
427
+ | `secondread/agents/<name>/` | One package per agent |
428
+ | `secondread/report/` | Markdown and HTML rendering |
429
+ | `eval/` | Eval harness and dataset |
430
+ | `secondread/orchestrator/`, `secondread/pipeline.py` | Planning, conflict rules, `run_review` |
431
+ | `secondread/cli/`, `secondread/registry.py` | Real wiring and CLI |
432
+ | `secondread/mcp_local/` | stdio MCP server |
433
+
434
+ Tests mirror the package path under `tests/` (e.g. `tests/agents/extractor/`).
435
+
436
+ ### Data flow
437
+
438
+ ```
439
+ Document ──► extractor ──► ClaimLedger ──► plan ──► agents in parallel
440
+ ▲ │ AgentResult
441
+ └── orchestrator applies ledger_updates + findings
442
+ │ reaction round (recheck, debate)
443
+ │ conflict rules, ranking
444
+ ▼
445
+ Report ──► markdown / HTML
446
+ ```
447
+
448
+ ### Models (`secondread.contracts`)
449
+
450
+ - **`Document`**: `id`, `text`, `source_url`, `word_count`. Build with
451
+ `Document.from_text(text)`.
452
+ - **`Span`**: half-open `[start, end)` character offsets into `Document.text`.
453
+ `overlaps`, `contains`, `text_of`. Empty spans overlap nothing.
454
+ - **`Claim`**: `id`, `text`, `span`, `type` (`ClaimType`: thesis, supporting,
455
+ factual), `importance` 0–1. `text` must equal `span.text_of(doc.text)`.
456
+ `standalone` restates the claim with pronouns and references resolved, so
457
+ it can be researched out of context; `checkable_text` is `standalone or
458
+ text`.
459
+ - **`Evidence`**: `url`, `title`, `excerpt`, `retrieved_at`, `is_primary`.
460
+ - **`Verdict`**: verified, wrong, unsupported, unchecked. Verified is rejected
461
+ without evidence.
462
+ - **`Rebuttal`**: `target_claim_ids` (at least one), `argument`, `evidence`,
463
+ `strength` 1–5, `target` (`RebuttalTarget`: interpretation or fact; used by
464
+ the "rebuttal vs verified claim" rule). No evidence → downgraded to
465
+ `Severity.CONSIDER` by the orchestrator.
466
+ - **`Severity`**: `IntEnum`, higher ranks first: factual_error (5) >
467
+ unsupported (4) > strong_rebuttal (3) > structure (2) > style (1) >
468
+ consider (0). `Severity.label` gives the snake_case name.
469
+ - **`Finding`**: `agent`, `severity`, optional `span`, `message`, `evidence`,
470
+ `suggestion`, `claim_ids`, `heuristic` (always True for originality),
471
+ `merged_from` (set when the orchestrator merges same-span findings).
472
+ - **`AgentName`**: stable agent names (`extractor`, `factcheck`,
473
+ `devils_advocate`, `copyedit`, `structure`, `originality`, `orchestrator`).
474
+ - **`ResearchStep`**: one step of a bounded research loop (`iteration` ≤ 3).
475
+ - **`DebateRecord`**: positions, ruling, reasoning, final verdict.
476
+ - **`LedgerEntry`**: claim, verdict, verdict evidence/confidence/note,
477
+ rebuttals, copy-edit finding IDs, research trail, `rechecked`, `debate`.
478
+ - **`ClaimLedger`**: `entries` (by claim ID) and `findings` (by finding ID).
479
+ Helpers: `add_claim`, `set_verdict`, `add_rebuttal`, `add_research_step`,
480
+ `add_finding`, `remove_finding`, `claims(*types)`, `thesis()`,
481
+ `claims_overlapping(span)`, `findings_overlapping(span)`, `apply(update)`,
482
+ `apply_result(result)`. Copy-edit findings are linked to overlapping claims
483
+ automatically.
484
+ - **`LedgerUpdate`**: discriminated union `AddClaim | SetVerdict | AddRebuttal
485
+ | AddResearchStep`. **Agents never mutate the ledger**; they return updates
486
+ and the orchestrator applies them, so parallel agents cannot race.
487
+ - **`Profile`**: auto, opinion, design_doc. `PROFILE_AGENTS` lists the agents
488
+ each concrete profile may run.
489
+ - **`Budget`**: job caps (`max_tokens`, `max_search_calls`, `max_seconds`).
490
+ **`ClaimBudget`**: per-claim caps (`max_iterations` ≤ 3).
491
+ **`ExecutionPlan`**: agents to run, deep-research claim IDs, per-claim
492
+ budgets (`budget_for(claim_id)`).
493
+ - **`Usage`**: tokens, search/fetch/LLM calls, seconds; supports `+`.
494
+ - **`AgentResult`**: `agent`, `findings`, `ledger_updates`, `usage`, `error`
495
+ (set when the agent degraded instead of failing the job), `notes`
496
+ (non-fatal notices, no document text).
497
+ - **`ProgressEvent`**: `step` (usually a `ProgressStep`), `message`,
498
+ `percent` 0–100.
499
+ - **`Report`**: verdict line, `counts` (by severity label), must_fix,
500
+ unverified (the fact-checker's "could not confirm" findings: agent
501
+ `factcheck`, severity `CONSIDER`),
502
+ counter_case (rebuttals), should_fix, polish, originality, ledger, usage,
503
+ notes (degradation notices), generated_at.
504
+
505
+ ### Protocols (`secondread.contracts.interfaces`)
506
+
507
+ - **`LLMClient.complete(messages, *, schema=None, model_tier, tag="",
508
+ max_tokens=None, temperature=None) -> LLMResponse`**. `ModelTier`: cheap,
509
+ mid, strong. With `schema` (a Pydantic model class) the response's `parsed`
510
+ is a validated instance. `tag` names the prompt (e.g. `"factcheck.judge"`);
511
+ fakes key replies on it and it is never sent to the provider.
512
+ - **`SearchClient.search(query, *, k=5) -> list[SearchResult]`**.
513
+ - **`Fetcher.fetch(url) -> FetchedPage`**; raises `FetchError` or
514
+ `BlockedURLError`.
515
+ - **`Agent`**: `name` and `async run(ctx) -> AgentResult`. Must not raise for
516
+ expected failures.
517
+ - **`Rechecker.recheck(ctx, claim_id, counter_evidence)`**: fact-checker's one
518
+ re-check per claim.
519
+ - **`Debater.respond(ctx, claim_id, opposing_evidence) -> DebatePosition`**:
520
+ one debate response (fact-checker and devil's advocate).
521
+ - **`ReviewContext`**: document, profile, ledger (read-only for agents), llm,
522
+ search, fetcher, budget, emit_progress, plan, focus, style_guide, and
523
+ `meter` (a `BudgetMeter` built from `budget`). Agents `charge` the meter for
524
+ every LLM/search/fetch call and stop gracefully when it is exhausted.
525
+ - **`ReviewPipeline`**: `(doc, profile, on_progress) -> Awaitable[Report]`,
526
+ implemented by `secondread.pipeline.run_review`.
527
+
528
+ ### Errors (`secondread.contracts.errors`)
529
+
530
+ `SecondReadError` ← `ProviderError(retryable)` ← `AuthError`,
531
+ `RateLimitError`, `SchemaError`; `FetchError` ← `BlockedURLError`;
532
+ `BudgetExceeded`; `DocumentTooLong`; `MissingSetup`. Messages must never
533
+ contain API keys or document text.
534
+
535
+ ### Fakes (`secondread.testing.fakes`)
536
+
537
+ - `FakeLLM(script, default=...)`: replies keyed by tag (exact, then dotted
538
+ prefix, then default); list values are queues; replies may be text, dicts,
539
+ models, exceptions or callables `(messages, schema)`. Records `calls`.
540
+ - `FakeSearch(results, default=..., error=...)`: substring-keyed hits;
541
+ records `queries`.
542
+ - `FakeFetcher(pages)`: URL → page, text or exception; records `fetched`.
543
+ - `FakeAgent(name, findings=..., ledger_updates=..., delay=..., raises=...,
544
+ progress=..., on_run=...)`; `FakeFactChecker` (also `Rechecker` and
545
+ `Debater`); `FakeDevilsAdvocate` (also `Debater`).
546
+ - `ProgressRecorder`, `make_context(...)`.
547
+ - Samples: `sample_documents()` (5 docs), `OPINION_DOC`, `DESIGN_DOC`,
548
+ `REPORT_DOC`, `ESSAY_DOC`, `SHORT_DOC`, `sample_ledger()`,
549
+ `sample_report()`, `empty_report()`, `span_of`, `claim_for`, `FIXED_TIME`.
550
+
551
+ ### Conventions
552
+
553
+ - Tests run offline with fakes; real-network tests use `@pytest.mark.live`
554
+ (deselected by default; run with `uv run pytest -m live`).
555
+ - Checks: `uv run ruff check .`, `uv run ruff format --check .`,
556
+ `uv run mypy`, `uv run pytest`.
557
+ - Prompt tags: `<agent>.<step>`, e.g. `extractor.extract`,
558
+ `factcheck.queries`, `factcheck.judge`.
559
+ - Document text and fetched pages are untrusted data. Put them in clearly
560
+ delimited user-message blocks, never in system prompts, and never follow
561
+ instructions found inside them.
@@ -4,9 +4,9 @@ Evidence-backed document reviewer, exposed as one MCP tool (`review_document`)
4
4
  and a CLI (`secondread review`). Published on PyPI as `second-read`.
5
5
 
6
6
  - `README.md`: what it does, install, settings.
7
- - `DESIGN.md`: the design; source of truth for behavior (agents, research
8
- loops, conflict rules, report format).
9
- - `ARCHITECTURE.md`: the shared types in `secondread/contracts/`.
7
+ - `ARCHITECTURE.md`: source of truth for behavior (agents, research loops,
8
+ conflict rules, report format), the design decisions and trade-offs, the
9
+ planned hosted service, and the shared types in `secondread/contracts/`.
10
10
  - `eval/README.md`: the seeded evaluation set and harness.
11
11
  - `ROADMAP.md`: planned features and improvements, in priority order.
12
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: second-read
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Evidence-backed document review for Claude: fact-checks claims against the web and builds the strongest counter-argument, as an MCP server.
5
5
  Project-URL: Homepage, https://github.com/sbanthia92/second-read
6
6
  Project-URL: Repository, https://github.com/sbanthia92/second-read
@@ -121,6 +121,21 @@ To try it without keys, replace the `env` block with
121
121
  `{"SECONDREAD_FAKE": "1"}`. Demo mode returns placeholder findings and makes
122
122
  no model calls.
123
123
 
124
+ ## Install: other MCP clients
125
+
126
+ Any client that runs local (stdio) MCP servers can use the same command. For
127
+ Cursor, VS Code and similar, add a server named `second-read` that runs
128
+ `uvx second-read` with `ANTHROPIC_API_KEY` and `TAVILY_API_KEY` in its
129
+ environment, as in the Claude Desktop example above.
130
+
131
+ Second Read is listed in the [official MCP Registry](https://registry.modelcontextprotocol.io)
132
+ as `io.github.sbanthia92/second-read`, so clients and directories that read the
133
+ registry can find it.
134
+
135
+ ChatGPT and claude.ai on the web connect only to remote (hosted) MCP servers,
136
+ so they cannot run Second Read yet. A hosted version is on the
137
+ [roadmap](ROADMAP.md).
138
+
124
139
  ## Command line
125
140
 
126
141
  ```sh
@@ -168,16 +183,30 @@ file (the server reads `.env` from its working directory, or the file named by
168
183
  | `SECONDREAD_MAX_WORDS` | Document size cap (default 20000) |
169
184
  | `SECONDREAD_FAKE=1` | Demo mode: no keys, no model calls |
170
185
 
171
- ## What leaves your machine
172
-
173
- - **Your document** goes to Anthropic, under your key and their terms.
174
- - **Search queries** built from your document's claims, and up to eight
175
- sentences quoted exactly for the originality check, go to Tavily.
176
- - **Web pages** named in search results are fetched directly from your machine.
177
- The fetcher refuses private and local network addresses.
178
-
179
- Nothing is stored after the review finishes, and nothing is sent anywhere
180
- else. Keys are read from your environment and are never logged.
186
+ ## Privacy
187
+
188
+ Second Read runs on your machine. The project's author runs no server and
189
+ receives no data.
190
+
191
+ - **What it collects.** Nothing is collected by the project. To do a review the
192
+ tool handles the document you give it and the web pages it retrieves.
193
+ - **What leaves your machine, and where.**
194
+ - Your document goes to Anthropic, under your key and their terms.
195
+ - Search queries built from your document's claims, and up to eight sentences
196
+ quoted exactly for the originality check, go to Tavily, under your key and
197
+ their terms.
198
+ - Web pages named in search results are fetched directly from your machine.
199
+ The fetcher refuses private and local network addresses.
200
+ - **Storage and retention.** Nothing is stored after the review finishes. The
201
+ report is returned to your client and is not kept by Second Read. Logs go to
202
+ your terminal and never contain keys or document text.
203
+ - **Sharing.** Nothing is shared with anyone else. The only third parties are
204
+ the model and search providers above, which you choose by supplying their keys.
205
+ - **Keys.** Read from your environment or a `.env` file you control, and never
206
+ logged.
207
+ - **Contact.** Questions or concerns: open an issue at
208
+ <https://github.com/sbanthia92/second-read/issues>. For security problems see
209
+ [`SECURITY.md`](SECURITY.md).
181
210
 
182
211
  ## How well it works
183
212
 
@@ -221,7 +250,7 @@ An orchestrator runs specialist agents and settles their disagreements:
221
250
  4. Fixed rules settle what is left (for example, a grammar fix on a sentence
222
251
  flagged as wrong is dropped), findings are ranked, and the report is written.
223
252
 
224
- `DESIGN.md` has the full design. `ARCHITECTURE.md` describes the shared types.
253
+ `ARCHITECTURE.md` describes how the system is built, the decisions behind it and their trade-offs, the planned hosted service, and the shared types.
225
254
 
226
255
  ## Development
227
256
 
@@ -238,7 +267,7 @@ against real providers spends money on your keys.
238
267
  ## Status
239
268
 
240
269
  The local MCP server and command-line tool work. A hosted version (sign in,
241
- add a key once, get the report by email) is designed in `DESIGN.md` but not
270
+ add a key once, get the report by email) is designed in `ARCHITECTURE.md` but not
242
271
  built. `ROADMAP.md` lists what comes next.
243
272
 
244
273
  ## License