sciwrite-lint 0.3.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.3.0 → sciwrite_lint-0.5.0}/PKG-INFO +37 -7
- sciwrite_lint-0.3.0/sciwrite_lint.egg-info/PKG-INFO → sciwrite_lint-0.5.0/README.md +25 -61
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/pyproject.toml +18 -41
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/__init__.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/__main__.py +64 -105
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/api.py +51 -6
- sciwrite_lint-0.5.0/sciwrite_lint/api_keys.py +93 -0
- sciwrite_lint-0.5.0/sciwrite_lint/checks/_diagnostics.py +237 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/_section_utils.py +27 -9
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/cite_purpose.py +5 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/cross_section_consistency.py +45 -19
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/dangling_cite.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/full_paper_consistency.py +88 -32
- sciwrite_lint-0.5.0/sciwrite_lint/checks/internal_consistency_pairs.py +473 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/prose_quality.py +38 -14
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/ref_internal_checks.py +103 -23
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/reference_unreliable.py +9 -3
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/registry.py +1 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/structure_promises.py +23 -12
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/claims.py +13 -4
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/claude_cli.py +8 -21
- sciwrite_lint-0.5.0/sciwrite_lint/cli/_common.py +106 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/check.py +86 -127
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/config.py +11 -81
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/fetch.py +2 -18
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/_monitor.py +517 -9
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/containers.py +3 -2
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/dismiss_claim.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/override.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/parse.py +6 -11
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/vision.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/vllm.py +6 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/rank.py +6 -2
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/verify.py +16 -9
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/config.py +220 -12
- sciwrite_lint-0.5.0/sciwrite_lint/cross_paper.py +59 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/eval_claims.py +732 -279
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/footnote_urls.py +5 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_orchestrator.py +12 -19
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_validation.py +52 -4
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/ideas.py +1 -16
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/latex_to_markdown.py +2 -2
- sciwrite_lint-0.5.0/sciwrite_lint/llm/__init__.py +5 -0
- sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/__init__.py +60 -0
- sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/compute_cap.py +84 -0
- sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/controller.py +464 -0
- sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/decide.py +462 -0
- sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/host_metrics.py +113 -0
- sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/metrics_probe.py +70 -0
- sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/registry.py +199 -0
- sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/telemetry.py +414 -0
- sciwrite_lint-0.3.0/sciwrite_lint/vllm/metrics.py → sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/vllm_metrics.py +34 -38
- sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/wiring.py +119 -0
- sciwrite_lint-0.5.0/sciwrite_lint/llm/manuscript_cache.py +173 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm_utils.py +292 -72
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/local_sources.py +5 -3
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/manuscript_store.py +162 -6
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/mhtml.py +17 -5
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/models.py +0 -4
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_web.py +2 -2
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pdf/grobid.py +159 -40
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pdf/pdf_download.py +2 -12
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/__init__.py +8 -4
- sciwrite_lint-0.5.0/sciwrite_lint/pipeline/checks.py +275 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/claims.py +2 -3
- sciwrite_lint-0.5.0/sciwrite_lint/pipeline/embeddings.py +350 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/fetch.py +17 -3
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/orchestration.py +38 -11
- sciwrite_lint-0.5.0/sciwrite_lint/pipeline/parse.py +153 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/runners.py +193 -38
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/staged.py +31 -25
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/swap.py +117 -89
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/tracking.py +2 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/verify.py +18 -5
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/vision.py +106 -13
- sciwrite_lint-0.5.0/sciwrite_lint/references/_embed_timing.py +63 -0
- {sciwrite_lint-0.3.0/sciwrite_lint/scoring → sciwrite_lint-0.5.0/sciwrite_lint/references}/chain.py +3 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/citations.py +57 -55
- sciwrite_lint-0.5.0/sciwrite_lint/references/embedding_store.py +429 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/matching.py +8 -34
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/metadata.py +6 -15
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/reference_store.py +420 -102
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/retraction_watch.py +4 -3
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/__init__.py +28 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/_core.py +95 -5
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/claim_results.py +31 -6
- sciwrite_lint-0.5.0/sciwrite_lint/references/workspace_db/manuscript_check_cache.py +103 -0
- sciwrite_lint-0.5.0/sciwrite_lint/references/workspace_db/manuscript_citations.py +89 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/report.py +14 -5
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/schemas.py +222 -6
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/scoring/contribution.py +2 -2
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/scoring/scilint_score.py +32 -10
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/usage.py +4 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/cache.py +60 -24
- sciwrite_lint-0.5.0/sciwrite_lint/vision/describe.py +1095 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/pipeline.py +1 -0
- sciwrite_lint-0.5.0/sciwrite_lint/vllm/metrics.py +42 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vllm/vllm_server.py +289 -20
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/web.py +16 -7
- sciwrite_lint-0.3.0/README.md → sciwrite_lint-0.5.0/sciwrite_lint.egg-info/PKG-INFO +91 -6
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/SOURCES.txt +20 -2
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/requires.txt +11 -0
- sciwrite_lint-0.3.0/sciwrite_lint/claude_backend.py +0 -94
- sciwrite_lint-0.3.0/sciwrite_lint/cross_paper.py +0 -174
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/checks.py +0 -64
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/embeddings.py +0 -234
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/parse.py +0 -57
- sciwrite_lint-0.3.0/sciwrite_lint/references/embedding_store.py +0 -389
- sciwrite_lint-0.3.0/sciwrite_lint/vision/describe.py +0 -632
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/LICENSE +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/_network.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/claim_support.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/dangling_ref.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/reference_accuracy.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/reference_exists.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/retracted_cite.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/grobid.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/init.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/exceptions.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_common.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_download.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_search.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/arxiv.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/biorxiv.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/core.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/eric.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/europepmc.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/hal.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/nasa_ads.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/nber.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/osf.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/pmc.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/s2.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/unpaywall.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_models.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_pdf.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_search.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pdf/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/pdf_context.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/ref_internal.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/unreliable.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/prompt_safety.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/py.typed +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/rate_limiter.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/crossref.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/bib_checks.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/citation_metadata.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/parse_cache.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/pipeline_stage.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/query_vectors.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/ref_internal.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/registry.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/vision_cache.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/scoring/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/tex_parser.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/image_extraction.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vllm/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/entry_points.txt +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/top_level.txt +0 -0
- {sciwrite_lint-0.3.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>
|
|
@@ -48,9 +48,20 @@ Requires-Dist: pdf2image>=1.17
|
|
|
48
48
|
Requires-Dist: lxml>=6.1
|
|
49
49
|
Requires-Dist: pypandoc-binary>=1.15
|
|
50
50
|
Requires-Dist: rfc3986>=2.0
|
|
51
|
+
Requires-Dist: psutil>=5.9
|
|
52
|
+
Requires-Dist: annotated-types>=0.6
|
|
51
53
|
Provides-Extra: dev
|
|
52
54
|
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
53
55
|
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
56
|
+
Requires-Dist: ruff>=0.15; extra == "dev"
|
|
57
|
+
Requires-Dist: mypy>=1.18; extra == "dev"
|
|
58
|
+
Requires-Dist: ast-grep-cli>=0.35; extra == "dev"
|
|
59
|
+
Requires-Dist: deptry>=0.25; extra == "dev"
|
|
60
|
+
Requires-Dist: bandit>=1.9; extra == "dev"
|
|
61
|
+
Requires-Dist: pip-audit>=2.10; extra == "dev"
|
|
62
|
+
Requires-Dist: pip-licenses>=5.5; extra == "dev"
|
|
63
|
+
Requires-Dist: twine>=6.0; extra == "dev"
|
|
64
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
54
65
|
Dynamic: license-file
|
|
55
66
|
|
|
56
67
|
# sciwrite-lint
|
|
@@ -136,22 +147,22 @@ sciwrite-lint contributions paper.pdf --format json
|
|
|
136
147
|
|
|
137
148
|
### Optimizations
|
|
138
149
|
|
|
139
|
-
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.
|
|
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:
|
|
140
151
|
|
|
141
|
-
- **
|
|
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
|
|
142
153
|
- **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
|
|
143
|
-
- **Per-call-site thinking budgets** — each LLM call site has
|
|
154
|
+
- **Per-call-site thinking budgets** — each LLM call site has its own (max_tokens, thinking_preset) pair tuned for that check's prompt and output shape
|
|
144
155
|
- **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
|
|
145
156
|
- **Batch-staged multi-paper pipeline** — when checking 2+ papers, GPU models load once per batch (vision/embedding/cited-vision) and vLLM/network stages run concurrently, giving a meaningful speedup over sequential per-paper runs. Tune via `--concurrency` (default 2, validated up to 4 on a single consumer GPU)
|
|
146
157
|
- **Phased API resolution** — citations flow through OpenAlex → Semantic Scholar → CrossRef → Open Library/LoC, each phase only processing what previous phases didn't resolve
|
|
147
158
|
- **14-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, NBER, RePEc/IDEAS, HAL, ERIC, NASA ADS, OSF Preprints, CORE
|
|
148
159
|
- **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
|
|
149
160
|
|
|
150
|
-
Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns.
|
|
161
|
+
Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. The pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50× faster than CPU) — same code path on WSL2 and native Linux.
|
|
151
162
|
|
|
152
163
|
## Install
|
|
153
164
|
|
|
154
|
-
**Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM).
|
|
165
|
+
**Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Tested on WSL2 with NVIDIA driver 546.01+. Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
|
|
155
166
|
|
|
156
167
|
Requires [uv](https://docs.astral.sh/uv/getting-started/installation/), a container runtime (podman or docker), CUDA drivers, and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html).
|
|
157
168
|
|
|
@@ -196,6 +207,14 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
|
|
|
196
207
|
sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
|
|
197
208
|
```
|
|
198
209
|
|
|
210
|
+
### WSL2: disable CUDA Sysmem Fallback (recommended)
|
|
211
|
+
|
|
212
|
+
By default Windows silently spills GPU overflow into system RAM via PCIe (30–100× slower than VRAM); you'll see this as a sudden throughput collapse during the heaviest pipeline stages with no error in the logs. Tell the NVIDIA driver to fail loudly instead:
|
|
213
|
+
|
|
214
|
+
> NVIDIA Control Panel → 3D Settings → Manage 3D Settings → *CUDA - Sysmem Fallback Policy* → **Prefer No Sysmem Fallback** → Apply.
|
|
215
|
+
|
|
216
|
+
Driver 546.01+. After this, WSL2 behaves like native Linux on overflow (CUDA OOM error). The pipeline already sequences GPU consumers so OOM should not happen in normal use; the setting just prevents silent slowdowns when something does go wrong. No setting needed on native Linux — `cudaMalloc` is already loud there.
|
|
217
|
+
|
|
199
218
|

|
|
200
219
|
|
|
201
220
|
`init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
|
|
@@ -263,7 +282,18 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
|
|
|
263
282
|
|
|
264
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.
|
|
265
284
|
|
|
266
|
-
|
|
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.
|
|
267
297
|
|
|
268
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.
|
|
269
299
|
|
|
@@ -1,58 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sciwrite-lint
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: A linter for scientific manuscripts — reference verification, consistency checking, and structural validation.
|
|
5
|
-
Author: Sergey Samsonau
|
|
6
|
-
Maintainer-email: Authentic Research Partners LLC <info@arpconnect.com>
|
|
7
|
-
License-Expression: MIT
|
|
8
|
-
Project-URL: Homepage, https://arpconnect.com
|
|
9
|
-
Project-URL: Repository, https://github.com/authentic-research-partners/sciwrite-lint
|
|
10
|
-
Project-URL: Documentation, https://github.com/authentic-research-partners/sciwrite-lint#readme
|
|
11
|
-
Project-URL: Issues, https://github.com/authentic-research-partners/sciwrite-lint/issues
|
|
12
|
-
Project-URL: Paper, https://arxiv.org/abs/2604.08501
|
|
13
|
-
Keywords: linter,latex,bibtex,pdf,scientific-writing,academic-writing,citation-checker,reference-verification,claim-verification,manuscript,peer-review,arxiv,vllm,qwen
|
|
14
|
-
Classifier: Development Status :: 3 - Alpha
|
|
15
|
-
Classifier: Intended Audience :: Developers
|
|
16
|
-
Classifier: Intended Audience :: Science/Research
|
|
17
|
-
Classifier: Topic :: Scientific/Engineering
|
|
18
|
-
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
-
Classifier: Programming Language :: Python :: 3
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
-
Classifier: Typing :: Typed
|
|
22
|
-
Requires-Python: <3.14,>=3.13
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
License-File: LICENSE
|
|
25
|
-
Requires-Dist: httpx>=0.28
|
|
26
|
-
Requires-Dist: tenacity>=9.0
|
|
27
|
-
Requires-Dist: trafilatura>=2.0
|
|
28
|
-
Requires-Dist: aiosqlite>=0.22
|
|
29
|
-
Requires-Dist: loguru>=0.7
|
|
30
|
-
Requires-Dist: anyascii>=0.3
|
|
31
|
-
Requires-Dist: rich>=14.0
|
|
32
|
-
Requires-Dist: defusedxml>=0.7
|
|
33
|
-
Requires-Dist: pydantic>=2.10
|
|
34
|
-
Requires-Dist: rapidfuzz>=3.0
|
|
35
|
-
Requires-Dist: bibtexparser>=1.4
|
|
36
|
-
Requires-Dist: pdfplumber>=0.11
|
|
37
|
-
Requires-Dist: openai>=2.0
|
|
38
|
-
Requires-Dist: grobid-tei-xml>=0.1
|
|
39
|
-
Requires-Dist: sentence-transformers>=5.0
|
|
40
|
-
Requires-Dist: numpy>=2.0
|
|
41
|
-
Requires-Dist: sqlite-vec>=0.1.6
|
|
42
|
-
Requires-Dist: torch>=2.5
|
|
43
|
-
Requires-Dist: transformers>=4.50
|
|
44
|
-
Requires-Dist: Pillow>=12.2
|
|
45
|
-
Requires-Dist: pypdf>=6.10.2
|
|
46
|
-
Requires-Dist: pypdfium2>=5.0
|
|
47
|
-
Requires-Dist: pdf2image>=1.17
|
|
48
|
-
Requires-Dist: lxml>=6.1
|
|
49
|
-
Requires-Dist: pypandoc-binary>=1.15
|
|
50
|
-
Requires-Dist: rfc3986>=2.0
|
|
51
|
-
Provides-Extra: dev
|
|
52
|
-
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
53
|
-
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
54
|
-
Dynamic: license-file
|
|
55
|
-
|
|
56
1
|
# sciwrite-lint
|
|
57
2
|
|
|
58
3
|
[](https://arxiv.org/abs/2604.08501)
|
|
@@ -136,22 +81,22 @@ sciwrite-lint contributions paper.pdf --format json
|
|
|
136
81
|
|
|
137
82
|
### Optimizations
|
|
138
83
|
|
|
139
|
-
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.
|
|
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:
|
|
140
85
|
|
|
141
|
-
- **
|
|
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
|
|
142
87
|
- **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
|
|
143
|
-
- **Per-call-site thinking budgets** — each LLM call site has
|
|
88
|
+
- **Per-call-site thinking budgets** — each LLM call site has its own (max_tokens, thinking_preset) pair tuned for that check's prompt and output shape
|
|
144
89
|
- **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
|
|
145
90
|
- **Batch-staged multi-paper pipeline** — when checking 2+ papers, GPU models load once per batch (vision/embedding/cited-vision) and vLLM/network stages run concurrently, giving a meaningful speedup over sequential per-paper runs. Tune via `--concurrency` (default 2, validated up to 4 on a single consumer GPU)
|
|
146
91
|
- **Phased API resolution** — citations flow through OpenAlex → Semantic Scholar → CrossRef → Open Library/LoC, each phase only processing what previous phases didn't resolve
|
|
147
92
|
- **14-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, NBER, RePEc/IDEAS, HAL, ERIC, NASA ADS, OSF Preprints, CORE
|
|
148
93
|
- **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
|
|
149
94
|
|
|
150
|
-
Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns.
|
|
95
|
+
Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. The pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50× faster than CPU) — same code path on WSL2 and native Linux.
|
|
151
96
|
|
|
152
97
|
## Install
|
|
153
98
|
|
|
154
|
-
**Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM).
|
|
99
|
+
**Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Tested on WSL2 with NVIDIA driver 546.01+. Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
|
|
155
100
|
|
|
156
101
|
Requires [uv](https://docs.astral.sh/uv/getting-started/installation/), a container runtime (podman or docker), CUDA drivers, and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html).
|
|
157
102
|
|
|
@@ -196,6 +141,14 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
|
|
|
196
141
|
sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
|
|
197
142
|
```
|
|
198
143
|
|
|
144
|
+
### WSL2: disable CUDA Sysmem Fallback (recommended)
|
|
145
|
+
|
|
146
|
+
By default Windows silently spills GPU overflow into system RAM via PCIe (30–100× slower than VRAM); you'll see this as a sudden throughput collapse during the heaviest pipeline stages with no error in the logs. Tell the NVIDIA driver to fail loudly instead:
|
|
147
|
+
|
|
148
|
+
> NVIDIA Control Panel → 3D Settings → Manage 3D Settings → *CUDA - Sysmem Fallback Policy* → **Prefer No Sysmem Fallback** → Apply.
|
|
149
|
+
|
|
150
|
+
Driver 546.01+. After this, WSL2 behaves like native Linux on overflow (CUDA OOM error). The pipeline already sequences GPU consumers so OOM should not happen in normal use; the setting just prevents silent slowdowns when something does go wrong. No setting needed on native Linux — `cudaMalloc` is already loud there.
|
|
151
|
+
|
|
199
152
|

|
|
200
153
|
|
|
201
154
|
`init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
|
|
@@ -263,7 +216,18 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
|
|
|
263
216
|
|
|
264
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.
|
|
265
218
|
|
|
266
|
-
|
|
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.
|
|
267
231
|
|
|
268
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.
|
|
269
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"
|
|
@@ -49,6 +49,8 @@ dependencies = [
|
|
|
49
49
|
"lxml>=6.1",
|
|
50
50
|
"pypandoc-binary>=1.15",
|
|
51
51
|
"rfc3986>=2.0",
|
|
52
|
+
"psutil>=5.9",
|
|
53
|
+
"annotated-types>=0.6",
|
|
52
54
|
]
|
|
53
55
|
|
|
54
56
|
[project.urls]
|
|
@@ -59,9 +61,23 @@ Issues = "https://github.com/authentic-research-partners/sciwrite-lint/issues"
|
|
|
59
61
|
Paper = "https://arxiv.org/abs/2604.08501"
|
|
60
62
|
|
|
61
63
|
[project.optional-dependencies]
|
|
64
|
+
# `pip install -e ".[dev]"` brings in every tool used by the mandatory checks
|
|
65
|
+
# and the release pipeline. Exception: the `claude` CLI is a Node app —
|
|
66
|
+
# install separately per https://docs.anthropic.com/en/docs/claude-code.
|
|
62
67
|
dev = [
|
|
68
|
+
# Mandatory checks — run after every code change
|
|
63
69
|
"pytest>=9.0.3",
|
|
64
70
|
"pytest-xdist>=3.0",
|
|
71
|
+
"ruff>=0.15",
|
|
72
|
+
"mypy>=1.18",
|
|
73
|
+
"ast-grep-cli>=0.35",
|
|
74
|
+
# Release pipeline (scripts/release.sh phases 3–6)
|
|
75
|
+
"deptry>=0.25",
|
|
76
|
+
"bandit>=1.9",
|
|
77
|
+
"pip-audit>=2.10",
|
|
78
|
+
"pip-licenses>=5.5",
|
|
79
|
+
"twine>=6.0",
|
|
80
|
+
"build>=1.0",
|
|
65
81
|
]
|
|
66
82
|
|
|
67
83
|
[project.scripts]
|
|
@@ -70,44 +86,5 @@ sciwrite-lint = "sciwrite_lint.__main__:main"
|
|
|
70
86
|
[tool.setuptools.packages.find]
|
|
71
87
|
include = ["sciwrite_lint*"]
|
|
72
88
|
|
|
73
|
-
[tool.
|
|
74
|
-
testpaths = ["tests"]
|
|
75
|
-
pythonpath = ["."]
|
|
76
|
-
markers = [
|
|
77
|
-
"integration: tests that read actual paper files",
|
|
78
|
-
"network: tests that make real network connections (slow)",
|
|
79
|
-
]
|
|
80
|
-
# PID-unique basetemp set in conftest.py — prevents FileExistsError
|
|
81
|
-
# when multiple pytest processes run concurrently.
|
|
82
|
-
addopts = "-n auto -p no:cacheprovider"
|
|
83
|
-
|
|
84
|
-
[tool.bandit]
|
|
85
|
-
skips = [
|
|
86
|
-
"B101", # assert — used for type narrowing and internal invariants
|
|
87
|
-
"B104", # bind 0.0.0.0 — container start commands for GROBID/vLLM
|
|
88
|
-
"B314", # xml.etree.fromstring — parsing trusted GROBID TEI output
|
|
89
|
-
"B404", # subprocess import — CLI tool, subprocess is core functionality
|
|
90
|
-
"B405", # xml.etree import — parsing trusted GROBID TEI output
|
|
91
|
-
"B603", # subprocess call — CLI invokes grobid/vllm/claude by design
|
|
92
|
-
"B607", # partial path — CLI tools resolved via PATH
|
|
93
|
-
"B608", # SQL string — internal usage.db, no user input in queries
|
|
94
|
-
"B615", # HF from_pretrained without revision pin — trusted publisher (Qwen), research tool
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
[tool.deptry]
|
|
98
|
-
optional_dependencies_dev_groups = ["dev"]
|
|
99
|
-
|
|
100
|
-
[tool.deptry.per_rule_ignores]
|
|
101
|
-
# Self-imports of shipped non-package directories
|
|
102
|
-
DEP001 = ["evals", "eval_real_world", "bench"]
|
|
103
|
-
# Lazy/conditional imports in optional groups — deptry can't detect these
|
|
104
|
-
DEP002 = ["bibtexparser", "pdfplumber", "sentence-transformers", "numpy", "sqlite-vec", "grobid-tei-xml", "torch", "transformers", "Pillow", "pypdf", "pdf2image"]
|
|
105
|
-
# Intra-package imports
|
|
106
|
-
DEP003 = ["sciwrite_lint", "evals", "eval_real_world"]
|
|
89
|
+
[tool.setuptools.package-data]
|
|
107
90
|
|
|
108
|
-
[tool.mypy]
|
|
109
|
-
plugins = ["pydantic.mypy"]
|
|
110
|
-
[[tool.mypy.overrides]]
|
|
111
|
-
# No py.typed marker or typeshed stubs available
|
|
112
|
-
module = ["bibtexparser", "sqlite_vec", "pypdf", "pypdfium2", "pypdfium2.raw", "transformers", "pdf2image", "lxml", "lxml.html", "lxml.html.etree", "rfc3986", "pypandoc"]
|
|
113
|
-
ignore_missing_imports = true
|
|
@@ -14,113 +14,55 @@ 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
|
-
from sciwrite_lint.
|
|
18
|
+
from sciwrite_lint.cli._common import (
|
|
19
|
+
_classify_verify_issue as _classify_verify_issue,
|
|
20
|
+
_load_config as _load_config,
|
|
21
|
+
_paper_names as _paper_names,
|
|
22
|
+
_resolve_input_files as _resolve_input_files,
|
|
23
|
+
_resolve_paper as _resolve_paper,
|
|
24
|
+
_setup_logging,
|
|
25
|
+
)
|
|
26
|
+
from sciwrite_lint.config import LintConfig, load_config
|
|
21
27
|
from sciwrite_lint.vllm.vllm_server import MODELS as _VLLM_MODELS
|
|
22
28
|
|
|
23
29
|
_ALL_MODELS = sorted(_VLLM_MODELS.keys())
|
|
24
30
|
_TEXT_MODELS = [k for k, v in _VLLM_MODELS.items() if v["kind"] == "text"]
|
|
25
31
|
|
|
32
|
+
_REPLACE_HELP = (
|
|
33
|
+
"Stop any other running vLLM container first (two vLLMs can't "
|
|
34
|
+
"share the GPU safely; without this flag the start is refused)"
|
|
35
|
+
)
|
|
36
|
+
|
|
26
37
|
|
|
27
38
|
# ---------------------------------------------------------------------------
|
|
28
|
-
#
|
|
39
|
+
# Main parser
|
|
29
40
|
# ---------------------------------------------------------------------------
|
|
30
41
|
|
|
31
42
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
config = load_config(None)
|
|
38
|
-
if config.config_path is None:
|
|
39
|
-
from sciwrite_lint.config import _detect_papers
|
|
40
|
-
|
|
41
|
-
logger.error("No .sciwrite-lint.toml found.")
|
|
42
|
-
detected = _detect_papers()
|
|
43
|
-
if detected:
|
|
44
|
-
logger.error(" Detected .tex files:")
|
|
45
|
-
for p in detected:
|
|
46
|
-
bib = f" (bib: {p['bib']})" if p.get("bib") else ""
|
|
47
|
-
logger.error(f" {p['file_path']}{bib}")
|
|
48
|
-
logger.error(" Run: sciwrite-lint init")
|
|
49
|
-
logger.error(" Then review .sciwrite-lint.toml before running checks.")
|
|
50
|
-
return config
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _resolve_paper(config: LintConfig, name: str) -> PaperConfig | None:
|
|
54
|
-
"""Resolve a paper name to its config. Print error if not found."""
|
|
55
|
-
pc = config.get_paper(name)
|
|
56
|
-
if not pc:
|
|
57
|
-
if config.papers:
|
|
58
|
-
names = ", ".join(p.name for p in config.papers)
|
|
59
|
-
logger.error(f"Unknown paper '{name}'. Registered: {names}")
|
|
60
|
-
else:
|
|
61
|
-
logger.error(f"Unknown paper '{name}'. No papers registered.")
|
|
62
|
-
logger.error(
|
|
63
|
-
f" Add [[papers]] to {config.config_path or '.sciwrite-lint.toml'}"
|
|
64
|
-
)
|
|
65
|
-
return pc
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _paper_names(config: LintConfig) -> list[str]:
|
|
69
|
-
return [p.name for p in config.papers]
|
|
70
|
-
|
|
43
|
+
def _register_optional_extensions(
|
|
44
|
+
sub: argparse._SubParsersAction, # type: ignore[type-arg]
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Load CLI extensions discovered under ``sciwrite_lint``.
|
|
71
47
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
Priority: positional file > --paper (from config) > all papers in config.
|
|
78
|
-
Returns list of (name, path) pairs.
|
|
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.
|
|
79
53
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return [(p.stem, p)]
|
|
83
|
-
|
|
84
|
-
paper_filter = getattr(args, "paper", None)
|
|
85
|
-
if paper_filter:
|
|
86
|
-
pc = _resolve_paper(config, paper_filter)
|
|
87
|
-
if not pc:
|
|
88
|
-
return []
|
|
89
|
-
return [(pc.name, pc.file_path)]
|
|
90
|
-
|
|
91
|
-
if config.papers:
|
|
92
|
-
return [(pc.name, pc.file_path) for pc in config.papers]
|
|
93
|
-
|
|
94
|
-
logger.error("No papers registered. Either:")
|
|
95
|
-
logger.error(" sciwrite-lint check <file.tex|file.pdf> — check a specific file")
|
|
96
|
-
logger.error(
|
|
97
|
-
" sciwrite-lint init — set up project with [[papers]]"
|
|
98
|
-
)
|
|
99
|
-
return []
|
|
100
|
-
|
|
54
|
+
import importlib
|
|
55
|
+
import pkgutil
|
|
101
56
|
|
|
102
|
-
|
|
103
|
-
"""Classify a verify issue string into (level, rule_id) for findings."""
|
|
104
|
-
from sciwrite_lint.pipeline import _classify_verify_issue as _classify
|
|
57
|
+
import sciwrite_lint as _pkg
|
|
105
58
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
rotation="10 MB",
|
|
114
|
-
retention="30 days",
|
|
115
|
-
compression="gz",
|
|
116
|
-
level=config.log_level,
|
|
117
|
-
format="{time:YYYY-MM-DD HH:mm:ss} | {level:<8} | {name}:{function}:{line} | {message}",
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# ---------------------------------------------------------------------------
|
|
122
|
-
# Main parser
|
|
123
|
-
# ---------------------------------------------------------------------------
|
|
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)
|
|
124
66
|
|
|
125
67
|
|
|
126
68
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -206,6 +148,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
206
148
|
p_checks = sub.add_parser("checks", help="List all registered checks")
|
|
207
149
|
p_checks.set_defaults(func=run_checks_list)
|
|
208
150
|
|
|
151
|
+
# Load optional underscore-prefixed CLI extension subpackages (if any).
|
|
152
|
+
_register_optional_extensions(sub)
|
|
153
|
+
|
|
209
154
|
# --- init ---
|
|
210
155
|
p_init = sub.add_parser(
|
|
211
156
|
"init", help="Initialize project: config + references directory"
|
|
@@ -309,12 +254,6 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
309
254
|
# --- verify-claims ---
|
|
310
255
|
p_vc = sub.add_parser("verify-claims", help="Verify claims against cited sources")
|
|
311
256
|
p_vc.add_argument("--paper", required=True, help="Paper to verify")
|
|
312
|
-
p_vc.add_argument(
|
|
313
|
-
"--backend",
|
|
314
|
-
choices=["vllm", "claude"],
|
|
315
|
-
default="vllm",
|
|
316
|
-
help="vllm (local LLM, fast) or claude (Claude CLI, deep)",
|
|
317
|
-
)
|
|
318
257
|
p_vc.add_argument(
|
|
319
258
|
"--model",
|
|
320
259
|
choices=_TEXT_MODELS,
|
|
@@ -457,6 +396,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
457
396
|
action="store_true",
|
|
458
397
|
help="Also start vision vLLM (qwen3-vl-8b-fp8 on port 5002)",
|
|
459
398
|
)
|
|
399
|
+
p_containers.add_argument("--replace", action="store_true", help=_REPLACE_HELP)
|
|
460
400
|
p_containers.add_argument("--config", help="Path to .sciwrite-lint.toml")
|
|
461
401
|
p_containers.set_defaults(func=run_containers)
|
|
462
402
|
|
|
@@ -488,6 +428,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
488
428
|
action="store_true",
|
|
489
429
|
help="Pull latest vLLM image before starting",
|
|
490
430
|
)
|
|
431
|
+
p_vllm_start.add_argument("--replace", action="store_true", help=_REPLACE_HELP)
|
|
491
432
|
p_vllm_start.add_argument("--config", help="Path to .sciwrite-lint.toml")
|
|
492
433
|
p_vllm_start.set_defaults(func=run_vllm)
|
|
493
434
|
|
|
@@ -536,14 +477,32 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
536
477
|
p_vllm.print_help()
|
|
537
478
|
return 0
|
|
538
479
|
|
|
539
|
-
# Set up file logging from config (stderr sink stays at INFO by default)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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":
|
|
546
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
|
|
547
506
|
_setup_logging(config)
|
|
548
507
|
|
|
549
508
|
return args.func(args)
|
|
@@ -116,8 +116,14 @@ class CitationAPI:
|
|
|
116
116
|
client=self._client,
|
|
117
117
|
)
|
|
118
118
|
except Exception as e:
|
|
119
|
+
# The waterfall caller distinguishes ``{"error": …}`` from
|
|
120
|
+
# ``None`` (not-found): error dicts mean "lookup itself
|
|
121
|
+
# failed" and stay visible, ``None`` means "no result for
|
|
122
|
+
# this citation". All API lookups in this module return the
|
|
123
|
+
# error shape so transient network failures don't masquerade
|
|
124
|
+
# as misses.
|
|
119
125
|
logger.debug("CrossRef lookup failed for {}: {}", citation.key, e)
|
|
120
|
-
return
|
|
126
|
+
return {"error": str(e), "source": "crossref"}
|
|
121
127
|
|
|
122
128
|
# ------------------------------------------------------------------
|
|
123
129
|
# OpenAlex
|
|
@@ -502,6 +508,13 @@ async def batch_openalex(
|
|
|
502
508
|
label="OpenAlex batch",
|
|
503
509
|
)
|
|
504
510
|
if resp.status_code != 200:
|
|
511
|
+
# API failure ≠ "no matches". Log at WARNING so the operator
|
|
512
|
+
# sees the API was unreachable; the verify-cascade still
|
|
513
|
+
# falls through to S2/CrossRef per the pipeline contract.
|
|
514
|
+
logger.warning(
|
|
515
|
+
"OpenAlex batch returned status {}; cascading to next provider",
|
|
516
|
+
resp.status_code,
|
|
517
|
+
)
|
|
505
518
|
return results
|
|
506
519
|
works = resp.json().get("results", [])
|
|
507
520
|
for work in works:
|
|
@@ -552,7 +565,7 @@ async def batch_s2(
|
|
|
552
565
|
async with tracked("semantic_scholar", ids=len(ids)):
|
|
553
566
|
try:
|
|
554
567
|
resp = await retry_on_transient(
|
|
555
|
-
lambda: client.post(
|
|
568
|
+
lambda ids=ids: client.post(
|
|
556
569
|
"https://api.semanticscholar.org/graph/v1/paper/batch",
|
|
557
570
|
json={"ids": ids},
|
|
558
571
|
params={"fields": _S2_FIELDS},
|
|
@@ -560,13 +573,27 @@ async def batch_s2(
|
|
|
560
573
|
label="S2 batch",
|
|
561
574
|
)
|
|
562
575
|
if resp.status_code != 200:
|
|
576
|
+
# API failure ≠ "no matches". Log at WARNING so the
|
|
577
|
+
# operator sees the API was unreachable.
|
|
578
|
+
logger.warning(
|
|
579
|
+
"S2 batch returned status {} for {} IDs; "
|
|
580
|
+
"cascading to next provider",
|
|
581
|
+
resp.status_code,
|
|
582
|
+
len(ids),
|
|
583
|
+
)
|
|
563
584
|
continue
|
|
564
585
|
papers = resp.json()
|
|
565
586
|
for paper, (key, _s2_id) in zip(papers, batch):
|
|
566
587
|
if paper: # S2 returns null for not-found
|
|
567
588
|
results[key] = _parse_s2(paper)
|
|
568
589
|
except httpx.HTTPError as e:
|
|
569
|
-
|
|
590
|
+
# Network error ≠ "no matches". WARNING so the operator
|
|
591
|
+
# can see API was unreachable; cascade continues.
|
|
592
|
+
logger.warning(
|
|
593
|
+
"S2 batch request failed ({}: {}); cascading to next provider",
|
|
594
|
+
type(e).__name__,
|
|
595
|
+
e,
|
|
596
|
+
)
|
|
570
597
|
continue
|
|
571
598
|
|
|
572
599
|
return results
|
|
@@ -606,10 +633,28 @@ async def parallel_crossref(
|
|
|
606
633
|
polite_email=config.polite_email,
|
|
607
634
|
client=client,
|
|
608
635
|
)
|
|
609
|
-
if result
|
|
610
|
-
|
|
636
|
+
if result is None:
|
|
637
|
+
return # not-found — fine, next provider handles
|
|
638
|
+
if result.get("error"):
|
|
639
|
+
# API failure ≠ "no match". Log at WARNING so
|
|
640
|
+
# the operator sees CrossRef was unreachable;
|
|
641
|
+
# don't propagate the error dict into ``results``
|
|
642
|
+
# because callers expect parsed-result shape.
|
|
643
|
+
logger.warning(
|
|
644
|
+
"CrossRef lookup failed for {}: {}",
|
|
645
|
+
c.key,
|
|
646
|
+
result["error"],
|
|
647
|
+
)
|
|
648
|
+
return
|
|
649
|
+
results[c.key] = result
|
|
611
650
|
except Exception as e:
|
|
612
|
-
|
|
651
|
+
# Network error ≠ "no match". WARNING so visible.
|
|
652
|
+
logger.warning(
|
|
653
|
+
"CrossRef lookup raised for {} ({}: {})",
|
|
654
|
+
c.key,
|
|
655
|
+
type(e).__name__,
|
|
656
|
+
e,
|
|
657
|
+
)
|
|
613
658
|
|
|
614
659
|
await asyncio.gather(*[_lookup_one(c) for c in citations])
|
|
615
660
|
return results
|