sciwrite-lint 0.5.0__tar.gz → 0.6.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 (167) hide show
  1. {sciwrite_lint-0.5.0/sciwrite_lint.egg-info → sciwrite_lint-0.6.0}/PKG-INFO +46 -5
  2. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/README.md +45 -4
  3. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/pyproject.toml +1 -1
  4. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/__init__.py +1 -1
  5. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/__main__.py +14 -0
  6. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/api.py +40 -18
  7. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/_diagnostics.py +67 -1
  8. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/dangling_cite.py +13 -7
  9. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/dangling_ref.py +31 -1
  10. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/full_paper_consistency.py +23 -0
  11. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/ref_internal_checks.py +65 -26
  12. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/registry.py +22 -0
  13. sciwrite_lint-0.6.0/sciwrite_lint/checks/unreferenced_figure.py +157 -0
  14. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/_common.py +18 -3
  15. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/check.py +52 -8
  16. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/config.py +11 -1
  17. sciwrite_lint-0.6.0/sciwrite_lint/cli/misc/init.py +37 -0
  18. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/rank.py +17 -6
  19. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/verify.py +113 -11
  20. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/config.py +155 -37
  21. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/eval_claims.py +170 -27
  22. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/footnote_urls.py +143 -6
  23. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/_common.py +4 -0
  24. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/_download.py +13 -0
  25. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/manuscript_store.py +55 -3
  26. sciwrite_lint-0.6.0/sciwrite_lint/markdown_cites.py +337 -0
  27. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/models.py +7 -0
  28. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/oa/_pdf.py +27 -2
  29. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/__init__.py +3 -1
  30. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/checks.py +23 -2
  31. sciwrite_lint-0.6.0/sciwrite_lint/pipeline/citation_setup.py +157 -0
  32. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/claims.py +10 -0
  33. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/embeddings.py +53 -21
  34. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/fetch.py +53 -27
  35. sciwrite_lint-0.6.0/sciwrite_lint/pipeline/markdown_context.py +111 -0
  36. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/orchestration.py +56 -42
  37. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/pdf_context.py +16 -8
  38. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/staged.py +16 -53
  39. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/verify.py +15 -0
  40. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/citations.py +10 -0
  41. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/reference_store.py +4 -4
  42. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/tex_parser.py +74 -0
  43. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/vllm/vllm_server.py +21 -1
  44. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0/sciwrite_lint.egg-info}/PKG-INFO +46 -5
  45. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint.egg-info/SOURCES.txt +3 -0
  46. sciwrite_lint-0.5.0/sciwrite_lint/checks/unreferenced_figure.py +0 -70
  47. sciwrite_lint-0.5.0/sciwrite_lint/cli/misc/init.py +0 -14
  48. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/LICENSE +0 -0
  49. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/_network.py +0 -0
  50. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/api_keys.py +0 -0
  51. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/__init__.py +0 -0
  52. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/_section_utils.py +0 -0
  53. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/cite_purpose.py +0 -0
  54. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/claim_support.py +0 -0
  55. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/cross_section_consistency.py +0 -0
  56. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/internal_consistency_pairs.py +0 -0
  57. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/prose_quality.py +0 -0
  58. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/reference_accuracy.py +0 -0
  59. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/reference_exists.py +0 -0
  60. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/reference_unreliable.py +0 -0
  61. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/retracted_cite.py +0 -0
  62. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/checks/structure_promises.py +0 -0
  63. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/claims.py +0 -0
  64. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/claude_cli.py +0 -0
  65. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/__init__.py +0 -0
  66. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/fetch.py +0 -0
  67. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/misc/__init__.py +0 -0
  68. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/misc/_monitor.py +0 -0
  69. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/misc/containers.py +0 -0
  70. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/misc/dismiss_claim.py +0 -0
  71. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/misc/grobid.py +0 -0
  72. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/misc/override.py +0 -0
  73. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/misc/parse.py +0 -0
  74. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/misc/vision.py +0 -0
  75. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cli/misc/vllm.py +0 -0
  76. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/cross_paper.py +0 -0
  77. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/exceptions.py +0 -0
  78. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/__init__.py +0 -0
  79. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/_orchestrator.py +0 -0
  80. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/_search.py +0 -0
  81. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/_validation.py +0 -0
  82. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/arxiv.py +0 -0
  83. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/biorxiv.py +0 -0
  84. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/core.py +0 -0
  85. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/eric.py +0 -0
  86. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/europepmc.py +0 -0
  87. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/hal.py +0 -0
  88. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/ideas.py +0 -0
  89. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/nasa_ads.py +0 -0
  90. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/nber.py +0 -0
  91. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/osf.py +0 -0
  92. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/pmc.py +0 -0
  93. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/s2.py +0 -0
  94. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/fulltext/unpaywall.py +0 -0
  95. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/latex_to_markdown.py +0 -0
  96. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/__init__.py +0 -0
  97. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/__init__.py +0 -0
  98. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/compute_cap.py +0 -0
  99. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/controller.py +0 -0
  100. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/decide.py +0 -0
  101. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/host_metrics.py +0 -0
  102. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/metrics_probe.py +0 -0
  103. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/registry.py +0 -0
  104. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/telemetry.py +0 -0
  105. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/vllm_metrics.py +0 -0
  106. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/concurrency_optimizer/wiring.py +0 -0
  107. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm/manuscript_cache.py +0 -0
  108. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/llm_utils.py +0 -0
  109. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/local_sources.py +0 -0
  110. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/mhtml.py +0 -0
  111. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/oa/__init__.py +0 -0
  112. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/oa/_models.py +0 -0
  113. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/oa/_search.py +0 -0
  114. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/oa/_web.py +0 -0
  115. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pdf/__init__.py +0 -0
  116. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pdf/grobid.py +0 -0
  117. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pdf/pdf_download.py +0 -0
  118. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/parse.py +0 -0
  119. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/ref_internal.py +0 -0
  120. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/runners.py +0 -0
  121. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/swap.py +0 -0
  122. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/tracking.py +0 -0
  123. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/unreliable.py +0 -0
  124. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/pipeline/vision.py +0 -0
  125. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/prompt_safety.py +0 -0
  126. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/py.typed +0 -0
  127. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/rate_limiter.py +0 -0
  128. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/__init__.py +0 -0
  129. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/_embed_timing.py +0 -0
  130. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/chain.py +0 -0
  131. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/crossref.py +0 -0
  132. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/embedding_store.py +0 -0
  133. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/matching.py +0 -0
  134. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/metadata.py +0 -0
  135. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/retraction_watch.py +0 -0
  136. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/__init__.py +0 -0
  137. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/_core.py +0 -0
  138. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/bib_checks.py +0 -0
  139. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/citation_metadata.py +0 -0
  140. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/claim_results.py +0 -0
  141. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/manuscript_check_cache.py +0 -0
  142. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/manuscript_citations.py +0 -0
  143. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/parse_cache.py +0 -0
  144. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/pipeline_stage.py +0 -0
  145. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/query_vectors.py +0 -0
  146. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/ref_internal.py +0 -0
  147. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/registry.py +0 -0
  148. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/references/workspace_db/vision_cache.py +0 -0
  149. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/report.py +0 -0
  150. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/schemas.py +0 -0
  151. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/scoring/__init__.py +0 -0
  152. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/scoring/contribution.py +0 -0
  153. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/scoring/scilint_score.py +0 -0
  154. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/usage.py +0 -0
  155. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/vision/__init__.py +0 -0
  156. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/vision/cache.py +0 -0
  157. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/vision/describe.py +0 -0
  158. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/vision/image_extraction.py +0 -0
  159. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/vision/pipeline.py +0 -0
  160. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/vllm/__init__.py +0 -0
  161. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/vllm/metrics.py +0 -0
  162. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint/web.py +0 -0
  163. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
  164. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint.egg-info/entry_points.txt +0 -0
  165. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint.egg-info/requires.txt +0 -0
  166. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/sciwrite_lint.egg-info/top_level.txt +0 -0
  167. {sciwrite_lint-0.5.0 → sciwrite_lint-0.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sciwrite-lint
3
- Version: 0.5.0
3
+ Version: 0.6.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>
@@ -158,11 +158,11 @@ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-ba
158
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
159
159
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
160
160
 
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.
161
+ The pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50× faster than CPU).
162
162
 
163
163
  ## Install
164
164
 
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.
165
+ **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Tested on WSL2 with NVIDIA driver 546.01+. Native Linux uses the same GPU-sequencing code path with no extra configuration (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)), though it isn't actively tested. Not tested on macOS.
166
166
 
167
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).
168
168
 
@@ -207,6 +207,8 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
207
207
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
208
208
  ```
209
209
 
210
+ `init` generates a `.sciwrite-lint.toml` config. With no argument it auto-detects the manuscript — a `.tex` file (bibliography read from `\bibliography{}`) or a markdown file with a sibling `{name}.bib` (so ordinary `.md` files aren't mistaken for papers); if it finds **several**, it lists them and asks you to pick one. Name a manuscript explicitly — including a **PDF** — with `sciwrite-lint init <file>`. Review to confirm the right file was registered.
211
+
210
212
  ### WSL2: disable CUDA Sysmem Fallback (recommended)
211
213
 
212
214
  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:
@@ -217,8 +219,6 @@ Driver 546.01+. After this, WSL2 behaves like native Linux on overflow (CUDA OOM
217
219
 
218
220
  ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
219
221
 
220
- `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
221
-
222
222
  **Providing references manually — two drop folders.** sciwrite-lint reads two local directories before going to the open-access waterfall, kept separate because the folder itself signals how much to trust the source:
223
223
 
224
224
  - `local_pdfs/` — **academic sources**. Accepts `.pdf` (primary) and `.md` summaries you've written yourself. Use this for paywalled papers, for OA papers whose publisher requires a browser (Cloudflare/JS walls, Cell, Springer, some PMC pages), and for anything you'd cite as peer-reviewed evidence.
@@ -275,11 +275,52 @@ sciwrite-lint check # all papers (batch-staged when
275
275
  sciwrite-lint check --concurrency 4 # batch parallelism (default 2, validated up to 4)
276
276
  sciwrite-lint check paper.tex # text + LLM rules on a .tex file
277
277
  sciwrite-lint check paper.pdf # check a PDF (GROBID required)
278
+ sciwrite-lint check paper.md # markdown manuscript (prose, structure, [@key] citations, @fig:/@sec: cross-refs)
278
279
  sciwrite-lint check --checks prose-quality paper.tex # run only the listed checks (comma-separated)
279
280
  sciwrite-lint contributions --paper my_paper # add contribution axes to SciLint Score
280
281
  sciwrite-lint contributions paper.pdf # standalone file scoring
281
282
  ```
282
283
 
284
+ **Supported inputs.**
285
+
286
+ *The manuscript you check:*
287
+
288
+ | Format | Notes |
289
+ |---|---|
290
+ | `.tex` | LaTeX — hand-written, or pandoc-generated (e.g. from Markdown); pandoc's heading anchors are unwrapped automatically. Full pipeline. |
291
+ | `.pdf` | Parsed via GROBID (container required). Full pipeline. |
292
+ | `.md` | Markdown (pandoc). Prose and structure checks run; pandoc `[@key]` citations are checked against a sibling `{name}.bib` and verified against external databases in the full pipeline; pandoc-crossref `@fig:`/`@sec:`/`@tbl:` references and `{#fig:…}` figures are checked for broken/unreferenced targets. Sections split by heading. |
293
+
294
+ *What runs on each format:*
295
+
296
+ | Capability | `.tex` | `.pdf` | `.md` |
297
+ |---|:---:|:---:|:---:|
298
+ | Prose & structure checks | ✓ | ✓ | ✓ |
299
+ | Citation ↔ bibliography (`dangling-cite`) | ✓ | ✓ | ✓ |
300
+ | Cross-references (`dangling-ref`) | ✓ | ✓ <sup>1</sup> | ✓ |
301
+ | Unreferenced figures | ✓ | ✓ <sup>2</sup> | ✓ |
302
+ | Reference verification (external APIs) | ✓ | ✓ | ✓ |
303
+ | Claim support / citation purpose | ✓ | ✓ | ✓ |
304
+ | Footnote-URL web sources | ✓ | — <sup>3</sup> | ✓ |
305
+ | `ref-health` (no external APIs) | ✓ | ✓ <sup>4</sup> | ✓ |
306
+ | `init` auto-detection | ✓ | — <sup>5</sup> | ✓ <sup>6</sup> |
307
+
308
+ <sup>1</sup> PDF detects broken references rendered as `??`; `.tex`/`.md` match each reference to its label.
309
+ <sup>2</sup> A PDF has no `\label`s, so figures are matched by the number GROBID reads from each figure's caption against the "Figure N" mentions in the prose (needs GROBID). Only cleanly-labelled figures are checked, so it stays conservative.
310
+ <sup>3</sup> Footnote-URL web sources are matched for LaTeX and markdown; not yet for PDF.
311
+ <sup>4</sup> A PDF's references come from GROBID (a local parser — still no external/network APIs), so PDF `ref-health` needs the GROBID container running.
312
+ <sup>5</sup> *Auto-scan* skips PDFs — no signal distinguishes a manuscript PDF from a project's reference/figure PDFs, the way a `.tex` or a `.md` with a sibling `.bib` is recognizable. Register one explicitly with `sciwrite-lint init paper.pdf` (the explicit form takes any type). When auto-scan finds several `.tex`/`.md`, it asks you to name one rather than registering all.
313
+ <sup>6</sup> Auto-detected when a sibling `{name}.bib` is present.
314
+
315
+ Markdown citation support targets pandoc `[@key]` citations with a bibliography. Numeric `[1]` citations are not supported by design — they carry no stable key (renumbering on every edit) and their reference list is free text rather than structured entries; a document using them is reported, not partially parsed.
316
+
317
+ *Cited sources you optionally supply (drop folders — see [Setup](#setup)):*
318
+
319
+ | Folder | Accepts | Trust level |
320
+ |---|---|---|
321
+ | `local_pdfs/` | `.pdf`, `.md` (your own summaries) | academic |
322
+ | `local_web/` | `.mhtml` / `.mht`, `.md` | web capture |
323
+
283
324
  `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.
284
325
 
285
326
  ### Iterative editing — no `--fresh` needed
@@ -92,11 +92,11 @@ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-ba
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. The pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50× faster than CPU) — same code path on WSL2 and native Linux.
95
+ The pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50× faster than CPU).
96
96
 
97
97
  ## Install
98
98
 
99
- **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Tested on WSL2 with NVIDIA driver 546.01+. Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
99
+ **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Tested on WSL2 with NVIDIA driver 546.01+. Native Linux uses the same GPU-sequencing code path with no extra configuration (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)), though it isn't actively tested. 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,8 @@ 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
+ `init` generates a `.sciwrite-lint.toml` config. With no argument it auto-detects the manuscript — a `.tex` file (bibliography read from `\bibliography{}`) or a markdown file with a sibling `{name}.bib` (so ordinary `.md` files aren't mistaken for papers); if it finds **several**, it lists them and asks you to pick one. Name a manuscript explicitly — including a **PDF** — with `sciwrite-lint init <file>`. Review to confirm the right file was registered.
145
+
144
146
  ### WSL2: disable CUDA Sysmem Fallback (recommended)
145
147
 
146
148
  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:
@@ -151,8 +153,6 @@ Driver 546.01+. After this, WSL2 behaves like native Linux on overflow (CUDA OOM
151
153
 
152
154
  ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
153
155
 
154
- `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
155
-
156
156
  **Providing references manually — two drop folders.** sciwrite-lint reads two local directories before going to the open-access waterfall, kept separate because the folder itself signals how much to trust the source:
157
157
 
158
158
  - `local_pdfs/` — **academic sources**. Accepts `.pdf` (primary) and `.md` summaries you've written yourself. Use this for paywalled papers, for OA papers whose publisher requires a browser (Cloudflare/JS walls, Cell, Springer, some PMC pages), and for anything you'd cite as peer-reviewed evidence.
@@ -209,11 +209,52 @@ sciwrite-lint check # all papers (batch-staged when
209
209
  sciwrite-lint check --concurrency 4 # batch parallelism (default 2, validated up to 4)
210
210
  sciwrite-lint check paper.tex # text + LLM rules on a .tex file
211
211
  sciwrite-lint check paper.pdf # check a PDF (GROBID required)
212
+ sciwrite-lint check paper.md # markdown manuscript (prose, structure, [@key] citations, @fig:/@sec: cross-refs)
212
213
  sciwrite-lint check --checks prose-quality paper.tex # run only the listed checks (comma-separated)
213
214
  sciwrite-lint contributions --paper my_paper # add contribution axes to SciLint Score
214
215
  sciwrite-lint contributions paper.pdf # standalone file scoring
215
216
  ```
216
217
 
218
+ **Supported inputs.**
219
+
220
+ *The manuscript you check:*
221
+
222
+ | Format | Notes |
223
+ |---|---|
224
+ | `.tex` | LaTeX — hand-written, or pandoc-generated (e.g. from Markdown); pandoc's heading anchors are unwrapped automatically. Full pipeline. |
225
+ | `.pdf` | Parsed via GROBID (container required). Full pipeline. |
226
+ | `.md` | Markdown (pandoc). Prose and structure checks run; pandoc `[@key]` citations are checked against a sibling `{name}.bib` and verified against external databases in the full pipeline; pandoc-crossref `@fig:`/`@sec:`/`@tbl:` references and `{#fig:…}` figures are checked for broken/unreferenced targets. Sections split by heading. |
227
+
228
+ *What runs on each format:*
229
+
230
+ | Capability | `.tex` | `.pdf` | `.md` |
231
+ |---|:---:|:---:|:---:|
232
+ | Prose & structure checks | ✓ | ✓ | ✓ |
233
+ | Citation ↔ bibliography (`dangling-cite`) | ✓ | ✓ | ✓ |
234
+ | Cross-references (`dangling-ref`) | ✓ | ✓ <sup>1</sup> | ✓ |
235
+ | Unreferenced figures | ✓ | ✓ <sup>2</sup> | ✓ |
236
+ | Reference verification (external APIs) | ✓ | ✓ | ✓ |
237
+ | Claim support / citation purpose | ✓ | ✓ | ✓ |
238
+ | Footnote-URL web sources | ✓ | — <sup>3</sup> | ✓ |
239
+ | `ref-health` (no external APIs) | ✓ | ✓ <sup>4</sup> | ✓ |
240
+ | `init` auto-detection | ✓ | — <sup>5</sup> | ✓ <sup>6</sup> |
241
+
242
+ <sup>1</sup> PDF detects broken references rendered as `??`; `.tex`/`.md` match each reference to its label.
243
+ <sup>2</sup> A PDF has no `\label`s, so figures are matched by the number GROBID reads from each figure's caption against the "Figure N" mentions in the prose (needs GROBID). Only cleanly-labelled figures are checked, so it stays conservative.
244
+ <sup>3</sup> Footnote-URL web sources are matched for LaTeX and markdown; not yet for PDF.
245
+ <sup>4</sup> A PDF's references come from GROBID (a local parser — still no external/network APIs), so PDF `ref-health` needs the GROBID container running.
246
+ <sup>5</sup> *Auto-scan* skips PDFs — no signal distinguishes a manuscript PDF from a project's reference/figure PDFs, the way a `.tex` or a `.md` with a sibling `.bib` is recognizable. Register one explicitly with `sciwrite-lint init paper.pdf` (the explicit form takes any type). When auto-scan finds several `.tex`/`.md`, it asks you to name one rather than registering all.
247
+ <sup>6</sup> Auto-detected when a sibling `{name}.bib` is present.
248
+
249
+ Markdown citation support targets pandoc `[@key]` citations with a bibliography. Numeric `[1]` citations are not supported by design — they carry no stable key (renumbering on every edit) and their reference list is free text rather than structured entries; a document using them is reported, not partially parsed.
250
+
251
+ *Cited sources you optionally supply (drop folders — see [Setup](#setup)):*
252
+
253
+ | Folder | Accepts | Trust level |
254
+ |---|---|---|
255
+ | `local_pdfs/` | `.pdf`, `.md` (your own summaries) | academic |
256
+ | `local_web/` | `.mhtml` / `.mht`, `.md` | web capture |
257
+
217
258
  `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.
218
259
 
219
260
  ### Iterative editing — no `--fresh` needed
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sciwrite-lint"
7
- version = "0.5.0"
7
+ version = "0.6.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"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from sciwrite_lint.exceptions import LLMConnectionError, SciWriteLintError
4
4
 
5
- __version__ = "0.5.0"
5
+ __version__ = "0.6.0"
6
6
 
7
7
  from sciwrite_lint.oa import (
8
8
  DownloadResult,
@@ -155,6 +155,13 @@ def main(argv: list[str] | None = None) -> int:
155
155
  p_init = sub.add_parser(
156
156
  "init", help="Initialize project: config + references directory"
157
157
  )
158
+ p_init.add_argument(
159
+ "file",
160
+ nargs="?",
161
+ help="Manuscript to register (.tex/.pdf/.md). Omit to auto-detect "
162
+ "(a single .tex / .md+.bib); name one explicitly when there are "
163
+ "several or for a PDF.",
164
+ )
158
165
  p_init.add_argument(
159
166
  "--force", action="store_true", help="Overwrite existing config"
160
167
  )
@@ -175,6 +182,13 @@ def main(argv: list[str] | None = None) -> int:
175
182
  )
176
183
  p_config_email.add_argument("email", help="Your email address")
177
184
  p_config_email.add_argument("--config", help="Path to .sciwrite-lint.toml")
185
+ p_config_email.add_argument(
186
+ "--global",
187
+ dest="set_global",
188
+ action="store_true",
189
+ help="Store as the machine-wide default (~/.sciwrite-lint/) instead of "
190
+ "this project's .sciwrite-lint.toml",
191
+ )
178
192
  p_config_email.set_defaults(func=run_config)
179
193
 
180
194
  p_config_set_key = config_sub.add_parser(
@@ -41,6 +41,20 @@ _openlibrary_limiter = MonotonicRateLimiter(5, 1.0) # ~5 req/s (be polite)
41
41
  _loc_limiter = MonotonicRateLimiter(5, 1.0) # ~5 req/s (be polite)
42
42
 
43
43
 
44
+ def _openalex_params(polite_email: str, **params: Any) -> dict[str, Any]:
45
+ """OpenAlex query params with the polite-pool ``mailto`` merged in.
46
+
47
+ Single source of truth for every OpenAlex call site: requests carrying
48
+ a ``mailto`` are routed into OpenAlex's polite pool, which has a higher
49
+ and more stable shared rate limit than the anonymous common pool. This
50
+ keeps reference verification off the common pool, where sustained use
51
+ draws HTTP 429s. No-op when no contact email is configured.
52
+ """
53
+ if polite_email:
54
+ params["mailto"] = polite_email
55
+ return params
56
+
57
+
44
58
  class CitationAPI:
45
59
  """Verify citations against CrossRef, OpenAlex, Semantic Scholar, Open Library, and Library of Congress."""
46
60
 
@@ -129,6 +143,15 @@ class CitationAPI:
129
143
  # OpenAlex
130
144
  # ------------------------------------------------------------------
131
145
 
146
+ def _oa_params(self, **params: Any) -> dict[str, Any]:
147
+ """OpenAlex query params for this client's configured contact email.
148
+
149
+ Thin instance-scoped wrapper over :func:`_openalex_params` so the
150
+ four ``_openalex_lookup`` call sites don't each repeat
151
+ ``self._config.polite_email``.
152
+ """
153
+ return _openalex_params(self._config.polite_email, **params)
154
+
132
155
  async def _openalex_lookup(self, citation: Citation) -> dict[str, Any] | None:
133
156
  try:
134
157
  # Try DOI lookup
@@ -137,7 +160,7 @@ class CitationAPI:
137
160
  resp = await rate_limited_get(
138
161
  _openalex_limiter,
139
162
  f"https://api.openalex.org/works/doi:{clean_doi}",
140
- params={"select": _OA_FIELDS},
163
+ params=self._oa_params(select=_OA_FIELDS),
141
164
  label="OpenAlex DOI",
142
165
  client=self._client,
143
166
  service="openalex",
@@ -155,7 +178,7 @@ class CitationAPI:
155
178
  resp = await rate_limited_get(
156
179
  _openalex_limiter,
157
180
  f"https://api.openalex.org/works/doi:{arxiv_doi}",
158
- params={"select": _OA_FIELDS},
181
+ params=self._oa_params(select=_OA_FIELDS),
159
182
  label="OpenAlex arXiv DOI",
160
183
  client=self._client,
161
184
  service="openalex",
@@ -173,10 +196,10 @@ class CitationAPI:
173
196
  resp = await rate_limited_get(
174
197
  _openalex_limiter,
175
198
  "https://api.openalex.org/works",
176
- params={
177
- "filter": f"ids.pmid:https://pubmed.ncbi.nlm.nih.gov/{citation.pmid}",
178
- "select": _OA_FIELDS,
179
- },
199
+ params=self._oa_params(
200
+ filter=f"ids.pmid:https://pubmed.ncbi.nlm.nih.gov/{citation.pmid}",
201
+ select=_OA_FIELDS,
202
+ ),
180
203
  label="OpenAlex PMID",
181
204
  client=self._client,
182
205
  service="openalex",
@@ -192,11 +215,11 @@ class CitationAPI:
192
215
  resp = await rate_limited_get(
193
216
  _openalex_limiter,
194
217
  "https://api.openalex.org/works",
195
- params={
196
- "filter": f"title.search:{query}",
197
- "per_page": 10,
198
- "select": _OA_FIELDS,
199
- },
218
+ params=self._oa_params(
219
+ filter=f"title.search:{query}",
220
+ per_page=10,
221
+ select=_OA_FIELDS,
222
+ ),
200
223
  label="OpenAlex search",
201
224
  client=self._client,
202
225
  service="openalex",
@@ -481,13 +504,12 @@ async def batch_openalex(
481
504
  doi_filter = "|".join(f"https://doi.org/{doi}" for _, doi in all_dois)
482
505
  doi_by_key = {doi.lower(): key for key, doi in all_dois}
483
506
 
484
- params: dict[str, str | int] = {
485
- "filter": f"doi:{doi_filter}",
486
- "per_page": 200,
487
- "select": _OA_FIELDS,
488
- }
489
- if config.polite_email:
490
- params["mailto"] = config.polite_email
507
+ params = _openalex_params(
508
+ config.polite_email,
509
+ filter=f"doi:{doi_filter}",
510
+ per_page=200,
511
+ select=_OA_FIELDS,
512
+ )
491
513
 
492
514
  # Secondary index by arXiv ID: OpenAlex may return a different
493
515
  # canonical DOI than the query DOI (e.g. arXiv DOI → publisher DOI).
@@ -38,7 +38,7 @@ Two orthogonal axes describe a Finding:
38
38
  issue". Same level vocabulary, different semantics — the panel /
39
39
  JSON-field disambiguates.
40
40
 
41
- The three diagnostic rule_ids:
41
+ The diagnostic rule_ids:
42
42
 
43
43
  - ``llm-unavailable`` — ``llm_query`` exhausted its retry ladder
44
44
  (medium → low → off) and returned ``None``. One per skipped check
@@ -59,6 +59,17 @@ The three diagnostic rule_ids:
59
59
  during ``build_queries`` or ``process_results`` / a text check's
60
60
  body raised mid-run. One per failing check. Emitted by the LLM
61
61
  batch runner and the text-check runner.
62
+ - ``source-unsupported`` — one or more checks declare
63
+ ``supports_markdown=False`` (they parse LaTeX citation/cross-reference/
64
+ figure syntax) and the manuscript is markdown. The text-check runner
65
+ skips those checks and emits one summary finding listing them, so the
66
+ coverage gap is explicit rather than a silent zero-result.
67
+ - ``no-references`` — citation extraction found zero references in the
68
+ manuscript, so the citation / reference / claim stages had nothing to
69
+ verify. Emitted once per run by the pipeline setup. A genuinely
70
+ citation-free note is harmless, but a zero usually means the
71
+ bibliography was not detected — surfacing it stops a clean-looking
72
+ report from hiding that the reference checks never ran.
62
73
 
63
74
  Adding a new system rule_id:
64
75
 
@@ -92,6 +103,8 @@ SYSTEM_RULE_IDS: frozenset[str] = frozenset(
92
103
  "vision-incomplete",
93
104
  "internal-error",
94
105
  "parse-failed",
106
+ "source-unsupported",
107
+ "no-references",
95
108
  }
96
109
  )
97
110
 
@@ -235,3 +248,56 @@ def internal_error_finding(check_id: str, error: BaseException) -> Finding:
235
248
  message=f"{check_id}: check raised an unexpected exception; check did not run",
236
249
  context=f"{type(error).__name__}: {error!s}"[:200],
237
250
  )
251
+
252
+
253
+ def source_unsupported_finding(
254
+ source_type: str, skipped_check_ids: list[str]
255
+ ) -> Finding:
256
+ """Build a finding for checks skipped because the source type lacks the structure they need.
257
+
258
+ Emitted once per run by the text-check runner when the manuscript is
259
+ markdown and one or more registered checks declare
260
+ ``supports_markdown=False`` (they parse LaTeX citation/cross-reference/
261
+ figure syntax or need a populated bibliography). Listing the skipped
262
+ checks makes the coverage gap explicit — a markdown manuscript still
263
+ gets prose and structure checks, but these did not run.
264
+
265
+ Parameters
266
+ ----------
267
+ source_type:
268
+ The manuscript source type (e.g. ``"markdown"``).
269
+ skipped_check_ids:
270
+ Registered check IDs that were skipped for this source.
271
+ """
272
+ checks = ", ".join(sorted(skipped_check_ids))
273
+ return Finding(
274
+ level="warning",
275
+ rule_id="source-unsupported",
276
+ message=(
277
+ f"{len(skipped_check_ids)} check(s) need citation/reference structure "
278
+ f"not extracted from {source_type} manuscripts and did not run: {checks}. "
279
+ "Prose and structure checks ran normally."
280
+ ),
281
+ )
282
+
283
+
284
+ def no_references_finding() -> Finding:
285
+ """Build a finding for a run that extracted zero references.
286
+
287
+ Emitted once by the pipeline setup when citation extraction yields an
288
+ empty list. The reference, citation, and claim stages then have
289
+ nothing to verify — surfacing this keeps a clean-looking report from
290
+ hiding that those checks never ran (usually a bibliography that was
291
+ not detected, not a genuinely citation-free manuscript).
292
+ """
293
+ return Finding(
294
+ level="warning",
295
+ rule_id="no-references",
296
+ message=(
297
+ "No references found in the manuscript — citation, reference, and "
298
+ "claim checks had nothing to verify. If the manuscript cites sources, "
299
+ "check that its bibliography is detected (LaTeX \\bibliography{} / "
300
+ ".bib, or for markdown the YAML bibliography: field or a sibling "
301
+ "{name}.bib)."
302
+ ),
303
+ )
@@ -48,12 +48,13 @@ def _check_latex(tex_path: Path, config: LintConfig) -> list[Finding]:
48
48
  return findings
49
49
 
50
50
 
51
- def _check_pdf(ctx: object) -> list[Finding]:
51
+ def _check_context(ctx: object) -> list[Finding]:
52
52
  """Check that inline citations link to a reference in the bibliography.
53
53
 
54
- Uses ManuscriptContext's inline_citations (from GROBID TEI <ref> tags)
55
- and parsed_references. Citations with keys not in the reference list
56
- are dangling.
54
+ Serves any ManuscriptContext that populates ``inline_citations`` and
55
+ ``parsed_references`` PDF (GROBID TEI <ref> tags) and markdown
56
+ (pandoc ``[@key]`` + sibling ``.bib``) both do. Citations whose key is
57
+ not in the reference list are dangling.
57
58
  """
58
59
  from sciwrite_lint.manuscript_store import ManuscriptContext
59
60
 
@@ -86,8 +87,13 @@ def _check_pdf(ctx: object) -> list[Finding]:
86
87
  description="Citation has no matching bibliography entry.",
87
88
  )
88
89
  def check_dangling_cite(tex_path: Path, config: LintConfig) -> list[Finding]:
89
- """Check dangling citations. Supports both LaTeX and PDF input."""
90
- if config.is_pdf:
91
- return _check_pdf(config.manuscript_context)
90
+ """Check dangling citations. Supports LaTeX, PDF, and markdown input.
91
+
92
+ PDF and markdown both populate ``inline_citations`` +
93
+ ``parsed_references`` on the context, so the same key-membership
94
+ check (``_check_context``) serves both; LaTeX is parsed from source.
95
+ """
96
+ if config.is_pdf or config.is_markdown:
97
+ return _check_context(config.manuscript_context)
92
98
 
93
99
  return _check_latex(tex_path, config)
@@ -2,6 +2,8 @@ r"""Check: dangling-ref — cross-reference has no matching target.
2
2
 
3
3
  For LaTeX: \ref{X} has no matching \label{X}.
4
4
  For PDF: rendered "??" patterns in section text (broken references).
5
+ For markdown: pandoc-crossref ``@fig:`` / ``@sec:`` / ``@tbl:`` reference
6
+ with no matching labelled element.
5
7
  """
6
8
 
7
9
  from __future__ import annotations
@@ -102,6 +104,32 @@ def _check_pdf(ctx: object) -> list[Finding]:
102
104
  return findings
103
105
 
104
106
 
107
+ def _check_markdown(md_path: Path) -> list[Finding]:
108
+ """Flag pandoc-crossref references with no matching labelled element.
109
+
110
+ ``@fig:x`` / ``@sec:x`` / ``@tbl:x`` whose target is not defined by any
111
+ block id in the document is dangling.
112
+ """
113
+ from sciwrite_lint.markdown_cites import analyze_markdown
114
+
115
+ analysis = analyze_markdown(md_path.read_text(encoding="utf-8"))
116
+ findings: list[Finding] = []
117
+ seen: set[str] = set()
118
+ for xref in analysis.crossrefs:
119
+ if xref.target not in analysis.labels and xref.target not in seen:
120
+ seen.add(xref.target)
121
+ findings.append(
122
+ Finding(
123
+ level="error",
124
+ rule_id="dangling-ref",
125
+ message=f"Cross-reference '@{xref.target}' has no matching label",
126
+ file=md_path.name,
127
+ context=xref.target,
128
+ )
129
+ )
130
+ return findings
131
+
132
+
105
133
  @check(
106
134
  id="dangling-ref",
107
135
  category="manuscript",
@@ -109,7 +137,9 @@ def _check_pdf(ctx: object) -> list[Finding]:
109
137
  description="Cross-reference has no matching target.",
110
138
  )
111
139
  def check_dangling_ref(tex_path: Path, config: LintConfig) -> list[Finding]:
112
- """Check dangling references. Supports both LaTeX and PDF input."""
140
+ """Check dangling references. Supports LaTeX, PDF, and markdown input."""
141
+ if config.is_markdown:
142
+ return _check_markdown(tex_path)
113
143
  if config.is_pdf:
114
144
  return _check_pdf(config.manuscript_context)
115
145
 
@@ -570,6 +570,29 @@ If FIGURE DESCRIPTIONS says "Not available.", return {{"issues": []}}.\
570
570
  ]
571
571
 
572
572
 
573
+ def selected_check_defs(
574
+ config: "LintConfig", *, has_figures: bool
575
+ ) -> list[tuple[str, str, str, str, bool]]:
576
+ """Full-paper check defs that should actually run.
577
+
578
+ Single source of truth for two filters applied together:
579
+
580
+ 1. Figure availability — figure checks are pointless without figure
581
+ descriptions.
582
+ 2. The enabled-check gate (``config.is_check_enabled`` — honors both
583
+ ``--checks`` and ``[lint] disabled_rules``).
584
+
585
+ Query-building and result-processing both call this so they select the
586
+ same checks in the same order. If they diverge, batched LLM results get
587
+ zipped against the wrong ``check_id``.
588
+ """
589
+ return [
590
+ d
591
+ for d in _CHECK_DEFS
592
+ if (has_figures or not d[4]) and config.is_check_enabled(d[0])
593
+ ]
594
+
595
+
573
596
  # ---------------------------------------------------------------------------
574
597
  # Factory: build_queries / process_results for each check
575
598
  # ---------------------------------------------------------------------------