sciwrite-lint 0.2.0__tar.gz → 0.2.2__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 (85) hide show
  1. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/PKG-INFO +90 -29
  2. sciwrite_lint-0.2.0/sciwrite_lint.egg-info/PKG-INFO → sciwrite_lint-0.2.2/README.md +63 -54
  3. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/pyproject.toml +27 -20
  4. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/__init__.py +1 -1
  5. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/__main__.py +30 -15
  6. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/cross_section_consistency.py +3 -0
  7. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/full_paper_consistency.py +26 -34
  8. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/ref_internal_checks.py +16 -49
  9. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/cli/check.py +22 -21
  10. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/cli/fetch.py +6 -0
  11. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/cli/misc.py +382 -75
  12. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/cli/verify.py +36 -14
  13. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/config.py +17 -0
  14. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/eval_claims.py +152 -77
  15. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/fulltext.py +13 -1
  16. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/llm_utils.py +99 -70
  17. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/pipeline.py +542 -134
  18. sciwrite_lint-0.2.2/sciwrite_lint/py.typed +0 -0
  19. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/references/embedding_store.py +23 -14
  20. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/references/reference_store.py +9 -23
  21. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/references/workspace_db.py +220 -102
  22. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/schemas.py +2 -2
  23. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/usage.py +18 -42
  24. sciwrite_lint-0.2.2/sciwrite_lint/vision/cache.py +234 -0
  25. sciwrite_lint-0.2.2/sciwrite_lint/vision/describe.py +632 -0
  26. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/vision/image_extraction.py +31 -0
  27. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/vision/pipeline.py +23 -3
  28. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/vllm/vllm_server.py +90 -11
  29. sciwrite_lint-0.2.0/README.md → sciwrite_lint-0.2.2/sciwrite_lint.egg-info/PKG-INFO +115 -9
  30. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint.egg-info/SOURCES.txt +1 -0
  31. sciwrite_lint-0.2.2/sciwrite_lint.egg-info/requires.txt +27 -0
  32. sciwrite_lint-0.2.0/sciwrite_lint/vision/cache.py +0 -140
  33. sciwrite_lint-0.2.0/sciwrite_lint/vision/describe.py +0 -311
  34. sciwrite_lint-0.2.0/sciwrite_lint.egg-info/requires.txt +0 -27
  35. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/LICENSE +0 -0
  36. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/_network.py +0 -0
  37. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/api.py +0 -0
  38. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/__init__.py +0 -0
  39. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/_section_utils.py +0 -0
  40. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/cite_purpose.py +0 -0
  41. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/claim_support.py +0 -0
  42. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/dangling_cite.py +0 -0
  43. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/dangling_ref.py +0 -0
  44. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/reference_accuracy.py +0 -0
  45. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/reference_exists.py +0 -0
  46. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/reference_unreliable.py +0 -0
  47. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/registry.py +0 -0
  48. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/retracted_cite.py +0 -0
  49. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/structure_promises.py +0 -0
  50. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
  51. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/claims.py +0 -0
  52. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/claude_backend.py +0 -0
  53. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/claude_cli.py +0 -0
  54. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/cli/__init__.py +0 -0
  55. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/cli/config.py +0 -0
  56. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/cli/rank.py +0 -0
  57. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/cross_paper.py +0 -0
  58. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/local_pdfs.py +0 -0
  59. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/manuscript_store.py +0 -0
  60. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/models.py +0 -0
  61. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/pdf/__init__.py +0 -0
  62. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/pdf/grobid.py +0 -0
  63. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/pdf/pdf_download.py +0 -0
  64. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/prompt_safety.py +0 -0
  65. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/rate_limiter.py +0 -0
  66. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/references/__init__.py +0 -0
  67. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/references/citations.py +0 -0
  68. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/references/crossref.py +0 -0
  69. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/references/matching.py +0 -0
  70. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/references/metadata.py +0 -0
  71. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/references/retraction_watch.py +0 -0
  72. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/report.py +0 -0
  73. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/scoring/__init__.py +0 -0
  74. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/scoring/chain.py +0 -0
  75. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/scoring/contribution.py +0 -0
  76. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/scoring/scilint_score.py +0 -0
  77. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/tex_parser.py +0 -0
  78. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/vision/__init__.py +0 -0
  79. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/vllm/__init__.py +0 -0
  80. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/vllm/metrics.py +0 -0
  81. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint/web.py +0 -0
  82. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
  83. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint.egg-info/entry_points.txt +0 -0
  84. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/sciwrite_lint.egg-info/top_level.txt +0 -0
  85. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.2}/setup.cfg +0 -0
@@ -1,43 +1,50 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sciwrite-lint
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A linter for scientific manuscripts — reference verification, consistency checking, and structural validation.
5
5
  Author: Sergey Samsonau
6
+ Maintainer-email: Authentic Research Partners LLC <info@arpconnect.com>
6
7
  License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/authentic-research-partners/sciwrite-lint
8
+ Project-URL: Homepage, https://arpconnect.com
8
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
9
11
  Project-URL: Issues, https://github.com/authentic-research-partners/sciwrite-lint/issues
10
- Keywords: linter,scientific-writing,citation-verification,latex
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
11
14
  Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
12
16
  Classifier: Intended Audience :: Science/Research
13
- Classifier: Topic :: Text Processing :: Markup :: LaTeX
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Classifier: Programming Language :: Python :: 3
14
20
  Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
15
22
  Requires-Python: <3.14,>=3.13
16
23
  Description-Content-Type: text/markdown
17
24
  License-File: LICENSE
18
- Requires-Dist: httpx>=0.27
19
- Requires-Dist: tenacity>=8.0
20
- Requires-Dist: trafilatura>=1.8
21
- Requires-Dist: aiosqlite>=0.20
25
+ Requires-Dist: httpx>=0.28
26
+ Requires-Dist: tenacity>=9.0
27
+ Requires-Dist: trafilatura>=2.0
28
+ Requires-Dist: aiosqlite>=0.22
22
29
  Requires-Dist: loguru>=0.7
23
30
  Requires-Dist: anyascii>=0.3
24
- Requires-Dist: rich>=13.0
31
+ Requires-Dist: rich>=14.0
25
32
  Requires-Dist: defusedxml>=0.7
26
- Requires-Dist: pydantic>=2.0
33
+ Requires-Dist: pydantic>=2.10
27
34
  Requires-Dist: rapidfuzz>=3.0
28
35
  Requires-Dist: bibtexparser>=1.4
29
- Requires-Dist: pdfplumber>=0.10
30
- Requires-Dist: openai>=1.0
36
+ Requires-Dist: pdfplumber>=0.11
37
+ Requires-Dist: openai>=2.0
31
38
  Requires-Dist: grobid-tei-xml>=0.1
32
- Requires-Dist: sentence-transformers>=3.0
33
- Requires-Dist: numpy>=1.24
39
+ Requires-Dist: sentence-transformers>=5.0
40
+ Requires-Dist: numpy>=2.0
34
41
  Requires-Dist: sqlite-vec>=0.1.6
35
- Requires-Dist: torch>=2.0
36
- Requires-Dist: transformers>=4.45
37
- Requires-Dist: Pillow>=10.0
38
- Requires-Dist: pypdf>=4.0
39
- Requires-Dist: pypdfium2>=4.0
40
- Requires-Dist: pdf2image>=1.16
42
+ Requires-Dist: torch>=2.5
43
+ Requires-Dist: transformers>=4.50
44
+ Requires-Dist: Pillow>=12.0
45
+ Requires-Dist: pypdf>=6.0
46
+ Requires-Dist: pypdfium2>=5.0
47
+ Requires-Dist: pdf2image>=1.17
41
48
  Provides-Extra: dev
42
49
  Requires-Dist: pytest>=8.0; extra == "dev"
43
50
  Requires-Dist: pytest-xdist>=3.0; extra == "dev"
@@ -45,6 +52,8 @@ Dynamic: license-file
45
52
 
46
53
  # sciwrite-lint
47
54
 
55
+ [![arXiv](https://img.shields.io/badge/arXiv-2604.08501-b31b1b.svg)](https://arxiv.org/abs/2604.08501)
56
+
48
57
  A linter for scientific manuscripts. Checks that your references exist, your metadata is accurate, your citations support the claims you make, and your cited papers' own bibliographies are real. Works on LaTeX and PDF. Runs entirely on your machine. Produces a **SciLint Score**.
49
58
 
50
59
  The only open-source tool that combines reference verification, claim checking, manuscript consistency, figure analysis, and bibliography verification in one pipeline — powered entirely by a local LLM on your own GPU.
@@ -123,7 +132,7 @@ sciwrite-lint contributions paper.pdf --format json
123
132
 
124
133
  ### Optimizations
125
134
 
126
- Three models — a vision model (Qwen3-VL-2B), 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:
135
+ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. Models run in separate pipeline stages; on WSL2, CUDA memory virtualization pages idle allocations to system RAM, letting all three share physical VRAM. FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
127
136
 
128
137
  - **Semantic section filtering** — embedding-based KNN retrieval sends only the ~5 most relevant sections per claim to the LLM, reducing LLM calls ~4x
129
138
  - **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
@@ -134,11 +143,11 @@ Three models — a vision model (Qwen3-VL-2B), an embedding model (Arctic Embed)
134
143
  - **8-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, CORE
135
144
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
136
145
 
137
- Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, embedding and vision default to CPU (tens of times slower) due to limited VRAM on consumer GPUs and no transparent paging of inactive vLLM container allocations; force GPU via config if VRAM permits.
146
+ Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, the pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50x faster than CPU). *(Native Linux GPU swap is preliminary tested on WSL2 only; expected to work, may need minor fixes.)*
138
147
 
139
148
  ## Install
140
149
 
141
- **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](docs/services.md#embedding-device)). Not tested on macOS.
150
+ **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Developed and tested on Windows (WSL2). Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
142
151
 
143
152
  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).
144
153
 
@@ -148,6 +157,32 @@ uv tool install sciwrite-lint --python 3.13
148
157
 
149
158
  uv downloads Python 3.13 automatically (does not affect any Python you may already have) and installs sciwrite-lint as a globally available command.
150
159
 
160
+ ### Reproducible environment (optional)
161
+
162
+ `pyproject.toml` declares lower bounds for dependencies, so `uv tool install` / `pip install` may resolve newer versions as upstream releases ship. If you want the exact versions the maintainer develops and tests against — for reproducing benchmarks, debugging version-drift issues, or CI — install from [`requirements-pinned.txt`](requirements-pinned.txt) instead.
163
+
164
+ **What's in it:** top-level packages declared in `pyproject.toml` pinned to `==` versions. Transitive dependencies are not pinned — pip resolves them within each top-level package's own constraints. This is a deliberate middle ground: it locks the packages sciwrite-lint actually imports and tests against, without over-constraining the dep tree.
165
+
166
+ ```bash
167
+ # Clone the public repo (required — uv tool install can't read a local file)
168
+ git clone https://github.com/authentic-research-partners/sciwrite-lint.git
169
+ cd sciwrite-lint
170
+
171
+ # With uv (recommended — fast, isolated, manages Python itself)
172
+ uv venv --python 3.13 .venv
173
+ source .venv/bin/activate
174
+ uv pip install -r requirements-pinned.txt
175
+ uv pip install -e . --no-deps # install sciwrite-lint source; deps already pinned
176
+
177
+ # Or with plain pip
178
+ python3.13 -m venv .venv
179
+ source .venv/bin/activate
180
+ pip install -r requirements-pinned.txt
181
+ pip install -e . --no-deps
182
+ ```
183
+
184
+ Activate the venv in each new terminal with `source .venv/bin/activate`. For most users, the plain `uv tool install sciwrite-lint` above is simpler — use the pinned file only when you need exact version reproduction.
185
+
151
186
  ## Setup
152
187
 
153
188
  ```bash
@@ -157,11 +192,22 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
157
192
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
158
193
  ```
159
194
 
160
- ![Monitor dashboard](docs/monitor.png)
195
+ ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
161
196
 
162
197
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
163
198
 
164
- **Paywalled references:** drop PDFs into `local_pdfs/` with filenames matching the reference title. The tool fuzzy-matches filenames against your `.bib` titles and uses local copies instead of downloading.
199
+ **Providing PDFs manually (`local_pdfs/`):** Drop any reference PDF into the `local_pdfs/` directory to skip the download step entirely. This is useful when you already have the file, when the paper is paywalled, or when the publisher blocks automated downloads (Cell, Springer, some PMC pages). Name files by the reference title:
200
+
201
+ ```
202
+ local_pdfs/
203
+ ├── States of Curiosity Modulate Hippocampus-Dependent Learning.pdf
204
+ ├── Mind in Society.pdf
205
+ └── The System of Professions.pdf
206
+ ```
207
+
208
+ The tool fuzzy-matches filenames against your `.bib` titles (threshold 0.80) and copies matches to the workspace. On the next run, matched references upgrade from T2 to T1 and go through GROBID parsing and claim verification like any other full-text reference.
209
+
210
+ **Finding what to download:** `sciwrite-lint verify` lists T2 references that are confirmed open access with direct URLs — open in your browser and save to `local_pdfs/`. For paywalled papers, use your institution's library access.
165
211
 
166
212
  **Optional API keys** increase rate limits for Semantic Scholar, NCBI, and CORE:
167
213
 
@@ -170,7 +216,7 @@ sciwrite-lint config show # see what's configured
170
216
  sciwrite-lint config set-key semantic-scholar YOUR_KEY # dedicated rate limit
171
217
  ```
172
218
 
173
- See [docs/services.md](docs/services.md) for GPU requirements, all external APIs, and API key details.
219
+ See [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) for GPU requirements, all external APIs, and API key details.
174
220
 
175
221
  ## Usage
176
222
 
@@ -256,12 +302,27 @@ Output formats: terminal (default) or `--format json`.
256
302
 
257
303
  - `sciwrite-lint checks` — list all checks
258
304
  - `sciwrite-lint <command> --help` — detailed usage for any command
259
- - [docs/services.md](docs/services.md) — GROBID, vLLM, external APIs, configuration
305
+ - [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) — GROBID, vLLM, external APIs, configuration
260
306
 
261
307
  For contributors and advanced users:
262
308
 
263
- - [docs/evals.md](docs/evals.md) — detection evaluation framework, adding test cases
264
- - [docs/calibration.md](docs/calibration.md) — SciLint Score calibration against ground-truth papers
309
+ - [docs/evals.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/evals.md) — detection evaluation framework, adding test cases
310
+ - [docs/calibration.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/calibration.md) — SciLint Score calibration against ground-truth papers
311
+
312
+ ## Citation
313
+
314
+ If you use sciwrite-lint in your research, please cite the accompanying paper ([arXiv:2604.08501](https://arxiv.org/abs/2604.08501)):
315
+
316
+ ```bibtex
317
+ @article{samsonau2026sciwritelint,
318
+ title = {sciwrite-lint: Verification Infrastructure for the Age of Science Vibe-Writing},
319
+ author = {Samsonau, Sergey V.},
320
+ journal = {arXiv preprint arXiv:2604.08501},
321
+ year = {2026},
322
+ }
323
+ ```
324
+
325
+ GitHub's "Cite this repository" button (powered by [`CITATION.cff`](CITATION.cff)) will also generate this for you.
265
326
 
266
327
  ## License
267
328
 
@@ -1,50 +1,7 @@
1
- Metadata-Version: 2.4
2
- Name: sciwrite-lint
3
- Version: 0.2.0
4
- Summary: A linter for scientific manuscripts — reference verification, consistency checking, and structural validation.
5
- Author: Sergey Samsonau
6
- License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/authentic-research-partners/sciwrite-lint
8
- Project-URL: Repository, https://github.com/authentic-research-partners/sciwrite-lint
9
- Project-URL: Issues, https://github.com/authentic-research-partners/sciwrite-lint/issues
10
- Keywords: linter,scientific-writing,citation-verification,latex
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Intended Audience :: Science/Research
13
- Classifier: Topic :: Text Processing :: Markup :: LaTeX
14
- Classifier: Programming Language :: Python :: 3.13
15
- Requires-Python: <3.14,>=3.13
16
- Description-Content-Type: text/markdown
17
- License-File: LICENSE
18
- Requires-Dist: httpx>=0.27
19
- Requires-Dist: tenacity>=8.0
20
- Requires-Dist: trafilatura>=1.8
21
- Requires-Dist: aiosqlite>=0.20
22
- Requires-Dist: loguru>=0.7
23
- Requires-Dist: anyascii>=0.3
24
- Requires-Dist: rich>=13.0
25
- Requires-Dist: defusedxml>=0.7
26
- Requires-Dist: pydantic>=2.0
27
- Requires-Dist: rapidfuzz>=3.0
28
- Requires-Dist: bibtexparser>=1.4
29
- Requires-Dist: pdfplumber>=0.10
30
- Requires-Dist: openai>=1.0
31
- Requires-Dist: grobid-tei-xml>=0.1
32
- Requires-Dist: sentence-transformers>=3.0
33
- Requires-Dist: numpy>=1.24
34
- Requires-Dist: sqlite-vec>=0.1.6
35
- Requires-Dist: torch>=2.0
36
- Requires-Dist: transformers>=4.45
37
- Requires-Dist: Pillow>=10.0
38
- Requires-Dist: pypdf>=4.0
39
- Requires-Dist: pypdfium2>=4.0
40
- Requires-Dist: pdf2image>=1.16
41
- Provides-Extra: dev
42
- Requires-Dist: pytest>=8.0; extra == "dev"
43
- Requires-Dist: pytest-xdist>=3.0; extra == "dev"
44
- Dynamic: license-file
45
-
46
1
  # sciwrite-lint
47
2
 
3
+ [![arXiv](https://img.shields.io/badge/arXiv-2604.08501-b31b1b.svg)](https://arxiv.org/abs/2604.08501)
4
+
48
5
  A linter for scientific manuscripts. Checks that your references exist, your metadata is accurate, your citations support the claims you make, and your cited papers' own bibliographies are real. Works on LaTeX and PDF. Runs entirely on your machine. Produces a **SciLint Score**.
49
6
 
50
7
  The only open-source tool that combines reference verification, claim checking, manuscript consistency, figure analysis, and bibliography verification in one pipeline — powered entirely by a local LLM on your own GPU.
@@ -123,7 +80,7 @@ sciwrite-lint contributions paper.pdf --format json
123
80
 
124
81
  ### Optimizations
125
82
 
126
- Three models — a vision model (Qwen3-VL-2B), 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:
83
+ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. Models run in separate pipeline stages; on WSL2, CUDA memory virtualization pages idle allocations to system RAM, letting all three share physical VRAM. FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
127
84
 
128
85
  - **Semantic section filtering** — embedding-based KNN retrieval sends only the ~5 most relevant sections per claim to the LLM, reducing LLM calls ~4x
129
86
  - **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
@@ -134,11 +91,11 @@ Three models — a vision model (Qwen3-VL-2B), an embedding model (Arctic Embed)
134
91
  - **8-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, CORE
135
92
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
136
93
 
137
- Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, embedding and vision default to CPU (tens of times slower) due to limited VRAM on consumer GPUs and no transparent paging of inactive vLLM container allocations; force GPU via config if VRAM permits.
94
+ Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, the pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50x faster than CPU). *(Native Linux GPU swap is preliminary tested on WSL2 only; expected to work, may need minor fixes.)*
138
95
 
139
96
  ## Install
140
97
 
141
- **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](docs/services.md#embedding-device)). Not tested on macOS.
98
+ **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Developed and tested on Windows (WSL2). Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
142
99
 
143
100
  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).
144
101
 
@@ -148,6 +105,32 @@ uv tool install sciwrite-lint --python 3.13
148
105
 
149
106
  uv downloads Python 3.13 automatically (does not affect any Python you may already have) and installs sciwrite-lint as a globally available command.
150
107
 
108
+ ### Reproducible environment (optional)
109
+
110
+ `pyproject.toml` declares lower bounds for dependencies, so `uv tool install` / `pip install` may resolve newer versions as upstream releases ship. If you want the exact versions the maintainer develops and tests against — for reproducing benchmarks, debugging version-drift issues, or CI — install from [`requirements-pinned.txt`](requirements-pinned.txt) instead.
111
+
112
+ **What's in it:** top-level packages declared in `pyproject.toml` pinned to `==` versions. Transitive dependencies are not pinned — pip resolves them within each top-level package's own constraints. This is a deliberate middle ground: it locks the packages sciwrite-lint actually imports and tests against, without over-constraining the dep tree.
113
+
114
+ ```bash
115
+ # Clone the public repo (required — uv tool install can't read a local file)
116
+ git clone https://github.com/authentic-research-partners/sciwrite-lint.git
117
+ cd sciwrite-lint
118
+
119
+ # With uv (recommended — fast, isolated, manages Python itself)
120
+ uv venv --python 3.13 .venv
121
+ source .venv/bin/activate
122
+ uv pip install -r requirements-pinned.txt
123
+ uv pip install -e . --no-deps # install sciwrite-lint source; deps already pinned
124
+
125
+ # Or with plain pip
126
+ python3.13 -m venv .venv
127
+ source .venv/bin/activate
128
+ pip install -r requirements-pinned.txt
129
+ pip install -e . --no-deps
130
+ ```
131
+
132
+ Activate the venv in each new terminal with `source .venv/bin/activate`. For most users, the plain `uv tool install sciwrite-lint` above is simpler — use the pinned file only when you need exact version reproduction.
133
+
151
134
  ## Setup
152
135
 
153
136
  ```bash
@@ -157,11 +140,22 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
157
140
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
158
141
  ```
159
142
 
160
- ![Monitor dashboard](docs/monitor.png)
143
+ ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
161
144
 
162
145
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
163
146
 
164
- **Paywalled references:** drop PDFs into `local_pdfs/` with filenames matching the reference title. The tool fuzzy-matches filenames against your `.bib` titles and uses local copies instead of downloading.
147
+ **Providing PDFs manually (`local_pdfs/`):** Drop any reference PDF into the `local_pdfs/` directory to skip the download step entirely. This is useful when you already have the file, when the paper is paywalled, or when the publisher blocks automated downloads (Cell, Springer, some PMC pages). Name files by the reference title:
148
+
149
+ ```
150
+ local_pdfs/
151
+ ├── States of Curiosity Modulate Hippocampus-Dependent Learning.pdf
152
+ ├── Mind in Society.pdf
153
+ └── The System of Professions.pdf
154
+ ```
155
+
156
+ The tool fuzzy-matches filenames against your `.bib` titles (threshold 0.80) and copies matches to the workspace. On the next run, matched references upgrade from T2 to T1 and go through GROBID parsing and claim verification like any other full-text reference.
157
+
158
+ **Finding what to download:** `sciwrite-lint verify` lists T2 references that are confirmed open access with direct URLs — open in your browser and save to `local_pdfs/`. For paywalled papers, use your institution's library access.
165
159
 
166
160
  **Optional API keys** increase rate limits for Semantic Scholar, NCBI, and CORE:
167
161
 
@@ -170,7 +164,7 @@ sciwrite-lint config show # see what's configured
170
164
  sciwrite-lint config set-key semantic-scholar YOUR_KEY # dedicated rate limit
171
165
  ```
172
166
 
173
- See [docs/services.md](docs/services.md) for GPU requirements, all external APIs, and API key details.
167
+ See [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) for GPU requirements, all external APIs, and API key details.
174
168
 
175
169
  ## Usage
176
170
 
@@ -256,12 +250,27 @@ Output formats: terminal (default) or `--format json`.
256
250
 
257
251
  - `sciwrite-lint checks` — list all checks
258
252
  - `sciwrite-lint <command> --help` — detailed usage for any command
259
- - [docs/services.md](docs/services.md) — GROBID, vLLM, external APIs, configuration
253
+ - [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) — GROBID, vLLM, external APIs, configuration
260
254
 
261
255
  For contributors and advanced users:
262
256
 
263
- - [docs/evals.md](docs/evals.md) — detection evaluation framework, adding test cases
264
- - [docs/calibration.md](docs/calibration.md) — SciLint Score calibration against ground-truth papers
257
+ - [docs/evals.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/evals.md) — detection evaluation framework, adding test cases
258
+ - [docs/calibration.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/calibration.md) — SciLint Score calibration against ground-truth papers
259
+
260
+ ## Citation
261
+
262
+ If you use sciwrite-lint in your research, please cite the accompanying paper ([arXiv:2604.08501](https://arxiv.org/abs/2604.08501)):
263
+
264
+ ```bibtex
265
+ @article{samsonau2026sciwritelint,
266
+ title = {sciwrite-lint: Verification Infrastructure for the Age of Science Vibe-Writing},
267
+ author = {Samsonau, Sergey V.},
268
+ journal = {arXiv preprint arXiv:2604.08501},
269
+ year = {2026},
270
+ }
271
+ ```
272
+
273
+ GitHub's "Cite this repository" button (powered by [`CITATION.cff`](CITATION.cff)) will also generate this for you.
265
274
 
266
275
  ## License
267
276
 
@@ -4,49 +4,56 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sciwrite-lint"
7
- version = "0.2.0"
7
+ version = "0.2.2"
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"
11
11
  authors = [{ name = "Sergey Samsonau" }]
12
+ maintainers = [{ name = "Authentic Research Partners LLC", email = "info@arpconnect.com" }]
12
13
  readme = "README.md"
13
- keywords = ["linter", "scientific-writing", "citation-verification", "latex"]
14
+ keywords = ["linter", "latex", "bibtex", "pdf", "scientific-writing", "academic-writing", "citation-checker", "reference-verification", "claim-verification", "manuscript", "peer-review", "arxiv", "vllm", "qwen"]
14
15
  classifiers = [
15
16
  "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
16
18
  "Intended Audience :: Science/Research",
17
- "Topic :: Text Processing :: Markup :: LaTeX",
19
+ "Topic :: Scientific/Engineering",
20
+ "Topic :: Software Development :: Quality Assurance",
21
+ "Programming Language :: Python :: 3",
18
22
  "Programming Language :: Python :: 3.13",
23
+ "Typing :: Typed",
19
24
  ]
20
25
  dependencies = [
21
- "httpx>=0.27",
22
- "tenacity>=8.0",
23
- "trafilatura>=1.8",
24
- "aiosqlite>=0.20",
26
+ "httpx>=0.28",
27
+ "tenacity>=9.0",
28
+ "trafilatura>=2.0",
29
+ "aiosqlite>=0.22",
25
30
  "loguru>=0.7",
26
31
  "anyascii>=0.3",
27
- "rich>=13.0",
32
+ "rich>=14.0",
28
33
  "defusedxml>=0.7",
29
- "pydantic>=2.0",
34
+ "pydantic>=2.10",
30
35
  "rapidfuzz>=3.0",
31
36
  "bibtexparser>=1.4",
32
- "pdfplumber>=0.10",
33
- "openai>=1.0",
37
+ "pdfplumber>=0.11",
38
+ "openai>=2.0",
34
39
  "grobid-tei-xml>=0.1",
35
- "sentence-transformers>=3.0",
36
- "numpy>=1.24",
40
+ "sentence-transformers>=5.0",
41
+ "numpy>=2.0",
37
42
  "sqlite-vec>=0.1.6",
38
- "torch>=2.0",
39
- "transformers>=4.45",
40
- "Pillow>=10.0",
41
- "pypdf>=4.0",
42
- "pypdfium2>=4.0",
43
- "pdf2image>=1.16",
43
+ "torch>=2.5",
44
+ "transformers>=4.50",
45
+ "Pillow>=12.0",
46
+ "pypdf>=6.0",
47
+ "pypdfium2>=5.0",
48
+ "pdf2image>=1.17",
44
49
  ]
45
50
 
46
51
  [project.urls]
47
- Homepage = "https://github.com/authentic-research-partners/sciwrite-lint"
52
+ Homepage = "https://arpconnect.com"
48
53
  Repository = "https://github.com/authentic-research-partners/sciwrite-lint"
54
+ Documentation = "https://github.com/authentic-research-partners/sciwrite-lint#readme"
49
55
  Issues = "https://github.com/authentic-research-partners/sciwrite-lint/issues"
56
+ Paper = "https://arxiv.org/abs/2604.08501"
50
57
 
51
58
  [project.optional-dependencies]
52
59
  dev = [
@@ -1,3 +1,3 @@
1
1
  """sciwrite-lint: a linter for scientific manuscripts."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
@@ -18,6 +18,10 @@ from loguru import logger
18
18
 
19
19
  from sciwrite_lint import __version__
20
20
  from sciwrite_lint.config import LintConfig, PaperConfig, load_config
21
+ from sciwrite_lint.vllm.vllm_server import MODELS as _VLLM_MODELS
22
+
23
+ _ALL_MODELS = sorted(_VLLM_MODELS.keys())
24
+ _TEXT_MODELS = [k for k, v in _VLLM_MODELS.items() if v["kind"] == "text"]
21
25
 
22
26
 
23
27
  # ---------------------------------------------------------------------------
@@ -179,6 +183,12 @@ def main(argv: list[str] | None = None) -> int:
179
183
  "limit. Above 4, the live monitor may fail to track progress and "
180
184
  "vLLM/API throughput plateaus.",
181
185
  )
186
+ p_check.add_argument(
187
+ "--vision-backend",
188
+ choices=["transformers", "vllm"],
189
+ default=None,
190
+ help="Vision backend: transformers (2B, default) or vllm (8B FP8 on port 5002)",
191
+ )
182
192
  p_check.set_defaults(func=run_check)
183
193
 
184
194
  # --- checks ---
@@ -296,7 +306,7 @@ def main(argv: list[str] | None = None) -> int:
296
306
  )
297
307
  p_vc.add_argument(
298
308
  "--model",
299
- choices=["qwen3", "gemma3"],
309
+ choices=_TEXT_MODELS,
300
310
  default="",
301
311
  help="vLLM model preset (default: qwen3)",
302
312
  )
@@ -381,14 +391,20 @@ def main(argv: list[str] | None = None) -> int:
381
391
  # --- vision ---
382
392
  p_vision = sub.add_parser(
383
393
  "vision",
384
- help="Extract and describe manuscript figures via Qwen3-VL-2B (also runs automatically in check pipeline).",
394
+ help="Extract and describe manuscript figures (transformers 2B or vLLM 8B FP8).",
385
395
  )
386
396
  p_vision.add_argument("--paper", required=True, help="Paper name from [[papers]]")
387
397
  p_vision.add_argument(
388
398
  "--device",
389
399
  choices=["auto", "cpu", "cuda"],
390
- default="auto",
391
- help="Device for VL inference (default: auto — CUDA if available)",
400
+ default=None,
401
+ help="Device for VL inference (default: auto — CUDA if available, transformers only)",
402
+ )
403
+ p_vision.add_argument(
404
+ "--backend",
405
+ choices=["transformers", "vllm"],
406
+ default=None,
407
+ help="Vision backend: transformers (2B, default) or vllm (8B FP8 on port 5002)",
392
408
  )
393
409
  p_vision.add_argument(
394
410
  "--fresh",
@@ -407,9 +423,7 @@ def main(argv: list[str] | None = None) -> int:
407
423
  choices=["start", "stop", "restart", "status", "monitor"],
408
424
  help="start/stop/restart/status/monitor of all containers",
409
425
  )
410
- p_containers.add_argument(
411
- "--model", choices=["qwen3", "gemma3"], help="vLLM model to use"
412
- )
426
+ p_containers.add_argument("--model", choices=_ALL_MODELS, help="vLLM model to use")
413
427
  p_containers.add_argument(
414
428
  "--update",
415
429
  action="store_true",
@@ -427,6 +441,11 @@ def main(argv: list[str] | None = None) -> int:
427
441
  action="store_true",
428
442
  help="Remove and recreate containers (applies config changes like memory limits)",
429
443
  )
444
+ p_containers.add_argument(
445
+ "--vision",
446
+ action="store_true",
447
+ help="Also start vision vLLM (qwen3-vl-8b-fp8 on port 5002)",
448
+ )
430
449
  p_containers.add_argument("--config", help="Path to .sciwrite-lint.toml")
431
450
  p_containers.set_defaults(func=run_containers)
432
451
 
@@ -452,9 +471,7 @@ def main(argv: list[str] | None = None) -> int:
452
471
  p_vllm_status.set_defaults(func=run_vllm)
453
472
 
454
473
  p_vllm_start = vllm_sub.add_parser("start", help="Start vLLM server")
455
- p_vllm_start.add_argument(
456
- "--model", choices=["qwen3", "gemma3"], help="Model to serve"
457
- )
474
+ p_vllm_start.add_argument("--model", choices=_ALL_MODELS, help="Model to serve")
458
475
  p_vllm_start.add_argument(
459
476
  "--update",
460
477
  action="store_true",
@@ -464,13 +481,11 @@ def main(argv: list[str] | None = None) -> int:
464
481
  p_vllm_start.set_defaults(func=run_vllm)
465
482
 
466
483
  p_vllm_stop = vllm_sub.add_parser("stop", help="Stop vLLM server")
467
- p_vllm_stop.add_argument(
468
- "--model", choices=["qwen3", "gemma3"], help="Model to stop"
469
- )
484
+ p_vllm_stop.add_argument("--model", choices=_ALL_MODELS, help="Model to stop")
470
485
  p_vllm_stop.set_defaults(func=run_vllm)
471
486
 
472
487
  p_vllm_logs = vllm_sub.add_parser("logs", help="Show vLLM container logs")
473
- p_vllm_logs.add_argument("--model", choices=["qwen3", "gemma3"])
488
+ p_vllm_logs.add_argument("--model", choices=_ALL_MODELS)
474
489
  p_vllm_logs.add_argument(
475
490
  "-f", "--follow", action="store_true", help="Follow log output"
476
491
  )
@@ -478,7 +493,7 @@ def main(argv: list[str] | None = None) -> int:
478
493
  p_vllm_logs.set_defaults(func=run_vllm)
479
494
 
480
495
  p_vllm_rm = vllm_sub.add_parser("rm", help="Remove vLLM container")
481
- p_vllm_rm.add_argument("--model", choices=["qwen3", "gemma3"])
496
+ p_vllm_rm.add_argument("--model", choices=_ALL_MODELS)
482
497
  p_vllm_rm.add_argument(
483
498
  "--force", action="store_true", help="Force remove even if running"
484
499
  )
@@ -44,6 +44,9 @@ For each item, set is_genuine to true ONLY if the values or claims \
44
44
  CONFLICT. Set is_genuine to false if the passages agree or discuss \
45
45
  different topics.
46
46
 
47
+ Report at most 4 contradictions. If you find more, return only the 4 \
48
+ most clear-cut cases.
49
+
47
50
  Reply with JSON: {"contradictions": [\
48
51
  {"type": "number|claim|framing", "section_a_says": "...", \
49
52
  "section_b_says": "...", "explanation": "...", "is_genuine": true/false}]}