sciwrite-lint 0.2.0__py3-none-any.whl

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 (76) hide show
  1. sciwrite_lint/__init__.py +3 -0
  2. sciwrite_lint/__main__.py +527 -0
  3. sciwrite_lint/_network.py +195 -0
  4. sciwrite_lint/api.py +1484 -0
  5. sciwrite_lint/checks/__init__.py +1 -0
  6. sciwrite_lint/checks/_section_utils.py +111 -0
  7. sciwrite_lint/checks/cite_purpose.py +122 -0
  8. sciwrite_lint/checks/claim_support.py +96 -0
  9. sciwrite_lint/checks/cross_section_consistency.py +185 -0
  10. sciwrite_lint/checks/dangling_cite.py +93 -0
  11. sciwrite_lint/checks/dangling_ref.py +116 -0
  12. sciwrite_lint/checks/full_paper_consistency.py +604 -0
  13. sciwrite_lint/checks/ref_internal_checks.py +919 -0
  14. sciwrite_lint/checks/reference_accuracy.py +277 -0
  15. sciwrite_lint/checks/reference_exists.py +119 -0
  16. sciwrite_lint/checks/reference_unreliable.py +244 -0
  17. sciwrite_lint/checks/registry.py +136 -0
  18. sciwrite_lint/checks/retracted_cite.py +96 -0
  19. sciwrite_lint/checks/structure_promises.py +115 -0
  20. sciwrite_lint/checks/unreferenced_figure.py +70 -0
  21. sciwrite_lint/claims.py +330 -0
  22. sciwrite_lint/claude_backend.py +94 -0
  23. sciwrite_lint/claude_cli.py +405 -0
  24. sciwrite_lint/cli/__init__.py +1 -0
  25. sciwrite_lint/cli/check.py +480 -0
  26. sciwrite_lint/cli/config.py +229 -0
  27. sciwrite_lint/cli/fetch.py +250 -0
  28. sciwrite_lint/cli/misc.py +1202 -0
  29. sciwrite_lint/cli/rank.py +470 -0
  30. sciwrite_lint/cli/verify.py +437 -0
  31. sciwrite_lint/config.py +646 -0
  32. sciwrite_lint/cross_paper.py +174 -0
  33. sciwrite_lint/eval_claims.py +1196 -0
  34. sciwrite_lint/fulltext.py +851 -0
  35. sciwrite_lint/llm_utils.py +386 -0
  36. sciwrite_lint/local_pdfs.py +122 -0
  37. sciwrite_lint/manuscript_store.py +674 -0
  38. sciwrite_lint/models.py +139 -0
  39. sciwrite_lint/pdf/__init__.py +1 -0
  40. sciwrite_lint/pdf/grobid.py +785 -0
  41. sciwrite_lint/pdf/pdf_download.py +258 -0
  42. sciwrite_lint/pipeline.py +2694 -0
  43. sciwrite_lint/prompt_safety.py +30 -0
  44. sciwrite_lint/rate_limiter.py +227 -0
  45. sciwrite_lint/references/__init__.py +1 -0
  46. sciwrite_lint/references/citations.py +715 -0
  47. sciwrite_lint/references/crossref.py +282 -0
  48. sciwrite_lint/references/embedding_store.py +380 -0
  49. sciwrite_lint/references/matching.py +273 -0
  50. sciwrite_lint/references/metadata.py +273 -0
  51. sciwrite_lint/references/reference_store.py +823 -0
  52. sciwrite_lint/references/retraction_watch.py +178 -0
  53. sciwrite_lint/references/workspace_db.py +1390 -0
  54. sciwrite_lint/report.py +163 -0
  55. sciwrite_lint/schemas.py +260 -0
  56. sciwrite_lint/scoring/__init__.py +1 -0
  57. sciwrite_lint/scoring/chain.py +716 -0
  58. sciwrite_lint/scoring/contribution.py +322 -0
  59. sciwrite_lint/scoring/scilint_score.py +611 -0
  60. sciwrite_lint/tex_parser.py +248 -0
  61. sciwrite_lint/usage.py +594 -0
  62. sciwrite_lint/vision/__init__.py +1 -0
  63. sciwrite_lint/vision/cache.py +140 -0
  64. sciwrite_lint/vision/describe.py +311 -0
  65. sciwrite_lint/vision/image_extraction.py +491 -0
  66. sciwrite_lint/vision/pipeline.py +207 -0
  67. sciwrite_lint/vllm/__init__.py +1 -0
  68. sciwrite_lint/vllm/metrics.py +157 -0
  69. sciwrite_lint/vllm/vllm_server.py +445 -0
  70. sciwrite_lint/web.py +369 -0
  71. sciwrite_lint-0.2.0.dist-info/METADATA +268 -0
  72. sciwrite_lint-0.2.0.dist-info/RECORD +76 -0
  73. sciwrite_lint-0.2.0.dist-info/WHEEL +5 -0
  74. sciwrite_lint-0.2.0.dist-info/entry_points.txt +2 -0
  75. sciwrite_lint-0.2.0.dist-info/licenses/LICENSE +21 -0
  76. sciwrite_lint-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,250 @@
1
+ """CLI handlers for 'fetch' command and fetch helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ from pathlib import Path
8
+
9
+ from loguru import logger
10
+
11
+ from sciwrite_lint.config import LintConfig
12
+
13
+
14
+ def eager_parse(key: str, pdf_path: Path, refs_dir: Path) -> None:
15
+ """Parse a newly-downloaded PDF and compute embeddings (best-effort)."""
16
+ try:
17
+ from sciwrite_lint.references.reference_store import parse_and_embed
18
+
19
+ text, chunks = asyncio.run(parse_and_embed(key, pdf_path, refs_dir))
20
+ if text:
21
+ suffix = f", {chunks} chunks embedded" if chunks else ""
22
+ logger.info(f"Parsed {key}: {len(text)} chars{suffix}")
23
+ else:
24
+ logger.warning(f"Parse failed for {key} (GROBID running?)")
25
+ except Exception as e:
26
+ logger.warning(f"Parse skipped for {key}: {e}")
27
+
28
+
29
+ def fetch_for_citations(
30
+ citations: list,
31
+ config: LintConfig,
32
+ references_dir: Path,
33
+ ) -> None:
34
+ """Attempt full-text acquisition for citations missing local files."""
35
+ from sciwrite_lint.fulltext import acquire_fulltext
36
+ from sciwrite_lint.references.metadata import (
37
+ compute_tier,
38
+ load_metadata,
39
+ save_metadata,
40
+ )
41
+
42
+ refs_dir = references_dir
43
+
44
+ # Check local_pdfs_dir for user-provided PDFs before downloading
45
+ from sciwrite_lint.local_pdfs import copy_local_pdf, match_local_pdfs
46
+
47
+ local_pdfs_dir = config.local_pdfs_dir
48
+ if local_pdfs_dir.is_dir() and any(local_pdfs_dir.glob("*.pdf")):
49
+ titles = {}
50
+ for c in citations:
51
+ if c.local_status != "none":
52
+ continue
53
+ meta = load_metadata(c.key, refs_dir)
54
+ if not meta or meta.access.get("tier") == "T1":
55
+ continue
56
+ titles[c.key] = c.title or meta.canonical.get("title", "")
57
+
58
+ if titles:
59
+ matched, _ = match_local_pdfs(local_pdfs_dir, titles)
60
+ for key, pdf_path in matched.items():
61
+ meta = load_metadata(key, refs_dir)
62
+ if not meta:
63
+ continue
64
+ local_path = copy_local_pdf(pdf_path, key, refs_dir)
65
+ meta.access["local_file"] = local_path
66
+ meta.access["tier"] = compute_tier(meta)
67
+ save_metadata(meta, refs_dir)
68
+ logger.info(f"{key}: using local PDF [{meta.access['tier']}]")
69
+
70
+ async def _do_fetch():
71
+ for c in citations:
72
+ if c.local_status != "none":
73
+ continue
74
+
75
+ meta = load_metadata(c.key, refs_dir)
76
+ if not meta:
77
+ continue
78
+
79
+ tier = meta.access.get("tier", "")
80
+ if tier == "T1":
81
+ continue
82
+
83
+ doi = meta.canonical.get("doi") or c.doi
84
+ arxiv_id = meta.canonical.get("arxiv_id")
85
+ oa_url = meta.access.get("oa_url")
86
+ s2_pdf_url = meta.canonical.get("s2_pdf_url")
87
+ pmcid = meta.canonical.get("pmcid")
88
+ expected_title = meta.canonical.get("title", "") or c.title
89
+ expected_authors = meta.canonical.get("authors") or c.authors
90
+
91
+ logger.info(f"Fetching {c.key}...")
92
+ result = await acquire_fulltext(
93
+ c.key,
94
+ refs_dir,
95
+ doi=doi,
96
+ arxiv_id=arxiv_id,
97
+ oa_url=oa_url,
98
+ s2_pdf_url=s2_pdf_url,
99
+ pmcid=pmcid,
100
+ expected_title=expected_title,
101
+ expected_authors=expected_authors,
102
+ )
103
+
104
+ if result.found and result.local_path:
105
+ meta.access["local_file"] = result.local_path
106
+ meta.access["tier"] = compute_tier(meta)
107
+ save_metadata(meta, refs_dir)
108
+ logger.info(
109
+ f"{c.key}: downloaded {result.local_path} [now {meta.access['tier']}]"
110
+ )
111
+
112
+ # Eager parse + embed: cache GROBID output and compute embeddings
113
+ if result.local_path.endswith(".pdf"):
114
+ from sciwrite_lint.references.reference_store import parse_and_embed
115
+
116
+ try:
117
+ text, chunks = await parse_and_embed(
118
+ c.key, refs_dir / result.local_path, refs_dir
119
+ )
120
+ if text:
121
+ suffix = f", {chunks} chunks embedded" if chunks else ""
122
+ logger.info(f"{c.key}: parsed {len(text)} chars{suffix}")
123
+ except Exception as e:
124
+ logger.warning(f"{c.key}: parse skipped: {e}")
125
+ elif result.url:
126
+ logger.warning(f"{c.key}: manual download needed: {result.url}")
127
+ else:
128
+ logger.warning(f"{c.key}: no source found")
129
+
130
+ if result.abstract and not meta.canonical.get("abstract"):
131
+ meta.canonical["abstract"] = result.abstract
132
+ meta.access["tier"] = compute_tier(meta)
133
+ save_metadata(meta, refs_dir)
134
+
135
+ asyncio.run(_do_fetch())
136
+
137
+
138
+ def run_fetch(args: argparse.Namespace) -> int:
139
+ """Download full text for citations missing local files."""
140
+ from sciwrite_lint.references.metadata import load_all_metadata
141
+ from sciwrite_lint.pipeline import extract_citations_for_paper
142
+
143
+ from sciwrite_lint.__main__ import _load_config, _resolve_paper
144
+ from sciwrite_lint.cli.config import check_api_config
145
+
146
+ config = _load_config(args)
147
+
148
+ api_errors = check_api_config(config, needs_email=True)
149
+ if api_errors:
150
+ for e in api_errors:
151
+ logger.error(f" ✗ {e}")
152
+ return 2
153
+ pc = _resolve_paper(config, args.paper)
154
+ if not pc:
155
+ return 2
156
+
157
+ ws = config.paper_workspace(pc.name)
158
+ ws.ensure_dirs()
159
+ refs_dir = ws.root
160
+
161
+ if args.key:
162
+ all_meta = load_all_metadata(refs_dir)
163
+ meta = all_meta.get(args.key)
164
+ if not meta:
165
+ print(f"No metadata for '{args.key}'. Run 'sciwrite-lint verify' first.")
166
+ return 1
167
+ fetch_single(
168
+ args.key,
169
+ meta,
170
+ config,
171
+ references_dir=refs_dir,
172
+ )
173
+ return 0
174
+
175
+ if not pc.file_path.exists():
176
+ logger.error(f"Error: {pc.file_path} not found")
177
+ return 1
178
+
179
+ citations = asyncio.run(
180
+ extract_citations_for_paper(pc.file_path, config, bib_path=pc.bib)
181
+ )
182
+
183
+ logger.info(f"Fetching full text for {pc.name}...")
184
+ fetch_for_citations(
185
+ citations,
186
+ config,
187
+ references_dir=refs_dir,
188
+ )
189
+ return 0
190
+
191
+
192
+ def fetch_single(
193
+ key: str,
194
+ meta: object,
195
+ config: LintConfig,
196
+ references_dir: Path,
197
+ ) -> None:
198
+ """Fetch full text for a single citation."""
199
+ from sciwrite_lint.fulltext import acquire_fulltext
200
+ from sciwrite_lint.references.metadata import compute_tier, save_metadata
201
+
202
+ refs_dir = references_dir
203
+
204
+ doi = meta.canonical.get("doi") # type: ignore[attr-defined]
205
+ arxiv_id = meta.canonical.get("arxiv_id") # type: ignore[attr-defined]
206
+ oa_url = meta.access.get("oa_url") # type: ignore[attr-defined]
207
+ s2_pdf_url = meta.canonical.get("s2_pdf_url") # type: ignore[attr-defined]
208
+ pmcid = meta.canonical.get("pmcid") # type: ignore[attr-defined]
209
+ expected_title = meta.canonical.get("title", "") # type: ignore[attr-defined]
210
+
211
+ async def _do():
212
+ logger.info(f"Fetching full text for {key}...")
213
+ result = await acquire_fulltext(
214
+ key,
215
+ refs_dir,
216
+ doi=doi,
217
+ arxiv_id=arxiv_id,
218
+ oa_url=oa_url,
219
+ s2_pdf_url=s2_pdf_url,
220
+ pmcid=pmcid,
221
+ expected_title=expected_title,
222
+ )
223
+
224
+ if result.found and result.local_path:
225
+ meta.access["local_file"] = result.local_path # type: ignore[attr-defined]
226
+ meta.access["tier"] = compute_tier(meta) # type: ignore[attr-defined]
227
+ save_metadata(meta, refs_dir) # type: ignore[arg-type]
228
+ logger.info(
229
+ f"{key}: downloaded {result.local_path} [now {meta.access['tier']}]" # type: ignore[attr-defined]
230
+ )
231
+
232
+ # Eager parse + embed
233
+ if result.local_path.endswith(".pdf"):
234
+ from sciwrite_lint.references.reference_store import parse_and_embed
235
+
236
+ try:
237
+ text, chunks = await parse_and_embed(
238
+ key, refs_dir / result.local_path, refs_dir
239
+ )
240
+ if text:
241
+ suffix = f", {chunks} chunks embedded" if chunks else ""
242
+ logger.info(f"{key}: parsed {len(text)} chars{suffix}")
243
+ except Exception as e:
244
+ logger.warning(f"{key}: parse skipped: {e}")
245
+ elif result.url:
246
+ logger.warning(f"{key}: manual download needed: {result.url}")
247
+ else:
248
+ logger.warning(f"{key}: no source found")
249
+
250
+ asyncio.run(_do())