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.
Files changed (152) hide show
  1. {sciwrite_lint-0.2.1/sciwrite_lint.egg-info → sciwrite_lint-0.3.0}/PKG-INFO +99 -11
  2. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/README.md +91 -7
  3. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/pyproject.toml +9 -5
  4. sciwrite_lint-0.3.0/sciwrite_lint/__init__.py +28 -0
  5. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/__main__.py +11 -0
  6. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/_network.py +17 -3
  7. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/api.py +23 -5
  8. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/full_paper_consistency.py +6 -2
  9. sciwrite_lint-0.3.0/sciwrite_lint/checks/prose_quality.py +541 -0
  10. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/ref_internal_checks.py +17 -3
  11. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/reference_exists.py +18 -2
  12. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/registry.py +1 -0
  13. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/check.py +96 -27
  14. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/config.py +5 -0
  15. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/fetch.py +81 -25
  16. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/__init__.py +62 -0
  17. sciwrite_lint-0.2.1/sciwrite_lint/cli/misc.py → sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/_monitor.py +7 -584
  18. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/containers.py +216 -0
  19. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/dismiss_claim.py +62 -0
  20. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/grobid.py +53 -0
  21. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/init.py +14 -0
  22. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/override.py +66 -0
  23. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/parse.py +100 -0
  24. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/vision.py +60 -0
  25. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/vllm.py +42 -0
  26. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/verify.py +83 -35
  27. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/config.py +118 -3
  28. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/eval_claims.py +85 -6
  29. sciwrite_lint-0.3.0/sciwrite_lint/exceptions.py +17 -0
  30. sciwrite_lint-0.3.0/sciwrite_lint/footnote_urls.py +580 -0
  31. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/__init__.py +193 -0
  32. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_common.py +221 -0
  33. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_download.py +125 -0
  34. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_orchestrator.py +127 -0
  35. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_search.py +219 -0
  36. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/_validation.py +413 -0
  37. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/arxiv.py +30 -0
  38. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/biorxiv.py +36 -0
  39. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/core.py +98 -0
  40. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/eric.py +159 -0
  41. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/europepmc.py +76 -0
  42. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/hal.py +171 -0
  43. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/ideas.py +239 -0
  44. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/nasa_ads.py +214 -0
  45. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/nber.py +157 -0
  46. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/osf.py +218 -0
  47. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/pmc.py +69 -0
  48. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/s2.py +28 -0
  49. sciwrite_lint-0.3.0/sciwrite_lint/fulltext/unpaywall.py +59 -0
  50. sciwrite_lint-0.3.0/sciwrite_lint/latex_to_markdown.py +136 -0
  51. sciwrite_lint-0.3.0/sciwrite_lint/llm_utils.py +606 -0
  52. sciwrite_lint-0.3.0/sciwrite_lint/local_sources.py +420 -0
  53. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/manuscript_store.py +100 -12
  54. sciwrite_lint-0.3.0/sciwrite_lint/mhtml.py +197 -0
  55. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/models.py +2 -0
  56. sciwrite_lint-0.3.0/sciwrite_lint/oa/__init__.py +42 -0
  57. sciwrite_lint-0.3.0/sciwrite_lint/oa/_models.py +91 -0
  58. sciwrite_lint-0.3.0/sciwrite_lint/oa/_pdf.py +404 -0
  59. sciwrite_lint-0.3.0/sciwrite_lint/oa/_search.py +113 -0
  60. sciwrite_lint-0.3.0/sciwrite_lint/oa/_web.py +165 -0
  61. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/pdf/grobid.py +109 -1
  62. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/pdf/pdf_download.py +29 -31
  63. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/__init__.py +147 -0
  64. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/checks.py +64 -0
  65. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/claims.py +116 -0
  66. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/embeddings.py +234 -0
  67. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/fetch.py +276 -0
  68. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/orchestration.py +413 -0
  69. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/parse.py +57 -0
  70. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/pdf_context.py +87 -0
  71. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/ref_internal.py +26 -0
  72. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/runners.py +496 -0
  73. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/staged.py +755 -0
  74. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/swap.py +277 -0
  75. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/tracking.py +105 -0
  76. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/unreliable.py +47 -0
  77. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/verify.py +526 -0
  78. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/vision.py +191 -0
  79. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/citations.py +58 -4
  80. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/metadata.py +10 -2
  81. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/__init__.py +134 -0
  82. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/_core.py +329 -0
  83. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/bib_checks.py +82 -0
  84. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/citation_metadata.py +178 -0
  85. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/claim_results.py +144 -0
  86. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/parse_cache.py +136 -0
  87. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/pipeline_stage.py +82 -0
  88. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/query_vectors.py +32 -0
  89. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/ref_internal.py +101 -0
  90. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/registry.py +259 -0
  91. sciwrite_lint-0.3.0/sciwrite_lint/references/workspace_db/vision_cache.py +143 -0
  92. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/schemas.py +32 -3
  93. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/tex_parser.py +67 -0
  94. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/pipeline.py +3 -3
  95. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vllm/vllm_server.py +1 -0
  96. sciwrite_lint-0.3.0/sciwrite_lint/web.py +595 -0
  97. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0/sciwrite_lint.egg-info}/PKG-INFO +99 -11
  98. sciwrite_lint-0.3.0/sciwrite_lint.egg-info/SOURCES.txt +142 -0
  99. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/requires.txt +6 -3
  100. sciwrite_lint-0.2.1/sciwrite_lint/__init__.py +0 -3
  101. sciwrite_lint-0.2.1/sciwrite_lint/fulltext.py +0 -851
  102. sciwrite_lint-0.2.1/sciwrite_lint/llm_utils.py +0 -415
  103. sciwrite_lint-0.2.1/sciwrite_lint/local_pdfs.py +0 -122
  104. sciwrite_lint-0.2.1/sciwrite_lint/pipeline.py +0 -3097
  105. sciwrite_lint-0.2.1/sciwrite_lint/references/workspace_db.py +0 -1508
  106. sciwrite_lint-0.2.1/sciwrite_lint/web.py +0 -369
  107. sciwrite_lint-0.2.1/sciwrite_lint.egg-info/SOURCES.txt +0 -80
  108. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/LICENSE +0 -0
  109. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/__init__.py +0 -0
  110. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/_section_utils.py +0 -0
  111. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/cite_purpose.py +0 -0
  112. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/claim_support.py +0 -0
  113. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/cross_section_consistency.py +0 -0
  114. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/dangling_cite.py +0 -0
  115. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/dangling_ref.py +0 -0
  116. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/reference_accuracy.py +0 -0
  117. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/reference_unreliable.py +0 -0
  118. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/retracted_cite.py +0 -0
  119. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/structure_promises.py +0 -0
  120. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
  121. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/claims.py +0 -0
  122. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/claude_backend.py +0 -0
  123. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/claude_cli.py +0 -0
  124. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/__init__.py +0 -0
  125. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/rank.py +0 -0
  126. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/cross_paper.py +0 -0
  127. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/pdf/__init__.py +0 -0
  128. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/prompt_safety.py +0 -0
  129. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/py.typed +0 -0
  130. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/rate_limiter.py +0 -0
  131. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/__init__.py +0 -0
  132. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/crossref.py +0 -0
  133. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/embedding_store.py +0 -0
  134. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/matching.py +0 -0
  135. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/reference_store.py +0 -0
  136. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/references/retraction_watch.py +0 -0
  137. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/report.py +0 -0
  138. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/__init__.py +0 -0
  139. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/chain.py +0 -0
  140. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/contribution.py +0 -0
  141. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/scilint_score.py +0 -0
  142. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/usage.py +0 -0
  143. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/__init__.py +0 -0
  144. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/cache.py +0 -0
  145. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/describe.py +0 -0
  146. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/image_extraction.py +0 -0
  147. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vllm/__init__.py +0 -0
  148. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint/vllm/metrics.py +0 -0
  149. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
  150. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/entry_points.txt +0 -0
  151. {sciwrite_lint-0.2.1 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/top_level.txt +0 -0
  152. {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.2.1
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.0
44
- Requires-Dist: pypdf>=6.0
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>=8.0; extra == "dev"
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
+ [![arXiv](https://img.shields.io/badge/arXiv-2604.08501-b31b1b.svg)](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
- **22 automated checks:**
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 8 open-access sources (arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv/medRxiv, CORE), parses via GROBID, embeds sections, and verifies each claim against the actual source text
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. Configurable exit codes for CI integration
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
- - **8-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, CORE
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
- **Paywalled references:** drop PDFs into `local_pdfs/` with filenames matching the reference title. The tool fuzzy-matches filenames against your `.bib` titles and uses local copies instead of downloading.
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
- **Optional API keys** increase rate limits for Semantic Scholar, NCBI, and CORE:
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
+ [![arXiv](https://img.shields.io/badge/arXiv-2604.08501-b31b1b.svg)](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
- **22 automated checks:**
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 8 open-access sources (arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv/medRxiv, CORE), parses via GROBID, embeds sections, and verifies each claim against the actual source text
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. Configurable exit codes for CI integration
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
- - **8-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, CORE
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
- **Paywalled references:** drop PDFs into `local_pdfs/` with filenames matching the reference title. The tool fuzzy-matches filenames against your `.bib` titles and uses local copies instead of downloading.
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
- **Optional API keys** increase rate limits for Semantic Scholar, NCBI, and CORE:
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.2.1"
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.0",
46
- "pypdf>=6.0",
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>=8.0",
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 populated.
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=resp.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
- if web_result.error and web_result.status_code == 0:
1444
- # Connection/DNS/timeout error — include the reason, not just "0"
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"]). FullPaperIssueList caps
100
- # the issues list at 5, which fits comfortably in this budget.
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).