sciwrite-lint 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/PKG-INFO +37 -7
  2. sciwrite_lint-0.3.0/sciwrite_lint.egg-info/PKG-INFO → sciwrite_lint-0.5.0/README.md +25 -61
  3. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/pyproject.toml +18 -41
  4. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/__init__.py +1 -1
  5. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/__main__.py +64 -105
  6. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/api.py +51 -6
  7. sciwrite_lint-0.5.0/sciwrite_lint/api_keys.py +93 -0
  8. sciwrite_lint-0.5.0/sciwrite_lint/checks/_diagnostics.py +237 -0
  9. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/_section_utils.py +27 -9
  10. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/cite_purpose.py +5 -0
  11. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/cross_section_consistency.py +45 -19
  12. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/dangling_cite.py +1 -1
  13. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/full_paper_consistency.py +88 -32
  14. sciwrite_lint-0.5.0/sciwrite_lint/checks/internal_consistency_pairs.py +473 -0
  15. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/prose_quality.py +38 -14
  16. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/ref_internal_checks.py +103 -23
  17. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/reference_unreliable.py +9 -3
  18. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/registry.py +1 -0
  19. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/structure_promises.py +23 -12
  20. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/claims.py +13 -4
  21. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/claude_cli.py +8 -21
  22. sciwrite_lint-0.5.0/sciwrite_lint/cli/_common.py +106 -0
  23. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/check.py +86 -127
  24. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/config.py +11 -81
  25. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/fetch.py +2 -18
  26. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/_monitor.py +517 -9
  27. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/containers.py +3 -2
  28. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/dismiss_claim.py +1 -1
  29. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/override.py +1 -1
  30. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/parse.py +6 -11
  31. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/vision.py +1 -1
  32. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/vllm.py +6 -1
  33. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/rank.py +6 -2
  34. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/verify.py +16 -9
  35. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/config.py +220 -12
  36. sciwrite_lint-0.5.0/sciwrite_lint/cross_paper.py +59 -0
  37. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/eval_claims.py +732 -279
  38. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/footnote_urls.py +5 -1
  39. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_orchestrator.py +12 -19
  40. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_validation.py +52 -4
  41. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/ideas.py +1 -16
  42. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/latex_to_markdown.py +2 -2
  43. sciwrite_lint-0.5.0/sciwrite_lint/llm/__init__.py +5 -0
  44. sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/__init__.py +60 -0
  45. sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/compute_cap.py +84 -0
  46. sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/controller.py +464 -0
  47. sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/decide.py +462 -0
  48. sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/host_metrics.py +113 -0
  49. sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/metrics_probe.py +70 -0
  50. sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/registry.py +199 -0
  51. sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/telemetry.py +414 -0
  52. sciwrite_lint-0.3.0/sciwrite_lint/vllm/metrics.py → sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/vllm_metrics.py +34 -38
  53. sciwrite_lint-0.5.0/sciwrite_lint/llm/concurrency_optimizer/wiring.py +119 -0
  54. sciwrite_lint-0.5.0/sciwrite_lint/llm/manuscript_cache.py +173 -0
  55. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/llm_utils.py +292 -72
  56. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/local_sources.py +5 -3
  57. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/manuscript_store.py +162 -6
  58. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/mhtml.py +17 -5
  59. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/models.py +0 -4
  60. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_web.py +2 -2
  61. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pdf/grobid.py +159 -40
  62. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pdf/pdf_download.py +2 -12
  63. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/__init__.py +8 -4
  64. sciwrite_lint-0.5.0/sciwrite_lint/pipeline/checks.py +275 -0
  65. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/claims.py +2 -3
  66. sciwrite_lint-0.5.0/sciwrite_lint/pipeline/embeddings.py +350 -0
  67. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/fetch.py +17 -3
  68. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/orchestration.py +38 -11
  69. sciwrite_lint-0.5.0/sciwrite_lint/pipeline/parse.py +153 -0
  70. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/runners.py +193 -38
  71. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/staged.py +31 -25
  72. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/swap.py +117 -89
  73. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/tracking.py +2 -1
  74. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/verify.py +18 -5
  75. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/vision.py +106 -13
  76. sciwrite_lint-0.5.0/sciwrite_lint/references/_embed_timing.py +63 -0
  77. {sciwrite_lint-0.3.0/sciwrite_lint/scoring → sciwrite_lint-0.5.0/sciwrite_lint/references}/chain.py +3 -1
  78. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/citations.py +57 -55
  79. sciwrite_lint-0.5.0/sciwrite_lint/references/embedding_store.py +429 -0
  80. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/matching.py +8 -34
  81. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/metadata.py +6 -15
  82. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/reference_store.py +420 -102
  83. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/retraction_watch.py +4 -3
  84. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/__init__.py +28 -0
  85. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/_core.py +95 -5
  86. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/claim_results.py +31 -6
  87. sciwrite_lint-0.5.0/sciwrite_lint/references/workspace_db/manuscript_check_cache.py +103 -0
  88. sciwrite_lint-0.5.0/sciwrite_lint/references/workspace_db/manuscript_citations.py +89 -0
  89. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/report.py +14 -5
  90. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/schemas.py +222 -6
  91. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/scoring/contribution.py +2 -2
  92. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/scoring/scilint_score.py +32 -10
  93. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/usage.py +4 -1
  94. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/cache.py +60 -24
  95. sciwrite_lint-0.5.0/sciwrite_lint/vision/describe.py +1095 -0
  96. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/pipeline.py +1 -0
  97. sciwrite_lint-0.5.0/sciwrite_lint/vllm/metrics.py +42 -0
  98. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vllm/vllm_server.py +289 -20
  99. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/web.py +16 -7
  100. sciwrite_lint-0.3.0/README.md → sciwrite_lint-0.5.0/sciwrite_lint.egg-info/PKG-INFO +91 -6
  101. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/SOURCES.txt +20 -2
  102. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/requires.txt +11 -0
  103. sciwrite_lint-0.3.0/sciwrite_lint/claude_backend.py +0 -94
  104. sciwrite_lint-0.3.0/sciwrite_lint/cross_paper.py +0 -174
  105. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/checks.py +0 -64
  106. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/embeddings.py +0 -234
  107. sciwrite_lint-0.3.0/sciwrite_lint/pipeline/parse.py +0 -57
  108. sciwrite_lint-0.3.0/sciwrite_lint/references/embedding_store.py +0 -389
  109. sciwrite_lint-0.3.0/sciwrite_lint/vision/describe.py +0 -632
  110. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/LICENSE +0 -0
  111. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/_network.py +0 -0
  112. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/__init__.py +0 -0
  113. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/claim_support.py +0 -0
  114. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/dangling_ref.py +0 -0
  115. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/reference_accuracy.py +0 -0
  116. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/reference_exists.py +0 -0
  117. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/retracted_cite.py +0 -0
  118. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
  119. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/__init__.py +0 -0
  120. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/__init__.py +0 -0
  121. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/grobid.py +0 -0
  122. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/cli/misc/init.py +0 -0
  123. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/exceptions.py +0 -0
  124. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/__init__.py +0 -0
  125. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_common.py +0 -0
  126. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_download.py +0 -0
  127. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/_search.py +0 -0
  128. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/arxiv.py +0 -0
  129. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/biorxiv.py +0 -0
  130. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/core.py +0 -0
  131. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/eric.py +0 -0
  132. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/europepmc.py +0 -0
  133. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/hal.py +0 -0
  134. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/nasa_ads.py +0 -0
  135. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/nber.py +0 -0
  136. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/osf.py +0 -0
  137. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/pmc.py +0 -0
  138. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/s2.py +0 -0
  139. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/fulltext/unpaywall.py +0 -0
  140. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/__init__.py +0 -0
  141. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_models.py +0 -0
  142. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_pdf.py +0 -0
  143. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/oa/_search.py +0 -0
  144. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pdf/__init__.py +0 -0
  145. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/pdf_context.py +0 -0
  146. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/ref_internal.py +0 -0
  147. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/pipeline/unreliable.py +0 -0
  148. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/prompt_safety.py +0 -0
  149. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/py.typed +0 -0
  150. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/rate_limiter.py +0 -0
  151. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/__init__.py +0 -0
  152. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/crossref.py +0 -0
  153. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/bib_checks.py +0 -0
  154. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/citation_metadata.py +0 -0
  155. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/parse_cache.py +0 -0
  156. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/pipeline_stage.py +0 -0
  157. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/query_vectors.py +0 -0
  158. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/ref_internal.py +0 -0
  159. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/registry.py +0 -0
  160. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/references/workspace_db/vision_cache.py +0 -0
  161. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/scoring/__init__.py +0 -0
  162. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/tex_parser.py +0 -0
  163. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/__init__.py +0 -0
  164. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vision/image_extraction.py +0 -0
  165. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint/vllm/__init__.py +0 -0
  166. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
  167. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/entry_points.txt +0 -0
  168. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/sciwrite_lint.egg-info/top_level.txt +0 -0
  169. {sciwrite_lint-0.3.0 → sciwrite_lint-0.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sciwrite-lint
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: A linter for scientific manuscripts — reference verification, consistency checking, and structural validation.
5
5
  Author: Sergey Samsonau
6
6
  Maintainer-email: Authentic Research Partners LLC <info@arpconnect.com>
@@ -48,9 +48,20 @@ Requires-Dist: pdf2image>=1.17
48
48
  Requires-Dist: lxml>=6.1
49
49
  Requires-Dist: pypandoc-binary>=1.15
50
50
  Requires-Dist: rfc3986>=2.0
51
+ Requires-Dist: psutil>=5.9
52
+ Requires-Dist: annotated-types>=0.6
51
53
  Provides-Extra: dev
52
54
  Requires-Dist: pytest>=9.0.3; extra == "dev"
53
55
  Requires-Dist: pytest-xdist>=3.0; extra == "dev"
56
+ Requires-Dist: ruff>=0.15; extra == "dev"
57
+ Requires-Dist: mypy>=1.18; extra == "dev"
58
+ Requires-Dist: ast-grep-cli>=0.35; extra == "dev"
59
+ Requires-Dist: deptry>=0.25; extra == "dev"
60
+ Requires-Dist: bandit>=1.9; extra == "dev"
61
+ Requires-Dist: pip-audit>=2.10; extra == "dev"
62
+ Requires-Dist: pip-licenses>=5.5; extra == "dev"
63
+ Requires-Dist: twine>=6.0; extra == "dev"
64
+ Requires-Dist: build>=1.0; extra == "dev"
54
65
  Dynamic: license-file
55
66
 
56
67
  # sciwrite-lint
@@ -136,22 +147,22 @@ sciwrite-lint contributions paper.pdf --format json
136
147
 
137
148
  ### Optimizations
138
149
 
139
- Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. 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 are baseline; every cache layer pins its inputs by hash so editing the paper, replacing a figure, or refreshing a reference recomputes only what's affected (see the *Iterative editing* subsection under Usage — `--fresh` is not part of the normal loop). On top of that:
140
151
 
141
- - **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
142
153
  - **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
143
- - **Per-call-site thinking budgets** — each LLM call site has 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
144
155
  - **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
145
156
  - **Batch-staged multi-paper pipeline** — when checking 2+ papers, GPU models load once per batch (vision/embedding/cited-vision) and vLLM/network stages run concurrently, giving a meaningful speedup over sequential per-paper runs. Tune via `--concurrency` (default 2, validated up to 4 on a single consumer GPU)
146
157
  - **Phased API resolution** — citations flow through OpenAlex → Semantic Scholar → CrossRef → Open Library/LoC, each phase only processing what previous phases didn't resolve
147
158
  - **14-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, NBER, RePEc/IDEAS, HAL, ERIC, NASA ADS, OSF Preprints, CORE
148
159
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
149
160
 
150
- Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. 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.
151
162
 
152
163
  ## Install
153
164
 
154
- **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.
155
166
 
156
167
  Requires [uv](https://docs.astral.sh/uv/getting-started/installation/), a container runtime (podman or docker), CUDA drivers, and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html).
157
168
 
@@ -196,6 +207,14 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
196
207
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
197
208
  ```
198
209
 
210
+ ### WSL2: disable CUDA Sysmem Fallback (recommended)
211
+
212
+ By default Windows silently spills GPU overflow into system RAM via PCIe (30–100× slower than VRAM); you'll see this as a sudden throughput collapse during the heaviest pipeline stages with no error in the logs. Tell the NVIDIA driver to fail loudly instead:
213
+
214
+ > NVIDIA Control Panel → 3D Settings → Manage 3D Settings → *CUDA - Sysmem Fallback Policy* → **Prefer No Sysmem Fallback** → Apply.
215
+
216
+ Driver 546.01+. After this, WSL2 behaves like native Linux on overflow (CUDA OOM error). The pipeline already sequences GPU consumers so OOM should not happen in normal use; the setting just prevents silent slowdowns when something does go wrong. No setting needed on native Linux — `cudaMalloc` is already loud there.
217
+
199
218
  ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
200
219
 
201
220
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
@@ -263,7 +282,18 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
263
282
 
264
283
  `check` runs the full pipeline in one command: text checks → figure analysis → LLM consistency → reference verification via APIs → download and parse cited papers → claim verification → consistency checks on cited papers → bibliography verification → aggregate reliability scores → SciLint Score. An initial run on a 50-reference paper takes up to 30 minutes (dominated by downloads and claim verification); subsequent cached runs complete in minutes.
265
284
 
266
- Use `--fresh` to start from scratch (backs up the existing workspace before recreating it).
285
+ ### Iterative editing no `--fresh` needed
286
+
287
+ `sciwrite-lint check` invalidates cached results whenever the inputs they were derived from change, so rerunning after an edit just works. Concretely:
288
+
289
+ - **Edit a sentence or paragraph** → the manuscript-LLM checks (prose-quality, cross-section consistency, etc.) re-run only on the touched sentences and their paragraph siblings; untouched prose stays cached
290
+ - **Edit a paragraph around a citation** → the claim verifier reruns on the edited prose
291
+ - **Replace a figure** (overwrite the PNG/PDF in your tree) → the vision cache re-describes it
292
+ - **Swap the local LLM model** (e.g. `qwen3` ↔ `gemma3` via `--model`) → manuscript-check rows and claim verdicts no longer count as cache hits
293
+ - **Overwrite a reference PDF** in your drop folder, or **re-fetch** an open-access PDF that has new bytes → the parser, embeddings, and claim verifier all rerun against the new content
294
+ - **Upgrade a summary `.md` to a real PDF** in your drop folder → claims are re-verified against the richer evidence
295
+
296
+ Use `--fresh` only when you want to wipe the workspace deliberately (it backs up the existing workspace before recreating it). It is not the answer to "I edited the paper and a finding looks stale" — if you ever see that, file an issue; a cache invariant is wrong and we'll fix it.
267
297
 
268
298
  Use `--checks ID[,ID...]` to run only the listed checks (comma-separated), disabling the rest for this invocation. Useful for fast iteration during editing (`--checks prose-quality` runs just the prose review), for debugging a single check, or for scripting a two-pass workflow. Unknown check IDs fail loudly — run `sciwrite-lint checks` to list available IDs.
269
299
 
@@ -1,58 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: sciwrite-lint
3
- Version: 0.3.0
4
- Summary: A linter for scientific manuscripts — reference verification, consistency checking, and structural validation.
5
- Author: Sergey Samsonau
6
- Maintainer-email: Authentic Research Partners LLC <info@arpconnect.com>
7
- License-Expression: MIT
8
- Project-URL: Homepage, https://arpconnect.com
9
- Project-URL: Repository, https://github.com/authentic-research-partners/sciwrite-lint
10
- Project-URL: Documentation, https://github.com/authentic-research-partners/sciwrite-lint#readme
11
- Project-URL: Issues, https://github.com/authentic-research-partners/sciwrite-lint/issues
12
- Project-URL: Paper, https://arxiv.org/abs/2604.08501
13
- Keywords: linter,latex,bibtex,pdf,scientific-writing,academic-writing,citation-checker,reference-verification,claim-verification,manuscript,peer-review,arxiv,vllm,qwen
14
- Classifier: Development Status :: 3 - Alpha
15
- Classifier: Intended Audience :: Developers
16
- Classifier: Intended Audience :: Science/Research
17
- Classifier: Topic :: Scientific/Engineering
18
- Classifier: Topic :: Software Development :: Quality Assurance
19
- Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3.13
21
- Classifier: Typing :: Typed
22
- Requires-Python: <3.14,>=3.13
23
- Description-Content-Type: text/markdown
24
- License-File: LICENSE
25
- Requires-Dist: httpx>=0.28
26
- Requires-Dist: tenacity>=9.0
27
- Requires-Dist: trafilatura>=2.0
28
- Requires-Dist: aiosqlite>=0.22
29
- Requires-Dist: loguru>=0.7
30
- Requires-Dist: anyascii>=0.3
31
- Requires-Dist: rich>=14.0
32
- Requires-Dist: defusedxml>=0.7
33
- Requires-Dist: pydantic>=2.10
34
- Requires-Dist: rapidfuzz>=3.0
35
- Requires-Dist: bibtexparser>=1.4
36
- Requires-Dist: pdfplumber>=0.11
37
- Requires-Dist: openai>=2.0
38
- Requires-Dist: grobid-tei-xml>=0.1
39
- Requires-Dist: sentence-transformers>=5.0
40
- Requires-Dist: numpy>=2.0
41
- Requires-Dist: sqlite-vec>=0.1.6
42
- Requires-Dist: torch>=2.5
43
- Requires-Dist: transformers>=4.50
44
- Requires-Dist: Pillow>=12.2
45
- Requires-Dist: pypdf>=6.10.2
46
- Requires-Dist: pypdfium2>=5.0
47
- Requires-Dist: pdf2image>=1.17
48
- Requires-Dist: lxml>=6.1
49
- Requires-Dist: pypandoc-binary>=1.15
50
- Requires-Dist: rfc3986>=2.0
51
- Provides-Extra: dev
52
- Requires-Dist: pytest>=9.0.3; extra == "dev"
53
- Requires-Dist: pytest-xdist>=3.0; extra == "dev"
54
- Dynamic: license-file
55
-
56
1
  # sciwrite-lint
57
2
 
58
3
  [![arXiv](https://img.shields.io/badge/arXiv-2604.08501-b31b1b.svg)](https://arxiv.org/abs/2604.08501)
@@ -136,22 +81,22 @@ sciwrite-lint contributions paper.pdf --format json
136
81
 
137
82
  ### Optimizations
138
83
 
139
- Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. 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 are baseline; every cache layer pins its inputs by hash so editing the paper, replacing a figure, or refreshing a reference recomputes only what's affected (see the *Iterative editing* subsection under Usage — `--fresh` is not part of the normal loop). On top of that:
140
85
 
141
- - **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
142
87
  - **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
143
- - **Per-call-site thinking budgets** — each LLM call site has 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
144
89
  - **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
145
90
  - **Batch-staged multi-paper pipeline** — when checking 2+ papers, GPU models load once per batch (vision/embedding/cited-vision) and vLLM/network stages run concurrently, giving a meaningful speedup over sequential per-paper runs. Tune via `--concurrency` (default 2, validated up to 4 on a single consumer GPU)
146
91
  - **Phased API resolution** — citations flow through OpenAlex → Semantic Scholar → CrossRef → Open Library/LoC, each phase only processing what previous phases didn't resolve
147
92
  - **14-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, NBER, RePEc/IDEAS, HAL, ERIC, NASA ADS, OSF Preprints, CORE
148
93
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
149
94
 
150
- Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. 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.
151
96
 
152
97
  ## Install
153
98
 
154
- **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.
155
100
 
156
101
  Requires [uv](https://docs.astral.sh/uv/getting-started/installation/), a container runtime (podman or docker), CUDA drivers, and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html).
157
102
 
@@ -196,6 +141,14 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
196
141
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
197
142
  ```
198
143
 
144
+ ### WSL2: disable CUDA Sysmem Fallback (recommended)
145
+
146
+ By default Windows silently spills GPU overflow into system RAM via PCIe (30–100× slower than VRAM); you'll see this as a sudden throughput collapse during the heaviest pipeline stages with no error in the logs. Tell the NVIDIA driver to fail loudly instead:
147
+
148
+ > NVIDIA Control Panel → 3D Settings → Manage 3D Settings → *CUDA - Sysmem Fallback Policy* → **Prefer No Sysmem Fallback** → Apply.
149
+
150
+ Driver 546.01+. After this, WSL2 behaves like native Linux on overflow (CUDA OOM error). The pipeline already sequences GPU consumers so OOM should not happen in normal use; the setting just prevents silent slowdowns when something does go wrong. No setting needed on native Linux — `cudaMalloc` is already loud there.
151
+
199
152
  ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
200
153
 
201
154
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
@@ -263,7 +216,18 @@ sciwrite-lint contributions paper.pdf # standalone file scoring
263
216
 
264
217
  `check` runs the full pipeline in one command: text checks → figure analysis → LLM consistency → reference verification via APIs → download and parse cited papers → claim verification → consistency checks on cited papers → bibliography verification → aggregate reliability scores → SciLint Score. An initial run on a 50-reference paper takes up to 30 minutes (dominated by downloads and claim verification); subsequent cached runs complete in minutes.
265
218
 
266
- Use `--fresh` to start from scratch (backs up the existing workspace before recreating it).
219
+ ### Iterative editing no `--fresh` needed
220
+
221
+ `sciwrite-lint check` invalidates cached results whenever the inputs they were derived from change, so rerunning after an edit just works. Concretely:
222
+
223
+ - **Edit a sentence or paragraph** → the manuscript-LLM checks (prose-quality, cross-section consistency, etc.) re-run only on the touched sentences and their paragraph siblings; untouched prose stays cached
224
+ - **Edit a paragraph around a citation** → the claim verifier reruns on the edited prose
225
+ - **Replace a figure** (overwrite the PNG/PDF in your tree) → the vision cache re-describes it
226
+ - **Swap the local LLM model** (e.g. `qwen3` ↔ `gemma3` via `--model`) → manuscript-check rows and claim verdicts no longer count as cache hits
227
+ - **Overwrite a reference PDF** in your drop folder, or **re-fetch** an open-access PDF that has new bytes → the parser, embeddings, and claim verifier all rerun against the new content
228
+ - **Upgrade a summary `.md` to a real PDF** in your drop folder → claims are re-verified against the richer evidence
229
+
230
+ Use `--fresh` only when you want to wipe the workspace deliberately (it backs up the existing workspace before recreating it). It is not the answer to "I edited the paper and a finding looks stale" — if you ever see that, file an issue; a cache invariant is wrong and we'll fix it.
267
231
 
268
232
  Use `--checks ID[,ID...]` to run only the listed checks (comma-separated), disabling the rest for this invocation. Useful for fast iteration during editing (`--checks prose-quality` runs just the prose review), for debugging a single check, or for scripting a two-pass workflow. Unknown check IDs fail loudly — run `sciwrite-lint checks` to list available IDs.
269
233
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sciwrite-lint"
7
- version = "0.3.0"
7
+ version = "0.5.0"
8
8
  description = "A linter for scientific manuscripts — reference verification, consistency checking, and structural validation."
9
9
  requires-python = ">=3.13,<3.14"
10
10
  license = "MIT"
@@ -49,6 +49,8 @@ dependencies = [
49
49
  "lxml>=6.1",
50
50
  "pypandoc-binary>=1.15",
51
51
  "rfc3986>=2.0",
52
+ "psutil>=5.9",
53
+ "annotated-types>=0.6",
52
54
  ]
53
55
 
54
56
  [project.urls]
@@ -59,9 +61,23 @@ Issues = "https://github.com/authentic-research-partners/sciwrite-lint/issues"
59
61
  Paper = "https://arxiv.org/abs/2604.08501"
60
62
 
61
63
  [project.optional-dependencies]
64
+ # `pip install -e ".[dev]"` brings in every tool used by the mandatory checks
65
+ # and the release pipeline. Exception: the `claude` CLI is a Node app —
66
+ # install separately per https://docs.anthropic.com/en/docs/claude-code.
62
67
  dev = [
68
+ # Mandatory checks — run after every code change
63
69
  "pytest>=9.0.3",
64
70
  "pytest-xdist>=3.0",
71
+ "ruff>=0.15",
72
+ "mypy>=1.18",
73
+ "ast-grep-cli>=0.35",
74
+ # Release pipeline (scripts/release.sh phases 3–6)
75
+ "deptry>=0.25",
76
+ "bandit>=1.9",
77
+ "pip-audit>=2.10",
78
+ "pip-licenses>=5.5",
79
+ "twine>=6.0",
80
+ "build>=1.0",
65
81
  ]
66
82
 
67
83
  [project.scripts]
@@ -70,44 +86,5 @@ sciwrite-lint = "sciwrite_lint.__main__:main"
70
86
  [tool.setuptools.packages.find]
71
87
  include = ["sciwrite_lint*"]
72
88
 
73
- [tool.pytest.ini_options]
74
- testpaths = ["tests"]
75
- pythonpath = ["."]
76
- markers = [
77
- "integration: tests that read actual paper files",
78
- "network: tests that make real network connections (slow)",
79
- ]
80
- # PID-unique basetemp set in conftest.py — prevents FileExistsError
81
- # when multiple pytest processes run concurrently.
82
- addopts = "-n auto -p no:cacheprovider"
83
-
84
- [tool.bandit]
85
- skips = [
86
- "B101", # assert — used for type narrowing and internal invariants
87
- "B104", # bind 0.0.0.0 — container start commands for GROBID/vLLM
88
- "B314", # xml.etree.fromstring — parsing trusted GROBID TEI output
89
- "B404", # subprocess import — CLI tool, subprocess is core functionality
90
- "B405", # xml.etree import — parsing trusted GROBID TEI output
91
- "B603", # subprocess call — CLI invokes grobid/vllm/claude by design
92
- "B607", # partial path — CLI tools resolved via PATH
93
- "B608", # SQL string — internal usage.db, no user input in queries
94
- "B615", # HF from_pretrained without revision pin — trusted publisher (Qwen), research tool
95
- ]
96
-
97
- [tool.deptry]
98
- optional_dependencies_dev_groups = ["dev"]
99
-
100
- [tool.deptry.per_rule_ignores]
101
- # Self-imports of shipped non-package directories
102
- DEP001 = ["evals", "eval_real_world", "bench"]
103
- # Lazy/conditional imports in optional groups — deptry can't detect these
104
- DEP002 = ["bibtexparser", "pdfplumber", "sentence-transformers", "numpy", "sqlite-vec", "grobid-tei-xml", "torch", "transformers", "Pillow", "pypdf", "pdf2image"]
105
- # Intra-package imports
106
- DEP003 = ["sciwrite_lint", "evals", "eval_real_world"]
89
+ [tool.setuptools.package-data]
107
90
 
108
- [tool.mypy]
109
- plugins = ["pydantic.mypy"]
110
- [[tool.mypy.overrides]]
111
- # No py.typed marker or typeshed stubs available
112
- module = ["bibtexparser", "sqlite_vec", "pypdf", "pypdfium2", "pypdfium2.raw", "transformers", "pdf2image", "lxml", "lxml.html", "lxml.html.etree", "rfc3986", "pypandoc"]
113
- ignore_missing_imports = true
@@ -2,7 +2,7 @@
2
2
 
3
3
  from sciwrite_lint.exceptions import LLMConnectionError, SciWriteLintError
4
4
 
5
- __version__ = "0.3.0"
5
+ __version__ = "0.5.0"
6
6
 
7
7
  from sciwrite_lint.oa import (
8
8
  DownloadResult,
@@ -14,113 +14,55 @@ import argparse
14
14
  import sys
15
15
  from pathlib import Path
16
16
 
17
- from loguru import logger
18
-
19
17
  from sciwrite_lint import __version__
20
- from sciwrite_lint.config import LintConfig, PaperConfig, load_config
18
+ from sciwrite_lint.cli._common import (
19
+ _classify_verify_issue as _classify_verify_issue,
20
+ _load_config as _load_config,
21
+ _paper_names as _paper_names,
22
+ _resolve_input_files as _resolve_input_files,
23
+ _resolve_paper as _resolve_paper,
24
+ _setup_logging,
25
+ )
26
+ from sciwrite_lint.config import LintConfig, load_config
21
27
  from sciwrite_lint.vllm.vllm_server import MODELS as _VLLM_MODELS
22
28
 
23
29
  _ALL_MODELS = sorted(_VLLM_MODELS.keys())
24
30
  _TEXT_MODELS = [k for k, v in _VLLM_MODELS.items() if v["kind"] == "text"]
25
31
 
32
+ _REPLACE_HELP = (
33
+ "Stop any other running vLLM container first (two vLLMs can't "
34
+ "share the GPU safely; without this flag the start is refused)"
35
+ )
36
+
26
37
 
27
38
  # ---------------------------------------------------------------------------
28
- # Helpers (used by CLI submodules)
39
+ # Main parser
29
40
  # ---------------------------------------------------------------------------
30
41
 
31
42
 
32
- def _load_config(args: argparse.Namespace) -> LintConfig:
33
- """Load config from --config flag or auto-discovery."""
34
- if getattr(args, "config", None):
35
- return load_config(Path(args.config))
36
-
37
- config = load_config(None)
38
- if config.config_path is None:
39
- from sciwrite_lint.config import _detect_papers
40
-
41
- logger.error("No .sciwrite-lint.toml found.")
42
- detected = _detect_papers()
43
- if detected:
44
- logger.error(" Detected .tex files:")
45
- for p in detected:
46
- bib = f" (bib: {p['bib']})" if p.get("bib") else ""
47
- logger.error(f" {p['file_path']}{bib}")
48
- logger.error(" Run: sciwrite-lint init")
49
- logger.error(" Then review .sciwrite-lint.toml before running checks.")
50
- return config
51
-
52
-
53
- def _resolve_paper(config: LintConfig, name: str) -> PaperConfig | None:
54
- """Resolve a paper name to its config. Print error if not found."""
55
- pc = config.get_paper(name)
56
- if not pc:
57
- if config.papers:
58
- names = ", ".join(p.name for p in config.papers)
59
- logger.error(f"Unknown paper '{name}'. Registered: {names}")
60
- else:
61
- logger.error(f"Unknown paper '{name}'. No papers registered.")
62
- logger.error(
63
- f" Add [[papers]] to {config.config_path or '.sciwrite-lint.toml'}"
64
- )
65
- return pc
66
-
67
-
68
- def _paper_names(config: LintConfig) -> list[str]:
69
- return [p.name for p in config.papers]
70
-
43
+ def _register_optional_extensions(
44
+ sub: argparse._SubParsersAction, # type: ignore[type-arg]
45
+ ) -> None:
46
+ """Load CLI extensions discovered under ``sciwrite_lint``.
71
47
 
72
- def _resolve_input_files(
73
- args: argparse.Namespace, config: LintConfig
74
- ) -> list[tuple[str, Path]]:
75
- """Resolve which files to check (.tex or .pdf).
76
-
77
- Priority: positional file > --paper (from config) > all papers in config.
78
- Returns list of (name, path) pairs.
48
+ An underscore-prefixed subpackage is a CLI extension when its
49
+ ``__init__.py`` defines ``register_cli(sub)``. Discovery walks
50
+ ``sciwrite_lint.__path__`` via ``pkgutil``, so no extension package
51
+ names appear in this source adding or removing one is a filesystem
52
+ change.
79
53
  """
80
- if hasattr(args, "file") and args.file:
81
- p = Path(args.file)
82
- return [(p.stem, p)]
83
-
84
- paper_filter = getattr(args, "paper", None)
85
- if paper_filter:
86
- pc = _resolve_paper(config, paper_filter)
87
- if not pc:
88
- return []
89
- return [(pc.name, pc.file_path)]
90
-
91
- if config.papers:
92
- return [(pc.name, pc.file_path) for pc in config.papers]
93
-
94
- logger.error("No papers registered. Either:")
95
- logger.error(" sciwrite-lint check <file.tex|file.pdf> — check a specific file")
96
- logger.error(
97
- " sciwrite-lint init — set up project with [[papers]]"
98
- )
99
- return []
100
-
54
+ import importlib
55
+ import pkgutil
101
56
 
102
- def _classify_verify_issue(issue: str) -> tuple[str, str]:
103
- """Classify a verify issue string into (level, rule_id) for findings."""
104
- from sciwrite_lint.pipeline import _classify_verify_issue as _classify
57
+ import sciwrite_lint as _pkg
105
58
 
106
- return _classify(issue)
107
-
108
-
109
- def _setup_logging(config: LintConfig) -> None:
110
- """Configure loguru rotating file sink from config."""
111
- logger.add(
112
- "logs/sciwrite-lint.log",
113
- rotation="10 MB",
114
- retention="30 days",
115
- compression="gz",
116
- level=config.log_level,
117
- format="{time:YYYY-MM-DD HH:mm:ss} | {level:<8} | {name}:{function}:{line} | {message}",
118
- )
119
-
120
-
121
- # ---------------------------------------------------------------------------
122
- # Main parser
123
- # ---------------------------------------------------------------------------
59
+ for _finder, name, ispkg in pkgutil.iter_modules(_pkg.__path__):
60
+ if not ispkg or not name.startswith("_"):
61
+ continue
62
+ ext = importlib.import_module(f"{_pkg.__name__}.{name}")
63
+ register = getattr(ext, "register_cli", None)
64
+ if register is not None:
65
+ register(sub)
124
66
 
125
67
 
126
68
  def main(argv: list[str] | None = None) -> int:
@@ -206,6 +148,9 @@ def main(argv: list[str] | None = None) -> int:
206
148
  p_checks = sub.add_parser("checks", help="List all registered checks")
207
149
  p_checks.set_defaults(func=run_checks_list)
208
150
 
151
+ # Load optional underscore-prefixed CLI extension subpackages (if any).
152
+ _register_optional_extensions(sub)
153
+
209
154
  # --- init ---
210
155
  p_init = sub.add_parser(
211
156
  "init", help="Initialize project: config + references directory"
@@ -309,12 +254,6 @@ def main(argv: list[str] | None = None) -> int:
309
254
  # --- verify-claims ---
310
255
  p_vc = sub.add_parser("verify-claims", help="Verify claims against cited sources")
311
256
  p_vc.add_argument("--paper", required=True, help="Paper to verify")
312
- p_vc.add_argument(
313
- "--backend",
314
- choices=["vllm", "claude"],
315
- default="vllm",
316
- help="vllm (local LLM, fast) or claude (Claude CLI, deep)",
317
- )
318
257
  p_vc.add_argument(
319
258
  "--model",
320
259
  choices=_TEXT_MODELS,
@@ -457,6 +396,7 @@ def main(argv: list[str] | None = None) -> int:
457
396
  action="store_true",
458
397
  help="Also start vision vLLM (qwen3-vl-8b-fp8 on port 5002)",
459
398
  )
399
+ p_containers.add_argument("--replace", action="store_true", help=_REPLACE_HELP)
460
400
  p_containers.add_argument("--config", help="Path to .sciwrite-lint.toml")
461
401
  p_containers.set_defaults(func=run_containers)
462
402
 
@@ -488,6 +428,7 @@ def main(argv: list[str] | None = None) -> int:
488
428
  action="store_true",
489
429
  help="Pull latest vLLM image before starting",
490
430
  )
431
+ p_vllm_start.add_argument("--replace", action="store_true", help=_REPLACE_HELP)
491
432
  p_vllm_start.add_argument("--config", help="Path to .sciwrite-lint.toml")
492
433
  p_vllm_start.set_defaults(func=run_vllm)
493
434
 
@@ -536,14 +477,32 @@ def main(argv: list[str] | None = None) -> int:
536
477
  p_vllm.print_help()
537
478
  return 0
538
479
 
539
- # Set up file logging from config (stderr sink stays at INFO by default)
540
- try:
541
- config = load_config(
542
- Path(args.config) if getattr(args, "config", None) else None
543
- )
544
- except Exception as e:
545
- logger.debug("Config load failed, using defaults: {}", e)
480
+ # Set up file logging from config (stderr sink stays at INFO by default).
481
+ # ``load_config`` already returns ``LintConfig()`` when no config file is
482
+ # discovered; reaching the exception path means a file WAS found (or
483
+ # explicitly passed via --config) but failed to parse or validate.
484
+ # Surface that instead of silently substituting hardcoded defaults — a
485
+ # broken TOML otherwise produces results computed against the wrong
486
+ # workspace, paper list, or API keys.
487
+ #
488
+ # The ``init`` command is the one exception: its job is to scaffold a
489
+ # config, so it must run against a missing, broken, or sentinel TOML
490
+ # without aborting. Use ``LintConfig()`` for logging setup only and let
491
+ # init proceed to (over)write the file.
492
+ if args.command == "init":
546
493
  config = LintConfig()
494
+ else:
495
+ try:
496
+ config = load_config(
497
+ Path(args.config) if getattr(args, "config", None) else None
498
+ )
499
+ except Exception as e:
500
+ cfg_arg = getattr(args, "config", None)
501
+ cfg_hint = f" ({cfg_arg})" if cfg_arg else " (.sciwrite-lint.toml)"
502
+ raise SystemExit(
503
+ f"ERROR: failed to load configuration{cfg_hint}: {type(e).__name__}: {e}\n"
504
+ " Fix the TOML or remove the file to use built-in defaults."
505
+ ) from e
547
506
  _setup_logging(config)
548
507
 
549
508
  return args.func(args)
@@ -116,8 +116,14 @@ class CitationAPI:
116
116
  client=self._client,
117
117
  )
118
118
  except Exception as e:
119
+ # The waterfall caller distinguishes ``{"error": …}`` from
120
+ # ``None`` (not-found): error dicts mean "lookup itself
121
+ # failed" and stay visible, ``None`` means "no result for
122
+ # this citation". All API lookups in this module return the
123
+ # error shape so transient network failures don't masquerade
124
+ # as misses.
119
125
  logger.debug("CrossRef lookup failed for {}: {}", citation.key, e)
120
- return None
126
+ return {"error": str(e), "source": "crossref"}
121
127
 
122
128
  # ------------------------------------------------------------------
123
129
  # OpenAlex
@@ -502,6 +508,13 @@ async def batch_openalex(
502
508
  label="OpenAlex batch",
503
509
  )
504
510
  if resp.status_code != 200:
511
+ # API failure ≠ "no matches". Log at WARNING so the operator
512
+ # sees the API was unreachable; the verify-cascade still
513
+ # falls through to S2/CrossRef per the pipeline contract.
514
+ logger.warning(
515
+ "OpenAlex batch returned status {}; cascading to next provider",
516
+ resp.status_code,
517
+ )
505
518
  return results
506
519
  works = resp.json().get("results", [])
507
520
  for work in works:
@@ -552,7 +565,7 @@ async def batch_s2(
552
565
  async with tracked("semantic_scholar", ids=len(ids)):
553
566
  try:
554
567
  resp = await retry_on_transient(
555
- lambda: client.post(
568
+ lambda ids=ids: client.post(
556
569
  "https://api.semanticscholar.org/graph/v1/paper/batch",
557
570
  json={"ids": ids},
558
571
  params={"fields": _S2_FIELDS},
@@ -560,13 +573,27 @@ async def batch_s2(
560
573
  label="S2 batch",
561
574
  )
562
575
  if resp.status_code != 200:
576
+ # API failure ≠ "no matches". Log at WARNING so the
577
+ # operator sees the API was unreachable.
578
+ logger.warning(
579
+ "S2 batch returned status {} for {} IDs; "
580
+ "cascading to next provider",
581
+ resp.status_code,
582
+ len(ids),
583
+ )
563
584
  continue
564
585
  papers = resp.json()
565
586
  for paper, (key, _s2_id) in zip(papers, batch):
566
587
  if paper: # S2 returns null for not-found
567
588
  results[key] = _parse_s2(paper)
568
589
  except httpx.HTTPError as e:
569
- logger.debug("S2 batch request failed: {}", e)
590
+ # Network error "no matches". WARNING so the operator
591
+ # can see API was unreachable; cascade continues.
592
+ logger.warning(
593
+ "S2 batch request failed ({}: {}); cascading to next provider",
594
+ type(e).__name__,
595
+ e,
596
+ )
570
597
  continue
571
598
 
572
599
  return results
@@ -606,10 +633,28 @@ async def parallel_crossref(
606
633
  polite_email=config.polite_email,
607
634
  client=client,
608
635
  )
609
- if result and not result.get("error"):
610
- results[c.key] = result
636
+ if result is None:
637
+ return # not-found — fine, next provider handles
638
+ if result.get("error"):
639
+ # API failure ≠ "no match". Log at WARNING so
640
+ # the operator sees CrossRef was unreachable;
641
+ # don't propagate the error dict into ``results``
642
+ # because callers expect parsed-result shape.
643
+ logger.warning(
644
+ "CrossRef lookup failed for {}: {}",
645
+ c.key,
646
+ result["error"],
647
+ )
648
+ return
649
+ results[c.key] = result
611
650
  except Exception as e:
612
- logger.debug("CrossRef lookup failed for {}: {}", c.key, e)
651
+ # Network error "no match". WARNING so visible.
652
+ logger.warning(
653
+ "CrossRef lookup raised for {} ({}: {})",
654
+ c.key,
655
+ type(e).__name__,
656
+ e,
657
+ )
613
658
 
614
659
  await asyncio.gather(*[_lookup_one(c) for c in citations])
615
660
  return results