sciwrite-lint 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sciwrite_lint-0.4.0/sciwrite_lint.egg-info → sciwrite_lint-0.5.0}/PKG-INFO +14 -3
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/README.md +13 -2
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/pyproject.toml +3 -1
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/__init__.py +1 -1
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/__main__.py +52 -56
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/parse.py +1 -1
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/verify.py +12 -5
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/eval_claims.py +107 -89
- sciwrite_lint-0.5.0/sciwrite_lint/llm/manuscript_cache.py +173 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pdf/grobid.py +24 -8
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/checks.py +85 -5
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/claims.py +0 -1
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/__init__.py +13 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/_core.py +43 -5
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/claim_results.py +8 -3
- sciwrite_lint-0.5.0/sciwrite_lint/references/workspace_db/manuscript_check_cache.py +103 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0/sciwrite_lint.egg-info}/PKG-INFO +14 -3
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/SOURCES.txt +2 -1
- sciwrite_lint-0.4.0/sciwrite_lint/claude_backend.py +0 -94
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/LICENSE +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/_network.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/api.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/api_keys.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/_diagnostics.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/_section_utils.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/cite_purpose.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/claim_support.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/cross_section_consistency.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/dangling_cite.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/dangling_ref.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/full_paper_consistency.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/internal_consistency_pairs.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/prose_quality.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/ref_internal_checks.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/reference_accuracy.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/reference_exists.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/reference_unreliable.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/registry.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/retracted_cite.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/structure_promises.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/claims.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/claude_cli.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/_common.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/check.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/config.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/fetch.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/_monitor.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/containers.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/dismiss_claim.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/grobid.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/init.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/override.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/vision.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/vllm.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/rank.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/config.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cross_paper.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/exceptions.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/footnote_urls.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_common.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_download.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_orchestrator.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_search.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_validation.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/arxiv.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/biorxiv.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/core.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/eric.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/europepmc.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/hal.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/ideas.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/nasa_ads.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/nber.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/osf.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/pmc.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/s2.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/unpaywall.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/latex_to_markdown.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/compute_cap.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/controller.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/decide.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/host_metrics.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/metrics_probe.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/registry.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/telemetry.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/vllm_metrics.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm/concurrency_optimizer/wiring.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm_utils.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/local_sources.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/manuscript_store.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/mhtml.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/models.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_models.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_pdf.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_search.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_web.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pdf/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pdf/pdf_download.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/embeddings.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/fetch.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/orchestration.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/parse.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/pdf_context.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/ref_internal.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/runners.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/staged.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/swap.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/tracking.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/unreliable.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/verify.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/vision.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/prompt_safety.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/py.typed +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/rate_limiter.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/_embed_timing.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/chain.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/citations.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/crossref.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/embedding_store.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/matching.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/metadata.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/reference_store.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/retraction_watch.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/bib_checks.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/citation_metadata.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/manuscript_citations.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/parse_cache.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/pipeline_stage.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/query_vectors.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/ref_internal.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/registry.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/vision_cache.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/report.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/schemas.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/scoring/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/scoring/contribution.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/scoring/scilint_score.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/tex_parser.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/usage.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/cache.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/describe.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/image_extraction.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/pipeline.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vllm/__init__.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vllm/metrics.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vllm/vllm_server.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint/web.py +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/entry_points.txt +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/requires.txt +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/top_level.txt +0 -0
- {sciwrite_lint-0.4.0 → sciwrite_lint-0.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sciwrite-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A linter for scientific manuscripts — reference verification, consistency checking, and structural validation.
|
|
5
5
|
Author: Sergey Samsonau
|
|
6
6
|
Maintainer-email: Authentic Research Partners LLC <info@arpconnect.com>
|
|
@@ -147,7 +147,7 @@ sciwrite-lint contributions paper.pdf --format json
|
|
|
147
147
|
|
|
148
148
|
### Optimizations
|
|
149
149
|
|
|
150
|
-
Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. The pipeline runs each in its own stage and explicitly stops one before starting the next, so only one ever owns GPU memory at a time (same code path on WSL2 and native Linux). FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching
|
|
150
|
+
Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. The pipeline runs each in its own stage and explicitly stops one before starting the next, so only one ever owns GPU memory at a time (same code path on WSL2 and native Linux). FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching are baseline; every cache layer pins its inputs by hash so editing the paper, replacing a figure, or refreshing a reference recomputes only what's affected (see the *Iterative editing* subsection under Usage — `--fresh` is not part of the normal loop). On top of that:
|
|
151
151
|
|
|
152
152
|
- **Cost-aware verify-claim ladder** — sentence chunk → paragraph chunk → whole section. Each level fans out top-N candidates in parallel; stops on a conclusive verdict. Most claims resolve at the cheap sentence level (~200-token prompts) instead of paying for whole sections (~5K tokens), with full-section escalation reserved for hard cases
|
|
153
153
|
- **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
|
|
@@ -282,7 +282,18 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
|
|
|
282
282
|
|
|
283
283
|
`check` runs the full pipeline in one command: text checks → figure analysis → LLM consistency → reference verification via APIs → download and parse cited papers → claim verification → consistency checks on cited papers → bibliography verification → aggregate reliability scores → SciLint Score. An initial run on a 50-reference paper takes up to 30 minutes (dominated by downloads and claim verification); subsequent cached runs complete in minutes.
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
### Iterative editing — no `--fresh` needed
|
|
286
|
+
|
|
287
|
+
`sciwrite-lint check` invalidates cached results whenever the inputs they were derived from change, so rerunning after an edit just works. Concretely:
|
|
288
|
+
|
|
289
|
+
- **Edit a sentence or paragraph** → the manuscript-LLM checks (prose-quality, cross-section consistency, etc.) re-run only on the touched sentences and their paragraph siblings; untouched prose stays cached
|
|
290
|
+
- **Edit a paragraph around a citation** → the claim verifier reruns on the edited prose
|
|
291
|
+
- **Replace a figure** (overwrite the PNG/PDF in your tree) → the vision cache re-describes it
|
|
292
|
+
- **Swap the local LLM model** (e.g. `qwen3` ↔ `gemma3` via `--model`) → manuscript-check rows and claim verdicts no longer count as cache hits
|
|
293
|
+
- **Overwrite a reference PDF** in your drop folder, or **re-fetch** an open-access PDF that has new bytes → the parser, embeddings, and claim verifier all rerun against the new content
|
|
294
|
+
- **Upgrade a summary `.md` to a real PDF** in your drop folder → claims are re-verified against the richer evidence
|
|
295
|
+
|
|
296
|
+
Use `--fresh` only when you want to wipe the workspace deliberately (it backs up the existing workspace before recreating it). It is not the answer to "I edited the paper and a finding looks stale" — if you ever see that, file an issue; a cache invariant is wrong and we'll fix it.
|
|
286
297
|
|
|
287
298
|
Use `--checks ID[,ID...]` to run only the listed checks (comma-separated), disabling the rest for this invocation. Useful for fast iteration during editing (`--checks prose-quality` runs just the prose review), for debugging a single check, or for scripting a two-pass workflow. Unknown check IDs fail loudly — run `sciwrite-lint checks` to list available IDs.
|
|
288
299
|
|
|
@@ -81,7 +81,7 @@ sciwrite-lint contributions paper.pdf --format json
|
|
|
81
81
|
|
|
82
82
|
### Optimizations
|
|
83
83
|
|
|
84
|
-
Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. The pipeline runs each in its own stage and explicitly stops one before starting the next, so only one ever owns GPU memory at a time (same code path on WSL2 and native Linux). FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching
|
|
84
|
+
Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. The pipeline runs each in its own stage and explicitly stops one before starting the next, so only one ever owns GPU memory at a time (same code path on WSL2 and native Linux). FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching are baseline; every cache layer pins its inputs by hash so editing the paper, replacing a figure, or refreshing a reference recomputes only what's affected (see the *Iterative editing* subsection under Usage — `--fresh` is not part of the normal loop). On top of that:
|
|
85
85
|
|
|
86
86
|
- **Cost-aware verify-claim ladder** — sentence chunk → paragraph chunk → whole section. Each level fans out top-N candidates in parallel; stops on a conclusive verdict. Most claims resolve at the cheap sentence level (~200-token prompts) instead of paying for whole sections (~5K tokens), with full-section escalation reserved for hard cases
|
|
87
87
|
- **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
|
|
@@ -216,7 +216,18 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
|
|
|
216
216
|
|
|
217
217
|
`check` runs the full pipeline in one command: text checks → figure analysis → LLM consistency → reference verification via APIs → download and parse cited papers → claim verification → consistency checks on cited papers → bibliography verification → aggregate reliability scores → SciLint Score. An initial run on a 50-reference paper takes up to 30 minutes (dominated by downloads and claim verification); subsequent cached runs complete in minutes.
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
### Iterative editing — no `--fresh` needed
|
|
220
|
+
|
|
221
|
+
`sciwrite-lint check` invalidates cached results whenever the inputs they were derived from change, so rerunning after an edit just works. Concretely:
|
|
222
|
+
|
|
223
|
+
- **Edit a sentence or paragraph** → the manuscript-LLM checks (prose-quality, cross-section consistency, etc.) re-run only on the touched sentences and their paragraph siblings; untouched prose stays cached
|
|
224
|
+
- **Edit a paragraph around a citation** → the claim verifier reruns on the edited prose
|
|
225
|
+
- **Replace a figure** (overwrite the PNG/PDF in your tree) → the vision cache re-describes it
|
|
226
|
+
- **Swap the local LLM model** (e.g. `qwen3` ↔ `gemma3` via `--model`) → manuscript-check rows and claim verdicts no longer count as cache hits
|
|
227
|
+
- **Overwrite a reference PDF** in your drop folder, or **re-fetch** an open-access PDF that has new bytes → the parser, embeddings, and claim verifier all rerun against the new content
|
|
228
|
+
- **Upgrade a summary `.md` to a real PDF** in your drop folder → claims are re-verified against the richer evidence
|
|
229
|
+
|
|
230
|
+
Use `--fresh` only when you want to wipe the workspace deliberately (it backs up the existing workspace before recreating it). It is not the answer to "I edited the paper and a finding looks stale" — if you ever see that, file an issue; a cache invariant is wrong and we'll fix it.
|
|
220
231
|
|
|
221
232
|
Use `--checks ID[,ID...]` to run only the listed checks (comma-separated), disabling the rest for this invocation. Useful for fast iteration during editing (`--checks prose-quality` runs just the prose review), for debugging a single check, or for scripting a two-pass workflow. Unknown check IDs fail loudly — run `sciwrite-lint checks` to list available IDs.
|
|
222
233
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sciwrite-lint"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "A linter for scientific manuscripts — reference verification, consistency checking, and structural validation."
|
|
9
9
|
requires-python = ">=3.13,<3.14"
|
|
10
10
|
license = "MIT"
|
|
@@ -86,3 +86,5 @@ sciwrite-lint = "sciwrite_lint.__main__:main"
|
|
|
86
86
|
[tool.setuptools.packages.find]
|
|
87
87
|
include = ["sciwrite_lint*"]
|
|
88
88
|
|
|
89
|
+
[tool.setuptools.package-data]
|
|
90
|
+
|
|
@@ -14,8 +14,6 @@ import argparse
|
|
|
14
14
|
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
-
from loguru import logger
|
|
18
|
-
|
|
19
17
|
from sciwrite_lint import __version__
|
|
20
18
|
from sciwrite_lint.cli._common import (
|
|
21
19
|
_classify_verify_issue as _classify_verify_issue,
|
|
@@ -42,6 +40,31 @@ _REPLACE_HELP = (
|
|
|
42
40
|
# ---------------------------------------------------------------------------
|
|
43
41
|
|
|
44
42
|
|
|
43
|
+
def _register_optional_extensions(
|
|
44
|
+
sub: argparse._SubParsersAction, # type: ignore[type-arg]
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Load CLI extensions discovered under ``sciwrite_lint``.
|
|
47
|
+
|
|
48
|
+
An underscore-prefixed subpackage is a CLI extension when its
|
|
49
|
+
``__init__.py`` defines ``register_cli(sub)``. Discovery walks
|
|
50
|
+
``sciwrite_lint.__path__`` via ``pkgutil``, so no extension package
|
|
51
|
+
names appear in this source — adding or removing one is a filesystem
|
|
52
|
+
change.
|
|
53
|
+
"""
|
|
54
|
+
import importlib
|
|
55
|
+
import pkgutil
|
|
56
|
+
|
|
57
|
+
import sciwrite_lint as _pkg
|
|
58
|
+
|
|
59
|
+
for _finder, name, ispkg in pkgutil.iter_modules(_pkg.__path__):
|
|
60
|
+
if not ispkg or not name.startswith("_"):
|
|
61
|
+
continue
|
|
62
|
+
ext = importlib.import_module(f"{_pkg.__name__}.{name}")
|
|
63
|
+
register = getattr(ext, "register_cli", None)
|
|
64
|
+
if register is not None:
|
|
65
|
+
register(sub)
|
|
66
|
+
|
|
67
|
+
|
|
45
68
|
def main(argv: list[str] | None = None) -> int:
|
|
46
69
|
from sciwrite_lint.cli.check import run_check, run_checks_list
|
|
47
70
|
from sciwrite_lint.cli.config import run_config
|
|
@@ -125,47 +148,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
125
148
|
p_checks = sub.add_parser("checks", help="List all registered checks")
|
|
126
149
|
p_checks.set_defaults(func=run_checks_list)
|
|
127
150
|
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
# while it stabilizes. Use ``importlib.util.find_spec`` to check whether
|
|
131
|
-
# the module is present rather than try/except ImportError (separately
|
|
132
|
-
# banned). Public installs (pip from PyPI) won't have it; dev installs
|
|
133
|
-
# do. Check the parent ``sciwrite_lint.aicode_lint`` first because
|
|
134
|
-
# ``find_spec`` raises ``ModuleNotFoundError`` on a missing intermediate
|
|
135
|
-
# path; once the parent is confirmed present, the child lookup is safe.
|
|
136
|
-
import importlib.util as _importlib_util
|
|
137
|
-
|
|
138
|
-
if _importlib_util.find_spec("sciwrite_lint.aicode_lint") is not None:
|
|
139
|
-
from sciwrite_lint.aicode_lint.cli import run_aicode_lint
|
|
140
|
-
|
|
141
|
-
p_aicode = sub.add_parser(
|
|
142
|
-
"aicode-lint",
|
|
143
|
-
help="LLM code review on Python sources via local vLLM (semantic antipatterns)",
|
|
144
|
-
)
|
|
145
|
-
p_aicode.add_argument(
|
|
146
|
-
"paths",
|
|
147
|
-
nargs="*",
|
|
148
|
-
help="Files, directories, or globs to scan (default: sciwrite_lint/**/*.py)",
|
|
149
|
-
)
|
|
150
|
-
p_aicode.add_argument(
|
|
151
|
-
"--rules",
|
|
152
|
-
default=None,
|
|
153
|
-
metavar="ID[,ID...]",
|
|
154
|
-
help="Run only the listed rule IDs (comma-separated). Default: all built-in rules.",
|
|
155
|
-
)
|
|
156
|
-
p_aicode.add_argument(
|
|
157
|
-
"--list-rules",
|
|
158
|
-
action="store_true",
|
|
159
|
-
help="Print the rule registry and exit.",
|
|
160
|
-
)
|
|
161
|
-
p_aicode.add_argument(
|
|
162
|
-
"--format",
|
|
163
|
-
choices=["terminal", "json"],
|
|
164
|
-
default=None,
|
|
165
|
-
help="Output format (default: terminal).",
|
|
166
|
-
)
|
|
167
|
-
p_aicode.add_argument("--config", help="Path to .sciwrite-lint.toml")
|
|
168
|
-
p_aicode.set_defaults(func=run_aicode_lint)
|
|
151
|
+
# Load optional underscore-prefixed CLI extension subpackages (if any).
|
|
152
|
+
_register_optional_extensions(sub)
|
|
169
153
|
|
|
170
154
|
# --- init ---
|
|
171
155
|
p_init = sub.add_parser(
|
|
@@ -270,12 +254,6 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
270
254
|
# --- verify-claims ---
|
|
271
255
|
p_vc = sub.add_parser("verify-claims", help="Verify claims against cited sources")
|
|
272
256
|
p_vc.add_argument("--paper", required=True, help="Paper to verify")
|
|
273
|
-
p_vc.add_argument(
|
|
274
|
-
"--backend",
|
|
275
|
-
choices=["vllm", "claude"],
|
|
276
|
-
default="vllm",
|
|
277
|
-
help="vllm (local LLM, fast) or claude (Claude CLI, deep)",
|
|
278
|
-
)
|
|
279
257
|
p_vc.add_argument(
|
|
280
258
|
"--model",
|
|
281
259
|
choices=_TEXT_MODELS,
|
|
@@ -499,14 +477,32 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
499
477
|
p_vllm.print_help()
|
|
500
478
|
return 0
|
|
501
479
|
|
|
502
|
-
# Set up file logging from config (stderr sink stays at INFO by default)
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
480
|
+
# Set up file logging from config (stderr sink stays at INFO by default).
|
|
481
|
+
# ``load_config`` already returns ``LintConfig()`` when no config file is
|
|
482
|
+
# discovered; reaching the exception path means a file WAS found (or
|
|
483
|
+
# explicitly passed via --config) but failed to parse or validate.
|
|
484
|
+
# Surface that instead of silently substituting hardcoded defaults — a
|
|
485
|
+
# broken TOML otherwise produces results computed against the wrong
|
|
486
|
+
# workspace, paper list, or API keys.
|
|
487
|
+
#
|
|
488
|
+
# The ``init`` command is the one exception: its job is to scaffold a
|
|
489
|
+
# config, so it must run against a missing, broken, or sentinel TOML
|
|
490
|
+
# without aborting. Use ``LintConfig()`` for logging setup only and let
|
|
491
|
+
# init proceed to (over)write the file.
|
|
492
|
+
if args.command == "init":
|
|
509
493
|
config = LintConfig()
|
|
494
|
+
else:
|
|
495
|
+
try:
|
|
496
|
+
config = load_config(
|
|
497
|
+
Path(args.config) if getattr(args, "config", None) else None
|
|
498
|
+
)
|
|
499
|
+
except Exception as e:
|
|
500
|
+
cfg_arg = getattr(args, "config", None)
|
|
501
|
+
cfg_hint = f" ({cfg_arg})" if cfg_arg else " (.sciwrite-lint.toml)"
|
|
502
|
+
raise SystemExit(
|
|
503
|
+
f"ERROR: failed to load configuration{cfg_hint}: {type(e).__name__}: {e}\n"
|
|
504
|
+
" Fix the TOML or remove the file to use built-in defaults."
|
|
505
|
+
) from e
|
|
510
506
|
_setup_logging(config)
|
|
511
507
|
|
|
512
508
|
return args.func(args)
|
|
@@ -84,7 +84,7 @@ def run_parse(args: argparse.Namespace) -> int:
|
|
|
84
84
|
n = compute_and_store_embeddings(key, text, refs_dir)
|
|
85
85
|
print(f" {key}: {n} chunks")
|
|
86
86
|
except Exception as e:
|
|
87
|
-
|
|
87
|
+
logger.warning("parse: embedding failed for {}: {}", key, e)
|
|
88
88
|
|
|
89
89
|
print(f"\n Summary: {cached} cached, {parsed} parsed, {failed} failed")
|
|
90
90
|
if failed:
|
|
@@ -335,6 +335,7 @@ def run_ref_health(args: argparse.Namespace) -> int:
|
|
|
335
335
|
academic_unmatched: list[Path] = []
|
|
336
336
|
web_matched: dict[str, Path] = {}
|
|
337
337
|
web_unmatched: list[Path] = []
|
|
338
|
+
local_match_error: str | None = None
|
|
338
339
|
try:
|
|
339
340
|
if tex_file:
|
|
340
341
|
from sciwrite_lint.config import load_config as _load_cfg
|
|
@@ -356,8 +357,13 @@ def run_ref_health(args: argparse.Namespace) -> int:
|
|
|
356
357
|
_cfg.local_web_dir, titles, WEB_SUFFIXES
|
|
357
358
|
)
|
|
358
359
|
except Exception as e:
|
|
359
|
-
|
|
360
|
-
|
|
360
|
+
# Record the failure so the report renders a visible notice instead
|
|
361
|
+
# of silently producing a ref-health output with the local-source
|
|
362
|
+
# section missing. Log at WARNING (not DEBUG) so the operator sees
|
|
363
|
+
# it even when running with the default log level.
|
|
364
|
+
local_match_error = f"{type(e).__name__}: {e}"
|
|
365
|
+
logger.warning(
|
|
366
|
+
"ref-health: local source matching skipped ({})", local_match_error
|
|
361
367
|
)
|
|
362
368
|
|
|
363
369
|
# --- Output ---
|
|
@@ -398,7 +404,9 @@ def run_ref_health(args: argparse.Namespace) -> int:
|
|
|
398
404
|
if web_count:
|
|
399
405
|
print(f" ({web_count} web resources)")
|
|
400
406
|
|
|
401
|
-
# Local sources — academic (PDFs + MD summaries) + web (MD + MHTML)
|
|
407
|
+
# Local sources — academic (PDFs + MD summaries) + web (MD + MHTML).
|
|
408
|
+
# Matching failures are surfaced via logger.warning (above) so they
|
|
409
|
+
# appear in the operator's terminal regardless of report formatting.
|
|
402
410
|
if academic_matched or academic_unmatched:
|
|
403
411
|
print("\n LOCAL ACADEMIC SOURCES")
|
|
404
412
|
if academic_matched:
|
|
@@ -425,7 +433,7 @@ def run_ref_health(args: argparse.Namespace) -> int:
|
|
|
425
433
|
|
|
426
434
|
|
|
427
435
|
def run_verify_claims(args: argparse.Namespace) -> int:
|
|
428
|
-
"""Verify claims against cited sources using local
|
|
436
|
+
"""Verify claims against cited sources using local vLLM."""
|
|
429
437
|
from sciwrite_lint.cli._common import _load_config, _resolve_paper
|
|
430
438
|
|
|
431
439
|
config = _load_config(args)
|
|
@@ -446,7 +454,6 @@ def run_verify_claims(args: argparse.Namespace) -> int:
|
|
|
446
454
|
refs_dir,
|
|
447
455
|
config=config,
|
|
448
456
|
bib_path=pc.bib,
|
|
449
|
-
backend=args.backend,
|
|
450
457
|
model=args.model,
|
|
451
458
|
key_filter=args.key,
|
|
452
459
|
limit=args.limit,
|
|
@@ -5,8 +5,8 @@ cited source (PDF via GROBID or .md), splits into sections, and checks
|
|
|
5
5
|
each section in parallel against the claim using a local LLM via vLLM.
|
|
6
6
|
|
|
7
7
|
Only works for T1 citations (full text available). Skips T2/T3.
|
|
8
|
-
Uses only local vLLM — no cloud services.
|
|
9
|
-
|
|
8
|
+
Uses only local vLLM — no cloud services. Manuscript text never leaves
|
|
9
|
+
the local machine.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -1421,16 +1421,16 @@ async def run_claim_verification(
|
|
|
1421
1421
|
config: LintConfig | None = None,
|
|
1422
1422
|
bib_format: str = "auto",
|
|
1423
1423
|
bib_path: Path | None = None,
|
|
1424
|
-
backend: str = "vllm",
|
|
1425
1424
|
model: str = "",
|
|
1426
1425
|
key_filter: str | None = None,
|
|
1427
1426
|
limit: int | None = None,
|
|
1428
1427
|
rerun: bool = False,
|
|
1429
1428
|
) -> list[dict]:
|
|
1430
|
-
"""Run claim verification for a paper.
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1429
|
+
"""Run claim verification for a paper against the local vLLM backend."""
|
|
1430
|
+
# ``backend`` is fixed to "vllm". The column is preserved on
|
|
1431
|
+
# ``claim_results`` to correctly invalidate any pre-0.5.0 rows that
|
|
1432
|
+
# stored a different value.
|
|
1433
|
+
backend = "vllm"
|
|
1434
1434
|
from sciwrite_lint.references.citations import check_local_sources, extract_bibitems
|
|
1435
1435
|
from sciwrite_lint.references.metadata import load_all_metadata
|
|
1436
1436
|
|
|
@@ -1449,7 +1449,7 @@ async def run_claim_verification(
|
|
|
1449
1449
|
# Standalone PDF (no prior build_pdf_context call)
|
|
1450
1450
|
from sciwrite_lint.pipeline import build_pdf_context, citations_from_pdf_context
|
|
1451
1451
|
|
|
1452
|
-
|
|
1452
|
+
await build_pdf_context(tex_path, config)
|
|
1453
1453
|
citations = citations_from_pdf_context(config)
|
|
1454
1454
|
else:
|
|
1455
1455
|
citations = extract_bibitems(tex_path, bib_format, bib_path=bib_path)
|
|
@@ -1532,42 +1532,52 @@ async def run_claim_verification(
|
|
|
1532
1532
|
# contexts from workspace.db. The standalone path has no findings
|
|
1533
1533
|
# list to populate, so any build-warning Finding is logged and
|
|
1534
1534
|
# dropped — pipeline runs route it to system_issues correctly.
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
)
|
|
1535
|
+
from sciwrite_lint.pipeline import (
|
|
1536
|
+
ensure_claim_query_vectors,
|
|
1537
|
+
persist_manuscript_citations,
|
|
1538
|
+
)
|
|
1540
1539
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1540
|
+
_, build_finding = persist_manuscript_citations(
|
|
1541
|
+
config, references_dir, tex_path=tex_path
|
|
1542
|
+
)
|
|
1543
|
+
if build_finding is not None:
|
|
1544
|
+
logger.warning(
|
|
1545
|
+
"Manuscript-context build warning surfaced (standalone "
|
|
1546
|
+
"verify-claims; not surfaced as system issue): {}",
|
|
1547
|
+
build_finding.context,
|
|
1543
1548
|
)
|
|
1544
|
-
|
|
1545
|
-
logger.warning(
|
|
1546
|
-
"Manuscript-context build warning surfaced (standalone "
|
|
1547
|
-
"verify-claims; not surfaced as system issue): {}",
|
|
1548
|
-
build_finding.context,
|
|
1549
|
-
)
|
|
1550
|
-
ensure_claim_query_vectors(references_dir, config)
|
|
1549
|
+
ensure_claim_query_vectors(references_dir, config)
|
|
1551
1550
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
logger.info("Backend: Claude Sonnet (via claude CLI)")
|
|
1556
|
-
else:
|
|
1557
|
-
vllm_model = model or config.llm_model or VLLM_DEFAULT_MODEL
|
|
1558
|
-
model_id = f"vllm:{vllm_model}"
|
|
1559
|
-
logger.info(f"Backend: vLLM ({VLLM_MODELS[vllm_model]['model']})")
|
|
1551
|
+
vllm_model = model or config.llm_model or VLLM_DEFAULT_MODEL
|
|
1552
|
+
model_id = f"vllm:{vllm_model}"
|
|
1553
|
+
logger.info(f"Backend: vLLM ({VLLM_MODELS[vllm_model]['model']})")
|
|
1560
1554
|
|
|
1561
1555
|
ref_paths: dict[str, Path | None] = {}
|
|
1562
1556
|
ref_types: dict[str, str] = {}
|
|
1557
|
+
ref_src_hashes: dict[str, str] = {}
|
|
1563
1558
|
ref_sections: dict[str, list[Section]] = {}
|
|
1564
1559
|
for key, local in local_files.items():
|
|
1565
1560
|
ref_paths[key] = _resolve_reference_path(local, references_dir)
|
|
1566
1561
|
meta = all_meta.get(key)
|
|
1567
1562
|
if meta:
|
|
1568
1563
|
ref_types[key] = meta.access.get("local_type", "none")
|
|
1564
|
+
ref_src_hashes[key] = meta.access.get("local_file_src_hash", "")
|
|
1569
1565
|
else:
|
|
1570
1566
|
ref_types[key] = "pdf" if local.endswith(".pdf") else "summary"
|
|
1567
|
+
ref_src_hashes[key] = ""
|
|
1568
|
+
|
|
1569
|
+
# parse_cache.pdf_hash moves whenever the GROBID-parsed PDF bytes
|
|
1570
|
+
# change — covers OA-fetched refs (which have no drop-folder src
|
|
1571
|
+
# hash) on top of the ref_src_hash signal for drop-folder refs.
|
|
1572
|
+
from sciwrite_lint.references.workspace_db import (
|
|
1573
|
+
get_db,
|
|
1574
|
+
load_all_parse_cache,
|
|
1575
|
+
)
|
|
1576
|
+
|
|
1577
|
+
ref_parse_hashes: dict[str, str] = {}
|
|
1578
|
+
with get_db(references_dir) as _pc_conn:
|
|
1579
|
+
for ref_key, row in load_all_parse_cache(_pc_conn).items():
|
|
1580
|
+
ref_parse_hashes[ref_key] = row.get("pdf_hash", "")
|
|
1571
1581
|
|
|
1572
1582
|
# Load previous results from workspace.db
|
|
1573
1583
|
from sciwrite_lint.references.workspace_db import (
|
|
@@ -1603,8 +1613,35 @@ async def run_claim_verification(
|
|
|
1603
1613
|
|
|
1604
1614
|
for i, claim in enumerate(verifiable):
|
|
1605
1615
|
pk = (claim.key, claim.line)
|
|
1606
|
-
|
|
1607
|
-
|
|
1616
|
+
cached = previous.get(pk)
|
|
1617
|
+
# Reuse a cached verdict only when every input that could change
|
|
1618
|
+
# it still matches: surrounding context, the backend/model that
|
|
1619
|
+
# produced it, the kind of local source consulted, and the
|
|
1620
|
+
# source-file hash (catches drop-folder PDF/MHTML re-ingests).
|
|
1621
|
+
# Empty cached ref_src_hash is treated as "no info" — we don't
|
|
1622
|
+
# invalidate on it alone, since OA-fetched refs have no ingest
|
|
1623
|
+
# hash to compare.
|
|
1624
|
+
cur_src_hash = ref_src_hashes.get(claim.key, "")
|
|
1625
|
+
cur_parse_hash = ref_parse_hashes.get(claim.key, "")
|
|
1626
|
+
cur_ref_type = ref_types.get(claim.key, "paper")
|
|
1627
|
+
if (
|
|
1628
|
+
cached is not None
|
|
1629
|
+
and cached.get("context", "") == claim.context
|
|
1630
|
+
and cached.get("backend", "") == backend
|
|
1631
|
+
and cached.get("model", "") == model_id
|
|
1632
|
+
and cached.get("ref_type", "") == cur_ref_type
|
|
1633
|
+
and (
|
|
1634
|
+
not cached.get("ref_src_hash", "")
|
|
1635
|
+
or not cur_src_hash
|
|
1636
|
+
or cached.get("ref_src_hash", "") == cur_src_hash
|
|
1637
|
+
)
|
|
1638
|
+
and (
|
|
1639
|
+
not cached.get("ref_parse_hash", "")
|
|
1640
|
+
or not cur_parse_hash
|
|
1641
|
+
or cached.get("ref_parse_hash", "") == cur_parse_hash
|
|
1642
|
+
)
|
|
1643
|
+
):
|
|
1644
|
+
results[i] = cached
|
|
1608
1645
|
skipped += 1
|
|
1609
1646
|
elif not ref_paths.get(claim.key):
|
|
1610
1647
|
results[i] = {
|
|
@@ -1628,11 +1665,6 @@ async def run_claim_verification(
|
|
|
1628
1665
|
ref_sections[claim.key] = split_sections(ref_text) if ref_text else []
|
|
1629
1666
|
|
|
1630
1667
|
_CLAIM_CONCURRENCY = 5
|
|
1631
|
-
# Use the dynamic controller only when the vllm backend is active.
|
|
1632
|
-
# Claude CLI has its own server-side admission, so a static
|
|
1633
|
-
# semaphore is the right tool for it. ``concurrency_slot`` handles
|
|
1634
|
-
# both branches uniformly so the loop body doesn't care which
|
|
1635
|
-
# backend is in play.
|
|
1636
1668
|
from sciwrite_lint.llm.concurrency_optimizer import (
|
|
1637
1669
|
ControllerParams,
|
|
1638
1670
|
concurrency_slot,
|
|
@@ -1643,7 +1675,7 @@ async def run_claim_verification(
|
|
|
1643
1675
|
target_kv_grow=config.concurrency_target_kv_grow,
|
|
1644
1676
|
target_kv_hi=config.concurrency_target_kv_hi,
|
|
1645
1677
|
)
|
|
1646
|
-
_use_dynamic =
|
|
1678
|
+
_use_dynamic = config.use_dynamic_concurrency
|
|
1647
1679
|
_slot_static_cap = (
|
|
1648
1680
|
config.llm_max_concurrency if _use_dynamic else _CLAIM_CONCURRENCY
|
|
1649
1681
|
)
|
|
@@ -1670,42 +1702,31 @@ async def run_claim_verification(
|
|
|
1670
1702
|
f"{ref_path.name} is a web page summary, not the actual paper"
|
|
1671
1703
|
)
|
|
1672
1704
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
logger.debug(f"Reference: {ref_path.name} ({len(sections)} sections)")
|
|
1700
|
-
verdict = await verify_claim_vllm(
|
|
1701
|
-
claim,
|
|
1702
|
-
sections,
|
|
1703
|
-
config=config,
|
|
1704
|
-
model_name=vllm_model,
|
|
1705
|
-
references_dir=references_dir,
|
|
1706
|
-
client=vllm_client,
|
|
1707
|
-
slot=_slot,
|
|
1708
|
-
)
|
|
1705
|
+
# vLLM: the slot wraps each individual LLM call inside
|
|
1706
|
+
# verify_claim_vllm, so the controller's dynamic cap bounds
|
|
1707
|
+
# the actual vLLM request volume regardless of how many
|
|
1708
|
+
# claims and ladder fan-outs run in parallel.
|
|
1709
|
+
sections = ref_sections[claim.key]
|
|
1710
|
+
if not sections:
|
|
1711
|
+
results[idx] = {
|
|
1712
|
+
"key": claim.key,
|
|
1713
|
+
"line": claim.line,
|
|
1714
|
+
"context": claim.context,
|
|
1715
|
+
"verdict": "CANNOT_DETERMINE",
|
|
1716
|
+
"explanation": "Could not read reference",
|
|
1717
|
+
}
|
|
1718
|
+
return
|
|
1719
|
+
|
|
1720
|
+
logger.debug(f"Reference: {ref_path.name} ({len(sections)} sections)")
|
|
1721
|
+
verdict = await verify_claim_vllm(
|
|
1722
|
+
claim,
|
|
1723
|
+
sections,
|
|
1724
|
+
config=config,
|
|
1725
|
+
model_name=vllm_model,
|
|
1726
|
+
references_dir=references_dir,
|
|
1727
|
+
client=vllm_client,
|
|
1728
|
+
slot=_slot,
|
|
1729
|
+
)
|
|
1709
1730
|
|
|
1710
1731
|
if verdict:
|
|
1711
1732
|
verdict["key"] = claim.key
|
|
@@ -1714,6 +1735,8 @@ async def run_claim_verification(
|
|
|
1714
1735
|
verdict["backend"] = backend
|
|
1715
1736
|
verdict["model"] = model_id
|
|
1716
1737
|
verdict["ref_type"] = ref_type
|
|
1738
|
+
verdict["ref_src_hash"] = ref_src_hashes.get(claim.key, "")
|
|
1739
|
+
verdict["ref_parse_hash"] = ref_parse_hashes.get(claim.key, "")
|
|
1717
1740
|
if ref_type == "web_page" and verdict.get("verdict") == "NOT_SUPPORTED":
|
|
1718
1741
|
verdict["verdict"] = "CANNOT_DETERMINE"
|
|
1719
1742
|
verdict["explanation"] = (
|
|
@@ -1744,23 +1767,18 @@ async def run_claim_verification(
|
|
|
1744
1767
|
label="claim-verify",
|
|
1745
1768
|
params=_ctrl_params,
|
|
1746
1769
|
) as _slot:
|
|
1747
|
-
|
|
1770
|
+
# Shared vLLM client for all claim verifications — avoids
|
|
1771
|
+
# creating and tearing down a connection per claim.
|
|
1772
|
+
from openai import AsyncOpenAI
|
|
1773
|
+
|
|
1774
|
+
async with AsyncOpenAI(
|
|
1775
|
+
base_url=config.llm_endpoint,
|
|
1776
|
+
api_key="dummy",
|
|
1777
|
+
timeout=config.llm_timeout,
|
|
1778
|
+
) as vllm_client:
|
|
1748
1779
|
await asyncio.gather(
|
|
1749
|
-
*[_verify_one(idx, c,
|
|
1780
|
+
*[_verify_one(idx, c, vllm_client) for idx, c in to_verify]
|
|
1750
1781
|
)
|
|
1751
|
-
else:
|
|
1752
|
-
# Shared vLLM client for all claim verifications — avoids
|
|
1753
|
-
# creating and tearing down a connection per claim.
|
|
1754
|
-
from openai import AsyncOpenAI
|
|
1755
|
-
|
|
1756
|
-
async with AsyncOpenAI(
|
|
1757
|
-
base_url=config.llm_endpoint,
|
|
1758
|
-
api_key="dummy",
|
|
1759
|
-
timeout=config.llm_timeout,
|
|
1760
|
-
) as vllm_client:
|
|
1761
|
-
await asyncio.gather(
|
|
1762
|
-
*[_verify_one(idx, c, vllm_client) for idx, c in to_verify]
|
|
1763
|
-
)
|
|
1764
1782
|
|
|
1765
1783
|
# Flatten — any None slots are claims that fell through (shouldn't happen)
|
|
1766
1784
|
verified_results = [r for r in results if r is not None]
|