sciwrite-lint 0.2.2__tar.gz → 0.4.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 (173) hide show
  1. {sciwrite_lint-0.2.2/sciwrite_lint.egg-info → sciwrite_lint-0.4.0}/PKG-INFO +74 -22
  2. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/README.md +56 -18
  3. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/pyproject.toml +23 -45
  4. sciwrite_lint-0.4.0/sciwrite_lint/__init__.py +28 -0
  5. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/__main__.py +68 -94
  6. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/_network.py +17 -3
  7. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/api.py +74 -11
  8. sciwrite_lint-0.4.0/sciwrite_lint/api_keys.py +93 -0
  9. sciwrite_lint-0.4.0/sciwrite_lint/checks/_diagnostics.py +237 -0
  10. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/_section_utils.py +27 -9
  11. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/cite_purpose.py +5 -0
  12. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/cross_section_consistency.py +45 -19
  13. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/dangling_cite.py +1 -1
  14. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/full_paper_consistency.py +88 -28
  15. sciwrite_lint-0.4.0/sciwrite_lint/checks/internal_consistency_pairs.py +473 -0
  16. sciwrite_lint-0.4.0/sciwrite_lint/checks/prose_quality.py +565 -0
  17. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/ref_internal_checks.py +120 -26
  18. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/reference_exists.py +18 -2
  19. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/reference_unreliable.py +9 -3
  20. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/registry.py +2 -0
  21. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/structure_promises.py +23 -12
  22. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/claims.py +13 -4
  23. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/claude_cli.py +8 -21
  24. sciwrite_lint-0.4.0/sciwrite_lint/cli/_common.py +106 -0
  25. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/check.py +131 -103
  26. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/config.py +11 -76
  27. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/fetch.py +60 -26
  28. sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/__init__.py +62 -0
  29. sciwrite_lint-0.2.2/sciwrite_lint/cli/misc.py → sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/_monitor.py +524 -593
  30. sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/containers.py +217 -0
  31. sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/dismiss_claim.py +62 -0
  32. sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/grobid.py +53 -0
  33. sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/init.py +14 -0
  34. sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/override.py +66 -0
  35. sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/parse.py +95 -0
  36. sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/vision.py +60 -0
  37. sciwrite_lint-0.4.0/sciwrite_lint/cli/misc/vllm.py +47 -0
  38. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/rank.py +6 -2
  39. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/verify.py +52 -26
  40. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/config.py +332 -9
  41. sciwrite_lint-0.4.0/sciwrite_lint/cross_paper.py +59 -0
  42. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/eval_claims.py +752 -238
  43. sciwrite_lint-0.4.0/sciwrite_lint/exceptions.py +17 -0
  44. sciwrite_lint-0.4.0/sciwrite_lint/footnote_urls.py +584 -0
  45. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/__init__.py +193 -0
  46. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/_common.py +221 -0
  47. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/_download.py +125 -0
  48. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/_orchestrator.py +120 -0
  49. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/_search.py +219 -0
  50. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/_validation.py +461 -0
  51. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/arxiv.py +30 -0
  52. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/biorxiv.py +36 -0
  53. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/core.py +98 -0
  54. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/eric.py +159 -0
  55. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/europepmc.py +76 -0
  56. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/hal.py +171 -0
  57. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/ideas.py +224 -0
  58. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/nasa_ads.py +214 -0
  59. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/nber.py +157 -0
  60. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/osf.py +218 -0
  61. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/pmc.py +69 -0
  62. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/s2.py +28 -0
  63. sciwrite_lint-0.4.0/sciwrite_lint/fulltext/unpaywall.py +59 -0
  64. sciwrite_lint-0.4.0/sciwrite_lint/latex_to_markdown.py +136 -0
  65. sciwrite_lint-0.4.0/sciwrite_lint/llm/__init__.py +5 -0
  66. sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/__init__.py +60 -0
  67. sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/compute_cap.py +84 -0
  68. sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/controller.py +464 -0
  69. sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/decide.py +462 -0
  70. sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/host_metrics.py +113 -0
  71. sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/metrics_probe.py +70 -0
  72. sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/registry.py +199 -0
  73. sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/telemetry.py +414 -0
  74. sciwrite_lint-0.2.2/sciwrite_lint/vllm/metrics.py → sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/vllm_metrics.py +34 -38
  75. sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/wiring.py +119 -0
  76. sciwrite_lint-0.4.0/sciwrite_lint/llm_utils.py +826 -0
  77. sciwrite_lint-0.4.0/sciwrite_lint/local_sources.py +422 -0
  78. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/manuscript_store.py +261 -17
  79. sciwrite_lint-0.4.0/sciwrite_lint/mhtml.py +209 -0
  80. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/models.py +2 -4
  81. sciwrite_lint-0.4.0/sciwrite_lint/oa/__init__.py +42 -0
  82. sciwrite_lint-0.4.0/sciwrite_lint/oa/_models.py +91 -0
  83. sciwrite_lint-0.4.0/sciwrite_lint/oa/_pdf.py +404 -0
  84. sciwrite_lint-0.4.0/sciwrite_lint/oa/_search.py +113 -0
  85. sciwrite_lint-0.4.0/sciwrite_lint/oa/_web.py +165 -0
  86. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/pdf/grobid.py +244 -33
  87. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/pdf/pdf_download.py +31 -43
  88. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/__init__.py +151 -0
  89. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/checks.py +195 -0
  90. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/claims.py +116 -0
  91. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/embeddings.py +350 -0
  92. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/fetch.py +290 -0
  93. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/orchestration.py +440 -0
  94. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/parse.py +153 -0
  95. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/pdf_context.py +87 -0
  96. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/ref_internal.py +26 -0
  97. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/runners.py +651 -0
  98. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/staged.py +761 -0
  99. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/swap.py +305 -0
  100. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/tracking.py +106 -0
  101. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/unreliable.py +47 -0
  102. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/verify.py +539 -0
  103. sciwrite_lint-0.4.0/sciwrite_lint/pipeline/vision.py +284 -0
  104. sciwrite_lint-0.4.0/sciwrite_lint/references/_embed_timing.py +63 -0
  105. {sciwrite_lint-0.2.2/sciwrite_lint/scoring → sciwrite_lint-0.4.0/sciwrite_lint/references}/chain.py +3 -1
  106. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/references/citations.py +115 -59
  107. sciwrite_lint-0.4.0/sciwrite_lint/references/embedding_store.py +429 -0
  108. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/references/matching.py +8 -34
  109. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/references/metadata.py +16 -17
  110. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/references/reference_store.py +420 -102
  111. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/references/retraction_watch.py +4 -3
  112. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/__init__.py +149 -0
  113. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/_core.py +381 -0
  114. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/bib_checks.py +82 -0
  115. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/citation_metadata.py +178 -0
  116. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/claim_results.py +164 -0
  117. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/manuscript_citations.py +89 -0
  118. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/parse_cache.py +136 -0
  119. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/pipeline_stage.py +82 -0
  120. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/query_vectors.py +32 -0
  121. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/ref_internal.py +101 -0
  122. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/registry.py +259 -0
  123. sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/vision_cache.py +143 -0
  124. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/report.py +14 -5
  125. sciwrite_lint-0.4.0/sciwrite_lint/schemas.py +505 -0
  126. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/scoring/contribution.py +2 -2
  127. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/scoring/scilint_score.py +32 -10
  128. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/tex_parser.py +67 -0
  129. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/usage.py +4 -1
  130. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/vision/cache.py +60 -24
  131. sciwrite_lint-0.4.0/sciwrite_lint/vision/describe.py +1095 -0
  132. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/vision/pipeline.py +4 -3
  133. sciwrite_lint-0.4.0/sciwrite_lint/vllm/metrics.py +42 -0
  134. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/vllm/vllm_server.py +290 -20
  135. sciwrite_lint-0.4.0/sciwrite_lint/web.py +604 -0
  136. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0/sciwrite_lint.egg-info}/PKG-INFO +74 -22
  137. sciwrite_lint-0.4.0/sciwrite_lint.egg-info/SOURCES.txt +159 -0
  138. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint.egg-info/requires.txt +17 -3
  139. sciwrite_lint-0.2.2/sciwrite_lint/__init__.py +0 -3
  140. sciwrite_lint-0.2.2/sciwrite_lint/cross_paper.py +0 -174
  141. sciwrite_lint-0.2.2/sciwrite_lint/fulltext.py +0 -863
  142. sciwrite_lint-0.2.2/sciwrite_lint/llm_utils.py +0 -415
  143. sciwrite_lint-0.2.2/sciwrite_lint/local_pdfs.py +0 -122
  144. sciwrite_lint-0.2.2/sciwrite_lint/pipeline.py +0 -3102
  145. sciwrite_lint-0.2.2/sciwrite_lint/references/embedding_store.py +0 -389
  146. sciwrite_lint-0.2.2/sciwrite_lint/references/workspace_db.py +0 -1508
  147. sciwrite_lint-0.2.2/sciwrite_lint/schemas.py +0 -260
  148. sciwrite_lint-0.2.2/sciwrite_lint/vision/describe.py +0 -632
  149. sciwrite_lint-0.2.2/sciwrite_lint/web.py +0 -369
  150. sciwrite_lint-0.2.2/sciwrite_lint.egg-info/SOURCES.txt +0 -80
  151. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/LICENSE +0 -0
  152. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/__init__.py +0 -0
  153. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/claim_support.py +0 -0
  154. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/dangling_ref.py +0 -0
  155. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/reference_accuracy.py +0 -0
  156. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/retracted_cite.py +0 -0
  157. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
  158. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/claude_backend.py +0 -0
  159. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/__init__.py +0 -0
  160. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/pdf/__init__.py +0 -0
  161. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/prompt_safety.py +0 -0
  162. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/py.typed +0 -0
  163. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/rate_limiter.py +0 -0
  164. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/references/__init__.py +0 -0
  165. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/references/crossref.py +0 -0
  166. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/scoring/__init__.py +0 -0
  167. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/vision/__init__.py +0 -0
  168. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/vision/image_extraction.py +0 -0
  169. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint/vllm/__init__.py +0 -0
  170. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
  171. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint.egg-info/entry_points.txt +0 -0
  172. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.0}/sciwrite_lint.egg-info/top_level.txt +0 -0
  173. {sciwrite_lint-0.2.2 → sciwrite_lint-0.4.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.4.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,13 +41,27 @@ 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
51
+ Requires-Dist: psutil>=5.9
52
+ Requires-Dist: annotated-types>=0.6
48
53
  Provides-Extra: dev
49
- Requires-Dist: pytest>=8.0; extra == "dev"
54
+ Requires-Dist: pytest>=9.0.3; extra == "dev"
50
55
  Requires-Dist: pytest-xdist>=3.0; extra == "dev"
56
+ Requires-Dist: ruff>=0.15; extra == "dev"
57
+ Requires-Dist: mypy>=1.18; extra == "dev"
58
+ Requires-Dist: ast-grep-cli>=0.35; extra == "dev"
59
+ Requires-Dist: deptry>=0.25; extra == "dev"
60
+ Requires-Dist: bandit>=1.9; extra == "dev"
61
+ Requires-Dist: pip-audit>=2.10; extra == "dev"
62
+ Requires-Dist: pip-licenses>=5.5; extra == "dev"
63
+ Requires-Dist: twine>=6.0; extra == "dev"
64
+ Requires-Dist: build>=1.0; extra == "dev"
51
65
  Dynamic: license-file
52
66
 
53
67
  # sciwrite-lint
@@ -66,7 +80,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
66
80
 
67
81
  ## Features
68
82
 
69
- **22 automated checks:**
83
+ **23 automated checks:**
70
84
 
71
85
  ### Reference verification
72
86
  - **Do your references exist?** — checked against CrossRef, OpenAlex, Semantic Scholar, Open Library, and Library of Congress
@@ -75,7 +89,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
75
89
  - **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
90
 
77
91
  ### 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
92
+ - **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
93
  - **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
94
  - **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
95
 
@@ -88,6 +102,7 @@ AI writing tools produce text that *looks* like good science — fluent prose, c
88
102
  - **Abstract–body alignment** — abstract makes factual claims the body contradicts
89
103
  - **Statistical reporting** — p-values vs. their verbal interpretation
90
104
  - **Structure promises** — contributions promised in the introduction but never delivered
105
+ - **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
106
 
92
107
  ### Figure checks (vision model + LLM)
93
108
  - **Caption vs. content** — does the caption match what the figure actually shows?
@@ -128,26 +143,26 @@ sciwrite-lint contributions paper.pdf --format json
128
143
  ### Two audiences
129
144
 
130
145
  - **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
146
+ - **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
147
 
133
148
  ### Optimizations
134
149
 
135
- Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. Models run in separate pipeline stages; on WSL2, CUDA memory virtualization pages idle allocations to system RAM, letting all three share physical VRAM. FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
150
+ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. The pipeline runs each in its own stage and explicitly stops one before starting the next, so only one ever owns GPU memory at a time (same code path on WSL2 and native Linux). FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
136
151
 
137
- - **Semantic section filtering** — embedding-based KNN retrieval sends only the ~5 most relevant sections per claim to the LLM, reducing LLM calls ~4x
152
+ - **Cost-aware verify-claim ladder** — sentence chunk → paragraph chunk → whole section. Each level fans out top-N candidates in parallel; stops on a conclusive verdict. Most claims resolve at the cheap sentence level (~200-token prompts) instead of paying for whole sections (~5K tokens), with full-section escalation reserved for hard cases
138
153
  - **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
139
- - **Per-call-site thinking budgets** — each LLM call site has empirically tuned (max_tokens, thinking_preset) pairs, measured via grid search to maximize detection quality
154
+ - **Per-call-site thinking budgets** — each LLM call site has its own (max_tokens, thinking_preset) pair tuned for that check's prompt and output shape
140
155
  - **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
141
156
  - **Batch-staged multi-paper pipeline** — when checking 2+ papers, GPU models load once per batch (vision/embedding/cited-vision) and vLLM/network stages run concurrently, giving a meaningful speedup over sequential per-paper runs. Tune via `--concurrency` (default 2, validated up to 4 on a single consumer GPU)
142
157
  - **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
158
+ - **14-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, NBER, RePEc/IDEAS, HAL, ERIC, NASA ADS, OSF Preprints, CORE
144
159
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
145
160
 
146
- 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.)*
161
+ Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. The pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50× faster than CPU) same code path on WSL2 and native Linux.
147
162
 
148
163
  ## Install
149
164
 
150
- **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Developed and tested on Windows (WSL2). Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
165
+ **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Tested on WSL2 with NVIDIA driver 546.01+. Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
151
166
 
152
167
  Requires [uv](https://docs.astral.sh/uv/getting-started/installation/), a container runtime (podman or docker), CUDA drivers, and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html).
153
168
 
@@ -186,30 +201,63 @@ Activate the venv in each new terminal with `source .venv/bin/activate`. For mos
186
201
  ## Setup
187
202
 
188
203
  ```bash
189
- sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/
204
+ sciwrite-lint init # scaffold .sciwrite-lint.toml + references/ + local_pdfs/ + local_web/
190
205
  sciwrite-lint config set-email you@example.com # required for Unpaywall + Retraction Watch
191
206
  sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM)
192
207
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
193
208
  ```
194
209
 
210
+ ### WSL2: disable CUDA Sysmem Fallback (recommended)
211
+
212
+ By default Windows silently spills GPU overflow into system RAM via PCIe (30–100× slower than VRAM); you'll see this as a sudden throughput collapse during the heaviest pipeline stages with no error in the logs. Tell the NVIDIA driver to fail loudly instead:
213
+
214
+ > NVIDIA Control Panel → 3D Settings → Manage 3D Settings → *CUDA - Sysmem Fallback Policy* → **Prefer No Sysmem Fallback** → Apply.
215
+
216
+ Driver 546.01+. After this, WSL2 behaves like native Linux on overflow (CUDA OOM error). The pipeline already sequences GPU consumers so OOM should not happen in normal use; the setting just prevents silent slowdowns when something does go wrong. No setting needed on native Linux — `cudaMalloc` is already loud there.
217
+
195
218
  ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
196
219
 
197
220
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
198
221
 
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:
222
+ **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
223
 
224
+ - `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.
225
+ - `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.
226
+
227
+ Name files by the reference title, or prefix them with the citekey:
228
+
229
+ ```
230
+ local_pdfs/ local_web/
231
+ ├── States of Curiosity … Learning.pdf ├── dewey1910_How_We_Think.mhtml
232
+ ├── Mind in Society.pdf ├── earthwatch2025_About.md
233
+ ├── vanfraassen1980_The_Scientific_Image.pdf └── bca2026_Research_Teachers.md
234
+ └── dewey2016_summary.md # reader-written
201
235
  ```
202
- local_pdfs/
203
- ├── States of Curiosity Modulate Hippocampus-Dependent Learning.pdf
204
- ├── Mind in Society.pdf
205
- └── The System of Professions.pdf
236
+
237
+ 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.
238
+
239
+ **Per-paper curated archives.** Each paper can point both drop folders at its own directories in its `[[papers]]` block:
240
+
241
+ ```toml
242
+ [[papers]]
243
+ name = "my-paper"
244
+ file_path = "papers/my-paper/my-paper.tex"
245
+ bib = "papers/my-paper/my-paper.bib"
246
+ local_pdfs_dir = "papers/my-paper/Sources/full_text" # academic archive
247
+ local_web_dir = "papers/my-paper/Sources/full_text_web" # web captures
206
248
  ```
207
249
 
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.
250
+ 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.
251
+
252
+ **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.
209
253
 
210
- **Finding what to download:** `sciwrite-lint verify` lists T2 references that are confirmed open access with direct URLsopen in your browser and save to `local_pdfs/`. For paywalled papers, use your institution's library access.
254
+ **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.
211
255
 
212
- **Optional API keys** increase rate limits for Semantic Scholar, NCBI, and CORE:
256
+ **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.
257
+
258
+ **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.
259
+
260
+ **API keys** increase rate limits for Semantic Scholar, NCBI, and CORE, and enable NASA ADS (required for that source):
213
261
 
214
262
  ```bash
215
263
  sciwrite-lint config show # see what's configured
@@ -227,6 +275,7 @@ sciwrite-lint check # all papers (batch-staged when
227
275
  sciwrite-lint check --concurrency 4 # batch parallelism (default 2, validated up to 4)
228
276
  sciwrite-lint check paper.tex # text + LLM rules on a .tex file
229
277
  sciwrite-lint check paper.pdf # check a PDF (GROBID required)
278
+ sciwrite-lint check --checks prose-quality paper.tex # run only the listed checks (comma-separated)
230
279
  sciwrite-lint contributions --paper my_paper # add contribution axes to SciLint Score
231
280
  sciwrite-lint contributions paper.pdf # standalone file scoring
232
281
  ```
@@ -235,6 +284,8 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
235
284
 
236
285
  Use `--fresh` to start from scratch (backs up the existing workspace before recreating it).
237
286
 
287
+ Use `--checks ID[,ID...]` to run only the listed checks (comma-separated), disabling the rest for this invocation. Useful for fast iteration during editing (`--checks prose-quality` runs just the prose review), for debugging a single check, or for scripting a two-pass workflow. Unknown check IDs fail loudly — run `sciwrite-lint checks` to list available IDs.
288
+
238
289
  ### Contribution axes (`sciwrite-lint contributions`)
239
290
 
240
291
  `contributions` computes 5 contribution axes from philosophy of science (Popper, Lakatos, Kitcher, Laudan, Mayo) and updates the SciLint Score. Requires vLLM.
@@ -303,6 +354,7 @@ Output formats: terminal (default) or `--format json`.
303
354
  - `sciwrite-lint checks` — list all checks
304
355
  - `sciwrite-lint <command> --help` — detailed usage for any command
305
356
  - [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) — GROBID, vLLM, external APIs, configuration
357
+ - [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
358
 
307
359
  For contributors and advanced users:
308
360
 
@@ -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,26 +77,26 @@ 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
 
83
- Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. Models run in separate pipeline stages; on WSL2, CUDA memory virtualization pages idle allocations to system RAM, letting all three share physical VRAM. FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
84
+ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. The pipeline runs each in its own stage and explicitly stops one before starting the next, so only one ever owns GPU memory at a time (same code path on WSL2 and native Linux). FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
84
85
 
85
- - **Semantic section filtering** — embedding-based KNN retrieval sends only the ~5 most relevant sections per claim to the LLM, reducing LLM calls ~4x
86
+ - **Cost-aware verify-claim ladder** — sentence chunk → paragraph chunk → whole section. Each level fans out top-N candidates in parallel; stops on a conclusive verdict. Most claims resolve at the cheap sentence level (~200-token prompts) instead of paying for whole sections (~5K tokens), with full-section escalation reserved for hard cases
86
87
  - **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
87
- - **Per-call-site thinking budgets** — each LLM call site has empirically tuned (max_tokens, thinking_preset) pairs, measured via grid search to maximize detection quality
88
+ - **Per-call-site thinking budgets** — each LLM call site has its own (max_tokens, thinking_preset) pair tuned for that check's prompt and output shape
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
- 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.)*
95
+ Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. The pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50× faster than CPU) same code path on WSL2 and native Linux.
95
96
 
96
97
  ## Install
97
98
 
98
- **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Developed and tested on Windows (WSL2). Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
99
+ **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Tested on WSL2 with NVIDIA driver 546.01+. Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
99
100
 
100
101
  Requires [uv](https://docs.astral.sh/uv/getting-started/installation/), a container runtime (podman or docker), CUDA drivers, and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html).
101
102
 
@@ -134,30 +135,63 @@ 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
141
142
  ```
142
143
 
144
+ ### WSL2: disable CUDA Sysmem Fallback (recommended)
145
+
146
+ By default Windows silently spills GPU overflow into system RAM via PCIe (30–100× slower than VRAM); you'll see this as a sudden throughput collapse during the heaviest pipeline stages with no error in the logs. Tell the NVIDIA driver to fail loudly instead:
147
+
148
+ > NVIDIA Control Panel → 3D Settings → Manage 3D Settings → *CUDA - Sysmem Fallback Policy* → **Prefer No Sysmem Fallback** → Apply.
149
+
150
+ Driver 546.01+. After this, WSL2 behaves like native Linux on overflow (CUDA OOM error). The pipeline already sequences GPU consumers so OOM should not happen in normal use; the setting just prevents silent slowdowns when something does go wrong. No setting needed on native Linux — `cudaMalloc` is already loud there.
151
+
143
152
  ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
144
153
 
145
154
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
146
155
 
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:
156
+ **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
157
 
158
+ - `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.
159
+ - `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.
160
+
161
+ Name files by the reference title, or prefix them with the citekey:
162
+
163
+ ```
164
+ local_pdfs/ local_web/
165
+ ├── States of Curiosity … Learning.pdf ├── dewey1910_How_We_Think.mhtml
166
+ ├── Mind in Society.pdf ├── earthwatch2025_About.md
167
+ ├── vanfraassen1980_The_Scientific_Image.pdf └── bca2026_Research_Teachers.md
168
+ └── dewey2016_summary.md # reader-written
149
169
  ```
150
- local_pdfs/
151
- ├── States of Curiosity Modulate Hippocampus-Dependent Learning.pdf
152
- ├── Mind in Society.pdf
153
- └── The System of Professions.pdf
170
+
171
+ 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.
172
+
173
+ **Per-paper curated archives.** Each paper can point both drop folders at its own directories in its `[[papers]]` block:
174
+
175
+ ```toml
176
+ [[papers]]
177
+ name = "my-paper"
178
+ file_path = "papers/my-paper/my-paper.tex"
179
+ bib = "papers/my-paper/my-paper.bib"
180
+ local_pdfs_dir = "papers/my-paper/Sources/full_text" # academic archive
181
+ local_web_dir = "papers/my-paper/Sources/full_text_web" # web captures
154
182
  ```
155
183
 
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.
184
+ 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.
185
+
186
+ **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.
157
187
 
158
- **Finding what to download:** `sciwrite-lint verify` lists T2 references that are confirmed open access with direct URLsopen in your browser and save to `local_pdfs/`. For paywalled papers, use your institution's library access.
188
+ **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.
159
189
 
160
- **Optional API keys** increase rate limits for Semantic Scholar, NCBI, and CORE:
190
+ **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.
191
+
192
+ **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.
193
+
194
+ **API keys** increase rate limits for Semantic Scholar, NCBI, and CORE, and enable NASA ADS (required for that source):
161
195
 
162
196
  ```bash
163
197
  sciwrite-lint config show # see what's configured
@@ -175,6 +209,7 @@ sciwrite-lint check # all papers (batch-staged when
175
209
  sciwrite-lint check --concurrency 4 # batch parallelism (default 2, validated up to 4)
176
210
  sciwrite-lint check paper.tex # text + LLM rules on a .tex file
177
211
  sciwrite-lint check paper.pdf # check a PDF (GROBID required)
212
+ sciwrite-lint check --checks prose-quality paper.tex # run only the listed checks (comma-separated)
178
213
  sciwrite-lint contributions --paper my_paper # add contribution axes to SciLint Score
179
214
  sciwrite-lint contributions paper.pdf # standalone file scoring
180
215
  ```
@@ -183,6 +218,8 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
183
218
 
184
219
  Use `--fresh` to start from scratch (backs up the existing workspace before recreating it).
185
220
 
221
+ Use `--checks ID[,ID...]` to run only the listed checks (comma-separated), disabling the rest for this invocation. Useful for fast iteration during editing (`--checks prose-quality` runs just the prose review), for debugging a single check, or for scripting a two-pass workflow. Unknown check IDs fail loudly — run `sciwrite-lint checks` to list available IDs.
222
+
186
223
  ### Contribution axes (`sciwrite-lint contributions`)
187
224
 
188
225
  `contributions` computes 5 contribution axes from philosophy of science (Popper, Lakatos, Kitcher, Laudan, Mayo) and updates the SciLint Score. Requires vLLM.
@@ -251,6 +288,7 @@ Output formats: terminal (default) or `--format json`.
251
288
  - `sciwrite-lint checks` — list all checks
252
289
  - `sciwrite-lint <command> --help` — detailed usage for any command
253
290
  - [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) — GROBID, vLLM, external APIs, configuration
291
+ - [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
292
 
255
293
  For contributors and advanced users:
256
294
 
@@ -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.4.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,15 @@ 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",
52
+ "psutil>=5.9",
53
+ "annotated-types>=0.6",
49
54
  ]
50
55
 
51
56
  [project.urls]
@@ -56,9 +61,23 @@ Issues = "https://github.com/authentic-research-partners/sciwrite-lint/issues"
56
61
  Paper = "https://arxiv.org/abs/2604.08501"
57
62
 
58
63
  [project.optional-dependencies]
64
+ # `pip install -e ".[dev]"` brings in every tool used by the mandatory checks
65
+ # and the release pipeline. Exception: the `claude` CLI is a Node app —
66
+ # install separately per https://docs.anthropic.com/en/docs/claude-code.
59
67
  dev = [
60
- "pytest>=8.0",
68
+ # Mandatory checks — run after every code change
69
+ "pytest>=9.0.3",
61
70
  "pytest-xdist>=3.0",
71
+ "ruff>=0.15",
72
+ "mypy>=1.18",
73
+ "ast-grep-cli>=0.35",
74
+ # Release pipeline (scripts/release.sh phases 3–6)
75
+ "deptry>=0.25",
76
+ "bandit>=1.9",
77
+ "pip-audit>=2.10",
78
+ "pip-licenses>=5.5",
79
+ "twine>=6.0",
80
+ "build>=1.0",
62
81
  ]
63
82
 
64
83
  [project.scripts]
@@ -67,44 +86,3 @@ sciwrite-lint = "sciwrite_lint.__main__:main"
67
86
  [tool.setuptools.packages.find]
68
87
  include = ["sciwrite_lint*"]
69
88
 
70
- [tool.pytest.ini_options]
71
- testpaths = ["tests"]
72
- pythonpath = ["."]
73
- markers = [
74
- "integration: tests that read actual paper files",
75
- "network: tests that make real network connections (slow)",
76
- ]
77
- # PID-unique basetemp set in conftest.py — prevents FileExistsError
78
- # when multiple pytest processes run concurrently.
79
- addopts = "-n auto -p no:cacheprovider"
80
-
81
- [tool.bandit]
82
- skips = [
83
- "B101", # assert — used for type narrowing and internal invariants
84
- "B104", # bind 0.0.0.0 — container start commands for GROBID/vLLM
85
- "B314", # xml.etree.fromstring — parsing trusted GROBID TEI output
86
- "B404", # subprocess import — CLI tool, subprocess is core functionality
87
- "B405", # xml.etree import — parsing trusted GROBID TEI output
88
- "B603", # subprocess call — CLI invokes grobid/vllm/claude by design
89
- "B607", # partial path — CLI tools resolved via PATH
90
- "B608", # SQL string — internal usage.db, no user input in queries
91
- "B615", # HF from_pretrained without revision pin — trusted publisher (Qwen), research tool
92
- ]
93
-
94
- [tool.deptry]
95
- optional_dependencies_dev_groups = ["dev"]
96
-
97
- [tool.deptry.per_rule_ignores]
98
- # Self-imports of shipped non-package directories
99
- DEP001 = ["evals", "eval_real_world", "bench"]
100
- # Lazy/conditional imports in optional groups — deptry can't detect these
101
- DEP002 = ["bibtexparser", "pdfplumber", "sentence-transformers", "numpy", "sqlite-vec", "grobid-tei-xml", "torch", "transformers", "Pillow", "pypdf", "pdf2image"]
102
- # Intra-package imports
103
- DEP003 = ["sciwrite_lint", "evals", "eval_real_world"]
104
-
105
- [tool.mypy]
106
- plugins = ["pydantic.mypy"]
107
- [[tool.mypy.overrides]]
108
- # No py.typed marker or typeshed stubs available
109
- module = ["bibtexparser", "sqlite_vec", "pypdf", "pypdfium2", "pypdfium2.raw", "transformers", "pdf2image"]
110
- 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.4.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
+ ]