sciwrite-lint 0.2.1__tar.gz → 0.3.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.2.1/sciwrite_lint.egg-info → sciwrite_lint-0.3.0}/PKG-INFO +99 -11
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/README.md +91 -7
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/pyproject.toml +9 -5
- sciwrite_lint-0.3.0/sciwrite_lint/__init__.py +28 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/__main__.py +11 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/_network.py +17 -3
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/api.py +23 -5
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/full_paper_consistency.py +6 -2
- sciwrite_lint-0.3.0/sciwrite_lint/checks/prose_quality.py +541 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/ref_internal_checks.py +17 -3
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/reference_exists.py +18 -2
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/registry.py +1 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/check.py +96 -27
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/config.py +5 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/fetch.py +81 -25
- sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/__init__.py +62 -0
- sciwrite_lint-0.2.1/sciwrite_lint/cli/misc.py → sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/_monitor.py +7 -584
- sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/containers.py +216 -0
- sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/dismiss_claim.py +62 -0
- sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/grobid.py +53 -0
- sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/init.py +14 -0
- sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/override.py +66 -0
- sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/parse.py +100 -0
- sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/vision.py +60 -0
- sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/vllm.py +42 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/verify.py +83 -35
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/config.py +118 -3
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/eval_claims.py +85 -6
- sciwrite_lint-0.3.0/sciwrite_lint/exceptions.py +17 -0
- sciwrite_lint-0.3.0/sciwrite_lint/footnote_urls.py +580 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/__init__.py +193 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_common.py +221 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_download.py +125 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_orchestrator.py +127 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_search.py +219 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_validation.py +413 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/arxiv.py +30 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/biorxiv.py +36 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/core.py +98 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/eric.py +159 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/europepmc.py +76 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/hal.py +171 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/ideas.py +239 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/nasa_ads.py +214 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/nber.py +157 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/osf.py +218 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/pmc.py +69 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/s2.py +28 -0
- sciwrite_lint-0.3.0/sciwrite_lint/fulltext/unpaywall.py +59 -0
- sciwrite_lint-0.3.0/sciwrite_lint/latex_to_markdown.py +136 -0
- sciwrite_lint-0.3.0/sciwrite_lint/llm_utils.py +606 -0
- sciwrite_lint-0.3.0/sciwrite_lint/local_sources.py +420 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/manuscript_store.py +100 -12
- sciwrite_lint-0.3.0/sciwrite_lint/mhtml.py +197 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/models.py +2 -0
- sciwrite_lint-0.3.0/sciwrite_lint/oa/__init__.py +42 -0
- sciwrite_lint-0.3.0/sciwrite_lint/oa/_models.py +91 -0
- sciwrite_lint-0.3.0/sciwrite_lint/oa/_pdf.py +404 -0
- sciwrite_lint-0.3.0/sciwrite_lint/oa/_search.py +113 -0
- sciwrite_lint-0.3.0/sciwrite_lint/oa/_web.py +165 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/pdf/grobid.py +109 -1
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/pdf/pdf_download.py +29 -31
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/__init__.py +147 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/checks.py +64 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/claims.py +116 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/embeddings.py +234 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/fetch.py +276 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/orchestration.py +413 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/parse.py +57 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/pdf_context.py +87 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/ref_internal.py +26 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/runners.py +496 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/staged.py +755 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/swap.py +277 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/tracking.py +105 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/unreliable.py +47 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/verify.py +526 -0
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/vision.py +191 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/citations.py +58 -4
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/metadata.py +10 -2
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/__init__.py +134 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/_core.py +329 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/bib_checks.py +82 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/citation_metadata.py +178 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/claim_results.py +144 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/parse_cache.py +136 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/pipeline_stage.py +82 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/query_vectors.py +32 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/ref_internal.py +101 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/registry.py +259 -0
- sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/vision_cache.py +143 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/schemas.py +32 -3
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/tex_parser.py +67 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/pipeline.py +3 -3
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vllm/vllm_server.py +1 -0
- sciwrite_lint-0.3.0/sciwrite_lint/web.py +595 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0/sciwrite_lint.egg-info}/PKG-INFO +99 -11
- sciwrite_lint-0.3.0/sciwrite_lint.egg-info/SOURCES.txt +142 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/requires.txt +6 -3
- sciwrite_lint-0.2.1/sciwrite_lint/__init__.py +0 -3
- sciwrite_lint-0.2.1/sciwrite_lint/fulltext.py +0 -851
- sciwrite_lint-0.2.1/sciwrite_lint/llm_utils.py +0 -415
- sciwrite_lint-0.2.1/sciwrite_lint/local_pdfs.py +0 -122
- sciwrite_lint-0.2.1/sciwrite_lint/pipeline.py +0 -3097
- sciwrite_lint-0.2.1/sciwrite_lint/references/workspace_db.py +0 -1508
- sciwrite_lint-0.2.1/sciwrite_lint/web.py +0 -369
- sciwrite_lint-0.2.1/sciwrite_lint.egg-info/SOURCES.txt +0 -80
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/LICENSE +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/__init__.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/_section_utils.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/cite_purpose.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/claim_support.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/cross_section_consistency.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/dangling_cite.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/dangling_ref.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/reference_accuracy.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/reference_unreliable.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/retracted_cite.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/structure_promises.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/claims.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/claude_backend.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/claude_cli.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/__init__.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/rank.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cross_paper.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/pdf/__init__.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/prompt_safety.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/py.typed +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/rate_limiter.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/__init__.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/crossref.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/embedding_store.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/matching.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/reference_store.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/retraction_watch.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/report.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/__init__.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/chain.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/contribution.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/scilint_score.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/usage.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/__init__.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/cache.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/describe.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/image_extraction.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vllm/__init__.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vllm/metrics.py +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/entry_points.txt +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/top_level.txt +0 -0
- {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.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.3.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>
|
|
@@ -9,6 +9,7 @@ Project-URL: Homepage, https://arpconnect.com
|
|
|
9
9
|
Project-URL: Repository, https://github.com/authentic-research-partners/sciwrite-lint
|
|
10
10
|
Project-URL: Documentation, https://github.com/authentic-research-partners/sciwrite-lint#readme
|
|
11
11
|
Project-URL: Issues, https://github.com/authentic-research-partners/sciwrite-lint/issues
|
|
12
|
+
Project-URL: Paper, https://arxiv.org/abs/2604.08501
|
|
12
13
|
Keywords: linter,latex,bibtex,pdf,scientific-writing,academic-writing,citation-checker,reference-verification,claim-verification,manuscript,peer-review,arxiv,vllm,qwen
|
|
13
14
|
Classifier: Development Status :: 3 - Alpha
|
|
14
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -40,17 +41,22 @@ Requires-Dist: numpy>=2.0
|
|
|
40
41
|
Requires-Dist: sqlite-vec>=0.1.6
|
|
41
42
|
Requires-Dist: torch>=2.5
|
|
42
43
|
Requires-Dist: transformers>=4.50
|
|
43
|
-
Requires-Dist: Pillow>=12.
|
|
44
|
-
Requires-Dist: pypdf>=6.
|
|
44
|
+
Requires-Dist: Pillow>=12.2
|
|
45
|
+
Requires-Dist: pypdf>=6.10.2
|
|
45
46
|
Requires-Dist: pypdfium2>=5.0
|
|
46
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
|
|
47
51
|
Provides-Extra: dev
|
|
48
|
-
Requires-Dist: pytest>=
|
|
52
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
49
53
|
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
50
54
|
Dynamic: license-file
|
|
51
55
|
|
|
52
56
|
# sciwrite-lint
|
|
53
57
|
|
|
58
|
+
[](https://arxiv.org/abs/2604.08501)
|
|
59
|
+
|
|
54
60
|
A linter for scientific manuscripts. Checks that your references exist, your metadata is accurate, your citations support the claims you make, and your cited papers' own bibliographies are real. Works on LaTeX and PDF. Runs entirely on your machine. Produces a **SciLint Score**.
|
|
55
61
|
|
|
56
62
|
The only open-source tool that combines reference verification, claim checking, manuscript consistency, figure analysis, and bibliography verification in one pipeline — powered entirely by a local LLM on your own GPU.
|
|
@@ -63,7 +69,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
|
|
|
63
69
|
|
|
64
70
|
## Features
|
|
65
71
|
|
|
66
|
-
**
|
|
72
|
+
**23 automated checks:**
|
|
67
73
|
|
|
68
74
|
### Reference verification
|
|
69
75
|
- **Do your references exist?** — checked against CrossRef, OpenAlex, Semantic Scholar, Open Library, and Library of Congress
|
|
@@ -72,7 +78,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
|
|
|
72
78
|
- **Robust matching** — when references lack DOIs, a multi-signal matching engine scores candidates across title, author, year, and venue (handles the metadata errors that LLMs routinely introduce)
|
|
73
79
|
|
|
74
80
|
### Claim verification (local LLM)
|
|
75
|
-
- **Do cited papers support your claims?** — downloads full text from
|
|
81
|
+
- **Do cited papers support your claims?** — downloads full text from 14 open-access sources (arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv/medRxiv, NBER, RePEc/IDEAS, HAL, ERIC, NASA ADS, OSF Preprints, CORE), parses via GROBID, embeds sections, and verifies each claim against the actual source text
|
|
76
82
|
- **What role does each citation play?** — classifies citation purpose (evidence, contrast, method, attribution, context…) with graduated weights: an unsupported *evidence* citation is serious; an unsupported *context* citation barely matters
|
|
77
83
|
- **Are your references' own bibliographies real?** — batch-checks cited papers' reference lists for existence, metadata accuracy, and retraction. Papers built on fabricated evidence are flagged
|
|
78
84
|
|
|
@@ -85,6 +91,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
|
|
|
85
91
|
- **Abstract–body alignment** — abstract makes factual claims the body contradicts
|
|
86
92
|
- **Statistical reporting** — p-values vs. their verbal interpretation
|
|
87
93
|
- **Structure promises** — contributions promised in the introduction but never delivered
|
|
94
|
+
- **Prose quality** — syntactic grammar errors and semantic word-choice mistakes (e.g. "object" where "purpose" is intended). Per-sentence review with paragraph context; severity driven by the model's confidence
|
|
88
95
|
|
|
89
96
|
### Figure checks (vision model + LLM)
|
|
90
97
|
- **Caption vs. content** — does the caption match what the figure actually shows?
|
|
@@ -125,7 +132,7 @@ sciwrite-lint contributions paper.pdf --format json
|
|
|
125
132
|
### Two audiences
|
|
126
133
|
|
|
127
134
|
- **Humans** — colored terminal output with severity levels, locations, and explanations. Decide in seconds whether each finding is real
|
|
128
|
-
- **AI writing agents** — `--format json` output with structured fields (`level`, `rule_id`, `message`, `context`). Run sciwrite-lint in a write → check → fix → recheck loop.
|
|
135
|
+
- **AI writing agents** — `--format json` output with structured fields (`level`, `rule_id`, `message`, `context`). Run sciwrite-lint in a write → check → fix → recheck loop. Standard `ruff`/`mypy`-style exit codes: `0` clean, `1` findings at error level, `2` tool error. Python-API consumers can `except sciwrite_lint.LLMConnectionError` (typed exception hierarchy under `SciWriteLintError`) instead of matching on error strings
|
|
129
136
|
|
|
130
137
|
### Optimizations
|
|
131
138
|
|
|
@@ -137,7 +144,7 @@ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-ba
|
|
|
137
144
|
- **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
|
|
138
145
|
- **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)
|
|
139
146
|
- **Phased API resolution** — citations flow through OpenAlex → Semantic Scholar → CrossRef → Open Library/LoC, each phase only processing what previous phases didn't resolve
|
|
140
|
-
- **
|
|
147
|
+
- **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
|
|
141
148
|
- **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
|
|
142
149
|
|
|
143
150
|
Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, the pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50x faster than CPU). *(Native Linux GPU swap is preliminary — tested on WSL2 only; expected to work, may need minor fixes.)*
|
|
@@ -154,10 +161,36 @@ uv tool install sciwrite-lint --python 3.13
|
|
|
154
161
|
|
|
155
162
|
uv downloads Python 3.13 automatically (does not affect any Python you may already have) and installs sciwrite-lint as a globally available command.
|
|
156
163
|
|
|
164
|
+
### Reproducible environment (optional)
|
|
165
|
+
|
|
166
|
+
`pyproject.toml` declares lower bounds for dependencies, so `uv tool install` / `pip install` may resolve newer versions as upstream releases ship. If you want the exact versions the maintainer develops and tests against — for reproducing benchmarks, debugging version-drift issues, or CI — install from [`requirements-pinned.txt`](requirements-pinned.txt) instead.
|
|
167
|
+
|
|
168
|
+
**What's in it:** top-level packages declared in `pyproject.toml` pinned to `==` versions. Transitive dependencies are not pinned — pip resolves them within each top-level package's own constraints. This is a deliberate middle ground: it locks the packages sciwrite-lint actually imports and tests against, without over-constraining the dep tree.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Clone the public repo (required — uv tool install can't read a local file)
|
|
172
|
+
git clone https://github.com/authentic-research-partners/sciwrite-lint.git
|
|
173
|
+
cd sciwrite-lint
|
|
174
|
+
|
|
175
|
+
# With uv (recommended — fast, isolated, manages Python itself)
|
|
176
|
+
uv venv --python 3.13 .venv
|
|
177
|
+
source .venv/bin/activate
|
|
178
|
+
uv pip install -r requirements-pinned.txt
|
|
179
|
+
uv pip install -e . --no-deps # install sciwrite-lint source; deps already pinned
|
|
180
|
+
|
|
181
|
+
# Or with plain pip
|
|
182
|
+
python3.13 -m venv .venv
|
|
183
|
+
source .venv/bin/activate
|
|
184
|
+
pip install -r requirements-pinned.txt
|
|
185
|
+
pip install -e . --no-deps
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Activate the venv in each new terminal with `source .venv/bin/activate`. For most users, the plain `uv tool install sciwrite-lint` above is simpler — use the pinned file only when you need exact version reproduction.
|
|
189
|
+
|
|
157
190
|
## Setup
|
|
158
191
|
|
|
159
192
|
```bash
|
|
160
|
-
sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/
|
|
193
|
+
sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/ + local_web/
|
|
161
194
|
sciwrite-lint config set-email you@example.com # required for Unpaywall + Retraction Watch
|
|
162
195
|
sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM)
|
|
163
196
|
sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
|
|
@@ -167,9 +200,45 @@ sciwrite-lint containers monitor # live dashboard: service health, VRAM, K
|
|
|
167
200
|
|
|
168
201
|
`init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
|
|
169
202
|
|
|
170
|
-
**
|
|
203
|
+
**Providing references manually — two drop folders.** sciwrite-lint reads two local directories before going to the open-access waterfall, kept separate because the folder itself signals how much to trust the source:
|
|
204
|
+
|
|
205
|
+
- `local_pdfs/` — **academic sources**. Accepts `.pdf` (primary) and `.md` summaries you've written yourself. Use this for paywalled papers, for OA papers whose publisher requires a browser (Cloudflare/JS walls, Cell, Springer, some PMC pages), and for anything you'd cite as peer-reviewed evidence.
|
|
206
|
+
- `local_web/` — **web captures**. Accepts `.md` (hand-written or pre-extracted) and `.mhtml` / `.mht` saved from a browser via **File → Save As → Webpage (Single File)**. Use this for JavaScript-heavy pages where a headless HTTP fetch would only see the pre-hydration shell; MHTML captures the rendered DOM post-JS. sciwrite-lint converts MHTML to markdown at ingest using the same trafilatura extractor it uses for live web fetches, so the output format is consistent either way. Anything landing here is classified `local_type=web_page` — softer evidence than a peer-reviewed PDF.
|
|
207
|
+
|
|
208
|
+
Name files by the reference title, or prefix them with the citekey:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
local_pdfs/ local_web/
|
|
212
|
+
├── States of Curiosity … Learning.pdf ├── dewey1910_How_We_Think.mhtml
|
|
213
|
+
├── Mind in Society.pdf ├── earthwatch2025_About.md
|
|
214
|
+
├── vanfraassen1980_The_Scientific_Image.pdf └── bca2026_Research_Teachers.md
|
|
215
|
+
└── dewey2016_summary.md # reader-written
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Matching runs in two passes for both folders. **(1) Citekey prefix:** if the filename begins with a known bib citekey (case-insensitive, e.g. `vanfraassen1980_…`), the file is matched directly to that reference — no fuzzy scoring. This is the authoritative path and works reliably for archives that follow a `{citekey}_{Title}.<ext>` convention, including short titles where length differences would otherwise penalize the score. **(2) Fuzzy title:** filenames without a recognized citekey prefix fall through to fuzzy matching against `.bib` titles (threshold 0.80). Academic matches win over web matches when the same reference is present in both folders — PDF trumps web capture. Matched files are copied into the workspace; on the next run the reference upgrades from T2 to T1 and goes through GROBID parsing (for PDFs) or direct read (for `.md`) during claim verification.
|
|
171
219
|
|
|
172
|
-
**
|
|
220
|
+
**Per-paper curated archives.** Each paper can point both drop folders at its own directories in its `[[papers]]` block:
|
|
221
|
+
|
|
222
|
+
```toml
|
|
223
|
+
[[papers]]
|
|
224
|
+
name = "my-paper"
|
|
225
|
+
file_path = "papers/my-paper/my-paper.tex"
|
|
226
|
+
bib = "papers/my-paper/my-paper.bib"
|
|
227
|
+
local_pdfs_dir = "papers/my-paper/Sources/full_text" # academic archive
|
|
228
|
+
local_web_dir = "papers/my-paper/Sources/full_text_web" # web captures
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
If you don't set them explicitly, sciwrite-lint auto-detects sibling `Sources/full_text/` and `Sources/full_text_web/` directories next to `file_path`. Resolution order per paper: explicit override → auto-detected `Sources/full_text{,_web}/` → project-wide default.
|
|
232
|
+
|
|
233
|
+
**Refreshing a source file in place.** sciwrite-lint records the SHA-256 of each drop-folder file when it first ingests it. On every subsequent run the hash is re-checked: if you overwrite `dewey1910.pdf` with a corrected scan (or re-capture an MHTML, or edit a markdown summary) under the same filename, the hash changes and the tool automatically re-copies the file into the workspace — triggering a fresh GROBID parse and embedding for PDFs, a fresh MHTML-to-markdown conversion for web captures. You don't need `--fresh`, and you don't need to rename files. Unchanged files cost a single hash read per run and are otherwise skipped.
|
|
234
|
+
|
|
235
|
+
**Footnote URLs (`\footnote{\url{…}}`) get verified too.** Papers often cite informational web pages inline as a footnote URL rather than through a formal `.bib` entry — organisation pages, program descriptions, press releases, FAQs. sciwrite-lint matches every such URL to a `.md` file in `local_web_dir` by reading a `Source: https://…` (or `Source URL: …`) header from the first 20 lines of each file. The matched capture becomes a T1 source for claim verification — same as a cited reference. Browser-saved `.mhtml` gets this header written automatically at ingest; for hand-authored `.md` you add one line at the top. See [docs/local-sources.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/local-sources.md) for details and examples.
|
|
236
|
+
|
|
237
|
+
**Finding what to download:** `sciwrite-lint verify` lists T2 references that are confirmed open access with direct URLs — open each in your browser and save to `local_pdfs/` (or `local_web/` if the "paper" is actually a blog post or documentation page that the publisher serves as HTML). For paywalled papers, use your institution's library access.
|
|
238
|
+
|
|
239
|
+
**Fetched-PDF match validation.** Every downloaded PDF goes through a multi-signal match gate (title similarity, author surname, DOI, year, plus hard rejects for common bibliographic-record landing pages) before being cached. Wrong-paper downloads from API mismatches and template landing pages are rejected and deleted; the tool moves on to the next open-access source. You can still drop the correct PDF into `local_pdfs_dir` to skip the waterfall entirely.
|
|
240
|
+
|
|
241
|
+
**API keys** increase rate limits for Semantic Scholar, NCBI, and CORE, and enable NASA ADS (required for that source):
|
|
173
242
|
|
|
174
243
|
```bash
|
|
175
244
|
sciwrite-lint config show # see what's configured
|
|
@@ -187,6 +256,7 @@ sciwrite-lint check # all papers (batch-staged when
|
|
|
187
256
|
sciwrite-lint check --concurrency 4 # batch parallelism (default 2, validated up to 4)
|
|
188
257
|
sciwrite-lint check paper.tex # text + LLM rules on a .tex file
|
|
189
258
|
sciwrite-lint check paper.pdf # check a PDF (GROBID required)
|
|
259
|
+
sciwrite-lint check --checks prose-quality paper.tex # run only the listed checks (comma-separated)
|
|
190
260
|
sciwrite-lint contributions --paper my_paper # add contribution axes to SciLint Score
|
|
191
261
|
sciwrite-lint contributions paper.pdf # standalone file scoring
|
|
192
262
|
```
|
|
@@ -195,6 +265,8 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
|
|
|
195
265
|
|
|
196
266
|
Use `--fresh` to start from scratch (backs up the existing workspace before recreating it).
|
|
197
267
|
|
|
268
|
+
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
|
+
|
|
198
270
|
### Contribution axes (`sciwrite-lint contributions`)
|
|
199
271
|
|
|
200
272
|
`contributions` computes 5 contribution axes from philosophy of science (Popper, Lakatos, Kitcher, Laudan, Mayo) and updates the SciLint Score. Requires vLLM.
|
|
@@ -263,12 +335,28 @@ Output formats: terminal (default) or `--format json`.
|
|
|
263
335
|
- `sciwrite-lint checks` — list all checks
|
|
264
336
|
- `sciwrite-lint <command> --help` — detailed usage for any command
|
|
265
337
|
- [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) — GROBID, vLLM, external APIs, configuration
|
|
338
|
+
- [docs/local-sources.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/local-sources.md) — drop directories, filename conventions, `Source:` header for footnote URLs
|
|
266
339
|
|
|
267
340
|
For contributors and advanced users:
|
|
268
341
|
|
|
269
342
|
- [docs/evals.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/evals.md) — detection evaluation framework, adding test cases
|
|
270
343
|
- [docs/calibration.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/calibration.md) — SciLint Score calibration against ground-truth papers
|
|
271
344
|
|
|
345
|
+
## Citation
|
|
346
|
+
|
|
347
|
+
If you use sciwrite-lint in your research, please cite the accompanying paper ([arXiv:2604.08501](https://arxiv.org/abs/2604.08501)):
|
|
348
|
+
|
|
349
|
+
```bibtex
|
|
350
|
+
@article{samsonau2026sciwritelint,
|
|
351
|
+
title = {sciwrite-lint: Verification Infrastructure for the Age of Science Vibe-Writing},
|
|
352
|
+
author = {Samsonau, Sergey V.},
|
|
353
|
+
journal = {arXiv preprint arXiv:2604.08501},
|
|
354
|
+
year = {2026},
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
GitHub's "Cite this repository" button (powered by [`CITATION.cff`](CITATION.cff)) will also generate this for you.
|
|
359
|
+
|
|
272
360
|
## License
|
|
273
361
|
|
|
274
362
|
MIT
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# sciwrite-lint
|
|
2
2
|
|
|
3
|
+
[](https://arxiv.org/abs/2604.08501)
|
|
4
|
+
|
|
3
5
|
A linter for scientific manuscripts. Checks that your references exist, your metadata is accurate, your citations support the claims you make, and your cited papers' own bibliographies are real. Works on LaTeX and PDF. Runs entirely on your machine. Produces a **SciLint Score**.
|
|
4
6
|
|
|
5
7
|
The only open-source tool that combines reference verification, claim checking, manuscript consistency, figure analysis, and bibliography verification in one pipeline — powered entirely by a local LLM on your own GPU.
|
|
@@ -12,7 +14,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
|
|
|
12
14
|
|
|
13
15
|
## Features
|
|
14
16
|
|
|
15
|
-
**
|
|
17
|
+
**23 automated checks:**
|
|
16
18
|
|
|
17
19
|
### Reference verification
|
|
18
20
|
- **Do your references exist?** — checked against CrossRef, OpenAlex, Semantic Scholar, Open Library, and Library of Congress
|
|
@@ -21,7 +23,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
|
|
|
21
23
|
- **Robust matching** — when references lack DOIs, a multi-signal matching engine scores candidates across title, author, year, and venue (handles the metadata errors that LLMs routinely introduce)
|
|
22
24
|
|
|
23
25
|
### Claim verification (local LLM)
|
|
24
|
-
- **Do cited papers support your claims?** — downloads full text from
|
|
26
|
+
- **Do cited papers support your claims?** — downloads full text from 14 open-access sources (arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv/medRxiv, NBER, RePEc/IDEAS, HAL, ERIC, NASA ADS, OSF Preprints, CORE), parses via GROBID, embeds sections, and verifies each claim against the actual source text
|
|
25
27
|
- **What role does each citation play?** — classifies citation purpose (evidence, contrast, method, attribution, context…) with graduated weights: an unsupported *evidence* citation is serious; an unsupported *context* citation barely matters
|
|
26
28
|
- **Are your references' own bibliographies real?** — batch-checks cited papers' reference lists for existence, metadata accuracy, and retraction. Papers built on fabricated evidence are flagged
|
|
27
29
|
|
|
@@ -34,6 +36,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
|
|
|
34
36
|
- **Abstract–body alignment** — abstract makes factual claims the body contradicts
|
|
35
37
|
- **Statistical reporting** — p-values vs. their verbal interpretation
|
|
36
38
|
- **Structure promises** — contributions promised in the introduction but never delivered
|
|
39
|
+
- **Prose quality** — syntactic grammar errors and semantic word-choice mistakes (e.g. "object" where "purpose" is intended). Per-sentence review with paragraph context; severity driven by the model's confidence
|
|
37
40
|
|
|
38
41
|
### Figure checks (vision model + LLM)
|
|
39
42
|
- **Caption vs. content** — does the caption match what the figure actually shows?
|
|
@@ -74,7 +77,7 @@ sciwrite-lint contributions paper.pdf --format json
|
|
|
74
77
|
### Two audiences
|
|
75
78
|
|
|
76
79
|
- **Humans** — colored terminal output with severity levels, locations, and explanations. Decide in seconds whether each finding is real
|
|
77
|
-
- **AI writing agents** — `--format json` output with structured fields (`level`, `rule_id`, `message`, `context`). Run sciwrite-lint in a write → check → fix → recheck loop.
|
|
80
|
+
- **AI writing agents** — `--format json` output with structured fields (`level`, `rule_id`, `message`, `context`). Run sciwrite-lint in a write → check → fix → recheck loop. Standard `ruff`/`mypy`-style exit codes: `0` clean, `1` findings at error level, `2` tool error. Python-API consumers can `except sciwrite_lint.LLMConnectionError` (typed exception hierarchy under `SciWriteLintError`) instead of matching on error strings
|
|
78
81
|
|
|
79
82
|
### Optimizations
|
|
80
83
|
|
|
@@ -86,7 +89,7 @@ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-ba
|
|
|
86
89
|
- **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
|
|
87
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)
|
|
88
91
|
- **Phased API resolution** — citations flow through OpenAlex → Semantic Scholar → CrossRef → Open Library/LoC, each phase only processing what previous phases didn't resolve
|
|
89
|
-
- **
|
|
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
|
|
90
93
|
- **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
|
|
91
94
|
|
|
92
95
|
Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, the pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50x faster than CPU). *(Native Linux GPU swap is preliminary — tested on WSL2 only; expected to work, may need minor fixes.)*
|
|
@@ -103,10 +106,36 @@ uv tool install sciwrite-lint --python 3.13
|
|
|
103
106
|
|
|
104
107
|
uv downloads Python 3.13 automatically (does not affect any Python you may already have) and installs sciwrite-lint as a globally available command.
|
|
105
108
|
|
|
109
|
+
### Reproducible environment (optional)
|
|
110
|
+
|
|
111
|
+
`pyproject.toml` declares lower bounds for dependencies, so `uv tool install` / `pip install` may resolve newer versions as upstream releases ship. If you want the exact versions the maintainer develops and tests against — for reproducing benchmarks, debugging version-drift issues, or CI — install from [`requirements-pinned.txt`](requirements-pinned.txt) instead.
|
|
112
|
+
|
|
113
|
+
**What's in it:** top-level packages declared in `pyproject.toml` pinned to `==` versions. Transitive dependencies are not pinned — pip resolves them within each top-level package's own constraints. This is a deliberate middle ground: it locks the packages sciwrite-lint actually imports and tests against, without over-constraining the dep tree.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Clone the public repo (required — uv tool install can't read a local file)
|
|
117
|
+
git clone https://github.com/authentic-research-partners/sciwrite-lint.git
|
|
118
|
+
cd sciwrite-lint
|
|
119
|
+
|
|
120
|
+
# With uv (recommended — fast, isolated, manages Python itself)
|
|
121
|
+
uv venv --python 3.13 .venv
|
|
122
|
+
source .venv/bin/activate
|
|
123
|
+
uv pip install -r requirements-pinned.txt
|
|
124
|
+
uv pip install -e . --no-deps # install sciwrite-lint source; deps already pinned
|
|
125
|
+
|
|
126
|
+
# Or with plain pip
|
|
127
|
+
python3.13 -m venv .venv
|
|
128
|
+
source .venv/bin/activate
|
|
129
|
+
pip install -r requirements-pinned.txt
|
|
130
|
+
pip install -e . --no-deps
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Activate the venv in each new terminal with `source .venv/bin/activate`. For most users, the plain `uv tool install sciwrite-lint` above is simpler — use the pinned file only when you need exact version reproduction.
|
|
134
|
+
|
|
106
135
|
## Setup
|
|
107
136
|
|
|
108
137
|
```bash
|
|
109
|
-
sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/
|
|
138
|
+
sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/ + local_web/
|
|
110
139
|
sciwrite-lint config set-email you@example.com # required for Unpaywall + Retraction Watch
|
|
111
140
|
sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM)
|
|
112
141
|
sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
|
|
@@ -116,9 +145,45 @@ sciwrite-lint containers monitor # live dashboard: service health, VRAM, K
|
|
|
116
145
|
|
|
117
146
|
`init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
|
|
118
147
|
|
|
119
|
-
**
|
|
148
|
+
**Providing references manually — two drop folders.** sciwrite-lint reads two local directories before going to the open-access waterfall, kept separate because the folder itself signals how much to trust the source:
|
|
149
|
+
|
|
150
|
+
- `local_pdfs/` — **academic sources**. Accepts `.pdf` (primary) and `.md` summaries you've written yourself. Use this for paywalled papers, for OA papers whose publisher requires a browser (Cloudflare/JS walls, Cell, Springer, some PMC pages), and for anything you'd cite as peer-reviewed evidence.
|
|
151
|
+
- `local_web/` — **web captures**. Accepts `.md` (hand-written or pre-extracted) and `.mhtml` / `.mht` saved from a browser via **File → Save As → Webpage (Single File)**. Use this for JavaScript-heavy pages where a headless HTTP fetch would only see the pre-hydration shell; MHTML captures the rendered DOM post-JS. sciwrite-lint converts MHTML to markdown at ingest using the same trafilatura extractor it uses for live web fetches, so the output format is consistent either way. Anything landing here is classified `local_type=web_page` — softer evidence than a peer-reviewed PDF.
|
|
152
|
+
|
|
153
|
+
Name files by the reference title, or prefix them with the citekey:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
local_pdfs/ local_web/
|
|
157
|
+
├── States of Curiosity … Learning.pdf ├── dewey1910_How_We_Think.mhtml
|
|
158
|
+
├── Mind in Society.pdf ├── earthwatch2025_About.md
|
|
159
|
+
├── vanfraassen1980_The_Scientific_Image.pdf └── bca2026_Research_Teachers.md
|
|
160
|
+
└── dewey2016_summary.md # reader-written
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Matching runs in two passes for both folders. **(1) Citekey prefix:** if the filename begins with a known bib citekey (case-insensitive, e.g. `vanfraassen1980_…`), the file is matched directly to that reference — no fuzzy scoring. This is the authoritative path and works reliably for archives that follow a `{citekey}_{Title}.<ext>` convention, including short titles where length differences would otherwise penalize the score. **(2) Fuzzy title:** filenames without a recognized citekey prefix fall through to fuzzy matching against `.bib` titles (threshold 0.80). Academic matches win over web matches when the same reference is present in both folders — PDF trumps web capture. Matched files are copied into the workspace; on the next run the reference upgrades from T2 to T1 and goes through GROBID parsing (for PDFs) or direct read (for `.md`) during claim verification.
|
|
120
164
|
|
|
121
|
-
**
|
|
165
|
+
**Per-paper curated archives.** Each paper can point both drop folders at its own directories in its `[[papers]]` block:
|
|
166
|
+
|
|
167
|
+
```toml
|
|
168
|
+
[[papers]]
|
|
169
|
+
name = "my-paper"
|
|
170
|
+
file_path = "papers/my-paper/my-paper.tex"
|
|
171
|
+
bib = "papers/my-paper/my-paper.bib"
|
|
172
|
+
local_pdfs_dir = "papers/my-paper/Sources/full_text" # academic archive
|
|
173
|
+
local_web_dir = "papers/my-paper/Sources/full_text_web" # web captures
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
If you don't set them explicitly, sciwrite-lint auto-detects sibling `Sources/full_text/` and `Sources/full_text_web/` directories next to `file_path`. Resolution order per paper: explicit override → auto-detected `Sources/full_text{,_web}/` → project-wide default.
|
|
177
|
+
|
|
178
|
+
**Refreshing a source file in place.** sciwrite-lint records the SHA-256 of each drop-folder file when it first ingests it. On every subsequent run the hash is re-checked: if you overwrite `dewey1910.pdf` with a corrected scan (or re-capture an MHTML, or edit a markdown summary) under the same filename, the hash changes and the tool automatically re-copies the file into the workspace — triggering a fresh GROBID parse and embedding for PDFs, a fresh MHTML-to-markdown conversion for web captures. You don't need `--fresh`, and you don't need to rename files. Unchanged files cost a single hash read per run and are otherwise skipped.
|
|
179
|
+
|
|
180
|
+
**Footnote URLs (`\footnote{\url{…}}`) get verified too.** Papers often cite informational web pages inline as a footnote URL rather than through a formal `.bib` entry — organisation pages, program descriptions, press releases, FAQs. sciwrite-lint matches every such URL to a `.md` file in `local_web_dir` by reading a `Source: https://…` (or `Source URL: …`) header from the first 20 lines of each file. The matched capture becomes a T1 source for claim verification — same as a cited reference. Browser-saved `.mhtml` gets this header written automatically at ingest; for hand-authored `.md` you add one line at the top. See [docs/local-sources.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/local-sources.md) for details and examples.
|
|
181
|
+
|
|
182
|
+
**Finding what to download:** `sciwrite-lint verify` lists T2 references that are confirmed open access with direct URLs — open each in your browser and save to `local_pdfs/` (or `local_web/` if the "paper" is actually a blog post or documentation page that the publisher serves as HTML). For paywalled papers, use your institution's library access.
|
|
183
|
+
|
|
184
|
+
**Fetched-PDF match validation.** Every downloaded PDF goes through a multi-signal match gate (title similarity, author surname, DOI, year, plus hard rejects for common bibliographic-record landing pages) before being cached. Wrong-paper downloads from API mismatches and template landing pages are rejected and deleted; the tool moves on to the next open-access source. You can still drop the correct PDF into `local_pdfs_dir` to skip the waterfall entirely.
|
|
185
|
+
|
|
186
|
+
**API keys** increase rate limits for Semantic Scholar, NCBI, and CORE, and enable NASA ADS (required for that source):
|
|
122
187
|
|
|
123
188
|
```bash
|
|
124
189
|
sciwrite-lint config show # see what's configured
|
|
@@ -136,6 +201,7 @@ sciwrite-lint check # all papers (batch-staged when
|
|
|
136
201
|
sciwrite-lint check --concurrency 4 # batch parallelism (default 2, validated up to 4)
|
|
137
202
|
sciwrite-lint check paper.tex # text + LLM rules on a .tex file
|
|
138
203
|
sciwrite-lint check paper.pdf # check a PDF (GROBID required)
|
|
204
|
+
sciwrite-lint check --checks prose-quality paper.tex # run only the listed checks (comma-separated)
|
|
139
205
|
sciwrite-lint contributions --paper my_paper # add contribution axes to SciLint Score
|
|
140
206
|
sciwrite-lint contributions paper.pdf # standalone file scoring
|
|
141
207
|
```
|
|
@@ -144,6 +210,8 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
|
|
|
144
210
|
|
|
145
211
|
Use `--fresh` to start from scratch (backs up the existing workspace before recreating it).
|
|
146
212
|
|
|
213
|
+
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.
|
|
214
|
+
|
|
147
215
|
### Contribution axes (`sciwrite-lint contributions`)
|
|
148
216
|
|
|
149
217
|
`contributions` computes 5 contribution axes from philosophy of science (Popper, Lakatos, Kitcher, Laudan, Mayo) and updates the SciLint Score. Requires vLLM.
|
|
@@ -212,12 +280,28 @@ Output formats: terminal (default) or `--format json`.
|
|
|
212
280
|
- `sciwrite-lint checks` — list all checks
|
|
213
281
|
- `sciwrite-lint <command> --help` — detailed usage for any command
|
|
214
282
|
- [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) — GROBID, vLLM, external APIs, configuration
|
|
283
|
+
- [docs/local-sources.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/local-sources.md) — drop directories, filename conventions, `Source:` header for footnote URLs
|
|
215
284
|
|
|
216
285
|
For contributors and advanced users:
|
|
217
286
|
|
|
218
287
|
- [docs/evals.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/evals.md) — detection evaluation framework, adding test cases
|
|
219
288
|
- [docs/calibration.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/calibration.md) — SciLint Score calibration against ground-truth papers
|
|
220
289
|
|
|
290
|
+
## Citation
|
|
291
|
+
|
|
292
|
+
If you use sciwrite-lint in your research, please cite the accompanying paper ([arXiv:2604.08501](https://arxiv.org/abs/2604.08501)):
|
|
293
|
+
|
|
294
|
+
```bibtex
|
|
295
|
+
@article{samsonau2026sciwritelint,
|
|
296
|
+
title = {sciwrite-lint: Verification Infrastructure for the Age of Science Vibe-Writing},
|
|
297
|
+
author = {Samsonau, Sergey V.},
|
|
298
|
+
journal = {arXiv preprint arXiv:2604.08501},
|
|
299
|
+
year = {2026},
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
GitHub's "Cite this repository" button (powered by [`CITATION.cff`](CITATION.cff)) will also generate this for you.
|
|
304
|
+
|
|
221
305
|
## License
|
|
222
306
|
|
|
223
307
|
MIT
|
|
@@ -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.3.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"
|
|
@@ -42,10 +42,13 @@ dependencies = [
|
|
|
42
42
|
"sqlite-vec>=0.1.6",
|
|
43
43
|
"torch>=2.5",
|
|
44
44
|
"transformers>=4.50",
|
|
45
|
-
"Pillow>=12.
|
|
46
|
-
"pypdf>=6.
|
|
45
|
+
"Pillow>=12.2",
|
|
46
|
+
"pypdf>=6.10.2",
|
|
47
47
|
"pypdfium2>=5.0",
|
|
48
48
|
"pdf2image>=1.17",
|
|
49
|
+
"lxml>=6.1",
|
|
50
|
+
"pypandoc-binary>=1.15",
|
|
51
|
+
"rfc3986>=2.0",
|
|
49
52
|
]
|
|
50
53
|
|
|
51
54
|
[project.urls]
|
|
@@ -53,10 +56,11 @@ Homepage = "https://arpconnect.com"
|
|
|
53
56
|
Repository = "https://github.com/authentic-research-partners/sciwrite-lint"
|
|
54
57
|
Documentation = "https://github.com/authentic-research-partners/sciwrite-lint#readme"
|
|
55
58
|
Issues = "https://github.com/authentic-research-partners/sciwrite-lint/issues"
|
|
59
|
+
Paper = "https://arxiv.org/abs/2604.08501"
|
|
56
60
|
|
|
57
61
|
[project.optional-dependencies]
|
|
58
62
|
dev = [
|
|
59
|
-
"pytest>=
|
|
63
|
+
"pytest>=9.0.3",
|
|
60
64
|
"pytest-xdist>=3.0",
|
|
61
65
|
]
|
|
62
66
|
|
|
@@ -105,5 +109,5 @@ DEP003 = ["sciwrite_lint", "evals", "eval_real_world"]
|
|
|
105
109
|
plugins = ["pydantic.mypy"]
|
|
106
110
|
[[tool.mypy.overrides]]
|
|
107
111
|
# No py.typed marker or typeshed stubs available
|
|
108
|
-
module = ["bibtexparser", "sqlite_vec", "pypdf", "pypdfium2", "pypdfium2.raw", "transformers", "pdf2image"]
|
|
112
|
+
module = ["bibtexparser", "sqlite_vec", "pypdf", "pypdfium2", "pypdfium2.raw", "transformers", "pdf2image", "lxml", "lxml.html", "lxml.html.etree", "rfc3986", "pypandoc"]
|
|
109
113
|
ignore_missing_imports = true
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""sciwrite-lint: a linter for scientific manuscripts."""
|
|
2
|
+
|
|
3
|
+
from sciwrite_lint.exceptions import LLMConnectionError, SciWriteLintError
|
|
4
|
+
|
|
5
|
+
__version__ = "0.3.0"
|
|
6
|
+
|
|
7
|
+
from sciwrite_lint.oa import (
|
|
8
|
+
DownloadResult,
|
|
9
|
+
FetchConfig,
|
|
10
|
+
SearchHit,
|
|
11
|
+
WebResult,
|
|
12
|
+
download_pdf,
|
|
13
|
+
fetch_web,
|
|
14
|
+
search_by_title,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"DownloadResult",
|
|
19
|
+
"FetchConfig",
|
|
20
|
+
"LLMConnectionError",
|
|
21
|
+
"SciWriteLintError",
|
|
22
|
+
"SearchHit",
|
|
23
|
+
"WebResult",
|
|
24
|
+
"__version__",
|
|
25
|
+
"download_pdf",
|
|
26
|
+
"fetch_web",
|
|
27
|
+
"search_by_title",
|
|
28
|
+
]
|
|
@@ -189,6 +189,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
189
189
|
default=None,
|
|
190
190
|
help="Vision backend: transformers (2B, default) or vllm (8B FP8 on port 5002)",
|
|
191
191
|
)
|
|
192
|
+
p_check.add_argument(
|
|
193
|
+
"--checks",
|
|
194
|
+
default=None,
|
|
195
|
+
metavar="ID[,ID...]",
|
|
196
|
+
help=(
|
|
197
|
+
"Run only the listed check IDs (comma-separated). Other checks "
|
|
198
|
+
"are disabled for this invocation. Example: "
|
|
199
|
+
"--checks prose-quality,dangling-cite. Run `sciwrite-lint checks` "
|
|
200
|
+
"to list available IDs."
|
|
201
|
+
),
|
|
202
|
+
)
|
|
192
203
|
p_check.set_defaults(func=run_check)
|
|
193
204
|
|
|
194
205
|
# --- checks ---
|
|
@@ -147,8 +147,15 @@ async def stream_with_limit(
|
|
|
147
147
|
size we actually read. Always enforces *max_bytes* on the
|
|
148
148
|
decompressed data stream via ``aiter_bytes()``.
|
|
149
149
|
|
|
150
|
-
Returns a synthetic ``httpx.Response`` with the full body
|
|
151
|
-
Raises ``ResponseTooLarge`` if the limit is exceeded.
|
|
150
|
+
Returns a synthetic ``httpx.Response`` with the full decompressed body
|
|
151
|
+
populated. Raises ``ResponseTooLarge`` if the limit is exceeded.
|
|
152
|
+
|
|
153
|
+
Content-Encoding (and the now-stale Content-Length) is stripped from the
|
|
154
|
+
synthetic response's headers. ``aiter_bytes`` already decompressed the
|
|
155
|
+
body; leaving the header intact would cause httpx to try to decompress a
|
|
156
|
+
second time when the caller reads ``.content``, which fails with
|
|
157
|
+
``DecodingError: incorrect header check`` on servers that gzip real
|
|
158
|
+
binary content (observed on ``mpra.ub.uni-muenchen.de`` serving PDFs).
|
|
152
159
|
"""
|
|
153
160
|
async with client.stream("GET", url, **kwargs) as resp: # type: ignore[arg-type]
|
|
154
161
|
# Non-success responses are small — read eagerly for diagnostics
|
|
@@ -176,9 +183,16 @@ async def stream_with_limit(
|
|
|
176
183
|
chunks.append(chunk)
|
|
177
184
|
|
|
178
185
|
body = b"".join(chunks)
|
|
186
|
+
synthetic_headers = httpx.Headers(
|
|
187
|
+
[
|
|
188
|
+
(name, value)
|
|
189
|
+
for name, value in resp.headers.raw
|
|
190
|
+
if name.lower() not in (b"content-encoding", b"content-length")
|
|
191
|
+
]
|
|
192
|
+
)
|
|
179
193
|
return httpx.Response(
|
|
180
194
|
status_code=resp.status_code,
|
|
181
|
-
headers=
|
|
195
|
+
headers=synthetic_headers,
|
|
182
196
|
content=body,
|
|
183
197
|
request=resp.request,
|
|
184
198
|
)
|
|
@@ -1436,15 +1436,33 @@ async def _verify_web_resource(
|
|
|
1436
1436
|
c.issues.append(
|
|
1437
1437
|
f"URL alive but content extraction failed: {web_result.error or 'unknown'}"
|
|
1438
1438
|
)
|
|
1439
|
+
elif web_result.blocked:
|
|
1440
|
+
# Unverifiable: 4xx refusal, 5xx error, TLS/timeout/connection/
|
|
1441
|
+
# decoding/protocol/oversized. The URL may still be valid — we
|
|
1442
|
+
# just could not confirm. Downstream emits a WARN telling the
|
|
1443
|
+
# user to verify manually; distinct from web_dead (ERROR).
|
|
1444
|
+
c.api_match = "web_blocked"
|
|
1445
|
+
c.api_source = "web"
|
|
1446
|
+
c.api_data = {
|
|
1447
|
+
"source": "web",
|
|
1448
|
+
"url": resolved_url,
|
|
1449
|
+
"status_code": web_result.status_code,
|
|
1450
|
+
}
|
|
1451
|
+
reason = web_result.error or "unknown reason"
|
|
1452
|
+
c.issues.append(
|
|
1453
|
+
f"Blocked by {reason}: {resolved_url} — "
|
|
1454
|
+
f"unable to verify automatically, please check manually"
|
|
1455
|
+
)
|
|
1439
1456
|
else:
|
|
1457
|
+
# Genuinely dead: server explicitly returned 404 or 410 (or the
|
|
1458
|
+
# URL was structurally invalid). Safe to flag as ERROR — cited
|
|
1459
|
+
# resource cannot be retrieved at this URL and likely cannot be
|
|
1460
|
+
# retrieved anywhere at this URL.
|
|
1440
1461
|
c.api_match = "web_dead"
|
|
1441
1462
|
c.api_source = "web"
|
|
1442
1463
|
c.api_data = {"source": "web", "url": resolved_url}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
c.issues.append(f"Dead URL ({web_result.error}): {resolved_url}")
|
|
1446
|
-
else:
|
|
1447
|
-
c.issues.append(f"Dead URL (HTTP {web_result.status_code}): {resolved_url}")
|
|
1464
|
+
reason = web_result.error or f"HTTP {web_result.status_code}"
|
|
1465
|
+
c.issues.append(f"Dead URL ({reason}): {resolved_url}")
|
|
1448
1466
|
|
|
1449
1467
|
return c.api_data
|
|
1450
1468
|
|
|
@@ -96,11 +96,15 @@ Return {{"issues": []}} if no genuine issues found.\
|
|
|
96
96
|
_OVERHEAD_TOKENS = 2500
|
|
97
97
|
|
|
98
98
|
# Output budget reserved for the JSON response. Matches the vLLM model
|
|
99
|
-
# default (VLLM_MODELS["qwen3"]["max_tokens"])
|
|
100
|
-
#
|
|
99
|
+
# default (VLLM_MODELS["qwen3"]["max_tokens"]) — this is the response
|
|
100
|
+
# portion only. ``llm_query`` adds the active thinking budget on top
|
|
101
|
+
# when dispatching to vLLM. FullPaperIssueList caps the issues list at
|
|
102
|
+
# 5, which fits comfortably in this budget.
|
|
101
103
|
_OUTPUT_RESERVE_TOKENS = 2048
|
|
102
104
|
|
|
103
105
|
# Worst-case thinking budget (medium preset, see THINKING_PRESETS).
|
|
106
|
+
# Reserved in the context-window accounting alongside the output budget;
|
|
107
|
+
# the sum matches what ``llm_query`` sends as ``max_tokens``.
|
|
104
108
|
_THINKING_RESERVE_TOKENS = 1024
|
|
105
109
|
|
|
106
110
|
# Rough chars-to-tokens ratio (conservative: overestimates tokens).
|