sciwrite-lint 0.2.2__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.2/sciwrite_lint.egg-info → sciwrite_lint-0.3.0}/PKG-INFO +50 -17
  2. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/README.md +43 -13
  3. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/pyproject.toml +8 -5
  4. sciwrite_lint-0.3.0/sciwrite_lint/__init__.py +28 -0
  5. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/__main__.py +11 -0
  6. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/_network.py +17 -3
  7. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/api.py +23 -5
  8. {sciwrite_lint-0.2.2 → 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.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/ref_internal_checks.py +17 -3
  11. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/reference_exists.py +18 -2
  12. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/registry.py +1 -0
  13. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/check.py +96 -27
  14. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/config.py +5 -0
  15. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/fetch.py +75 -25
  16. sciwrite_lint-0.3.0/sciwrite_lint/cli/misc/__init__.py +62 -0
  17. sciwrite_lint-0.2.2/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.2 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/verify.py +48 -22
  27. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/config.py +118 -3
  28. {sciwrite_lint-0.2.2 → 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.2 → 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.2 → 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.2 → sciwrite_lint-0.3.0}/sciwrite_lint/pdf/grobid.py +109 -1
  62. {sciwrite_lint-0.2.2 → 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.2 → sciwrite_lint-0.3.0}/sciwrite_lint/references/citations.py +58 -4
  80. {sciwrite_lint-0.2.2 → 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.2 → sciwrite_lint-0.3.0}/sciwrite_lint/schemas.py +32 -3
  93. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/tex_parser.py +67 -0
  94. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/pipeline.py +3 -3
  95. {sciwrite_lint-0.2.2 → 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.2 → sciwrite_lint-0.3.0/sciwrite_lint.egg-info}/PKG-INFO +50 -17
  98. sciwrite_lint-0.3.0/sciwrite_lint.egg-info/SOURCES.txt +142 -0
  99. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/requires.txt +6 -3
  100. sciwrite_lint-0.2.2/sciwrite_lint/__init__.py +0 -3
  101. sciwrite_lint-0.2.2/sciwrite_lint/fulltext.py +0 -863
  102. sciwrite_lint-0.2.2/sciwrite_lint/llm_utils.py +0 -415
  103. sciwrite_lint-0.2.2/sciwrite_lint/local_pdfs.py +0 -122
  104. sciwrite_lint-0.2.2/sciwrite_lint/pipeline.py +0 -3102
  105. sciwrite_lint-0.2.2/sciwrite_lint/references/workspace_db.py +0 -1508
  106. sciwrite_lint-0.2.2/sciwrite_lint/web.py +0 -369
  107. sciwrite_lint-0.2.2/sciwrite_lint.egg-info/SOURCES.txt +0 -80
  108. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/LICENSE +0 -0
  109. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/__init__.py +0 -0
  110. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/_section_utils.py +0 -0
  111. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/cite_purpose.py +0 -0
  112. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/claim_support.py +0 -0
  113. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/cross_section_consistency.py +0 -0
  114. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/dangling_cite.py +0 -0
  115. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/dangling_ref.py +0 -0
  116. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/reference_accuracy.py +0 -0
  117. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/reference_unreliable.py +0 -0
  118. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/retracted_cite.py +0 -0
  119. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/structure_promises.py +0 -0
  120. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
  121. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/claims.py +0 -0
  122. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/claude_backend.py +0 -0
  123. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/claude_cli.py +0 -0
  124. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/__init__.py +0 -0
  125. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/cli/rank.py +0 -0
  126. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/cross_paper.py +0 -0
  127. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/pdf/__init__.py +0 -0
  128. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/prompt_safety.py +0 -0
  129. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/py.typed +0 -0
  130. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/rate_limiter.py +0 -0
  131. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/references/__init__.py +0 -0
  132. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/references/crossref.py +0 -0
  133. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/references/embedding_store.py +0 -0
  134. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/references/matching.py +0 -0
  135. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/references/reference_store.py +0 -0
  136. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/references/retraction_watch.py +0 -0
  137. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/report.py +0 -0
  138. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/__init__.py +0 -0
  139. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/chain.py +0 -0
  140. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/contribution.py +0 -0
  141. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/scoring/scilint_score.py +0 -0
  142. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/usage.py +0 -0
  143. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/__init__.py +0 -0
  144. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/cache.py +0 -0
  145. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/describe.py +0 -0
  146. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/vision/image_extraction.py +0 -0
  147. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/vllm/__init__.py +0 -0
  148. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint/vllm/metrics.py +0 -0
  149. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
  150. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/entry_points.txt +0 -0
  151. {sciwrite_lint-0.2.2 → sciwrite_lint-0.3.0}/sciwrite_lint.egg-info/top_level.txt +0 -0
  152. {sciwrite_lint-0.2.2 → 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.2
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>
@@ -41,12 +41,15 @@ Requires-Dist: numpy>=2.0
41
41
  Requires-Dist: sqlite-vec>=0.1.6
42
42
  Requires-Dist: torch>=2.5
43
43
  Requires-Dist: transformers>=4.50
44
- Requires-Dist: Pillow>=12.0
45
- Requires-Dist: pypdf>=6.0
44
+ Requires-Dist: Pillow>=12.2
45
+ Requires-Dist: pypdf>=6.10.2
46
46
  Requires-Dist: pypdfium2>=5.0
47
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
48
51
  Provides-Extra: dev
49
- Requires-Dist: pytest>=8.0; extra == "dev"
52
+ Requires-Dist: pytest>=9.0.3; extra == "dev"
50
53
  Requires-Dist: pytest-xdist>=3.0; extra == "dev"
51
54
  Dynamic: license-file
52
55
 
@@ -66,7 +69,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
66
69
 
67
70
  ## Features
68
71
 
69
- **22 automated checks:**
72
+ **23 automated checks:**
70
73
 
71
74
  ### Reference verification
72
75
  - **Do your references exist?** — checked against CrossRef, OpenAlex, Semantic Scholar, Open Library, and Library of Congress
@@ -75,7 +78,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
75
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)
76
79
 
77
80
  ### Claim verification (local LLM)
78
- - **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
79
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
80
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
81
84
 
@@ -88,6 +91,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
88
91
  - **Abstract–body alignment** — abstract makes factual claims the body contradicts
89
92
  - **Statistical reporting** — p-values vs. their verbal interpretation
90
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
91
95
 
92
96
  ### Figure checks (vision model + LLM)
93
97
  - **Caption vs. content** — does the caption match what the figure actually shows?
@@ -128,7 +132,7 @@ sciwrite-lint contributions paper.pdf --format json
128
132
  ### Two audiences
129
133
 
130
134
  - **Humans** — colored terminal output with severity levels, locations, and explanations. Decide in seconds whether each finding is real
131
- - **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
132
136
 
133
137
  ### Optimizations
134
138
 
@@ -140,7 +144,7 @@ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-ba
140
144
  - **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
141
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)
142
146
  - **Phased API resolution** — citations flow through OpenAlex → Semantic Scholar → CrossRef → Open Library/LoC, each phase only processing what previous phases didn't resolve
143
- - **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
144
148
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
145
149
 
146
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.)*
@@ -186,7 +190,7 @@ Activate the venv in each new terminal with `source .venv/bin/activate`. For mos
186
190
  ## Setup
187
191
 
188
192
  ```bash
189
- sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/
193
+ sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/ + local_web/
190
194
  sciwrite-lint config set-email you@example.com # required for Unpaywall + Retraction Watch
191
195
  sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM)
192
196
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
@@ -196,20 +200,45 @@ sciwrite-lint containers monitor # live dashboard: service health, VRAM, K
196
200
 
197
201
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
198
202
 
199
- **Providing PDFs manually (`local_pdfs/`):** Drop any reference PDF into the `local_pdfs/` directory to skip the download step entirely. This is useful when you already have the file, when the paper is paywalled, or when the publisher blocks automated downloads (Cell, Springer, some PMC pages). Name files by the reference title:
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:
200
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
201
216
  ```
202
- local_pdfs/
203
- ├── States of Curiosity Modulate Hippocampus-Dependent Learning.pdf
204
- ├── Mind in Society.pdf
205
- └── The System of Professions.pdf
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.
219
+
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
206
229
  ```
207
230
 
208
- The tool fuzzy-matches filenames against your `.bib` titles (threshold 0.80) and copies matches to the workspace. On the next run, matched references upgrade from T2 to T1 and go through GROBID parsing and claim verification like any other full-text reference.
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.
209
236
 
210
- **Finding what to download:** `sciwrite-lint verify` lists T2 references that are confirmed open access with direct URLs — open in your browser and save to `local_pdfs/`. For paywalled papers, use your institution's library access.
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.
211
238
 
212
- **Optional API keys** increase rate limits for Semantic Scholar, NCBI, and CORE:
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):
213
242
 
214
243
  ```bash
215
244
  sciwrite-lint config show # see what's configured
@@ -227,6 +256,7 @@ sciwrite-lint check # all papers (batch-staged when
227
256
  sciwrite-lint check --concurrency 4 # batch parallelism (default 2, validated up to 4)
228
257
  sciwrite-lint check paper.tex # text + LLM rules on a .tex file
229
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)
230
260
  sciwrite-lint contributions --paper my_paper # add contribution axes to SciLint Score
231
261
  sciwrite-lint contributions paper.pdf # standalone file scoring
232
262
  ```
@@ -235,6 +265,8 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
235
265
 
236
266
  Use `--fresh` to start from scratch (backs up the existing workspace before recreating it).
237
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
+
238
270
  ### Contribution axes (`sciwrite-lint contributions`)
239
271
 
240
272
  `contributions` computes 5 contribution axes from philosophy of science (Popper, Lakatos, Kitcher, Laudan, Mayo) and updates the SciLint Score. Requires vLLM.
@@ -303,6 +335,7 @@ Output formats: terminal (default) or `--format json`.
303
335
  - `sciwrite-lint checks` — list all checks
304
336
  - `sciwrite-lint <command> --help` — detailed usage for any command
305
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
306
339
 
307
340
  For contributors and advanced users:
308
341
 
@@ -14,7 +14,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
14
14
 
15
15
  ## Features
16
16
 
17
- **22 automated checks:**
17
+ **23 automated checks:**
18
18
 
19
19
  ### Reference verification
20
20
  - **Do your references exist?** — checked against CrossRef, OpenAlex, Semantic Scholar, Open Library, and Library of Congress
@@ -23,7 +23,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
23
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)
24
24
 
25
25
  ### Claim verification (local LLM)
26
- - **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
27
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
28
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
29
29
 
@@ -36,6 +36,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
36
36
  - **Abstract–body alignment** — abstract makes factual claims the body contradicts
37
37
  - **Statistical reporting** — p-values vs. their verbal interpretation
38
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
39
40
 
40
41
  ### Figure checks (vision model + LLM)
41
42
  - **Caption vs. content** — does the caption match what the figure actually shows?
@@ -76,7 +77,7 @@ sciwrite-lint contributions paper.pdf --format json
76
77
  ### Two audiences
77
78
 
78
79
  - **Humans** — colored terminal output with severity levels, locations, and explanations. Decide in seconds whether each finding is real
79
- - **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
80
81
 
81
82
  ### Optimizations
82
83
 
@@ -88,7 +89,7 @@ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-ba
88
89
  - **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
89
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)
90
91
  - **Phased API resolution** — citations flow through OpenAlex → Semantic Scholar → CrossRef → Open Library/LoC, each phase only processing what previous phases didn't resolve
91
- - **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
92
93
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
93
94
 
94
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.)*
@@ -134,7 +135,7 @@ Activate the venv in each new terminal with `source .venv/bin/activate`. For mos
134
135
  ## Setup
135
136
 
136
137
  ```bash
137
- sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/
138
+ sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/ + local_web/
138
139
  sciwrite-lint config set-email you@example.com # required for Unpaywall + Retraction Watch
139
140
  sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM)
140
141
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
@@ -144,20 +145,45 @@ sciwrite-lint containers monitor # live dashboard: service health, VRAM, K
144
145
 
145
146
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
146
147
 
147
- **Providing PDFs manually (`local_pdfs/`):** Drop any reference PDF into the `local_pdfs/` directory to skip the download step entirely. This is useful when you already have the file, when the paper is paywalled, or when the publisher blocks automated downloads (Cell, Springer, some PMC pages). Name files by the reference title:
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:
148
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
149
161
  ```
150
- local_pdfs/
151
- ├── States of Curiosity Modulate Hippocampus-Dependent Learning.pdf
152
- ├── Mind in Society.pdf
153
- └── The System of Professions.pdf
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.
164
+
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
154
174
  ```
155
175
 
156
- The tool fuzzy-matches filenames against your `.bib` titles (threshold 0.80) and copies matches to the workspace. On the next run, matched references upgrade from T2 to T1 and go through GROBID parsing and claim verification like any other full-text reference.
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.
157
181
 
158
- **Finding what to download:** `sciwrite-lint verify` lists T2 references that are confirmed open access with direct URLs — open in your browser and save to `local_pdfs/`. For paywalled papers, use your institution's library access.
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.
159
183
 
160
- **Optional API keys** increase rate limits for Semantic Scholar, NCBI, and CORE:
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):
161
187
 
162
188
  ```bash
163
189
  sciwrite-lint config show # see what's configured
@@ -175,6 +201,7 @@ sciwrite-lint check # all papers (batch-staged when
175
201
  sciwrite-lint check --concurrency 4 # batch parallelism (default 2, validated up to 4)
176
202
  sciwrite-lint check paper.tex # text + LLM rules on a .tex file
177
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)
178
205
  sciwrite-lint contributions --paper my_paper # add contribution axes to SciLint Score
179
206
  sciwrite-lint contributions paper.pdf # standalone file scoring
180
207
  ```
@@ -183,6 +210,8 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
183
210
 
184
211
  Use `--fresh` to start from scratch (backs up the existing workspace before recreating it).
185
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
+
186
215
  ### Contribution axes (`sciwrite-lint contributions`)
187
216
 
188
217
  `contributions` computes 5 contribution axes from philosophy of science (Popper, Lakatos, Kitcher, Laudan, Mayo) and updates the SciLint Score. Requires vLLM.
@@ -251,6 +280,7 @@ Output formats: terminal (default) or `--format json`.
251
280
  - `sciwrite-lint checks` — list all checks
252
281
  - `sciwrite-lint <command> --help` — detailed usage for any command
253
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
254
284
 
255
285
  For contributors and advanced users:
256
286
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sciwrite-lint"
7
- version = "0.2.2"
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]
@@ -57,7 +60,7 @@ Paper = "https://arxiv.org/abs/2604.08501"
57
60
 
58
61
  [project.optional-dependencies]
59
62
  dev = [
60
- "pytest>=8.0",
63
+ "pytest>=9.0.3",
61
64
  "pytest-xdist>=3.0",
62
65
  ]
63
66
 
@@ -106,5 +109,5 @@ DEP003 = ["sciwrite_lint", "evals", "eval_real_world"]
106
109
  plugins = ["pydantic.mypy"]
107
110
  [[tool.mypy.overrides]]
108
111
  # No py.typed marker or typeshed stubs available
109
- 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"]
110
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).