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.
- sciwrite_lint/__init__.py +3 -0
- sciwrite_lint/__main__.py +527 -0
- sciwrite_lint/_network.py +195 -0
- sciwrite_lint/api.py +1484 -0
- sciwrite_lint/checks/__init__.py +1 -0
- sciwrite_lint/checks/_section_utils.py +111 -0
- sciwrite_lint/checks/cite_purpose.py +122 -0
- sciwrite_lint/checks/claim_support.py +96 -0
- sciwrite_lint/checks/cross_section_consistency.py +185 -0
- sciwrite_lint/checks/dangling_cite.py +93 -0
- sciwrite_lint/checks/dangling_ref.py +116 -0
- sciwrite_lint/checks/full_paper_consistency.py +604 -0
- sciwrite_lint/checks/ref_internal_checks.py +919 -0
- sciwrite_lint/checks/reference_accuracy.py +277 -0
- sciwrite_lint/checks/reference_exists.py +119 -0
- sciwrite_lint/checks/reference_unreliable.py +244 -0
- sciwrite_lint/checks/registry.py +136 -0
- sciwrite_lint/checks/retracted_cite.py +96 -0
- sciwrite_lint/checks/structure_promises.py +115 -0
- sciwrite_lint/checks/unreferenced_figure.py +70 -0
- sciwrite_lint/claims.py +330 -0
- sciwrite_lint/claude_backend.py +94 -0
- sciwrite_lint/claude_cli.py +405 -0
- sciwrite_lint/cli/__init__.py +1 -0
- sciwrite_lint/cli/check.py +480 -0
- sciwrite_lint/cli/config.py +229 -0
- sciwrite_lint/cli/fetch.py +250 -0
- sciwrite_lint/cli/misc.py +1202 -0
- sciwrite_lint/cli/rank.py +470 -0
- sciwrite_lint/cli/verify.py +437 -0
- sciwrite_lint/config.py +646 -0
- sciwrite_lint/cross_paper.py +174 -0
- sciwrite_lint/eval_claims.py +1196 -0
- sciwrite_lint/fulltext.py +851 -0
- sciwrite_lint/llm_utils.py +386 -0
- sciwrite_lint/local_pdfs.py +122 -0
- sciwrite_lint/manuscript_store.py +674 -0
- sciwrite_lint/models.py +139 -0
- sciwrite_lint/pdf/__init__.py +1 -0
- sciwrite_lint/pdf/grobid.py +785 -0
- sciwrite_lint/pdf/pdf_download.py +258 -0
- sciwrite_lint/pipeline.py +2694 -0
- sciwrite_lint/prompt_safety.py +30 -0
- sciwrite_lint/rate_limiter.py +227 -0
- sciwrite_lint/references/__init__.py +1 -0
- sciwrite_lint/references/citations.py +715 -0
- sciwrite_lint/references/crossref.py +282 -0
- sciwrite_lint/references/embedding_store.py +380 -0
- sciwrite_lint/references/matching.py +273 -0
- sciwrite_lint/references/metadata.py +273 -0
- sciwrite_lint/references/reference_store.py +823 -0
- sciwrite_lint/references/retraction_watch.py +178 -0
- sciwrite_lint/references/workspace_db.py +1390 -0
- sciwrite_lint/report.py +163 -0
- sciwrite_lint/schemas.py +260 -0
- sciwrite_lint/scoring/__init__.py +1 -0
- sciwrite_lint/scoring/chain.py +716 -0
- sciwrite_lint/scoring/contribution.py +322 -0
- sciwrite_lint/scoring/scilint_score.py +611 -0
- sciwrite_lint/tex_parser.py +248 -0
- sciwrite_lint/usage.py +594 -0
- sciwrite_lint/vision/__init__.py +1 -0
- sciwrite_lint/vision/cache.py +140 -0
- sciwrite_lint/vision/describe.py +311 -0
- sciwrite_lint/vision/image_extraction.py +491 -0
- sciwrite_lint/vision/pipeline.py +207 -0
- sciwrite_lint/vllm/__init__.py +1 -0
- sciwrite_lint/vllm/metrics.py +157 -0
- sciwrite_lint/vllm/vllm_server.py +445 -0
- sciwrite_lint/web.py +369 -0
- sciwrite_lint-0.2.0.dist-info/METADATA +268 -0
- sciwrite_lint-0.2.0.dist-info/RECORD +76 -0
- sciwrite_lint-0.2.0.dist-info/WHEEL +5 -0
- sciwrite_lint-0.2.0.dist-info/entry_points.txt +2 -0
- sciwrite_lint-0.2.0.dist-info/licenses/LICENSE +21 -0
- sciwrite_lint-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""CLI handlers for 'verify', 'verify-claims', 'status', and 'ref-health' commands."""
|
|
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.models import CheckResult, Finding
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_TIER_MARKERS = {
|
|
15
|
+
"T1": ("\u2713", "has full text"),
|
|
16
|
+
"T2": ("~", "confirmed"),
|
|
17
|
+
"T3": ("\u2717", "unverified"),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_verify(args: argparse.Namespace) -> int:
|
|
22
|
+
"""Verify citations against APIs (CrossRef, OpenAlex, Semantic Scholar, Open Library, Library of Congress)."""
|
|
23
|
+
from sciwrite_lint.api import CitationAPI, verify_citations
|
|
24
|
+
from sciwrite_lint.references.citations import get_last_citation_source
|
|
25
|
+
from sciwrite_lint.pipeline import extract_citations_for_paper
|
|
26
|
+
|
|
27
|
+
from sciwrite_lint.__main__ import _load_config, _resolve_paper
|
|
28
|
+
from sciwrite_lint.cli.config import check_api_config
|
|
29
|
+
from sciwrite_lint.cli.fetch import fetch_for_citations
|
|
30
|
+
|
|
31
|
+
config = _load_config(args)
|
|
32
|
+
check_api_config(config, needs_email=False)
|
|
33
|
+
|
|
34
|
+
pc = _resolve_paper(config, args.paper)
|
|
35
|
+
if not pc:
|
|
36
|
+
return 2
|
|
37
|
+
|
|
38
|
+
if not pc.file_path.exists():
|
|
39
|
+
logger.error(f"Error: {pc.file_path} not found")
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
ws = config.paper_workspace(pc.name)
|
|
43
|
+
ws.ensure_dirs()
|
|
44
|
+
refs_dir = ws.root
|
|
45
|
+
|
|
46
|
+
logger.info(f"Verifying {pc.name}...")
|
|
47
|
+
citations = asyncio.run(
|
|
48
|
+
extract_citations_for_paper(pc.file_path, config, bib_path=pc.bib)
|
|
49
|
+
)
|
|
50
|
+
src = get_last_citation_source()
|
|
51
|
+
if src:
|
|
52
|
+
logger.info(f"Citation source: {src}")
|
|
53
|
+
|
|
54
|
+
if args.key:
|
|
55
|
+
citations = [c for c in citations if c.key == args.key]
|
|
56
|
+
if not citations:
|
|
57
|
+
logger.error(f"Key '{args.key}' not found in {pc.name}")
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
async def _verify():
|
|
61
|
+
async with CitationAPI(config=config) as api:
|
|
62
|
+
await verify_citations(
|
|
63
|
+
citations,
|
|
64
|
+
api,
|
|
65
|
+
config=config,
|
|
66
|
+
references_dir=refs_dir,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
asyncio.run(_verify())
|
|
70
|
+
|
|
71
|
+
from sciwrite_lint.pipeline import _classify_verify_issue
|
|
72
|
+
|
|
73
|
+
result = CheckResult(checker="verify", paper=pc.name)
|
|
74
|
+
for c in citations:
|
|
75
|
+
for issue in c.issues:
|
|
76
|
+
level, rule_id = _classify_verify_issue(issue)
|
|
77
|
+
result.findings.append(
|
|
78
|
+
Finding(
|
|
79
|
+
level=level,
|
|
80
|
+
rule_id=rule_id,
|
|
81
|
+
message=f"{c.key}: {issue}",
|
|
82
|
+
file=pc.file_path.name,
|
|
83
|
+
context=f"Source: {c.api_source}" if c.api_source else "",
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Reference-unreliable: fast path (metadata only)
|
|
88
|
+
from sciwrite_lint.checks.reference_unreliable import (
|
|
89
|
+
metadata_to_unreliable_findings,
|
|
90
|
+
)
|
|
91
|
+
from sciwrite_lint.references.metadata import load_all_metadata
|
|
92
|
+
|
|
93
|
+
all_meta = load_all_metadata(refs_dir)
|
|
94
|
+
if all_meta:
|
|
95
|
+
result.findings.extend(metadata_to_unreliable_findings(all_meta, pc.file_path))
|
|
96
|
+
|
|
97
|
+
from sciwrite_lint.report import format_findings
|
|
98
|
+
|
|
99
|
+
label = f"{pc.name} ({pc.file_path.name})"
|
|
100
|
+
format_findings(result.findings, label, fmt=args.format, color=config.color)
|
|
101
|
+
|
|
102
|
+
if args.fetch:
|
|
103
|
+
from sciwrite_lint.cli.config import check_api_config
|
|
104
|
+
|
|
105
|
+
api_errors = check_api_config(config, needs_email=True)
|
|
106
|
+
if api_errors:
|
|
107
|
+
for e in api_errors:
|
|
108
|
+
logger.error(f" ✗ {e}")
|
|
109
|
+
return 2
|
|
110
|
+
fetch_for_citations(
|
|
111
|
+
citations,
|
|
112
|
+
config,
|
|
113
|
+
references_dir=refs_dir,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return 1 if result.errors else 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_status(args: argparse.Namespace) -> int:
|
|
120
|
+
"""Show citation verification status."""
|
|
121
|
+
from sciwrite_lint.references.citations import get_last_citation_source
|
|
122
|
+
from sciwrite_lint.references.metadata import load_all_metadata
|
|
123
|
+
from sciwrite_lint.pipeline import extract_citations_for_paper
|
|
124
|
+
|
|
125
|
+
from sciwrite_lint.__main__ import _load_config, _resolve_paper
|
|
126
|
+
|
|
127
|
+
config = _load_config(args)
|
|
128
|
+
pc = _resolve_paper(config, args.paper)
|
|
129
|
+
if not pc:
|
|
130
|
+
return 2
|
|
131
|
+
|
|
132
|
+
ws = config.paper_workspace(pc.name)
|
|
133
|
+
all_meta = load_all_metadata(ws.root)
|
|
134
|
+
|
|
135
|
+
if not all_meta:
|
|
136
|
+
print("No metadata found. Run 'sciwrite-lint verify' first.")
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
if not pc.file_path.exists():
|
|
140
|
+
logger.error(f"{pc.file_path} not found")
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
citations = asyncio.run(
|
|
144
|
+
extract_citations_for_paper(pc.file_path, config, bib_path=pc.bib)
|
|
145
|
+
)
|
|
146
|
+
src = get_last_citation_source()
|
|
147
|
+
|
|
148
|
+
print(f"\n{'=' * 70}")
|
|
149
|
+
print(f" {pc.name} \u2014 {len(citations)} citations")
|
|
150
|
+
if src:
|
|
151
|
+
print(f" Source: {src}")
|
|
152
|
+
print(f"{'=' * 70}")
|
|
153
|
+
print(
|
|
154
|
+
f" {'Key':<25} {'Tier':<5} {'Type':<5} {'API Match':<15} {'Local File':<30} {'Verified'}"
|
|
155
|
+
)
|
|
156
|
+
print(f" {'-' * 25} {'-' * 5} {'-' * 5} {'-' * 15} {'-' * 30} {'-' * 10}")
|
|
157
|
+
|
|
158
|
+
tier_counts = {"T1": 0, "T2": 0, "T3": 0, "NC": 0}
|
|
159
|
+
academic_count = 0
|
|
160
|
+
web_count = 0
|
|
161
|
+
|
|
162
|
+
for c in sorted(citations, key=lambda x: x.key):
|
|
163
|
+
meta = all_meta.get(c.key)
|
|
164
|
+
if meta:
|
|
165
|
+
tier = meta.access.get("tier", "NC")
|
|
166
|
+
api_match: str = meta.api_match or "?"
|
|
167
|
+
local = meta.access.get("local_file") or "\u2014"
|
|
168
|
+
verified = meta.verified_date or "\u2014"
|
|
169
|
+
is_web = api_match in ("web_verified", "web_dead")
|
|
170
|
+
else:
|
|
171
|
+
tier = "NC"
|
|
172
|
+
api_match = "not checked"
|
|
173
|
+
local = c.local_path.split("/")[-1] if c.local_path else "\u2014"
|
|
174
|
+
verified = "\u2014"
|
|
175
|
+
is_web = False
|
|
176
|
+
|
|
177
|
+
if is_web:
|
|
178
|
+
web_count += 1
|
|
179
|
+
else:
|
|
180
|
+
academic_count += 1
|
|
181
|
+
|
|
182
|
+
if args.tier and tier != args.tier:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
tier_counts[tier if tier in tier_counts else "NC"] += 1
|
|
186
|
+
|
|
187
|
+
marker, _ = _TIER_MARKERS.get(tier, ("\u00b7", ""))
|
|
188
|
+
rtype = "web" if is_web else "acad"
|
|
189
|
+
print(
|
|
190
|
+
f" {c.key:<25} {marker}{tier:<4} {rtype:<5} {api_match:<15} {local:<30} {verified}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
print()
|
|
194
|
+
total = sum(tier_counts.values())
|
|
195
|
+
t1, t2, t3, nc = (tier_counts.get(k, 0) for k in ("T1", "T2", "T3", "NC"))
|
|
196
|
+
print(
|
|
197
|
+
f" Summary: {t1} T1 (has full text), "
|
|
198
|
+
f"{t2} T2 (confirmed), "
|
|
199
|
+
f"{t3} T3 (unverified), "
|
|
200
|
+
f"{nc} not checked | {total} total"
|
|
201
|
+
)
|
|
202
|
+
print(f" Types: {academic_count} academic, {web_count} web resources")
|
|
203
|
+
print()
|
|
204
|
+
print(" T1 = verified + full text (can check claims)")
|
|
205
|
+
print(" T2 = verified, no full text (confirmed real)")
|
|
206
|
+
print(" T3 = not found in APIs or dead URL")
|
|
207
|
+
|
|
208
|
+
# Show acquisition reasons for T2 refs
|
|
209
|
+
t2_reasons: list[tuple[str, str]] = []
|
|
210
|
+
for c in sorted(citations, key=lambda x: x.key):
|
|
211
|
+
meta = all_meta.get(c.key)
|
|
212
|
+
if not meta:
|
|
213
|
+
continue
|
|
214
|
+
tier = meta.access.get("tier", "NC")
|
|
215
|
+
if tier == "T2":
|
|
216
|
+
reason = meta.access.get("acquisition_reason", "")
|
|
217
|
+
if reason:
|
|
218
|
+
t2_reasons.append((c.key, reason))
|
|
219
|
+
if t2_reasons:
|
|
220
|
+
print()
|
|
221
|
+
print(" T2 acquisition details:")
|
|
222
|
+
for key, reason in t2_reasons:
|
|
223
|
+
print(f" {key}: {reason}")
|
|
224
|
+
|
|
225
|
+
return 0
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def run_ref_health(args: argparse.Namespace) -> int:
|
|
229
|
+
"""Fast deterministic reference health check (no API calls).
|
|
230
|
+
|
|
231
|
+
Parses the .tex and .bib, reports:
|
|
232
|
+
- cite/bib mismatches (cited but missing from bib, in bib but never cited)
|
|
233
|
+
- ID coverage (which refs have DOI, ISBN, arXiv, PMID, etc.)
|
|
234
|
+
- entry type breakdown
|
|
235
|
+
"""
|
|
236
|
+
from sciwrite_lint.references.citations import (
|
|
237
|
+
extract_bibitems,
|
|
238
|
+
find_orphans,
|
|
239
|
+
is_web_resource,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Resolve input: positional file or --paper
|
|
243
|
+
tex_file = getattr(args, "file", None)
|
|
244
|
+
paper_name = getattr(args, "paper", None)
|
|
245
|
+
if not tex_file and not paper_name:
|
|
246
|
+
logger.error("Provide a .tex file or --paper NAME")
|
|
247
|
+
return 2
|
|
248
|
+
|
|
249
|
+
if tex_file:
|
|
250
|
+
tex_path = Path(tex_file).resolve()
|
|
251
|
+
bib_path = None
|
|
252
|
+
name = tex_path.stem
|
|
253
|
+
else:
|
|
254
|
+
from sciwrite_lint.__main__ import _load_config, _resolve_paper
|
|
255
|
+
|
|
256
|
+
config = _load_config(args)
|
|
257
|
+
assert paper_name is not None # guarded above
|
|
258
|
+
pc = _resolve_paper(config, paper_name)
|
|
259
|
+
if not pc:
|
|
260
|
+
return 2
|
|
261
|
+
tex_path = pc.file_path
|
|
262
|
+
bib_path = pc.bib
|
|
263
|
+
name = pc.name
|
|
264
|
+
|
|
265
|
+
if not tex_path.exists():
|
|
266
|
+
logger.error(f"{tex_path} not found")
|
|
267
|
+
return 1
|
|
268
|
+
|
|
269
|
+
if tex_path.suffix != ".tex":
|
|
270
|
+
logger.error(f"ref-health requires a .tex file, got {tex_path.suffix}")
|
|
271
|
+
return 1
|
|
272
|
+
|
|
273
|
+
citations = extract_bibitems(tex_path, bib_path=bib_path)
|
|
274
|
+
|
|
275
|
+
if not citations:
|
|
276
|
+
print(f"\n {name}: no references found in {tex_path.name}")
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
cited_no_bib, bib_no_cite = find_orphans(citations, tex_path)
|
|
280
|
+
|
|
281
|
+
# --- ID coverage ---
|
|
282
|
+
id_fields = ["doi", "arxiv_id", "pmid", "pmc_id", "isbn", "lccn", "url"]
|
|
283
|
+
has_any_id: list[str] = []
|
|
284
|
+
no_id: list[str] = []
|
|
285
|
+
id_counts: dict[str, int] = {f: 0 for f in id_fields}
|
|
286
|
+
|
|
287
|
+
for c in citations:
|
|
288
|
+
ids_present = [f for f in id_fields if getattr(c, f, "")]
|
|
289
|
+
if ids_present:
|
|
290
|
+
has_any_id.append(c.key)
|
|
291
|
+
for f in ids_present:
|
|
292
|
+
id_counts[f] += 1
|
|
293
|
+
else:
|
|
294
|
+
no_id.append(c.key)
|
|
295
|
+
|
|
296
|
+
# --- Entry type breakdown ---
|
|
297
|
+
type_counts: dict[str, int] = {}
|
|
298
|
+
web_count = 0
|
|
299
|
+
for c in citations:
|
|
300
|
+
et = c.entry_type or "unknown"
|
|
301
|
+
type_counts[et] = type_counts.get(et, 0) + 1
|
|
302
|
+
if is_web_resource(c):
|
|
303
|
+
web_count += 1
|
|
304
|
+
|
|
305
|
+
# --- Local PDFs ---
|
|
306
|
+
local_matched: dict[str, Path] = {}
|
|
307
|
+
local_unmatched: list[Path] = []
|
|
308
|
+
try:
|
|
309
|
+
if tex_file:
|
|
310
|
+
from sciwrite_lint.config import load_config as _load_cfg
|
|
311
|
+
|
|
312
|
+
_cfg = _load_cfg()
|
|
313
|
+
else:
|
|
314
|
+
_cfg = config # type: ignore[possibly-undefined] # noqa: F841
|
|
315
|
+
local_pdfs_dir = _cfg.local_pdfs_dir
|
|
316
|
+
if local_pdfs_dir.is_dir() and any(local_pdfs_dir.glob("*.pdf")):
|
|
317
|
+
from sciwrite_lint.local_pdfs import match_local_pdfs
|
|
318
|
+
|
|
319
|
+
titles = {c.key: c.title for c in citations if c.title}
|
|
320
|
+
local_matched, local_unmatched = match_local_pdfs(local_pdfs_dir, titles)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.debug(
|
|
323
|
+
f"ref-health: local PDF matching skipped ({type(e).__name__}: {e})"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# --- Output ---
|
|
327
|
+
total = len(citations)
|
|
328
|
+
print(f"\n{'=' * 60}")
|
|
329
|
+
print(f" {name} — reference health ({total} references)")
|
|
330
|
+
print(f"{'=' * 60}")
|
|
331
|
+
|
|
332
|
+
# Cite/bib mismatches
|
|
333
|
+
if cited_no_bib or bib_no_cite:
|
|
334
|
+
print("\n MISMATCHES")
|
|
335
|
+
if cited_no_bib:
|
|
336
|
+
print(f" Cited but missing from bib ({len(cited_no_bib)}):")
|
|
337
|
+
for k in sorted(cited_no_bib):
|
|
338
|
+
print(f" - {k}")
|
|
339
|
+
if bib_no_cite:
|
|
340
|
+
print(f" In bib but never cited ({len(bib_no_cite)}):")
|
|
341
|
+
for k in sorted(bib_no_cite):
|
|
342
|
+
print(f" - {k}")
|
|
343
|
+
else:
|
|
344
|
+
print("\n Cite/bib: all keys match")
|
|
345
|
+
|
|
346
|
+
# ID coverage
|
|
347
|
+
print("\n ID COVERAGE")
|
|
348
|
+
print(f" With any ID: {len(has_any_id)}/{total}")
|
|
349
|
+
for f in id_fields:
|
|
350
|
+
if id_counts[f]:
|
|
351
|
+
print(f" {f:<10} {id_counts[f]}")
|
|
352
|
+
print(f" No ID: {len(no_id)}/{total}")
|
|
353
|
+
if no_id:
|
|
354
|
+
for k in sorted(no_id):
|
|
355
|
+
print(f" - {k}")
|
|
356
|
+
|
|
357
|
+
# Entry types
|
|
358
|
+
print("\n ENTRY TYPES")
|
|
359
|
+
for et, count in sorted(type_counts.items(), key=lambda x: -x[1]):
|
|
360
|
+
print(f" {et:<15} {count}")
|
|
361
|
+
if web_count:
|
|
362
|
+
print(f" ({web_count} web resources)")
|
|
363
|
+
|
|
364
|
+
# Local PDFs
|
|
365
|
+
if local_matched or local_unmatched:
|
|
366
|
+
print("\n LOCAL PDFs")
|
|
367
|
+
if local_matched:
|
|
368
|
+
print(f" Matched: {len(local_matched)}")
|
|
369
|
+
for key, pdf_path in sorted(local_matched.items()):
|
|
370
|
+
print(f" {pdf_path.name} → {key}")
|
|
371
|
+
if local_unmatched:
|
|
372
|
+
print(f" Unmatched ({len(local_unmatched)}):")
|
|
373
|
+
for pdf_path in sorted(local_unmatched):
|
|
374
|
+
print(f" - {pdf_path.name} (no matching reference title)")
|
|
375
|
+
|
|
376
|
+
print()
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def run_verify_claims(args: argparse.Namespace) -> int:
|
|
381
|
+
"""Verify claims against cited sources using local LLM or Claude Opus."""
|
|
382
|
+
from sciwrite_lint.__main__ import _load_config, _resolve_paper
|
|
383
|
+
|
|
384
|
+
config = _load_config(args)
|
|
385
|
+
pc = _resolve_paper(config, args.paper)
|
|
386
|
+
if not pc:
|
|
387
|
+
return 2
|
|
388
|
+
|
|
389
|
+
ws = config.paper_workspace(pc.name)
|
|
390
|
+
ws.ensure_dirs()
|
|
391
|
+
refs_dir = ws.root
|
|
392
|
+
from sciwrite_lint.eval_claims import run_claim_verification
|
|
393
|
+
|
|
394
|
+
results = asyncio.run(
|
|
395
|
+
run_claim_verification(
|
|
396
|
+
pc.name,
|
|
397
|
+
pc.file_path,
|
|
398
|
+
refs_dir,
|
|
399
|
+
config=config,
|
|
400
|
+
bib_path=pc.bib,
|
|
401
|
+
backend=args.backend,
|
|
402
|
+
model=args.model,
|
|
403
|
+
key_filter=args.key,
|
|
404
|
+
limit=args.limit,
|
|
405
|
+
rerun=args.fresh,
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if results is None:
|
|
410
|
+
return 1
|
|
411
|
+
|
|
412
|
+
# Convert depth-1 results to findings
|
|
413
|
+
from sciwrite_lint.pipeline import _claims_to_findings
|
|
414
|
+
from sciwrite_lint.report import format_findings
|
|
415
|
+
|
|
416
|
+
findings = _claims_to_findings(results, pc.file_path)
|
|
417
|
+
|
|
418
|
+
# Reference-unreliable: deep path (claims + metadata)
|
|
419
|
+
from sciwrite_lint.checks.reference_unreliable import (
|
|
420
|
+
claims_to_unreliable_findings,
|
|
421
|
+
)
|
|
422
|
+
from sciwrite_lint.references.metadata import load_all_metadata
|
|
423
|
+
|
|
424
|
+
all_meta = load_all_metadata(refs_dir)
|
|
425
|
+
findings.extend(
|
|
426
|
+
claims_to_unreliable_findings(
|
|
427
|
+
results,
|
|
428
|
+
pc.file_path,
|
|
429
|
+
metadata_map=all_meta or None,
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Print all findings in one pass
|
|
434
|
+
label = f"{pc.name} ({pc.file_path.name})"
|
|
435
|
+
format_findings(findings, label, fmt=args.format, color=config.color)
|
|
436
|
+
|
|
437
|
+
return 1 if any(f.level == "error" for f in findings) else 0
|