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,470 @@
|
|
|
1
|
+
"""CLI handlers for 'contributions' command — SciLint Score (integrity × contribution)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from sciwrite_lint.config import LintConfig, PaperConfig
|
|
14
|
+
from sciwrite_lint.models import Finding
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_claims_from_context(
|
|
18
|
+
ctx: Any, # ManuscriptContext
|
|
19
|
+
file_path: Path,
|
|
20
|
+
) -> list[dict[str, Any]]:
|
|
21
|
+
"""Extract claim dicts from a ManuscriptContext (PDF or .tex)."""
|
|
22
|
+
if ctx.source_type == "latex":
|
|
23
|
+
from sciwrite_lint.eval_claims import extract_claim_contexts
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
{"key": cc.key, "context": cc.context, "line": cc.line}
|
|
27
|
+
for cc in extract_claim_contexts(file_path)
|
|
28
|
+
]
|
|
29
|
+
# PDF: convert InlineCitation objects to claim dicts
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
"key": ic.key,
|
|
33
|
+
"context": ic.context,
|
|
34
|
+
"line": ic.line or 0,
|
|
35
|
+
"source_section": ic.section,
|
|
36
|
+
}
|
|
37
|
+
for ic in ctx.inline_citations
|
|
38
|
+
if ic.context
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def score_standalone_async(
|
|
43
|
+
file_path: Path,
|
|
44
|
+
config: LintConfig,
|
|
45
|
+
*,
|
|
46
|
+
contribution: bool = True,
|
|
47
|
+
model: str = "",
|
|
48
|
+
) -> tuple[Any, list[Finding], dict[str, float] | None, dict[str, str] | None]:
|
|
49
|
+
"""Async core: parse, check, extract claims, compute contribution axes.
|
|
50
|
+
|
|
51
|
+
Designed to be called from asyncio.run() (single paper) or from a batch
|
|
52
|
+
loop that scores multiple papers within one event loop.
|
|
53
|
+
"""
|
|
54
|
+
from sciwrite_lint.manuscript_store import ManuscriptContext
|
|
55
|
+
from sciwrite_lint.pipeline import (
|
|
56
|
+
build_pdf_context,
|
|
57
|
+
run_llm_checks_batched,
|
|
58
|
+
run_text_checks,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
suffix = file_path.suffix.lower()
|
|
62
|
+
|
|
63
|
+
# 1. Parse manuscript and run text checks
|
|
64
|
+
if suffix == ".pdf":
|
|
65
|
+
await build_pdf_context(file_path, config)
|
|
66
|
+
ctx = config.manuscript_context
|
|
67
|
+
else:
|
|
68
|
+
ctx = ManuscriptContext.from_latex(file_path, config)
|
|
69
|
+
findings = run_text_checks(file_path, config)
|
|
70
|
+
findings.extend(await run_llm_checks_batched(file_path, config))
|
|
71
|
+
|
|
72
|
+
# 2. Extract claims
|
|
73
|
+
claim_dicts = extract_claims_from_context(ctx, file_path)
|
|
74
|
+
|
|
75
|
+
# 3. Contribution axes
|
|
76
|
+
c_scores: dict[str, float] | None = None
|
|
77
|
+
c_reasoning: dict[str, str] | None = None
|
|
78
|
+
if contribution:
|
|
79
|
+
ns = argparse.Namespace(model=model)
|
|
80
|
+
c_scores, c_reasoning = await compute_contribution_axes_from_ctx(
|
|
81
|
+
ctx, claim_dicts, config, ns
|
|
82
|
+
)
|
|
83
|
+
return ctx, findings, c_scores, c_reasoning
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_scilint_result(
|
|
87
|
+
file_path: Path,
|
|
88
|
+
findings: list[Finding],
|
|
89
|
+
contribution_scores: dict[str, float] | None,
|
|
90
|
+
contribution_reasoning: dict[str, str] | None,
|
|
91
|
+
output_dir: Path | None = None,
|
|
92
|
+
) -> Any:
|
|
93
|
+
"""Compute SciLint Score from findings + contribution and optionally save."""
|
|
94
|
+
from sciwrite_lint.scoring.scilint_score import (
|
|
95
|
+
SciLintScoreResult,
|
|
96
|
+
compute_scilint_score,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
paper_name = file_path.stem
|
|
100
|
+
findings_dicts = [f.model_dump() for f in findings]
|
|
101
|
+
result: SciLintScoreResult = compute_scilint_score(
|
|
102
|
+
paper_name,
|
|
103
|
+
claim_results=[],
|
|
104
|
+
findings=findings_dicts,
|
|
105
|
+
contribution_scores=contribution_scores,
|
|
106
|
+
contribution_reasoning=contribution_reasoning,
|
|
107
|
+
)
|
|
108
|
+
if output_dir:
|
|
109
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
out_path = output_dir / f"scilint_{paper_name}.json"
|
|
111
|
+
out_path.write_text(
|
|
112
|
+
json.dumps(result.to_dict(), indent=2, ensure_ascii=False) + "\n",
|
|
113
|
+
encoding="utf-8",
|
|
114
|
+
)
|
|
115
|
+
logger.info(f"SciLint Score saved to {out_path}")
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def score_standalone_file(
|
|
120
|
+
file_path: Path,
|
|
121
|
+
config: LintConfig,
|
|
122
|
+
*,
|
|
123
|
+
contribution: bool = True,
|
|
124
|
+
model: str = "",
|
|
125
|
+
output_dir: Path | None = None,
|
|
126
|
+
) -> Any:
|
|
127
|
+
"""Score a standalone PDF/tex file. Single-paper entry point (uses asyncio.run).
|
|
128
|
+
|
|
129
|
+
For batch scoring (multiple papers), use score_standalone_async directly
|
|
130
|
+
within a single event loop to avoid repeated asyncio.run() calls.
|
|
131
|
+
"""
|
|
132
|
+
_ctx, findings, contribution_scores, contribution_reasoning = asyncio.run(
|
|
133
|
+
score_standalone_async(
|
|
134
|
+
file_path, config, contribution=contribution, model=model
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return build_scilint_result(
|
|
139
|
+
file_path, findings, contribution_scores, contribution_reasoning, output_dir
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def run_contributions(args: argparse.Namespace) -> int:
|
|
144
|
+
"""Compute SciLint Score from claim verification results."""
|
|
145
|
+
from sciwrite_lint.__main__ import _load_config, _resolve_paper
|
|
146
|
+
|
|
147
|
+
config = _load_config(args)
|
|
148
|
+
|
|
149
|
+
# File path mode — standalone ranking without verify-claims
|
|
150
|
+
if hasattr(args, "file") and args.file:
|
|
151
|
+
return run_contributions_file(Path(args.file), config, args)
|
|
152
|
+
|
|
153
|
+
if not getattr(args, "paper", None):
|
|
154
|
+
logger.error("Either a file path or --paper is required.")
|
|
155
|
+
return 2
|
|
156
|
+
|
|
157
|
+
from sciwrite_lint.scoring.scilint_score import (
|
|
158
|
+
run_contributions as scilint_run_contributions,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
pc = _resolve_paper(config, args.paper)
|
|
162
|
+
if not pc:
|
|
163
|
+
return 2
|
|
164
|
+
|
|
165
|
+
output_dir = Path(args.output_dir) if args.output_dir else config.results_dir
|
|
166
|
+
|
|
167
|
+
ws = config.paper_workspace(pc.name)
|
|
168
|
+
refs_dir = ws.root
|
|
169
|
+
|
|
170
|
+
# Load claims from workspace.db
|
|
171
|
+
from sciwrite_lint.references.workspace_db import get_db, load_claim_results
|
|
172
|
+
|
|
173
|
+
if not refs_dir.exists():
|
|
174
|
+
logger.error(
|
|
175
|
+
f"No workspace found for paper '{pc.name}'. "
|
|
176
|
+
f"Run 'sciwrite-lint check --paper {pc.name}' first."
|
|
177
|
+
)
|
|
178
|
+
return 1
|
|
179
|
+
|
|
180
|
+
with get_db(refs_dir) as conn:
|
|
181
|
+
claims = load_claim_results(conn)
|
|
182
|
+
|
|
183
|
+
if not claims:
|
|
184
|
+
logger.error(
|
|
185
|
+
f"No claim results for paper '{pc.name}'. "
|
|
186
|
+
f"Run 'sciwrite-lint check --paper {pc.name}' first."
|
|
187
|
+
)
|
|
188
|
+
return 1
|
|
189
|
+
|
|
190
|
+
findings_path = Path(args.findings) if getattr(args, "findings", None) else None
|
|
191
|
+
|
|
192
|
+
# Contribution axes — always computed for score command
|
|
193
|
+
contribution_scores, contribution_reasoning = asyncio.run(
|
|
194
|
+
compute_contribution_axes(pc, claims, config, args)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
result = scilint_run_contributions(
|
|
198
|
+
pc.name,
|
|
199
|
+
claims,
|
|
200
|
+
findings_path=findings_path,
|
|
201
|
+
references_dir=refs_dir,
|
|
202
|
+
contribution_scores=contribution_scores,
|
|
203
|
+
contribution_reasoning=contribution_reasoning,
|
|
204
|
+
output_dir=output_dir,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if getattr(args, "format", "terminal") == "json":
|
|
208
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
ir = result.integrity_result
|
|
212
|
+
print(f"\n SciLint Score for '{pc.name}':")
|
|
213
|
+
print(f" SciLint Score: {result.scilint_score:.4f}")
|
|
214
|
+
print(f" Internal Consistency: {ir.internal_consistency:.4f}")
|
|
215
|
+
print(f" Referencing Quality: {ir.referencing_quality:.4f}")
|
|
216
|
+
if ir.reference_reliability:
|
|
217
|
+
print(
|
|
218
|
+
f" Reference reliability: {len(ir.reference_reliability)} refs scored"
|
|
219
|
+
)
|
|
220
|
+
print(f" Contribution: {result.contribution.overall:.4f}")
|
|
221
|
+
c = result.contribution
|
|
222
|
+
if c.empirical_content > 0:
|
|
223
|
+
print(f" Empirical: {c.empirical_content:.4f}")
|
|
224
|
+
print(f" Progressive: {c.progressiveness:.4f}")
|
|
225
|
+
print(f" Unification: {c.unification:.4f}")
|
|
226
|
+
print(f" Problem-solv: {c.problem_solving:.4f}")
|
|
227
|
+
print(f" Test severity: {c.test_severity:.4f}")
|
|
228
|
+
print(f" Claims verified: {result.total_claims}")
|
|
229
|
+
print(f" Refs scored: {result.total_refs_scored}")
|
|
230
|
+
|
|
231
|
+
if result.ref_scores:
|
|
232
|
+
print("\n Per-reference breakdown:")
|
|
233
|
+
for rs in sorted(result.ref_scores, key=lambda r: r.weighted_score):
|
|
234
|
+
print(
|
|
235
|
+
f" {rs.key:30s} {rs.verdict:20s} "
|
|
236
|
+
f"w={rs.weight:.1f} score={rs.verification_score:.2f} "
|
|
237
|
+
f"({rs.purpose})"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Summary counts — quick overview of actionable issues
|
|
241
|
+
_print_issue_summary(claims)
|
|
242
|
+
|
|
243
|
+
return 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _print_issue_summary(
|
|
247
|
+
claims: list[dict[str, Any]],
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Print a one-line issue summary below the score breakdown."""
|
|
250
|
+
from sciwrite_lint.checks.cite_purpose import PURPOSE_WEIGHTS, UNSPECIFIED_THRESHOLD
|
|
251
|
+
|
|
252
|
+
active = [r for r in claims if not r.get("dismissed")]
|
|
253
|
+
|
|
254
|
+
not_supported = sum(1 for r in active if r.get("verdict") == "NOT_SUPPORTED")
|
|
255
|
+
partial = sum(1 for r in active if r.get("verdict") == "PARTIALLY_SUPPORTS")
|
|
256
|
+
weak_cite = sum(
|
|
257
|
+
1
|
|
258
|
+
for r in active
|
|
259
|
+
if PURPOSE_WEIGHTS.get(
|
|
260
|
+
r.get("cite_purpose") or r.get("citation_purpose", ""), 1.0
|
|
261
|
+
)
|
|
262
|
+
<= UNSPECIFIED_THRESHOLD
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
parts: list[str] = []
|
|
266
|
+
if not_supported:
|
|
267
|
+
parts.append(f"{not_supported} unsupported")
|
|
268
|
+
if partial:
|
|
269
|
+
parts.append(f"{partial} partial")
|
|
270
|
+
if weak_cite:
|
|
271
|
+
parts.append(f"{weak_cite} weak citation{'s' if weak_cite != 1 else ''}")
|
|
272
|
+
|
|
273
|
+
if parts:
|
|
274
|
+
print(f"\n Issues: {' | '.join(parts)}")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def run_contributions_file(
|
|
278
|
+
file_path: Path, config: LintConfig, args: argparse.Namespace
|
|
279
|
+
) -> int:
|
|
280
|
+
"""Score a standalone file (PDF or .tex) without prior verify-claims."""
|
|
281
|
+
if not file_path.exists():
|
|
282
|
+
logger.error(f"Error: {file_path} not found")
|
|
283
|
+
return 2
|
|
284
|
+
|
|
285
|
+
suffix = file_path.suffix.lower()
|
|
286
|
+
if suffix not in (".pdf", ".tex"):
|
|
287
|
+
logger.error(f"Unsupported file type: {suffix}. Use .pdf or .tex")
|
|
288
|
+
return 2
|
|
289
|
+
|
|
290
|
+
output_dir = Path(args.output_dir) if args.output_dir else file_path.parent
|
|
291
|
+
result = score_standalone_file(
|
|
292
|
+
file_path,
|
|
293
|
+
config,
|
|
294
|
+
contribution=True,
|
|
295
|
+
model=getattr(args, "model", ""),
|
|
296
|
+
output_dir=output_dir,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if getattr(args, "format", "terminal") == "json":
|
|
300
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
301
|
+
return 0
|
|
302
|
+
|
|
303
|
+
ir = result.integrity_result
|
|
304
|
+
print(f"\n SciLint Score for '{file_path.stem}' (standalone file):")
|
|
305
|
+
print(f" SciLint Score: {result.scilint_score:.4f}")
|
|
306
|
+
print(f" Internal Consistency: {ir.internal_consistency:.4f}")
|
|
307
|
+
print(f" Internal Consistency: {ir.internal_consistency:.4f}")
|
|
308
|
+
print(f" Contribution: {result.contribution.overall:.4f}")
|
|
309
|
+
if result.contribution.empirical_content > 0:
|
|
310
|
+
c = result.contribution
|
|
311
|
+
print(f" Empirical: {c.empirical_content:.4f}")
|
|
312
|
+
print(f" Progressive: {c.progressiveness:.4f}")
|
|
313
|
+
print(f" Unification: {c.unification:.4f}")
|
|
314
|
+
print(f" Problem-solv: {c.problem_solving:.4f}")
|
|
315
|
+
print(f" Test severity: {c.test_severity:.4f}")
|
|
316
|
+
|
|
317
|
+
return 0
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def compute_contribution_axes_from_ctx(
|
|
321
|
+
ctx: Any, # ManuscriptContext
|
|
322
|
+
claim_dicts: list[dict[str, Any]],
|
|
323
|
+
config: LintConfig,
|
|
324
|
+
args: argparse.Namespace,
|
|
325
|
+
) -> tuple[dict[str, float], dict[str, str]]:
|
|
326
|
+
"""Contribution axes from a ManuscriptContext (no PaperConfig needed)."""
|
|
327
|
+
from sciwrite_lint.claims import classify_claims_batch
|
|
328
|
+
from sciwrite_lint.scoring.contribution import compute_all_contribution_axes
|
|
329
|
+
|
|
330
|
+
model = getattr(args, "model", "") or ""
|
|
331
|
+
|
|
332
|
+
# Enrich claims with paper context
|
|
333
|
+
abstract = ctx.abstract or ""
|
|
334
|
+
methods_sections = ctx.get_section_by_title(
|
|
335
|
+
"method",
|
|
336
|
+
"approach",
|
|
337
|
+
"experiment",
|
|
338
|
+
"setup",
|
|
339
|
+
"implementation",
|
|
340
|
+
)
|
|
341
|
+
methods_text = "\n\n".join(s.clean_text for s in methods_sections)
|
|
342
|
+
results_sections = ctx.get_section_by_title(
|
|
343
|
+
"result",
|
|
344
|
+
"evaluation",
|
|
345
|
+
"finding",
|
|
346
|
+
"ablation",
|
|
347
|
+
"analysis",
|
|
348
|
+
)
|
|
349
|
+
results_text = "\n\n".join(s.clean_text for s in results_sections)
|
|
350
|
+
|
|
351
|
+
paper_context = f"ABSTRACT: {abstract[:1000]}"
|
|
352
|
+
if methods_text:
|
|
353
|
+
paper_context += f"\n\nMETHODS SUMMARY: {methods_text[:1500]}"
|
|
354
|
+
if results_text:
|
|
355
|
+
paper_context += f"\n\nRESULTS SUMMARY: {results_text[:1500]}"
|
|
356
|
+
|
|
357
|
+
enriched_claims = []
|
|
358
|
+
for c in claim_dicts:
|
|
359
|
+
enriched = dict(c)
|
|
360
|
+
original_ctx = enriched.get("context", "")
|
|
361
|
+
enriched["context"] = f"{original_ctx}\n\n{paper_context}"
|
|
362
|
+
enriched_claims.append(enriched)
|
|
363
|
+
|
|
364
|
+
logger.info("Classifying claims for contribution scoring...")
|
|
365
|
+
classifications = await classify_claims_batch(enriched_claims, config, model)
|
|
366
|
+
|
|
367
|
+
# Laudan sections
|
|
368
|
+
intro_sections = ctx.get_section_by_title(
|
|
369
|
+
"introduction",
|
|
370
|
+
"intro",
|
|
371
|
+
"overview",
|
|
372
|
+
"background",
|
|
373
|
+
)
|
|
374
|
+
limit_sections = ctx.get_section_by_title(
|
|
375
|
+
"limitation",
|
|
376
|
+
"discussion",
|
|
377
|
+
"conclusion",
|
|
378
|
+
"threat",
|
|
379
|
+
"future work",
|
|
380
|
+
"shortcoming",
|
|
381
|
+
)
|
|
382
|
+
intro_text = "\n\n".join(s.clean_text for s in intro_sections)
|
|
383
|
+
limitations_text = "\n\n".join(s.clean_text for s in limit_sections)
|
|
384
|
+
|
|
385
|
+
if len(intro_text) < 200 and abstract:
|
|
386
|
+
intro_text = f"{abstract}\n\n{intro_text}"
|
|
387
|
+
|
|
388
|
+
return await compute_all_contribution_axes(
|
|
389
|
+
claim_dicts, classifications, intro_text, limitations_text, config, model
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
async def compute_contribution_axes(
|
|
394
|
+
pc: PaperConfig,
|
|
395
|
+
claims: list[dict[str, Any]],
|
|
396
|
+
config: LintConfig,
|
|
397
|
+
args: argparse.Namespace,
|
|
398
|
+
) -> tuple[dict[str, float], dict[str, str]]:
|
|
399
|
+
"""Run claim taxonomy + contribution axes for a paper."""
|
|
400
|
+
from sciwrite_lint.claims import classify_claims_batch
|
|
401
|
+
from sciwrite_lint.manuscript_store import ManuscriptContext
|
|
402
|
+
from sciwrite_lint.scoring.contribution import compute_all_contribution_axes
|
|
403
|
+
|
|
404
|
+
model = getattr(args, "model", "") or ""
|
|
405
|
+
|
|
406
|
+
# Build manuscript context for section selection
|
|
407
|
+
ctx = ManuscriptContext.from_latex(pc.file_path, config)
|
|
408
|
+
|
|
409
|
+
# Enrich claim context with abstract + methods for better taxonomy
|
|
410
|
+
abstract = ctx.abstract or ""
|
|
411
|
+
methods_sections = ctx.get_section_by_title(
|
|
412
|
+
"method",
|
|
413
|
+
"approach",
|
|
414
|
+
"experiment",
|
|
415
|
+
"setup",
|
|
416
|
+
"implementation",
|
|
417
|
+
)
|
|
418
|
+
methods_text = "\n\n".join(s.clean_text for s in methods_sections)
|
|
419
|
+
results_sections = ctx.get_section_by_title(
|
|
420
|
+
"result",
|
|
421
|
+
"evaluation",
|
|
422
|
+
"finding",
|
|
423
|
+
"ablation",
|
|
424
|
+
"analysis",
|
|
425
|
+
)
|
|
426
|
+
results_text = "\n\n".join(s.clean_text for s in results_sections)
|
|
427
|
+
|
|
428
|
+
# Add paper context to each claim for better classification
|
|
429
|
+
paper_context = f"ABSTRACT: {abstract[:1000]}"
|
|
430
|
+
if methods_text:
|
|
431
|
+
paper_context += f"\n\nMETHODS SUMMARY: {methods_text[:1500]}"
|
|
432
|
+
if results_text:
|
|
433
|
+
paper_context += f"\n\nRESULTS SUMMARY: {results_text[:1500]}"
|
|
434
|
+
|
|
435
|
+
enriched_claims = []
|
|
436
|
+
for c in claims:
|
|
437
|
+
enriched = dict(c)
|
|
438
|
+
original_ctx = enriched.get("context", "")
|
|
439
|
+
enriched["context"] = f"{original_ctx}\n\n{paper_context}"
|
|
440
|
+
enriched_claims.append(enriched)
|
|
441
|
+
|
|
442
|
+
# Classify claims with enriched context
|
|
443
|
+
logger.info("Classifying claims for contribution scoring...")
|
|
444
|
+
classifications = await classify_claims_batch(enriched_claims, config, model)
|
|
445
|
+
|
|
446
|
+
# Extract sections for Laudan (intro + limitations)
|
|
447
|
+
intro_sections = ctx.get_section_by_title(
|
|
448
|
+
"introduction",
|
|
449
|
+
"intro",
|
|
450
|
+
"overview",
|
|
451
|
+
"background",
|
|
452
|
+
)
|
|
453
|
+
limit_sections = ctx.get_section_by_title(
|
|
454
|
+
"limitation",
|
|
455
|
+
"discussion",
|
|
456
|
+
"conclusion",
|
|
457
|
+
"threat",
|
|
458
|
+
"future work",
|
|
459
|
+
"shortcoming",
|
|
460
|
+
)
|
|
461
|
+
intro_text = "\n\n".join(s.clean_text for s in intro_sections)
|
|
462
|
+
limitations_text = "\n\n".join(s.clean_text for s in limit_sections)
|
|
463
|
+
|
|
464
|
+
# Prepend abstract to intro if intro is thin
|
|
465
|
+
if len(intro_text) < 200 and abstract:
|
|
466
|
+
intro_text = f"{abstract}\n\n{intro_text}"
|
|
467
|
+
|
|
468
|
+
return await compute_all_contribution_axes(
|
|
469
|
+
claims, classifications, intro_text, limitations_text, config, model
|
|
470
|
+
)
|