sciwrite-lint 0.3.0__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.
- {sciwrite_lint-0.3.0/sciwrite_lint.egg-info → sciwrite_lint-0.4.0}/PKG-INFO +25 -6
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/README.md +13 -5
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/pyproject.toml +17 -42
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/__init__.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/__main__.py +57 -94
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/api.py +51 -6
- sciwrite_lint-0.4.0/sciwrite_lint/api_keys.py +93 -0
- sciwrite_lint-0.4.0/sciwrite_lint/checks/_diagnostics.py +237 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/_section_utils.py +27 -9
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/cite_purpose.py +5 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/cross_section_consistency.py +45 -19
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/dangling_cite.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/full_paper_consistency.py +88 -32
- sciwrite_lint-0.4.0/sciwrite_lint/checks/internal_consistency_pairs.py +473 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/prose_quality.py +38 -14
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/ref_internal_checks.py +103 -23
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/reference_unreliable.py +9 -3
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/registry.py +1 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/structure_promises.py +23 -12
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/claims.py +13 -4
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/claude_cli.py +8 -21
- sciwrite_lint-0.4.0/sciwrite_lint/cli/_common.py +106 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/check.py +86 -127
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/config.py +11 -81
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/fetch.py +2 -18
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/_monitor.py +517 -9
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/containers.py +3 -2
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/dismiss_claim.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/override.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/parse.py +5 -10
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/vision.py +1 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/vllm.py +6 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/rank.py +6 -2
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/verify.py +4 -4
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/config.py +220 -12
- sciwrite_lint-0.4.0/sciwrite_lint/cross_paper.py +59 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/eval_claims.py +699 -264
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/footnote_urls.py +5 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/_orchestrator.py +12 -19
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/_validation.py +52 -4
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/ideas.py +1 -16
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/latex_to_markdown.py +2 -2
- sciwrite_lint-0.4.0/sciwrite_lint/llm/__init__.py +5 -0
- sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/__init__.py +60 -0
- sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/compute_cap.py +84 -0
- sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/controller.py +464 -0
- sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/decide.py +462 -0
- sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/host_metrics.py +113 -0
- sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/metrics_probe.py +70 -0
- sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/registry.py +199 -0
- sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/telemetry.py +414 -0
- sciwrite_lint-0.3.0/sciwrite_lint/vllm/metrics.py → sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/vllm_metrics.py +34 -38
- sciwrite_lint-0.4.0/sciwrite_lint/llm/concurrency_optimizer/wiring.py +119 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/llm_utils.py +292 -72
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/local_sources.py +5 -3
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/manuscript_store.py +162 -6
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/mhtml.py +17 -5
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/models.py +0 -4
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/oa/_web.py +2 -2
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pdf/grobid.py +137 -34
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pdf/pdf_download.py +2 -12
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/__init__.py +8 -4
- sciwrite_lint-0.4.0/sciwrite_lint/pipeline/checks.py +195 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/claims.py +2 -2
- sciwrite_lint-0.4.0/sciwrite_lint/pipeline/embeddings.py +350 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/fetch.py +17 -3
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/orchestration.py +38 -11
- sciwrite_lint-0.4.0/sciwrite_lint/pipeline/parse.py +153 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/runners.py +193 -38
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/staged.py +31 -25
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/swap.py +117 -89
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/tracking.py +2 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/verify.py +18 -5
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/vision.py +106 -13
- sciwrite_lint-0.4.0/sciwrite_lint/references/_embed_timing.py +63 -0
- {sciwrite_lint-0.3.0/sciwrite_lint/scoring → sciwrite_lint-0.4.0/sciwrite_lint/references}/chain.py +3 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/citations.py +57 -55
- sciwrite_lint-0.4.0/sciwrite_lint/references/embedding_store.py +429 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/matching.py +8 -34
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/metadata.py +6 -15
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/reference_store.py +420 -102
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/retraction_watch.py +4 -3
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/__init__.py +15 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/_core.py +55 -3
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/claim_results.py +26 -6
- sciwrite_lint-0.4.0/sciwrite_lint/references/workspace_db/manuscript_citations.py +89 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/report.py +14 -5
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/schemas.py +222 -6
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/scoring/contribution.py +2 -2
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/scoring/scilint_score.py +32 -10
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/usage.py +4 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/vision/cache.py +60 -24
- sciwrite_lint-0.4.0/sciwrite_lint/vision/describe.py +1095 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/vision/pipeline.py +1 -0
- sciwrite_lint-0.4.0/sciwrite_lint/vllm/metrics.py +42 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/vllm/vllm_server.py +289 -20
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/web.py +16 -7
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0/sciwrite_lint.egg-info}/PKG-INFO +25 -6
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint.egg-info/SOURCES.txt +18 -1
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint.egg-info/requires.txt +11 -0
- sciwrite_lint-0.3.0/sciwrite_lint/cross_paper.py +0 -174
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/checks.py +0 -64
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/embeddings.py +0 -234
- sciwrite_lint-0.3.0/sciwrite_lint/pipeline/parse.py +0 -57
- sciwrite_lint-0.3.0/sciwrite_lint/references/embedding_store.py +0 -389
- sciwrite_lint-0.3.0/sciwrite_lint/vision/describe.py +0 -632
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/LICENSE +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/_network.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/claim_support.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/dangling_ref.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/reference_accuracy.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/reference_exists.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/retracted_cite.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/claude_backend.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/grobid.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/cli/misc/init.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/exceptions.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/_common.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/_download.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/_search.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/arxiv.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/biorxiv.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/core.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/eric.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/europepmc.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/hal.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/nasa_ads.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/nber.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/osf.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/pmc.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/s2.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/fulltext/unpaywall.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/oa/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/oa/_models.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/oa/_pdf.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/oa/_search.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pdf/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/pdf_context.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/ref_internal.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/pipeline/unreliable.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/prompt_safety.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/py.typed +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/rate_limiter.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/crossref.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/bib_checks.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/citation_metadata.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/parse_cache.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/pipeline_stage.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/query_vectors.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/ref_internal.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/registry.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/references/workspace_db/vision_cache.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/scoring/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/tex_parser.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/vision/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/vision/image_extraction.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint/vllm/__init__.py +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint.egg-info/entry_points.txt +0 -0
- {sciwrite_lint-0.3.0 → sciwrite_lint-0.4.0}/sciwrite_lint.egg-info/top_level.txt +0 -0
- {sciwrite_lint-0.3.0 → 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.
|
|
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>
|
|
@@ -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.
|
|
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:
|
|
140
151
|
|
|
141
|
-
- **
|
|
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
|
|
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.
|
|
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).
|
|
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
|

|
|
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.
|
|
@@ -81,22 +81,22 @@ sciwrite-lint contributions paper.pdf --format json
|
|
|
81
81
|
|
|
82
82
|
### Optimizations
|
|
83
83
|
|
|
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.
|
|
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:
|
|
85
85
|
|
|
86
|
-
- **
|
|
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
|
|
87
87
|
- **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
|
|
88
|
-
- **Per-call-site thinking budgets** — each LLM call site has
|
|
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
|
|
89
89
|
- **Adaptive embedding batches** — token-aware batch sizing gives ~50x speedup over CPU while staying within VRAM limits
|
|
90
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)
|
|
91
91
|
- **Phased API resolution** — citations flow through OpenAlex → Semantic Scholar → CrossRef → Open Library/LoC, each phase only processing what previous phases didn't resolve
|
|
92
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
|
|
93
93
|
- **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
|
|
94
94
|
|
|
95
|
-
Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns.
|
|
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.
|
|
96
96
|
|
|
97
97
|
## Install
|
|
98
98
|
|
|
99
|
-
**Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM).
|
|
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.
|
|
100
100
|
|
|
101
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).
|
|
102
102
|
|
|
@@ -141,6 +141,14 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
|
|
|
141
141
|
sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
|
|
142
142
|
```
|
|
143
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
|
+
|
|
144
152
|

|
|
145
153
|
|
|
146
154
|
`init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sciwrite-lint"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.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"
|
|
@@ -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,3 @@ 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"]
|
|
107
|
-
|
|
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
|
|
@@ -17,105 +17,24 @@ from pathlib import Path
|
|
|
17
17
|
from loguru import logger
|
|
18
18
|
|
|
19
19
|
from sciwrite_lint import __version__
|
|
20
|
-
from sciwrite_lint.
|
|
20
|
+
from sciwrite_lint.cli._common import (
|
|
21
|
+
_classify_verify_issue as _classify_verify_issue,
|
|
22
|
+
_load_config as _load_config,
|
|
23
|
+
_paper_names as _paper_names,
|
|
24
|
+
_resolve_input_files as _resolve_input_files,
|
|
25
|
+
_resolve_paper as _resolve_paper,
|
|
26
|
+
_setup_logging,
|
|
27
|
+
)
|
|
28
|
+
from sciwrite_lint.config import LintConfig, load_config
|
|
21
29
|
from sciwrite_lint.vllm.vllm_server import MODELS as _VLLM_MODELS
|
|
22
30
|
|
|
23
31
|
_ALL_MODELS = sorted(_VLLM_MODELS.keys())
|
|
24
32
|
_TEXT_MODELS = [k for k, v in _VLLM_MODELS.items() if v["kind"] == "text"]
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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.
|
|
79
|
-
"""
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
105
|
-
|
|
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
|
-
)
|
|
34
|
+
_REPLACE_HELP = (
|
|
35
|
+
"Stop any other running vLLM container first (two vLLMs can't "
|
|
36
|
+
"share the GPU safely; without this flag the start is refused)"
|
|
37
|
+
)
|
|
119
38
|
|
|
120
39
|
|
|
121
40
|
# ---------------------------------------------------------------------------
|
|
@@ -206,6 +125,48 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
206
125
|
p_checks = sub.add_parser("checks", help="List all registered checks")
|
|
207
126
|
p_checks.set_defaults(func=run_checks_list)
|
|
208
127
|
|
|
128
|
+
# --- aicode-lint (optional — only registered when the WIP module is installed) ---
|
|
129
|
+
# ``sciwrite_lint.aicode_lint`` is excluded from the public release rsync
|
|
130
|
+
# while it stabilizes. Use ``importlib.util.find_spec`` to check whether
|
|
131
|
+
# the module is present rather than try/except ImportError (separately
|
|
132
|
+
# banned). Public installs (pip from PyPI) won't have it; dev installs
|
|
133
|
+
# do. Check the parent ``sciwrite_lint.aicode_lint`` first because
|
|
134
|
+
# ``find_spec`` raises ``ModuleNotFoundError`` on a missing intermediate
|
|
135
|
+
# path; once the parent is confirmed present, the child lookup is safe.
|
|
136
|
+
import importlib.util as _importlib_util
|
|
137
|
+
|
|
138
|
+
if _importlib_util.find_spec("sciwrite_lint.aicode_lint") is not None:
|
|
139
|
+
from sciwrite_lint.aicode_lint.cli import run_aicode_lint
|
|
140
|
+
|
|
141
|
+
p_aicode = sub.add_parser(
|
|
142
|
+
"aicode-lint",
|
|
143
|
+
help="LLM code review on Python sources via local vLLM (semantic antipatterns)",
|
|
144
|
+
)
|
|
145
|
+
p_aicode.add_argument(
|
|
146
|
+
"paths",
|
|
147
|
+
nargs="*",
|
|
148
|
+
help="Files, directories, or globs to scan (default: sciwrite_lint/**/*.py)",
|
|
149
|
+
)
|
|
150
|
+
p_aicode.add_argument(
|
|
151
|
+
"--rules",
|
|
152
|
+
default=None,
|
|
153
|
+
metavar="ID[,ID...]",
|
|
154
|
+
help="Run only the listed rule IDs (comma-separated). Default: all built-in rules.",
|
|
155
|
+
)
|
|
156
|
+
p_aicode.add_argument(
|
|
157
|
+
"--list-rules",
|
|
158
|
+
action="store_true",
|
|
159
|
+
help="Print the rule registry and exit.",
|
|
160
|
+
)
|
|
161
|
+
p_aicode.add_argument(
|
|
162
|
+
"--format",
|
|
163
|
+
choices=["terminal", "json"],
|
|
164
|
+
default=None,
|
|
165
|
+
help="Output format (default: terminal).",
|
|
166
|
+
)
|
|
167
|
+
p_aicode.add_argument("--config", help="Path to .sciwrite-lint.toml")
|
|
168
|
+
p_aicode.set_defaults(func=run_aicode_lint)
|
|
169
|
+
|
|
209
170
|
# --- init ---
|
|
210
171
|
p_init = sub.add_parser(
|
|
211
172
|
"init", help="Initialize project: config + references directory"
|
|
@@ -457,6 +418,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
457
418
|
action="store_true",
|
|
458
419
|
help="Also start vision vLLM (qwen3-vl-8b-fp8 on port 5002)",
|
|
459
420
|
)
|
|
421
|
+
p_containers.add_argument("--replace", action="store_true", help=_REPLACE_HELP)
|
|
460
422
|
p_containers.add_argument("--config", help="Path to .sciwrite-lint.toml")
|
|
461
423
|
p_containers.set_defaults(func=run_containers)
|
|
462
424
|
|
|
@@ -488,6 +450,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
488
450
|
action="store_true",
|
|
489
451
|
help="Pull latest vLLM image before starting",
|
|
490
452
|
)
|
|
453
|
+
p_vllm_start.add_argument("--replace", action="store_true", help=_REPLACE_HELP)
|
|
491
454
|
p_vllm_start.add_argument("--config", help="Path to .sciwrite-lint.toml")
|
|
492
455
|
p_vllm_start.set_defaults(func=run_vllm)
|
|
493
456
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
610
|
-
|
|
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
|
-
|
|
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
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Optional-API-key registry, on-disk reader, and config validator.
|
|
2
|
+
|
|
3
|
+
Holds the cross-layer surface that both the CLI (``cli/config.py`` for
|
|
4
|
+
show / set-key / remove-key commands) and the pipeline preflight
|
|
5
|
+
(``pipeline/orchestration.py``) need to share. Living here keeps the
|
|
6
|
+
pipeline layer from importing the CLI layer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sciwrite_lint.config import LintConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Services that accept API keys, stored in ~/.sciwrite-lint/<filename>
|
|
21
|
+
_API_KEY_SERVICES: dict[str, dict[str, str]] = {
|
|
22
|
+
"semantic-scholar": {
|
|
23
|
+
"file": "s2_api_key",
|
|
24
|
+
"description": "Semantic Scholar (1 → 100 req/s)",
|
|
25
|
+
"url": "https://www.semanticscholar.org/product/api#api-key",
|
|
26
|
+
},
|
|
27
|
+
"ncbi": {
|
|
28
|
+
"file": "ncbi_api_key",
|
|
29
|
+
"description": "NCBI / PubMed Central (3 → 10 req/s)",
|
|
30
|
+
"url": "https://www.ncbi.nlm.nih.gov/account/settings/",
|
|
31
|
+
},
|
|
32
|
+
"core": {
|
|
33
|
+
"file": "core_api_key",
|
|
34
|
+
"description": "CORE (institutional repository access)",
|
|
35
|
+
"url": "https://core.ac.uk/services/api",
|
|
36
|
+
},
|
|
37
|
+
"nasa-ads": {
|
|
38
|
+
"file": "nasa_ads_api_key",
|
|
39
|
+
"description": "NASA ADS (astronomy/astrophysics; 5000 req/day per token)",
|
|
40
|
+
"url": "https://ui.adsabs.harvard.edu/user/settings/token",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_KEY_DIR = Path.home() / ".sciwrite-lint"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _read_key(filename: str) -> str | None:
|
|
48
|
+
"""Read an API key from ~/.sciwrite-lint/{filename}."""
|
|
49
|
+
path = _KEY_DIR / filename
|
|
50
|
+
if path.exists():
|
|
51
|
+
val = path.read_text().strip()
|
|
52
|
+
return val or None
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_api_config(config: "LintConfig", needs_email: bool = False) -> list[str]:
|
|
57
|
+
"""Validate API configuration. Returns list of errors (hard stops).
|
|
58
|
+
|
|
59
|
+
Also logs warnings for missing optional API keys.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
config: Resolved LintConfig.
|
|
63
|
+
needs_email: If True, missing polite_email is an error (not just a warning).
|
|
64
|
+
"""
|
|
65
|
+
errors: list[str] = []
|
|
66
|
+
|
|
67
|
+
# polite_email: required for Unpaywall + Retraction Watch
|
|
68
|
+
if not config.polite_email:
|
|
69
|
+
if needs_email:
|
|
70
|
+
errors.append(
|
|
71
|
+
"polite_email not set — Unpaywall and Retraction Watch will not work. "
|
|
72
|
+
"Set with: sciwrite-lint config set-email you@example.com"
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
logger.warning(
|
|
76
|
+
"polite_email not set — Unpaywall and Retraction Watch disabled. "
|
|
77
|
+
"Set with: sciwrite-lint config set-email you@example.com"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Optional API keys: warn about rate limit benefits
|
|
81
|
+
missing_keys: list[str] = []
|
|
82
|
+
for service, info in _API_KEY_SERVICES.items():
|
|
83
|
+
if not _read_key(info["file"]):
|
|
84
|
+
missing_keys.append(f"{service} ({info['description']})")
|
|
85
|
+
|
|
86
|
+
if missing_keys:
|
|
87
|
+
logger.info(
|
|
88
|
+
"Optional API keys not configured (slower rate limits): {}. "
|
|
89
|
+
"See: sciwrite-lint config show",
|
|
90
|
+
", ".join(missing_keys),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return errors
|