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.
- second_read-0.1.4/ARCHITECTURE.md +561 -0
- {second_read-0.1.3 → second_read-0.1.4}/CLAUDE.md +3 -3
- {second_read-0.1.3 → second_read-0.1.4}/PKG-INFO +42 -13
- {second_read-0.1.3 → second_read-0.1.4}/README.md +41 -12
- {second_read-0.1.3 → second_read-0.1.4}/ROADMAP.md +4 -4
- second_read-0.1.4/SECURITY.md +34 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/README.md +2 -2
- second_read-0.1.4/glama.json +4 -0
- {second_read-0.1.3 → second_read-0.1.4}/pyproject.toml +1 -1
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/prompts.py +5 -1
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/research.py +10 -1
- {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/__init__.py +1 -1
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/assemble.py +1 -1
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/conflicts.py +1 -1
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/reactions.py +2 -2
- {second_read-0.1.3 → second_read-0.1.4}/server.json +2 -2
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/test_agent.py +32 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_dataset.py +1 -1
- {second_read-0.1.3 → second_read-0.1.4}/uv.lock +1 -1
- second_read-0.1.3/ARCHITECTURE.md +0 -150
- second_read-0.1.3/DESIGN.md +0 -343
- second_read-0.1.3/secondread/mcp_local/README.md +0 -96
- {second_read-0.1.3 → second_read-0.1.4}/.env.example +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/.github/workflows/ci.yml +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/.github/workflows/publish.yml +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/.gitignore +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/.python-version +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/LICENSE +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/__main__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/baseline.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/cli.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/01-leicester-money.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/01-leicester-money.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/02-possession.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/02-possession.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/03-invincibles.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/03-invincibles.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/04-var.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/04-var.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/05-four-day-week.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/05-four-day-week.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/06-sqlite-production.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/06-sqlite-production.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/07-postgres-queue.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/07-postgres-queue.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/08-uuid-keys.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/08-uuid-keys.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/09-cdn-cache.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/09-cdn-cache.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/10-best-league.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/10-best-league.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/11-greece-2004.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/11-greece-2004.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/12-istanbul-momentum.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/12-istanbul-momentum.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/13-five-substitutes.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/13-five-substitutes.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/14-brazil-1970.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/14-brazil-1970.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/15-penalty-lottery.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/15-penalty-lottery.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/16-womens-game-stadiums.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/16-womens-game-stadiums.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/17-no-kubernetes.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/17-no-kubernetes.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/18-python-type-hints.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/18-python-type-hints.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/19-big-rewrite.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/19-big-rewrite.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/20-nuclear-climate.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/20-nuclear-climate.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/21-phones-in-schools.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/21-phones-in-schools.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/22-clock-changes.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/22-clock-changes.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/23-close-the-libraries.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/23-close-the-libraries.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/24-robots-not-astronauts.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/24-robots-not-astronauts.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/25-jwt-sessions.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/25-jwt-sessions.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/26-kafka-stream.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/26-kafka-stream.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/27-rate-limiter.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/27-rate-limiter.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/28-audit-log-archive.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/28-audit-log-archive.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/29-raft-config-store.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/29-raft-config-store.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/30-tls13-internal.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/30-tls13-internal.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/31-womens-champions-league.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/31-womens-champions-league.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/32-national-league-third-place.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/32-national-league-third-place.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/33-mjallby-model.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/33-mjallby-model.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/34-congestion-charge.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/34-congestion-charge.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/35-postgres-18-upgrade.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/35-postgres-18-upgrade.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/36-wsl-fourteen-teams.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/36-wsl-fourteen-teams.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/37-metlife-final.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/37-metlife-final.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/38-votes-at-sixteen.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/38-votes-at-sixteen.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/39-logical-replication-reporting.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/39-logical-replication-reporting.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/40-hpa-ingest-workers.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/data/40-hpa-ingest-workers.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/dataset.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/fake_pipeline.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/harness.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/items.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/judge.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/live.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/metrics.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/results.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/eval/textspan.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/scripts/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/scripts/compare_search.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/copyedit/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/copyedit/agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/copyedit/prompts.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/copyedit/text.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/prompts.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/schemas.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/devils_advocate/text.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/chunks.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/prompts.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/extractor/spans.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/schemas.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/factcheck/sources.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/originality/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/originality/agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/originality/sentences.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/originality/similarity.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/structure/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/structure/agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/agents/structure/prompts.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/__main__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/envfile.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/errors.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/cli/main.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/contracts/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/contracts/errors.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/contracts/interfaces.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/contracts/models.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/__main__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/demo.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/errors.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/main.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/progress.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/server.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/mcp_local/wiring.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/authwatch.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/classify.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/labels.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/orchestrator.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/planning.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/progress.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/prompts.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/registry.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/orchestrator/schemas.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/pipeline.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/extract.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/fetcher.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/guarded.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/fetch/ssrf.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/_common.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/anthropic_client.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/config.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/llm/openai_client.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/base.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/brave.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/config.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/exa.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/retry.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/providers/search/tavily.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/registry.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/report/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/report/email.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/report/html.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/report/markdown.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/report/templates/email.html.j2 +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/report/text.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/report/view.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/testing/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/testing/fakes.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/secondread/testing/samples.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/copyedit/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/copyedit/test_agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/copyedit/test_quotes.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/copyedit/test_text.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/devils_advocate/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/devils_advocate/test_agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/devils_advocate/test_text.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/extractor/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/extractor/test_agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/extractor/test_spans.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/extractor/test_standalone.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/test_precision.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/test_sources.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/factcheck/test_standalone.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/originality/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/originality/test_agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/originality/test_sentences.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/originality/test_similarity.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/structure/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/agents/structure/test_agent.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/cli/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/cli/test_cli.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/cli/test_envfile.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/cli/test_live.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/cli/test_registry.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/conftest.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/contracts/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/contracts/test_models.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/eval/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/eval/helpers.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_baseline.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_harness.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_judge_and_spans.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_live.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/eval/test_metrics.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/helpers.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_env_file.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_progress.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_server.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_stdio.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/mcp_local/test_wiring.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/builders.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_authwatch.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_conflicts.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_pipeline.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_planning.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/orchestrator/test_progress_classify.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_extract.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_fetcher.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_guarded.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_live.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/fetch/test_ssrf.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_auth_error.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_bad_request.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_low_credit.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_overloaded.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_rate_limit.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_refusal.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_text.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_tool_use.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_tool_use_invalid.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/anthropic_truncated.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_auth_error.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_insufficient_quota.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_json_invalid.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_json_schema.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_rate_limit.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_refusal.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_server_error.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_text.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/fixtures/openai_truncated.json +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/replay.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/schemas.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/test_anthropic.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/test_common.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/test_live.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/llm/test_openai.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/search/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/search/test_adapters.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/search/test_config.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/providers/search/test_live.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/empty.html +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/empty.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/empty.txt +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_document.html +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_document.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_document.txt +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_notes.html +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_notes.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_with_notes.txt +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_without_document.html +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_without_document.md +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/snapshots/sample_without_document.txt +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/test_render.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/report/test_snapshots.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/scripts/__init__.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/scripts/test_compare_search.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/test_server_json.py +0 -0
- {second_read-0.1.3 → second_read-0.1.4}/tests/testing/__init__.py +0 -0
- {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
|
-
- `
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
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
|
-
##
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
- **
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
`
|
|
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 `
|
|
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
|