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,322 @@
|
|
|
1
|
+
"""Contribution axes for SciLint Score scoring.
|
|
2
|
+
|
|
3
|
+
Five axes, each returning a score in [0, 1]:
|
|
4
|
+
|
|
5
|
+
1. Empirical content (Popper) — from claim taxonomy
|
|
6
|
+
2. Progressiveness (Lakatos) — from claim taxonomy
|
|
7
|
+
3. Unification (Kitcher) — citation graph computation, no LLM
|
|
8
|
+
4. Problem-solving (Laudan) — LLM prompt on intro + limitations
|
|
9
|
+
5. Test severity (Mayo) — from claim taxonomy
|
|
10
|
+
|
|
11
|
+
Axes 1, 2, 5 are computed from ClaimClassification (see claims.py).
|
|
12
|
+
Axis 3 is pure graph computation on the citation co-occurrence matrix.
|
|
13
|
+
Axis 4 requires one LLM call per paper.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from loguru import logger
|
|
22
|
+
|
|
23
|
+
from sciwrite_lint.claims import (
|
|
24
|
+
ClaimClassification,
|
|
25
|
+
empirical_content_score,
|
|
26
|
+
progressiveness_score,
|
|
27
|
+
severity_score,
|
|
28
|
+
)
|
|
29
|
+
from sciwrite_lint.config import LintConfig
|
|
30
|
+
from sciwrite_lint.llm_utils import llm_query
|
|
31
|
+
from sciwrite_lint.schemas import LaudanProblemSolving, vllm_schema
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Axis 3: Unification (Kitcher) — graph computation
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _build_co_citation_graph(
|
|
40
|
+
claim_results: list[dict[str, Any]],
|
|
41
|
+
) -> dict[str, set[str]]:
|
|
42
|
+
"""Build co-citation adjacency: refs that appear in the same claim context.
|
|
43
|
+
|
|
44
|
+
Two references are co-cited if they appear in claim contexts from the
|
|
45
|
+
same section or within a small text window. We approximate this by
|
|
46
|
+
grouping claims by their source section (if available) or by line
|
|
47
|
+
proximity.
|
|
48
|
+
"""
|
|
49
|
+
# Group refs by section or line bucket (50-line windows)
|
|
50
|
+
buckets: dict[str, set[str]] = defaultdict(set)
|
|
51
|
+
for c in claim_results:
|
|
52
|
+
key = c.get("key", "")
|
|
53
|
+
if not key:
|
|
54
|
+
continue
|
|
55
|
+
section = c.get("source_section", "")
|
|
56
|
+
line = c.get("line", 0)
|
|
57
|
+
bucket_id = section if section else f"lines_{line // 50}"
|
|
58
|
+
buckets[bucket_id].add(key)
|
|
59
|
+
|
|
60
|
+
# Build adjacency from co-occurrence in buckets
|
|
61
|
+
adjacency: dict[str, set[str]] = defaultdict(set)
|
|
62
|
+
for refs in buckets.values():
|
|
63
|
+
refs_list = list(refs)
|
|
64
|
+
for i, r1 in enumerate(refs_list):
|
|
65
|
+
for r2 in refs_list[i + 1 :]:
|
|
66
|
+
adjacency[r1].add(r2)
|
|
67
|
+
adjacency[r2].add(r1)
|
|
68
|
+
|
|
69
|
+
return dict(adjacency)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _count_clusters(adjacency: dict[str, set[str]], all_refs: set[str]) -> int:
|
|
73
|
+
"""Count connected components in the co-citation graph via BFS."""
|
|
74
|
+
visited: set[str] = set()
|
|
75
|
+
clusters = 0
|
|
76
|
+
|
|
77
|
+
for ref in all_refs:
|
|
78
|
+
if ref in visited:
|
|
79
|
+
continue
|
|
80
|
+
clusters += 1
|
|
81
|
+
queue = [ref]
|
|
82
|
+
while queue:
|
|
83
|
+
node = queue.pop()
|
|
84
|
+
if node in visited:
|
|
85
|
+
continue
|
|
86
|
+
visited.add(node)
|
|
87
|
+
for neighbor in adjacency.get(node, set()):
|
|
88
|
+
if neighbor not in visited:
|
|
89
|
+
queue.append(neighbor)
|
|
90
|
+
|
|
91
|
+
return clusters
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _count_bridges(adjacency: dict[str, set[str]], all_refs: set[str]) -> int:
|
|
95
|
+
"""Count refs that bridge multiple clusters (appear in multiple components
|
|
96
|
+
when removed)."""
|
|
97
|
+
bridges = 0
|
|
98
|
+
for ref in all_refs:
|
|
99
|
+
if ref not in adjacency or not adjacency[ref]:
|
|
100
|
+
continue
|
|
101
|
+
# Check if removing this ref increases the number of components
|
|
102
|
+
reduced: dict[str, set[str]] = {}
|
|
103
|
+
for r in all_refs:
|
|
104
|
+
if r == ref:
|
|
105
|
+
continue
|
|
106
|
+
reduced[r] = adjacency.get(r, set()) - {ref}
|
|
107
|
+
|
|
108
|
+
remaining = all_refs - {ref}
|
|
109
|
+
original_clusters = _count_clusters(adjacency, all_refs)
|
|
110
|
+
reduced_clusters = _count_clusters(reduced, remaining)
|
|
111
|
+
if reduced_clusters > original_clusters:
|
|
112
|
+
bridges += 1
|
|
113
|
+
|
|
114
|
+
return bridges
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def compute_unification_score(claim_results: list[dict[str, Any]]) -> float:
|
|
118
|
+
"""Kitcher unification: how many distinct citation clusters does the paper bridge?
|
|
119
|
+
|
|
120
|
+
Score = 1 - (1 / num_clusters) if clusters > 1, else 0.
|
|
121
|
+
A paper that bridges many distinct citation clusters scores higher.
|
|
122
|
+
Bonus for having bridge nodes (refs connecting different clusters).
|
|
123
|
+
"""
|
|
124
|
+
all_refs = {c.get("key", "") for c in claim_results if c.get("key")}
|
|
125
|
+
if len(all_refs) < 2:
|
|
126
|
+
return 0.0
|
|
127
|
+
|
|
128
|
+
adjacency = _build_co_citation_graph(claim_results)
|
|
129
|
+
num_clusters = _count_clusters(adjacency, all_refs)
|
|
130
|
+
|
|
131
|
+
if num_clusters <= 1:
|
|
132
|
+
return 0.0 # everything is one cluster — no unification
|
|
133
|
+
|
|
134
|
+
# Base score: more clusters bridged = higher score
|
|
135
|
+
# Formula: 1 - 1/clusters (asymptotes to 1.0 as clusters increase)
|
|
136
|
+
base = 1.0 - (1.0 / num_clusters)
|
|
137
|
+
|
|
138
|
+
# Bonus for bridge nodes (cap at 0.2)
|
|
139
|
+
bridges = _count_bridges(adjacency, all_refs)
|
|
140
|
+
bridge_bonus = min(0.2, bridges * 0.05)
|
|
141
|
+
|
|
142
|
+
return min(1.0, base + bridge_bonus)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Axis 4: Problem-solving (Laudan) — LLM prompt
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
LAUDAN_SYSTEM = """\
|
|
150
|
+
You are a critical philosophy-of-science evaluator assessing a paper's \
|
|
151
|
+
problem-solving effectiveness (Larry Laudan's framework). Be skeptical.
|
|
152
|
+
|
|
153
|
+
Given the introduction and limitations/discussion sections:
|
|
154
|
+
|
|
155
|
+
1. Count PROBLEMS CLAIMED SOLVED — only count problems with concrete evidence, \
|
|
156
|
+
not aspirational statements. "We address X" without results is not solved.
|
|
157
|
+
|
|
158
|
+
2. Count ACKNOWLEDGED LIMITATIONS — explicit admissions of weaknesses.
|
|
159
|
+
|
|
160
|
+
3. Count UNACKNOWLEDGED LIMITATIONS — be thorough. Always check for these \
|
|
161
|
+
common omissions that authors frequently overlook:
|
|
162
|
+
- Generalizability: tested on limited domains/datasets/languages?
|
|
163
|
+
- Scalability: will the approach work at larger scale?
|
|
164
|
+
- Reproducibility: are all details provided to reproduce?
|
|
165
|
+
- Fairness/bias: any social impact considerations missing?
|
|
166
|
+
- Comparison fairness: are baselines truly comparable?
|
|
167
|
+
- Statistical rigor: single seed? No confidence intervals?
|
|
168
|
+
|
|
169
|
+
If the limitations section is EMPTY or MISSING, count at least 2 \
|
|
170
|
+
unacknowledged limitations (missing self-assessment is itself a red flag).
|
|
171
|
+
|
|
172
|
+
If the paper makes grand claims ("solve AGI", "outperform in all domains") \
|
|
173
|
+
without proportional evidence, count each unsupported grand claim as an \
|
|
174
|
+
unacknowledged limitation.
|
|
175
|
+
|
|
176
|
+
Compute: score = num_problems_solved / (num_problems_solved + num_unacknowledged)
|
|
177
|
+
If num_problems_solved is 0, score is 0.
|
|
178
|
+
|
|
179
|
+
Respond with ONLY a valid JSON object:
|
|
180
|
+
{
|
|
181
|
+
"num_problems_solved": <integer>,
|
|
182
|
+
"num_acknowledged": <integer>,
|
|
183
|
+
"num_unacknowledged": <integer>,
|
|
184
|
+
"score": <float 0-1>
|
|
185
|
+
}"""
|
|
186
|
+
|
|
187
|
+
LAUDAN_USER_TEMPLATE = """\
|
|
188
|
+
INTRODUCTION:
|
|
189
|
+
<section>
|
|
190
|
+
{intro_text}
|
|
191
|
+
</section>
|
|
192
|
+
|
|
193
|
+
LIMITATIONS / DISCUSSION:
|
|
194
|
+
<section>
|
|
195
|
+
{limitations_text}
|
|
196
|
+
</section>"""
|
|
197
|
+
|
|
198
|
+
LAUDAN_SCHEMA = vllm_schema(LaudanProblemSolving)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def compute_problem_solving_score(
|
|
202
|
+
intro_text: str,
|
|
203
|
+
limitations_text: str,
|
|
204
|
+
config: LintConfig | None = None,
|
|
205
|
+
model_name: str = "",
|
|
206
|
+
) -> tuple[float, str]:
|
|
207
|
+
"""Laudan problem-solving: LLM evaluates solved vs unacknowledged problems.
|
|
208
|
+
|
|
209
|
+
Returns (score, reasoning). Score 0-1. Returns (0.0, error_msg) on failure.
|
|
210
|
+
"""
|
|
211
|
+
if not intro_text.strip():
|
|
212
|
+
return 0.5, "No introduction text available (neutral default)"
|
|
213
|
+
|
|
214
|
+
# If no limitations section found, that's itself a limitation
|
|
215
|
+
if not limitations_text.strip():
|
|
216
|
+
limitations_text = "(No limitations section found in the paper.)"
|
|
217
|
+
|
|
218
|
+
user = LAUDAN_USER_TEMPLATE.format(
|
|
219
|
+
intro_text=intro_text[:4000],
|
|
220
|
+
limitations_text=limitations_text[:4000],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
result = await llm_query(
|
|
224
|
+
system=LAUDAN_SYSTEM,
|
|
225
|
+
user=user,
|
|
226
|
+
schema=LAUDAN_SCHEMA,
|
|
227
|
+
schema_name="LaudanProblemSolving",
|
|
228
|
+
config=config,
|
|
229
|
+
model_name=model_name,
|
|
230
|
+
thinking="off",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if not result:
|
|
234
|
+
logger.warning("Laudan problem-solving LLM call failed")
|
|
235
|
+
return 0.0, "LLM call failed"
|
|
236
|
+
|
|
237
|
+
score = result.get("score", 0.0)
|
|
238
|
+
score = max(0.0, min(1.0, float(score)))
|
|
239
|
+
n_solved = result.get("num_problems_solved", 0)
|
|
240
|
+
n_ack = result.get("num_acknowledged", 0)
|
|
241
|
+
n_unack = result.get("num_unacknowledged", 0)
|
|
242
|
+
reasoning = f"{n_solved} solved, {n_ack} acknowledged, {n_unack} unacknowledged"
|
|
243
|
+
|
|
244
|
+
return score, reasoning
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Full contribution computation
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def compute_all_contribution_axes(
|
|
253
|
+
claim_results: list[dict[str, Any]],
|
|
254
|
+
classifications: list[ClaimClassification],
|
|
255
|
+
intro_text: str = "",
|
|
256
|
+
limitations_text: str = "",
|
|
257
|
+
config: LintConfig | None = None,
|
|
258
|
+
model_name: str = "",
|
|
259
|
+
) -> tuple[dict[str, float], dict[str, str]]:
|
|
260
|
+
"""Compute all five contribution axes.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
claim_results: Claim verification results (for unification graph).
|
|
264
|
+
classifications: Claim taxonomy classifications (for Popper/Lakatos/Mayo).
|
|
265
|
+
intro_text: Introduction section text (for Laudan).
|
|
266
|
+
limitations_text: Limitations/discussion section text (for Laudan).
|
|
267
|
+
config: Lint configuration.
|
|
268
|
+
model_name: vLLM model preset.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Tuple of (axis_scores, axis_reasoning) dicts.
|
|
272
|
+
"""
|
|
273
|
+
scores: dict[str, float] = {}
|
|
274
|
+
reasoning: dict[str, str] = {}
|
|
275
|
+
|
|
276
|
+
# Axes 1, 2, 5: from claim taxonomy (no LLM calls needed)
|
|
277
|
+
if classifications:
|
|
278
|
+
scores["empirical_content"] = empirical_content_score(classifications)
|
|
279
|
+
reasoning["empirical_content"] = (
|
|
280
|
+
f"{sum(1 for c in classifications if c.testability == 'falsifiable')}"
|
|
281
|
+
f"/{len(classifications)} claims falsifiable"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
scores["progressiveness"] = progressiveness_score(classifications)
|
|
285
|
+
predictions = sum(1 for c in classifications if c.type == "prediction")
|
|
286
|
+
explanations = sum(1 for c in classifications if c.type == "explanation")
|
|
287
|
+
reasoning["progressiveness"] = (
|
|
288
|
+
f"{predictions} predictions, {explanations} explanations"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
scores["test_severity"] = severity_score(classifications)
|
|
292
|
+
severe = sum(1 for c in classifications if c.support == "severe_test")
|
|
293
|
+
reasoning["test_severity"] = (
|
|
294
|
+
f"{severe}/{len(classifications)} claims with severe tests"
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
scores["empirical_content"] = 0.0
|
|
298
|
+
scores["progressiveness"] = 0.5
|
|
299
|
+
scores["test_severity"] = 0.0
|
|
300
|
+
reasoning["empirical_content"] = "No claim classifications available"
|
|
301
|
+
reasoning["progressiveness"] = "No claim classifications available"
|
|
302
|
+
reasoning["test_severity"] = "No claim classifications available"
|
|
303
|
+
|
|
304
|
+
# Axis 3: Unification (graph, no LLM)
|
|
305
|
+
scores["unification"] = compute_unification_score(claim_results)
|
|
306
|
+
all_refs = {c.get("key", "") for c in claim_results if c.get("key")}
|
|
307
|
+
reasoning["unification"] = f"{len(all_refs)} references in citation graph"
|
|
308
|
+
|
|
309
|
+
# Axis 4: Problem-solving (LLM)
|
|
310
|
+
if intro_text:
|
|
311
|
+
ps_score, ps_reasoning = await compute_problem_solving_score(
|
|
312
|
+
intro_text, limitations_text, config, model_name
|
|
313
|
+
)
|
|
314
|
+
scores["problem_solving"] = ps_score
|
|
315
|
+
reasoning["problem_solving"] = ps_reasoning
|
|
316
|
+
else:
|
|
317
|
+
scores["problem_solving"] = 0.5
|
|
318
|
+
reasoning["problem_solving"] = (
|
|
319
|
+
"No introduction text available (neutral default)"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return scores, reasoning
|