second-read 0.1.1__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 (307) hide show
  1. second_read-0.1.1/.env.example +5 -0
  2. second_read-0.1.1/.github/workflows/ci.yml +26 -0
  3. second_read-0.1.1/.github/workflows/publish.yml +41 -0
  4. second_read-0.1.1/.gitignore +25 -0
  5. second_read-0.1.1/.python-version +1 -0
  6. second_read-0.1.1/ARCHITECTURE.md +150 -0
  7. second_read-0.1.1/DESIGN.md +342 -0
  8. second_read-0.1.1/LICENSE +21 -0
  9. second_read-0.1.1/PKG-INFO +243 -0
  10. second_read-0.1.1/README.md +216 -0
  11. second_read-0.1.1/eval/README.md +686 -0
  12. second_read-0.1.1/eval/__init__.py +13 -0
  13. second_read-0.1.1/eval/__main__.py +5 -0
  14. second_read-0.1.1/eval/baseline.py +264 -0
  15. second_read-0.1.1/eval/cli.py +200 -0
  16. second_read-0.1.1/eval/data/01-leicester-money.json +58 -0
  17. second_read-0.1.1/eval/data/01-leicester-money.md +11 -0
  18. second_read-0.1.1/eval/data/02-possession.json +51 -0
  19. second_read-0.1.1/eval/data/02-possession.md +11 -0
  20. second_read-0.1.1/eval/data/03-invincibles.json +51 -0
  21. second_read-0.1.1/eval/data/03-invincibles.md +11 -0
  22. second_read-0.1.1/eval/data/04-var.json +57 -0
  23. second_read-0.1.1/eval/data/04-var.md +11 -0
  24. second_read-0.1.1/eval/data/05-four-day-week.json +51 -0
  25. second_read-0.1.1/eval/data/05-four-day-week.md +11 -0
  26. second_read-0.1.1/eval/data/06-sqlite-production.json +51 -0
  27. second_read-0.1.1/eval/data/06-sqlite-production.md +11 -0
  28. second_read-0.1.1/eval/data/07-postgres-queue.json +51 -0
  29. second_read-0.1.1/eval/data/07-postgres-queue.md +19 -0
  30. second_read-0.1.1/eval/data/08-uuid-keys.json +57 -0
  31. second_read-0.1.1/eval/data/08-uuid-keys.md +17 -0
  32. second_read-0.1.1/eval/data/09-cdn-cache.json +51 -0
  33. second_read-0.1.1/eval/data/09-cdn-cache.md +17 -0
  34. second_read-0.1.1/eval/data/10-best-league.json +57 -0
  35. second_read-0.1.1/eval/data/10-best-league.md +11 -0
  36. second_read-0.1.1/eval/data/11-greece-2004.json +77 -0
  37. second_read-0.1.1/eval/data/11-greece-2004.md +17 -0
  38. second_read-0.1.1/eval/data/12-istanbul-momentum.json +71 -0
  39. second_read-0.1.1/eval/data/12-istanbul-momentum.md +17 -0
  40. second_read-0.1.1/eval/data/13-five-substitutes.json +72 -0
  41. second_read-0.1.1/eval/data/13-five-substitutes.md +15 -0
  42. second_read-0.1.1/eval/data/14-brazil-1970.json +77 -0
  43. second_read-0.1.1/eval/data/14-brazil-1970.md +19 -0
  44. second_read-0.1.1/eval/data/15-penalty-lottery.json +78 -0
  45. second_read-0.1.1/eval/data/15-penalty-lottery.md +15 -0
  46. second_read-0.1.1/eval/data/16-womens-game-stadiums.json +72 -0
  47. second_read-0.1.1/eval/data/16-womens-game-stadiums.md +15 -0
  48. second_read-0.1.1/eval/data/17-no-kubernetes.json +77 -0
  49. second_read-0.1.1/eval/data/17-no-kubernetes.md +29 -0
  50. second_read-0.1.1/eval/data/18-python-type-hints.json +72 -0
  51. second_read-0.1.1/eval/data/18-python-type-hints.md +13 -0
  52. second_read-0.1.1/eval/data/19-big-rewrite.json +90 -0
  53. second_read-0.1.1/eval/data/19-big-rewrite.md +33 -0
  54. second_read-0.1.1/eval/data/20-nuclear-climate.json +89 -0
  55. second_read-0.1.1/eval/data/20-nuclear-climate.md +19 -0
  56. second_read-0.1.1/eval/data/21-phones-in-schools.json +71 -0
  57. second_read-0.1.1/eval/data/21-phones-in-schools.md +17 -0
  58. second_read-0.1.1/eval/data/22-clock-changes.json +72 -0
  59. second_read-0.1.1/eval/data/22-clock-changes.md +13 -0
  60. second_read-0.1.1/eval/data/23-close-the-libraries.json +84 -0
  61. second_read-0.1.1/eval/data/23-close-the-libraries.md +17 -0
  62. second_read-0.1.1/eval/data/24-robots-not-astronauts.json +96 -0
  63. second_read-0.1.1/eval/data/24-robots-not-astronauts.md +37 -0
  64. second_read-0.1.1/eval/data/25-jwt-sessions.json +78 -0
  65. second_read-0.1.1/eval/data/25-jwt-sessions.md +33 -0
  66. second_read-0.1.1/eval/data/26-kafka-stream.json +85 -0
  67. second_read-0.1.1/eval/data/26-kafka-stream.md +40 -0
  68. second_read-0.1.1/eval/data/27-rate-limiter.json +78 -0
  69. second_read-0.1.1/eval/data/27-rate-limiter.md +39 -0
  70. second_read-0.1.1/eval/data/28-audit-log-archive.json +78 -0
  71. second_read-0.1.1/eval/data/28-audit-log-archive.md +38 -0
  72. second_read-0.1.1/eval/data/29-raft-config-store.json +91 -0
  73. second_read-0.1.1/eval/data/29-raft-config-store.md +55 -0
  74. second_read-0.1.1/eval/data/30-tls13-internal.json +78 -0
  75. second_read-0.1.1/eval/data/30-tls13-internal.md +31 -0
  76. second_read-0.1.1/eval/data/31-womens-champions-league.json +114 -0
  77. second_read-0.1.1/eval/data/31-womens-champions-league.md +15 -0
  78. second_read-0.1.1/eval/data/32-national-league-third-place.json +120 -0
  79. second_read-0.1.1/eval/data/32-national-league-third-place.md +17 -0
  80. second_read-0.1.1/eval/data/33-mjallby-model.json +121 -0
  81. second_read-0.1.1/eval/data/33-mjallby-model.md +17 -0
  82. second_read-0.1.1/eval/data/34-congestion-charge.json +115 -0
  83. second_read-0.1.1/eval/data/34-congestion-charge.md +19 -0
  84. second_read-0.1.1/eval/data/35-postgres-18-upgrade.json +122 -0
  85. second_read-0.1.1/eval/data/35-postgres-18-upgrade.md +19 -0
  86. second_read-0.1.1/eval/data/36-wsl-fourteen-teams.json +114 -0
  87. second_read-0.1.1/eval/data/36-wsl-fourteen-teams.md +17 -0
  88. second_read-0.1.1/eval/data/37-metlife-final.json +121 -0
  89. second_read-0.1.1/eval/data/37-metlife-final.md +17 -0
  90. second_read-0.1.1/eval/data/38-votes-at-sixteen.json +114 -0
  91. second_read-0.1.1/eval/data/38-votes-at-sixteen.md +15 -0
  92. second_read-0.1.1/eval/data/39-logical-replication-reporting.json +120 -0
  93. second_read-0.1.1/eval/data/39-logical-replication-reporting.md +53 -0
  94. second_read-0.1.1/eval/data/40-hpa-ingest-workers.json +114 -0
  95. second_read-0.1.1/eval/data/40-hpa-ingest-workers.md +53 -0
  96. second_read-0.1.1/eval/dataset.py +273 -0
  97. second_read-0.1.1/eval/fake_pipeline.py +481 -0
  98. second_read-0.1.1/eval/harness.py +171 -0
  99. second_read-0.1.1/eval/items.py +181 -0
  100. second_read-0.1.1/eval/judge.py +198 -0
  101. second_read-0.1.1/eval/live.py +63 -0
  102. second_read-0.1.1/eval/metrics.py +443 -0
  103. second_read-0.1.1/eval/results.py +215 -0
  104. second_read-0.1.1/eval/textspan.py +67 -0
  105. second_read-0.1.1/pyproject.toml +85 -0
  106. second_read-0.1.1/scripts/__init__.py +0 -0
  107. second_read-0.1.1/scripts/compare_search.py +236 -0
  108. second_read-0.1.1/secondread/__init__.py +0 -0
  109. second_read-0.1.1/secondread/agents/__init__.py +0 -0
  110. second_read-0.1.1/secondread/agents/copyedit/__init__.py +10 -0
  111. second_read-0.1.1/secondread/agents/copyedit/agent.py +261 -0
  112. second_read-0.1.1/secondread/agents/copyedit/prompts.py +123 -0
  113. second_read-0.1.1/secondread/agents/copyedit/text.py +274 -0
  114. second_read-0.1.1/secondread/agents/devils_advocate/__init__.py +18 -0
  115. second_read-0.1.1/secondread/agents/devils_advocate/agent.py +681 -0
  116. second_read-0.1.1/secondread/agents/devils_advocate/prompts.py +263 -0
  117. second_read-0.1.1/secondread/agents/devils_advocate/schemas.py +86 -0
  118. second_read-0.1.1/secondread/agents/devils_advocate/text.py +123 -0
  119. second_read-0.1.1/secondread/agents/extractor/__init__.py +20 -0
  120. second_read-0.1.1/secondread/agents/extractor/agent.py +330 -0
  121. second_read-0.1.1/secondread/agents/extractor/chunks.py +53 -0
  122. second_read-0.1.1/secondread/agents/extractor/prompts.py +121 -0
  123. second_read-0.1.1/secondread/agents/extractor/spans.py +177 -0
  124. second_read-0.1.1/secondread/agents/factcheck/__init__.py +12 -0
  125. second_read-0.1.1/secondread/agents/factcheck/agent.py +267 -0
  126. second_read-0.1.1/secondread/agents/factcheck/prompts.py +264 -0
  127. second_read-0.1.1/secondread/agents/factcheck/research.py +737 -0
  128. second_read-0.1.1/secondread/agents/factcheck/schemas.py +124 -0
  129. second_read-0.1.1/secondread/agents/factcheck/sources.py +295 -0
  130. second_read-0.1.1/secondread/agents/originality/__init__.py +29 -0
  131. second_read-0.1.1/secondread/agents/originality/agent.py +277 -0
  132. second_read-0.1.1/secondread/agents/originality/sentences.py +311 -0
  133. second_read-0.1.1/secondread/agents/originality/similarity.py +114 -0
  134. second_read-0.1.1/secondread/agents/structure/__init__.py +25 -0
  135. second_read-0.1.1/secondread/agents/structure/agent.py +301 -0
  136. second_read-0.1.1/secondread/agents/structure/prompts.py +179 -0
  137. second_read-0.1.1/secondread/cli/__init__.py +5 -0
  138. second_read-0.1.1/secondread/cli/__main__.py +6 -0
  139. second_read-0.1.1/secondread/cli/envfile.py +80 -0
  140. second_read-0.1.1/secondread/cli/errors.py +114 -0
  141. second_read-0.1.1/secondread/cli/main.py +327 -0
  142. second_read-0.1.1/secondread/contracts/__init__.py +134 -0
  143. second_read-0.1.1/secondread/contracts/errors.py +60 -0
  144. second_read-0.1.1/secondread/contracts/interfaces.py +314 -0
  145. second_read-0.1.1/secondread/contracts/models.py +689 -0
  146. second_read-0.1.1/secondread/mcp_local/README.md +96 -0
  147. second_read-0.1.1/secondread/mcp_local/__init__.py +35 -0
  148. second_read-0.1.1/secondread/mcp_local/__main__.py +6 -0
  149. second_read-0.1.1/secondread/mcp_local/demo.py +111 -0
  150. second_read-0.1.1/secondread/mcp_local/errors.py +107 -0
  151. second_read-0.1.1/secondread/mcp_local/main.py +80 -0
  152. second_read-0.1.1/secondread/mcp_local/progress.py +86 -0
  153. second_read-0.1.1/secondread/mcp_local/server.py +187 -0
  154. second_read-0.1.1/secondread/mcp_local/wiring.py +251 -0
  155. second_read-0.1.1/secondread/orchestrator/__init__.py +54 -0
  156. second_read-0.1.1/secondread/orchestrator/assemble.py +148 -0
  157. second_read-0.1.1/secondread/orchestrator/authwatch.py +83 -0
  158. second_read-0.1.1/secondread/orchestrator/classify.py +75 -0
  159. second_read-0.1.1/secondread/orchestrator/conflicts.py +406 -0
  160. second_read-0.1.1/secondread/orchestrator/labels.py +39 -0
  161. second_read-0.1.1/secondread/orchestrator/orchestrator.py +601 -0
  162. second_read-0.1.1/secondread/orchestrator/planning.py +221 -0
  163. second_read-0.1.1/secondread/orchestrator/progress.py +114 -0
  164. second_read-0.1.1/secondread/orchestrator/prompts.py +132 -0
  165. second_read-0.1.1/secondread/orchestrator/reactions.py +153 -0
  166. second_read-0.1.1/secondread/orchestrator/registry.py +56 -0
  167. second_read-0.1.1/secondread/orchestrator/schemas.py +45 -0
  168. second_read-0.1.1/secondread/pipeline.py +73 -0
  169. second_read-0.1.1/secondread/providers/__init__.py +0 -0
  170. second_read-0.1.1/secondread/providers/fetch/__init__.py +29 -0
  171. second_read-0.1.1/secondread/providers/fetch/extract.py +153 -0
  172. second_read-0.1.1/secondread/providers/fetch/fetcher.py +241 -0
  173. second_read-0.1.1/secondread/providers/fetch/guarded.py +110 -0
  174. second_read-0.1.1/secondread/providers/fetch/ssrf.py +207 -0
  175. second_read-0.1.1/secondread/providers/llm/__init__.py +49 -0
  176. second_read-0.1.1/secondread/providers/llm/_common.py +259 -0
  177. second_read-0.1.1/secondread/providers/llm/anthropic_client.py +332 -0
  178. second_read-0.1.1/secondread/providers/llm/config.py +93 -0
  179. second_read-0.1.1/secondread/providers/llm/openai_client.py +229 -0
  180. second_read-0.1.1/secondread/providers/search/__init__.py +45 -0
  181. second_read-0.1.1/secondread/providers/search/base.py +218 -0
  182. second_read-0.1.1/secondread/providers/search/brave.py +55 -0
  183. second_read-0.1.1/secondread/providers/search/config.py +63 -0
  184. second_read-0.1.1/secondread/providers/search/exa.py +62 -0
  185. second_read-0.1.1/secondread/providers/search/retry.py +60 -0
  186. second_read-0.1.1/secondread/providers/search/tavily.py +60 -0
  187. second_read-0.1.1/secondread/registry.py +270 -0
  188. second_read-0.1.1/secondread/report/__init__.py +30 -0
  189. second_read-0.1.1/secondread/report/email.py +43 -0
  190. second_read-0.1.1/secondread/report/html.py +49 -0
  191. second_read-0.1.1/secondread/report/markdown.py +195 -0
  192. second_read-0.1.1/secondread/report/templates/email.html.j2 +139 -0
  193. second_read-0.1.1/secondread/report/text.py +158 -0
  194. second_read-0.1.1/secondread/report/view.py +408 -0
  195. second_read-0.1.1/secondread/testing/__init__.py +1 -0
  196. second_read-0.1.1/secondread/testing/fakes.py +458 -0
  197. second_read-0.1.1/secondread/testing/samples.py +348 -0
  198. second_read-0.1.1/tests/__init__.py +0 -0
  199. second_read-0.1.1/tests/agents/__init__.py +0 -0
  200. second_read-0.1.1/tests/agents/copyedit/__init__.py +0 -0
  201. second_read-0.1.1/tests/agents/copyedit/test_agent.py +343 -0
  202. second_read-0.1.1/tests/agents/copyedit/test_text.py +91 -0
  203. second_read-0.1.1/tests/agents/devils_advocate/__init__.py +0 -0
  204. second_read-0.1.1/tests/agents/devils_advocate/test_agent.py +850 -0
  205. second_read-0.1.1/tests/agents/devils_advocate/test_text.py +66 -0
  206. second_read-0.1.1/tests/agents/extractor/__init__.py +0 -0
  207. second_read-0.1.1/tests/agents/extractor/test_agent.py +513 -0
  208. second_read-0.1.1/tests/agents/extractor/test_spans.py +149 -0
  209. second_read-0.1.1/tests/agents/extractor/test_standalone.py +33 -0
  210. second_read-0.1.1/tests/agents/factcheck/__init__.py +0 -0
  211. second_read-0.1.1/tests/agents/factcheck/test_agent.py +795 -0
  212. second_read-0.1.1/tests/agents/factcheck/test_precision.py +458 -0
  213. second_read-0.1.1/tests/agents/factcheck/test_sources.py +131 -0
  214. second_read-0.1.1/tests/agents/factcheck/test_standalone.py +31 -0
  215. second_read-0.1.1/tests/agents/originality/__init__.py +0 -0
  216. second_read-0.1.1/tests/agents/originality/test_agent.py +391 -0
  217. second_read-0.1.1/tests/agents/originality/test_sentences.py +135 -0
  218. second_read-0.1.1/tests/agents/originality/test_similarity.py +56 -0
  219. second_read-0.1.1/tests/agents/structure/__init__.py +0 -0
  220. second_read-0.1.1/tests/agents/structure/test_agent.py +334 -0
  221. second_read-0.1.1/tests/cli/__init__.py +0 -0
  222. second_read-0.1.1/tests/cli/test_cli.py +457 -0
  223. second_read-0.1.1/tests/cli/test_envfile.py +40 -0
  224. second_read-0.1.1/tests/cli/test_live.py +45 -0
  225. second_read-0.1.1/tests/cli/test_registry.py +284 -0
  226. second_read-0.1.1/tests/conftest.py +5 -0
  227. second_read-0.1.1/tests/contracts/__init__.py +0 -0
  228. second_read-0.1.1/tests/contracts/test_models.py +230 -0
  229. second_read-0.1.1/tests/eval/__init__.py +0 -0
  230. second_read-0.1.1/tests/eval/helpers.py +121 -0
  231. second_read-0.1.1/tests/eval/test_baseline.py +102 -0
  232. second_read-0.1.1/tests/eval/test_dataset.py +286 -0
  233. second_read-0.1.1/tests/eval/test_harness.py +534 -0
  234. second_read-0.1.1/tests/eval/test_judge_and_spans.py +90 -0
  235. second_read-0.1.1/tests/eval/test_live.py +34 -0
  236. second_read-0.1.1/tests/eval/test_metrics.py +281 -0
  237. second_read-0.1.1/tests/mcp_local/__init__.py +0 -0
  238. second_read-0.1.1/tests/mcp_local/helpers.py +83 -0
  239. second_read-0.1.1/tests/mcp_local/test_env_file.py +22 -0
  240. second_read-0.1.1/tests/mcp_local/test_progress.py +52 -0
  241. second_read-0.1.1/tests/mcp_local/test_server.py +346 -0
  242. second_read-0.1.1/tests/mcp_local/test_stdio.py +59 -0
  243. second_read-0.1.1/tests/mcp_local/test_wiring.py +295 -0
  244. second_read-0.1.1/tests/orchestrator/__init__.py +0 -0
  245. second_read-0.1.1/tests/orchestrator/builders.py +108 -0
  246. second_read-0.1.1/tests/orchestrator/test_authwatch.py +100 -0
  247. second_read-0.1.1/tests/orchestrator/test_conflicts.py +517 -0
  248. second_read-0.1.1/tests/orchestrator/test_pipeline.py +684 -0
  249. second_read-0.1.1/tests/orchestrator/test_planning.py +234 -0
  250. second_read-0.1.1/tests/orchestrator/test_progress_classify.py +88 -0
  251. second_read-0.1.1/tests/providers/__init__.py +0 -0
  252. second_read-0.1.1/tests/providers/fetch/__init__.py +0 -0
  253. second_read-0.1.1/tests/providers/fetch/test_extract.py +93 -0
  254. second_read-0.1.1/tests/providers/fetch/test_fetcher.py +349 -0
  255. second_read-0.1.1/tests/providers/fetch/test_guarded.py +104 -0
  256. second_read-0.1.1/tests/providers/fetch/test_live.py +21 -0
  257. second_read-0.1.1/tests/providers/fetch/test_ssrf.py +220 -0
  258. second_read-0.1.1/tests/providers/llm/__init__.py +0 -0
  259. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_auth_error.json +13 -0
  260. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_bad_request.json +13 -0
  261. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_low_credit.json +13 -0
  262. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_overloaded.json +13 -0
  263. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_rate_limit.json +14 -0
  264. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_refusal.json +21 -0
  265. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_text.json +26 -0
  266. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_tool_use.json +34 -0
  267. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_tool_use_invalid.json +31 -0
  268. second_read-0.1.1/tests/providers/llm/fixtures/anthropic_truncated.json +26 -0
  269. second_read-0.1.1/tests/providers/llm/fixtures/openai_auth_error.json +14 -0
  270. second_read-0.1.1/tests/providers/llm/fixtures/openai_insufficient_quota.json +14 -0
  271. second_read-0.1.1/tests/providers/llm/fixtures/openai_json_invalid.json +29 -0
  272. second_read-0.1.1/tests/providers/llm/fixtures/openai_json_schema.json +29 -0
  273. second_read-0.1.1/tests/providers/llm/fixtures/openai_rate_limit.json +15 -0
  274. second_read-0.1.1/tests/providers/llm/fixtures/openai_refusal.json +29 -0
  275. second_read-0.1.1/tests/providers/llm/fixtures/openai_server_error.json +14 -0
  276. second_read-0.1.1/tests/providers/llm/fixtures/openai_text.json +29 -0
  277. second_read-0.1.1/tests/providers/llm/fixtures/openai_truncated.json +29 -0
  278. second_read-0.1.1/tests/providers/llm/replay.py +70 -0
  279. second_read-0.1.1/tests/providers/llm/schemas.py +33 -0
  280. second_read-0.1.1/tests/providers/llm/test_anthropic.py +239 -0
  281. second_read-0.1.1/tests/providers/llm/test_common.py +146 -0
  282. second_read-0.1.1/tests/providers/llm/test_live.py +80 -0
  283. second_read-0.1.1/tests/providers/llm/test_openai.py +174 -0
  284. second_read-0.1.1/tests/providers/search/__init__.py +0 -0
  285. second_read-0.1.1/tests/providers/search/test_adapters.py +269 -0
  286. second_read-0.1.1/tests/providers/search/test_config.py +49 -0
  287. second_read-0.1.1/tests/providers/search/test_live.py +22 -0
  288. second_read-0.1.1/tests/report/__init__.py +0 -0
  289. second_read-0.1.1/tests/report/snapshots/empty.html +41 -0
  290. second_read-0.1.1/tests/report/snapshots/empty.md +38 -0
  291. second_read-0.1.1/tests/report/snapshots/empty.txt +44 -0
  292. second_read-0.1.1/tests/report/snapshots/sample_with_document.html +132 -0
  293. second_read-0.1.1/tests/report/snapshots/sample_with_document.md +114 -0
  294. second_read-0.1.1/tests/report/snapshots/sample_with_document.txt +97 -0
  295. second_read-0.1.1/tests/report/snapshots/sample_with_notes.html +137 -0
  296. second_read-0.1.1/tests/report/snapshots/sample_with_notes.md +119 -0
  297. second_read-0.1.1/tests/report/snapshots/sample_with_notes.txt +100 -0
  298. second_read-0.1.1/tests/report/snapshots/sample_without_document.html +130 -0
  299. second_read-0.1.1/tests/report/snapshots/sample_without_document.md +110 -0
  300. second_read-0.1.1/tests/report/snapshots/sample_without_document.txt +95 -0
  301. second_read-0.1.1/tests/report/test_render.py +383 -0
  302. second_read-0.1.1/tests/report/test_snapshots.py +86 -0
  303. second_read-0.1.1/tests/scripts/__init__.py +0 -0
  304. second_read-0.1.1/tests/scripts/test_compare_search.py +148 -0
  305. second_read-0.1.1/tests/testing/__init__.py +0 -0
  306. second_read-0.1.1/tests/testing/test_fakes.py +138 -0
  307. second_read-0.1.1/uv.lock +875 -0
@@ -0,0 +1,5 @@
1
+ # Copy to .env and fill in. Never commit .env.
2
+ # Anthropic model key.
3
+ ANTHROPIC_API_KEY=
4
+ # Tavily web search key.
5
+ TAVILY_API_KEY=
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ check:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v6
13
+ with:
14
+ enable-cache: true
15
+ - name: Install Python
16
+ run: uv python install
17
+ - name: Install
18
+ run: uv sync --locked --managed-python
19
+ - name: Lint
20
+ run: |
21
+ uv run ruff check .
22
+ uv run ruff format --check .
23
+ - name: Type-check
24
+ run: uv run mypy
25
+ - name: Offline tests
26
+ run: uv run pytest
@@ -0,0 +1,41 @@
1
+ name: Publish to PyPI
2
+
3
+ # Runs when a GitHub release is published (or by hand). Uses PyPI trusted
4
+ # publishing: no API token is stored anywhere. PyPI must list this repository,
5
+ # this workflow file and the "pypi" environment as a trusted publisher.
6
+ on:
7
+ release:
8
+ types: [published]
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v6
17
+ - name: Install Python
18
+ run: uv python install
19
+ - name: Test
20
+ run: |
21
+ uv sync --locked --managed-python
22
+ uv run pytest
23
+ - name: Build
24
+ run: uv build
25
+ - uses: actions/upload-artifact@v4
26
+ with:
27
+ name: dist
28
+ path: dist/
29
+
30
+ publish:
31
+ needs: build
32
+ runs-on: ubuntu-latest
33
+ environment: pypi
34
+ permissions:
35
+ id-token: write
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,25 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+ .env
8
+ .env.*
9
+ !.env.example
10
+ .DS_Store
11
+ dist/
12
+ build/
13
+ *.egg-info/
14
+ eval/results/
15
+ .claude/worktrees/
16
+
17
+ # Local build-process notes and Claude Code project config (kept on disk, not published)
18
+ CLAUDE.md
19
+ TASKS.md
20
+ CONTRACT_CHANGES.md
21
+ .claude/
22
+
23
+ # Local review outputs
24
+ report.md
25
+ flag-report.md
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,150 @@
1
+ # Architecture: shared contracts
2
+
3
+ The shared types every part of Second Read codes against. `DESIGN.md` is the
4
+ source of truth for behavior; this file explains the types.
5
+
6
+ ## Layout
7
+
8
+ | Path | What |
9
+ | --- | --- |
10
+ | `secondread/contracts/models.py` | Pydantic v2 data models |
11
+ | `secondread/contracts/interfaces.py` | Protocols, `ReviewContext`, `BudgetMeter` |
12
+ | `secondread/contracts/errors.py` | Exception hierarchy |
13
+ | `secondread/testing/fakes.py` | Fakes for every protocol, `make_context` |
14
+ | `secondread/testing/samples.py` | Sample documents, ledger, reports |
15
+ | `secondread/providers/{llm,search,fetch}/` | Provider adapters |
16
+ | `secondread/agents/<name>/` | One package per agent |
17
+ | `secondread/report/` | Markdown and HTML rendering |
18
+ | `eval/` | Eval harness and dataset |
19
+ | `secondread/orchestrator/`, `secondread/pipeline.py` | Planning, conflict rules, `run_review` |
20
+ | `secondread/cli/`, `secondread/registry.py` | Real wiring and CLI |
21
+ | `secondread/mcp_local/` | stdio MCP server |
22
+
23
+ Tests mirror the package path under `tests/` (e.g. `tests/agents/extractor/`).
24
+
25
+ ## Data flow
26
+
27
+ ```
28
+ Document ──► extractor ──► ClaimLedger ──► plan ──► agents in parallel
29
+ ▲ │ AgentResult
30
+ └── orchestrator applies ledger_updates + findings
31
+ │ reaction round (recheck, debate)
32
+ │ conflict rules, ranking
33
+ ▼
34
+ Report ──► markdown / HTML
35
+ ```
36
+
37
+ ## Models (`secondread.contracts`)
38
+
39
+ - **`Document`**: `id`, `text`, `source_url`, `word_count`. Build with
40
+ `Document.from_text(text)`.
41
+ - **`Span`**: half-open `[start, end)` character offsets into `Document.text`.
42
+ `overlaps`, `contains`, `text_of`. Empty spans overlap nothing.
43
+ - **`Claim`**: `id`, `text`, `span`, `type` (`ClaimType`: thesis, supporting,
44
+ factual), `importance` 0–1. `text` must equal `span.text_of(doc.text)`.
45
+ `standalone` restates the claim with pronouns and references resolved, so
46
+ it can be researched out of context; `checkable_text` is `standalone or
47
+ text`.
48
+ - **`Evidence`**: `url`, `title`, `excerpt`, `retrieved_at`, `is_primary`.
49
+ - **`Verdict`**: verified, wrong, unsupported, unchecked. Verified is rejected
50
+ without evidence.
51
+ - **`Rebuttal`**: `target_claim_ids` (at least one), `argument`, `evidence`,
52
+ `strength` 1–5, `target` (`RebuttalTarget`: interpretation or fact; used by
53
+ the "rebuttal vs verified claim" rule). No evidence → downgraded to
54
+ `Severity.CONSIDER` by the orchestrator.
55
+ - **`Severity`**: `IntEnum`, higher ranks first: factual_error (5) >
56
+ unsupported (4) > strong_rebuttal (3) > structure (2) > style (1) >
57
+ consider (0). `Severity.label` gives the snake_case name.
58
+ - **`Finding`**: `agent`, `severity`, optional `span`, `message`, `evidence`,
59
+ `suggestion`, `claim_ids`, `heuristic` (always True for originality),
60
+ `merged_from` (set when the orchestrator merges same-span findings).
61
+ - **`AgentName`**: stable agent names (`extractor`, `factcheck`,
62
+ `devils_advocate`, `copyedit`, `structure`, `originality`, `orchestrator`).
63
+ - **`ResearchStep`**: one step of a bounded research loop (`iteration` ≤ 3).
64
+ - **`DebateRecord`**: positions, ruling, reasoning, final verdict.
65
+ - **`LedgerEntry`**: claim, verdict, verdict evidence/confidence/note,
66
+ rebuttals, copy-edit finding IDs, research trail, `rechecked`, `debate`.
67
+ - **`ClaimLedger`**: `entries` (by claim ID) and `findings` (by finding ID).
68
+ Helpers: `add_claim`, `set_verdict`, `add_rebuttal`, `add_research_step`,
69
+ `add_finding`, `remove_finding`, `claims(*types)`, `thesis()`,
70
+ `claims_overlapping(span)`, `findings_overlapping(span)`, `apply(update)`,
71
+ `apply_result(result)`. Copy-edit findings are linked to overlapping claims
72
+ automatically.
73
+ - **`LedgerUpdate`**: discriminated union `AddClaim | SetVerdict | AddRebuttal
74
+ | AddResearchStep`. **Agents never mutate the ledger**; they return updates
75
+ and the orchestrator applies them, so parallel agents cannot race.
76
+ - **`Profile`**: auto, opinion, design_doc. `PROFILE_AGENTS` lists the agents
77
+ each concrete profile may run.
78
+ - **`Budget`**: job caps (`max_tokens`, `max_search_calls`, `max_seconds`).
79
+ **`ClaimBudget`**: per-claim caps (`max_iterations` ≤ 3).
80
+ **`ExecutionPlan`**: agents to run, deep-research claim IDs, per-claim
81
+ budgets (`budget_for(claim_id)`).
82
+ - **`Usage`**: tokens, search/fetch/LLM calls, seconds; supports `+`.
83
+ - **`AgentResult`**: `agent`, `findings`, `ledger_updates`, `usage`, `error`
84
+ (set when the agent degraded instead of failing the job), `notes`
85
+ (non-fatal notices, no document text).
86
+ - **`ProgressEvent`**: `step` (usually a `ProgressStep`), `message`,
87
+ `percent` 0–100.
88
+ - **`Report`**: verdict line, `counts` (by severity label), must_fix,
89
+ unverified (the fact-checker's "could not confirm" findings: agent
90
+ `factcheck`, severity `CONSIDER`),
91
+ counter_case (rebuttals), should_fix, polish, originality, ledger, usage,
92
+ notes (degradation notices), generated_at.
93
+
94
+ ## Protocols (`secondread.contracts.interfaces`)
95
+
96
+ - **`LLMClient.complete(messages, *, schema=None, model_tier, tag="",
97
+ max_tokens=None, temperature=None) -> LLMResponse`**. `ModelTier`: cheap,
98
+ mid, strong. With `schema` (a Pydantic model class) the response's `parsed`
99
+ is a validated instance. `tag` names the prompt (e.g. `"factcheck.judge"`);
100
+ fakes key replies on it and it is never sent to the provider.
101
+ - **`SearchClient.search(query, *, k=5) -> list[SearchResult]`**.
102
+ - **`Fetcher.fetch(url) -> FetchedPage`**; raises `FetchError` or
103
+ `BlockedURLError`.
104
+ - **`Agent`**: `name` and `async run(ctx) -> AgentResult`. Must not raise for
105
+ expected failures.
106
+ - **`Rechecker.recheck(ctx, claim_id, counter_evidence)`**: fact-checker's one
107
+ re-check per claim.
108
+ - **`Debater.respond(ctx, claim_id, opposing_evidence) -> DebatePosition`**:
109
+ one debate response (fact-checker and devil's advocate).
110
+ - **`ReviewContext`**: document, profile, ledger (read-only for agents), llm,
111
+ search, fetcher, budget, emit_progress, plan, focus, style_guide, and
112
+ `meter` (a `BudgetMeter` built from `budget`). Agents `charge` the meter for
113
+ every LLM/search/fetch call and stop gracefully when it is exhausted.
114
+ - **`ReviewPipeline`**: `(doc, profile, on_progress) -> Awaitable[Report]`,
115
+ implemented by `secondread.pipeline.run_review`.
116
+
117
+ ## Errors (`secondread.contracts.errors`)
118
+
119
+ `SecondReadError` ← `ProviderError(retryable)` ← `AuthError`,
120
+ `RateLimitError`, `SchemaError`; `FetchError` ← `BlockedURLError`;
121
+ `BudgetExceeded`; `DocumentTooLong`; `MissingSetup`. Messages must never
122
+ contain API keys or document text.
123
+
124
+ ## Fakes (`secondread.testing.fakes`)
125
+
126
+ - `FakeLLM(script, default=...)`: replies keyed by tag (exact, then dotted
127
+ prefix, then default); list values are queues; replies may be text, dicts,
128
+ models, exceptions or callables `(messages, schema)`. Records `calls`.
129
+ - `FakeSearch(results, default=..., error=...)`: substring-keyed hits;
130
+ records `queries`.
131
+ - `FakeFetcher(pages)`: URL → page, text or exception; records `fetched`.
132
+ - `FakeAgent(name, findings=..., ledger_updates=..., delay=..., raises=...,
133
+ progress=..., on_run=...)`; `FakeFactChecker` (also `Rechecker` and
134
+ `Debater`); `FakeDevilsAdvocate` (also `Debater`).
135
+ - `ProgressRecorder`, `make_context(...)`.
136
+ - Samples: `sample_documents()` (5 docs), `OPINION_DOC`, `DESIGN_DOC`,
137
+ `REPORT_DOC`, `ESSAY_DOC`, `SHORT_DOC`, `sample_ledger()`,
138
+ `sample_report()`, `empty_report()`, `span_of`, `claim_for`, `FIXED_TIME`.
139
+
140
+ ## Conventions
141
+
142
+ - Tests run offline with fakes; real-network tests use `@pytest.mark.live`
143
+ (deselected by default; run with `uv run pytest -m live`).
144
+ - Checks: `uv run ruff check .`, `uv run ruff format --check .`,
145
+ `uv run mypy`, `uv run pytest`.
146
+ - Prompt tags: `<agent>.<step>`, e.g. `extractor.extract`,
147
+ `factcheck.queries`, `factcheck.judge`.
148
+ - Document text and fetched pages are untrusted data. Put them in clearly
149
+ delimited user-message blocks, never in system prompts, and never follow
150
+ instructions found inside them.
@@ -0,0 +1,342 @@
1
+ # Second Read — Design Doc
2
+
3
+ Exported 2026-09-30 from the Claude Docs design doc. The doc is the source of truth; update this file if the doc changes.
4
+
5
+ ## Overview
6
+
7
+ Second Read is a hosted, multi-agent document reviewer exposed as a single MCP tool, `review_document`. A user connects it to Claude, ChatGPT or Cursor, signs in with Google once, adds their own model API key, and asks for a review of any draft or URL. An orchestrator dispatches specialist agents in parallel, resolves their disagreements, and emails one ranked report.
8
+
9
+ **Problem.** Existing AI critique tools argue from the model's own knowledge. They fix grammar or generate generic objections, but they do not check facts against sources or build an opposing case from real evidence.
10
+
11
+ **Differentiator.** An evidence-backed review: every claim is extracted and verified with citations, and a devil's advocate builds the strongest counter-argument from retrieved sources.
12
+
13
+ **Goals**
14
+
15
+ - Review any prose document (opinion piece, technical design doc, report) through one MCP tool.
16
+ - Ground fact-checks and rebuttals in cited sources, not model memory.
17
+ - Keep all orchestration server-side and hidden behind `review_document`.
18
+ - Handle long reviews: async with email delivery, plus live progress where the client supports it.
19
+ - Retain nothing beyond what one-time setup needs; documents and reports are deleted after delivery.
20
+ - Measure quality with a seeded-error evaluation set.
21
+ - Serve as the review pipeline for the author's own football blog.
22
+
23
+ **Name:** Second Read.
24
+
25
+ ## Non-goals
26
+
27
+ - **Writing for the user.** Agents critique and verify; they never draft or rewrite the argument.
28
+ - **Granular public tools.** No `extract_claims` or `find_counterevidence` tools; exposing them would hand orchestration to the client.
29
+ - **Hosted models.** The service never pays for inference; users bring their own key.
30
+ - **Guaranteed plagiarism detection.** Originality checks are a heuristic and are labelled as such.
31
+ - **Auto-publishing or social posting.** Out of scope for v1.
32
+ - **Monetization.** None; this is a free portfolio and personal project.
33
+
34
+ ## User experience
35
+
36
+ Setup takes about a minute, once; every review after that is one sentence in chat.
37
+
38
+ **One-time setup (web)**
39
+
40
+ 1. User adds the Second Read MCP server URL in their client (Claude, ChatGPT, Cursor).
41
+ 2. On first use the client runs the OAuth flow; the user clicks **Sign in with Google**. Google supplies a verified email, so no separate email verification is needed.
42
+ 3. A setup page asks for their model provider and API key. The key is validated with a cheap test call, then encrypted and stored.
43
+
44
+ **Each review (chat)**
45
+
46
+ 1. User: "Review this draft" (pasted text, or a URL).
47
+ 2. The client calls `review_document`.
48
+ 3. The server enqueues a job and responds in one of three ways, based on client capability:
49
+ 1. **MCP Tasks supported:** returns a task handle; the client shows live status.
50
+ 2. **Progress notifications supported:** holds the call open and streams steps ("Fact-checking claim 4 of 12").
51
+ 3. **Neither:** returns immediately: "Review started; the report will be emailed to you."
52
+ 4. The report is always emailed. When the call is synchronous, it is also returned inline.
53
+ 5. The document and report are deleted after delivery.
54
+
55
+ ## System architecture
56
+
57
+ The MCP server stays thin: it authenticates, enqueues, and relays status. All agent work happens in workers.
58
+
59
+ ```
60
+ MCP clients (Claude, ChatGPT, Cursor)
61
+ │ MCP
62
+ ▼
63
+ MCP + setup server (OAuth, review_document) ──OAuth──► Google Sign-in
64
+ │ enqueue │ keys, job status
65
+ ▼ ▼
66
+ Job queue Postgres (users, keys, job status)
67
+ │ jobs ▲ progress
68
+ ▼ │
69
+ Workers: orchestrator + agents ───┘ (decrypt key via KMS)
70
+ Orchestrator · Claim extractor · Fact-checker · Devil's advocate
71
+ · Copy editor · Originality · Structure — all share one claim ledger
72
+ │ inference │ search │ send report
73
+ ▼ ▼ ▼
74
+ Model provider Search API + page fetch Email provider
75
+ (user's own key) (evidence, citations) (verified email)
76
+ ```
77
+
78
+ Workers decrypt the user's key through KMS for the job's duration, call the model provider and search, write progress to Postgres for the MCP server to relay, and email the final report.
79
+
80
+ ## Agents and orchestration
81
+
82
+ The system is an orchestrator-workers design with agents that react to each other: the orchestrator plans each review, research agents run their own bounded loops, and disagreements trigger re-checks and a short debate before fixed rules settle the result.
83
+
84
+ | Agent | Job | Input | Output | Model tier |
85
+ | --- | --- | --- | --- | --- |
86
+ | Orchestrator | Classify document, pick profile, dispatch agents, resolve conflicts, rank findings | Document + profile | Final report | Strong |
87
+ | Claim extractor | Split into thesis, supporting claims, factual claims (numbers, dates, attributions) | Document | Claim ledger entries | Cheap |
88
+ | Fact-checker | Verify each factual claim against retrieved sources | Claim ledger | Verdict per claim: verified / wrong / unsupported, with citations | Mid |
89
+ | Devil's advocate | Retrieve counter-evidence and build the strongest opposing case | Thesis + supporting claims | Rebuttals, each tied to claims and sources; exposure rating | Strong |
90
+ | Copy editor | Grammar, clarity, concision; preserves author voice | Document + optional style guide | Line-level suggestions | Cheap |
91
+ | Originality checker | Sample distinctive sentences, search for near-matches | Document | Possible matches with URLs (heuristic) | Cheap |
92
+ | Structure reviewer | Flow, missing sections, argument order (design docs: gaps, unstated assumptions) | Document + profile | Structural findings | Mid |
93
+
94
+ **Shared state: the claim ledger.** Agents do not pass text to each other. They read and write one structured ledger keyed by claim ID. Each entry holds the claim text and span, type, fact-check verdict, sources, rebuttals, and copy edits touching its span. This makes conflicts detectable by span overlap.
95
+
96
+ **Execution plan (dynamic)**
97
+
98
+ The orchestrator writes a plan for each document instead of following a fixed order; code checks the plan against guardrails.
99
+
100
+ 1. Orchestrator classifies the document and selects a profile.
101
+ 2. Claim extractor runs first; everything else depends on the ledger.
102
+ 3. Orchestrator reads the ledger and writes an execution plan: which agents run, which claims get deep research, and each claim's budget. It skips agents that add nothing (originality on a design doc) and adds fact-check depth for number-heavy text.
103
+ 4. Code validates the plan against the profile's allowed agents and the job budget; an invalid plan falls back to the profile default.
104
+ 5. Planned agents run in parallel.
105
+ 6. Reaction round: re-checks and debate (below).
106
+ 7. Guardrail rules settle remaining conflicts, then ranking and the report.
107
+
108
+ **Research loops**
109
+
110
+ The fact-checker and devil's advocate run a bounded loop per claim instead of a single search: plan, act (`search`, `fetch_page`, `find_in_page`), assess the source, then refine the query, follow a link to the primary source, or stop.
111
+
112
+ - **Cap: 3 iterations per claim**, plus per-claim search and token budgets set by the plan.
113
+ - Stop when a primary source directly settles the claim, when two independent sources agree, or at the cap. Hitting the cap gives "unsupported" with its confidence noted, never a guess.
114
+ - **Verified** needs a primary source, two independent sources, or one reputable source (an edited reference work, major news organisation or official body, actually fetched) that nothing retrieved contradicts. Social and user-generated hosts never count towards settling a claim. Whether a source is reputable is judged per source (the assessing model classifies it as a primary record, reference work, major news organisation, official body, specialist site, blog or social post), with a known-host list as a backstop; social hosts never qualify. A part also counts as verified when it follows by arithmetic or simple logic from sourced facts.
115
+ - **Part by part:** a claim is verified only if every checkable part is (each number, date, name, place, cause or sequence). Research continues while parts remain unconfirmed, and the judge returns a verdict per part.
116
+ - **Wrong** needs a primary source or two independent sources that contradict the claim, or one reputable fetched source when nothing retrieved supports the disputed detail.
117
+ - **Second look:** before giving up on a claim, the fact-checker re-reads pages already fetched for the document's other claims. This costs no searches.
118
+ - **"Could not verify" is not "unsupported".** When sources were found but do not back the claim as written, it is an unsupported claim (must fix). When no adequate source was found, the claim is reported as "could not verify" at lower severity: failing to find a source is not evidence of an error.
119
+ - Tools are read-only, so a malicious page can waste budget but cannot act.
120
+ - Every step (query, source, finding) is saved as a research trail on the claim's ledger entry.
121
+
122
+ **Agents react to each other**
123
+
124
+ - **Re-check:** if the devil's advocate finds evidence contradicting a claim marked verified, the orchestrator sends the claim back to the fact-checker with that evidence. One re-check per claim.
125
+ - **Debate:** for the top 3 claims by importance where the two agents still disagree, each side gets one response to the other's evidence. The orchestrator rules and records its reasoning in the ledger. One round per claim.
126
+
127
+ **Conflict resolution rules (guardrails, applied after the reaction round)**
128
+
129
+ - Copy edit touches a span flagged wrong or unsupported → drop the copy edit; the fact finding wins.
130
+ - Devil's advocate attacks a claim the fact-checker verified → keep the rebuttal only if it targets the interpretation, not the fact; otherwise discard.
131
+ - Two agents flag the same span → merge into one finding with both reasons.
132
+ - Rebuttal with no retrieved source → downgrade to "consider" severity.
133
+ - Severity order: factual error > unsupported claim > strong rebuttal > structure > style.
134
+
135
+ **Budgets.** Each job has caps on tokens, search calls and wall-clock time. The orchestrator splits the job budget across claims by importance, and research loops stop at 3 iterations regardless. When the budget runs short, lower-importance claims get a single search or are marked unchecked rather than failing the job.
136
+
137
+ ## Review profiles and report format
138
+
139
+ v1 ships two profiles; the orchestrator picks one automatically unless the user names it.
140
+
141
+ | Profile | Agents | Emphasis |
142
+ | --- | --- | --- |
143
+ | Opinion / article | Extractor, fact-checker, devil's advocate, copy editor, originality | Facts right, strongest counter-case addressed |
144
+ | Technical design doc | Extractor, fact-checker, devil's advocate (argues against the design), structure reviewer, copy editor | Gaps, unstated assumptions, alternatives not considered |
145
+
146
+ Later candidates: report / analyst note (number consistency, source checks) and essay.
147
+
148
+ **Report structure**
149
+
150
+ 1. **Verdict line:** one sentence on overall readiness, plus counts by severity.
151
+ 2. **Must fix:** factual errors and unsupported claims, each with the quoted span, the problem, and sources.
152
+ - **Could not verify:** factual claims the fact-checker could not confirm, at lower severity.
153
+ 3. **Strongest counter-case:** the devil's advocate's top 3 rebuttals, each with sources and the claim it targets.
154
+ 4. **Should fix:** structure findings.
155
+ 5. **Polish:** copy edits, grouped.
156
+ 6. **Originality notes:** possible matches, labelled heuristic.
157
+ 7. **Appendix:** full claim ledger with verdicts.
158
+
159
+ The report is generated as structured JSON, then rendered to HTML for email and to markdown for inline return.
160
+
161
+ ## MCP interface
162
+
163
+ One tool, `review_document`, over Streamable HTTP with OAuth 2.1.
164
+
165
+ **Inputs**
166
+
167
+ | Field | Type | Required | Notes |
168
+ | --- | --- | --- | --- |
169
+ | `content` | string | one of content/url | Document text or markdown; size-capped (e.g. 20k words) |
170
+ | `url` | string | one of content/url | Public page; fetched and extracted server-side |
171
+ | `profile` | enum | no | `auto` (default), `opinion`, `design_doc` |
172
+ | `focus` | string | no | Free-text steer, e.g. "check the financial figures" |
173
+
174
+ The API key and email never appear as tool arguments; they come from the signed-in account.
175
+
176
+ **Response modes**, chosen from the client's declared capabilities at call time:
177
+
178
+ 1. **Tasks:** return a task handle; status updates carry the current step and percent; the result is the markdown report. See the [MCP Tasks spec](https://modelcontextprotocol.io/extensions/tasks/overview).
179
+ 2. **Progress:** stream `notifications/progress` with step messages; return the markdown report at the end.
180
+ 3. **Fire-and-forget:** return a short confirmation with a job ID and a note that the report is on its way by email.
181
+
182
+ In all modes the report is emailed, so a dropped connection loses nothing.
183
+
184
+ **Errors:** missing setup (returns a link to the setup page), invalid or out-of-credit provider key, document too long, URL unreachable, rate limit exceeded. Each returns a clear, human-readable message.
185
+
186
+ ## Async jobs and email delivery
187
+
188
+ Every review is a queued job, even when the caller waits synchronously; the MCP layer only watches the job.
189
+
190
+ **Job lifecycle:** queued → extracting → reviewing → resolving → reporting → emailed → purged (or failed).
191
+
192
+ - **Queue:** a durable queue; workers pull jobs and run the orchestrator.
193
+ - **Progress:** workers write step, message and percent to the job record; the MCP layer relays them as task status or progress notifications.
194
+ - **Retries:** agent calls retry with backoff on transient errors (rate limits, timeouts). A failed agent degrades the report ("fact-check unavailable") rather than failing the job.
195
+ - **Timeout:** hard cap per job (e.g. 10 minutes); partial results are emailed with a note.
196
+ - **Idempotency:** a content hash plus user ID de-duplicates accidental double submits within a short window.
197
+ - **Email:** sent through a transactional email provider to the Google-verified address only. HTML report with a plain-text fallback. Sender domain set up with SPF and DKIM.
198
+ - **Failure email:** if the job fails, the user gets a short email with the reason and whether a retry is likely to help.
199
+
200
+ ## Auth, API keys and security
201
+
202
+ Sign in with Google is the only identity provider; the MCP server acts as the OAuth authorization server toward clients and delegates login to Google.
203
+
204
+ **Auth flow**
205
+
206
+ 1. Client connects; the server returns 401 with its OAuth metadata.
207
+ 2. Client opens the authorize URL; the user signs in with Google.
208
+ 3. Server creates or looks up the user by Google subject ID and verified email.
209
+ 4. Server issues its own short-lived access token and refresh token to the client (never Google's tokens).
210
+
211
+ **API key handling**
212
+
213
+ - Entered only on the setup web page, never in chat or tool arguments.
214
+ - Validated with a minimal test call before saving.
215
+ - Encrypted at rest with envelope encryption: a per-user data key, wrapped by a master key in a managed KMS or secrets store.
216
+ - Decrypted only inside a worker for the duration of a job; never logged, never sent to the client.
217
+ - Users can rotate or delete their key, and delete their account, from the setup page.
218
+
219
+ **Other controls**
220
+
221
+ - **Prompt injection:** documents and fetched pages are untrusted data. Agents get no tools that act outside the job (no email, no writes). Only the report step emails, and only to the account's verified address.
222
+ - **URL fetching:** block private IP ranges and non-HTTP schemes (SSRF protection); cap size and redirects.
223
+ - **Rate limits:** per user (e.g. 20 reviews per day) and global, plus a per-job cost ceiling.
224
+ - **Transport:** HTTPS only; secure, HTTP-only session cookies on the setup page.
225
+
226
+ ## Data model and retention
227
+
228
+ Only three tables persist; document content never outlives its job.
229
+
230
+ | Table | Fields | Retention |
231
+ | --- | --- | --- |
232
+ | `users` | id, google_sub, email, created_at, daily_count, last_review_at | Until account deletion |
233
+ | `provider_keys` | user_id, provider, encrypted_key, wrapped_data_key, created_at, last_validated_at | Until the user deletes it or the account |
234
+ | `jobs` | id, user_id, status, step, percent, profile, content_hash, created_at, finished_at, error | Metadata kept 30 days for debugging and cost stats; no content |
235
+
236
+ **Ephemeral (object storage or the queue payload)**
237
+
238
+ - Document text and fetched pages: encrypted, deleted when the job finishes.
239
+ - Claim ledger and agent outputs: held during the job, deleted after the report is emailed.
240
+ - Report: deleted after email delivery.
241
+
242
+ **OAuth tokens:** refresh tokens are stored hashed; access tokens are short-lived.
243
+
244
+ **Privacy promise (README):** "Your documents and reports are deleted after delivery. We store your email and your encrypted API key, nothing else."
245
+
246
+ ## Evaluation
247
+
248
+ Quality is measured on a seeded-error set, and the headline results table goes in the README.
249
+
250
+ **Dataset**
251
+
252
+ - Start with 30 documents, growing to 100: blog posts (including your own drafts), op-eds, and design docs.
253
+ - For each, create a seeded copy with known defects and an answer key:
254
+ - wrong numbers, dates or attributions (fact-checker);
255
+ - unsupported claims (fact-checker);
256
+ - a deliberately weak argument with a known strong rebuttal (devil's advocate);
257
+ - borrowed sentences from a known source (originality);
258
+ - grammar and clarity errors (copy editor).
259
+
260
+ **Metrics**
261
+
262
+ | Metric | What it measures |
263
+ | --- | --- |
264
+ | Recall per defect type | Share of seeded defects caught |
265
+ | Precision | Share of findings that are real (reviewed by hand, or by an LLM judge spot-checked by hand) |
266
+ | Citation validity | Share of cited sources that actually support the verdict |
267
+ | Rebuttal strength | LLM-judge score 1–5 against the known strongest rebuttal |
268
+ | Conflict-resolution accuracy | Share of seeded conflicts resolved per the rules |
269
+ | Cost and latency | Tokens, search calls and seconds per job |
270
+
271
+ **Baselines:** three variants run on the same set: (1) a single-model prompt ("review this and argue the other side"); (2) a fixed pipeline with one search per claim, a fixed plan and no reaction round; (3) the full system. Each document runs 3 times so results report variance, not just averages. The gap from (1) to (2) shows the multi-agent design earns its complexity; the gap from (2) to (3) shows the research loops, re-checks and debate earn theirs. The dataset also seeds multi-part claims and claims whose top search results are secondary sources, where research loops should matter most.
272
+
273
+ **Regression:** the eval runs in CI on every change to prompts, agents or the orchestrator.
274
+
275
+ ## Observability, cost and rate limits
276
+
277
+ Every job produces a trace, but traces record metadata and never document content.
278
+
279
+ - **Tracing:** one trace per job, with a span per agent call recording model, tokens in and out, latency, retries and the orchestrator's decisions (profile chosen, conflicts resolved and why). OpenTelemetry, exported to a hosted tracing backend's free tier.
280
+ - **Content redaction:** trace attributes hold claim IDs and counts, not text, to keep the privacy promise.
281
+ - **Cost:** inference is billed to the user's key. The service pays only for hosting, queue, storage, search and email. Target: under $10 a month at portfolio-level traffic, within free tiers where possible.
282
+ - **Per-job cost estimate:** shown in the report footer (tokens used, estimated cost on the user's key).
283
+ - **Rate limits:** per-user daily cap, per-job token and search caps, and a global circuit breaker on the search provider.
284
+ - **Dashboards:** jobs per day, p50 and p95 latency, failure rate by agent, and search calls per job.
285
+
286
+ ## Tech stack
287
+
288
+ Python throughout, with managed services everywhere except the agent layer, so build time goes into the multi-agent core. Candidates are not yet checked against current pricing; confirm free-tier limits before committing.
289
+
290
+ | Layer | Choice | Why |
291
+ | --- | --- | --- |
292
+ | MCP server | Official MCP Python SDK, Streamable HTTP | First-party support for Tasks, progress and OAuth |
293
+ | Web and setup page | FastAPI plus a minimal HTML page | Same process as the MCP server; small surface |
294
+ | Orchestration | Plain Python asyncio with typed ledger models (Pydantic) | Hand-built orchestration shows the design; a framework hides it |
295
+ | Model access | Thin provider adapter (Anthropic first, OpenAI second) | Bring-your-own-key across providers |
296
+ | Queue and workers | Postgres-backed job queue, or Redis with a worker library | One fewer service if Postgres |
297
+ | Database | Managed Postgres | Users, keys, job metadata |
298
+ | Secrets and encryption | Cloud KMS or a secrets manager for the master key | Envelope encryption |
299
+ | Search and retrieval | A search API with a free tier, plus page fetch and extraction | Evidence for the fact-checker and devil's advocate |
300
+ | Email | Transactional email provider | Deliverability, SPF and DKIM |
301
+ | Hosting | Container platform with scale-to-zero | Near-zero idle cost |
302
+ | Tracing | OpenTelemetry to a hosted backend | Free tier; standard |
303
+ | CI | GitHub Actions: tests plus the eval subset | Public repo |
304
+
305
+ ## Build phases
306
+
307
+ Five phases, each with a gate; no hosting work starts until evidence retrieval proves itself in Phase 0.
308
+
309
+ 1. **Phase 0 · Evidence prototype.** Extractor + devil's advocate script on your own drafts; pick a search API; publish 2–3 posts by hand.
310
+ - Gate: rebuttals cite real, relevant sources on your drafts.
311
+ 2. **Phase 1 · Core engine (local).** All agents, claim ledger, orchestrator with conflict rules, CLI; seeded eval set of 30 documents.
312
+ - Gate: beats the single-prompt baseline on the eval set.
313
+ 3. **Phase 2 · Local MCP server.** `review_document` over stdio, progress notifications, key from an environment variable.
314
+ - Gate: used end to end on your own blog posts.
315
+ 4. **Phase 3 · Hosted service.** Google OAuth, setup page, encrypted keys, queue and workers, email delivery, MCP Tasks.
316
+ - Gate: full review from Claude and ChatGPT, report arrives by email.
317
+ 5. **Phase 4 · Harden and launch.** Tracing, rate limits, eval in CI, README with results table, write-up post.
318
+
319
+ Phases 0–2 cost nothing to run and already make the tool useful for your blog; Phases 3–4 turn it into the portfolio version.
320
+
321
+ ## Risks and open questions
322
+
323
+ The biggest risk is retrieval quality; it is prototyped first, in Phase 0.
324
+
325
+ | Risk | Impact | Mitigation |
326
+ | --- | --- | --- |
327
+ | Weak counter-evidence retrieval | The main differentiator fails | Prototype first; query rewriting per claim; primary-source preference |
328
+ | Infrastructure consumes the timeline | Multi-agent core gets thin | Managed services; strict phase gates |
329
+ | Client support for Tasks and progress varies | Poor experience in some clients | Email delivery in every mode |
330
+ | Bring-your-own-key friction | Few outside users | Accept; judge success on the eval and your own usage |
331
+ | Prompt injection via documents | Data leakage or misuse | Agents have no external actions; email only to the verified address |
332
+ | Search API free-tier limits | Jobs throttle or fail | Per-job search budget; degrade gracefully |
333
+ | Research loops and debate raise cost and latency | Slower, pricier reviews | 3-iteration cap, per-claim budgets, async email delivery |
334
+ | Existing critique MCP servers | Looks derivative | Lead the README with evidence-backed results against the baseline |
335
+
336
+ **Open questions**
337
+
338
+ - [ ] Which search API: compare quality and free-tier limits in Phase 0.
339
+ - [ ] Offer a short-lived "view report online" link, or email only?
340
+ - [ ] Which clients must work at launch: Claude Desktop, claude.ai, ChatGPT, Cursor?
341
+ - [ ] Allow keys for more than one provider per user in v1?
342
+ - [x] Product name: Second Read.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shubham Banthia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.