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,163 @@
1
+ """Output formatters: terminal (rich) and JSON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from typing import TextIO
8
+
9
+ from sciwrite_lint.models import Finding
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Level styling
13
+ # ---------------------------------------------------------------------------
14
+
15
+ _LEVEL_STYLE = {
16
+ "error": ("bold red", "red"),
17
+ "warning": ("bold yellow", "yellow"),
18
+ "info": ("bold blue", "blue"),
19
+ }
20
+
21
+ _LEVEL_LABEL = {
22
+ "error": "ERROR",
23
+ "warning": "WARN",
24
+ "info": "INFO",
25
+ }
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Terminal (rich) output
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def format_terminal(
34
+ findings: list[Finding],
35
+ file: str,
36
+ color: bool = True,
37
+ out: TextIO = sys.stdout,
38
+ ) -> None:
39
+ """Print findings using rich for styled terminal output."""
40
+ from rich.console import Console
41
+ from rich.panel import Panel
42
+ from rich.table import Table
43
+ from rich.text import Text
44
+
45
+ console = Console(file=out, force_terminal=color, no_color=not color)
46
+
47
+ if not findings:
48
+ console.print(f"{file} — no issues found.")
49
+ return
50
+
51
+ errors = sum(1 for f in findings if f.level == "error")
52
+ warnings = sum(1 for f in findings if f.level == "warning")
53
+ infos = sum(1 for f in findings if f.level == "info")
54
+
55
+ # Build findings table
56
+ table = Table(
57
+ show_header=False,
58
+ box=None,
59
+ padding=(0, 1),
60
+ expand=True,
61
+ )
62
+ table.add_column("level", width=5, no_wrap=True)
63
+ table.add_column("rule", style="bold", no_wrap=True)
64
+ table.add_column("detail", ratio=1)
65
+
66
+ for f in findings:
67
+ label_style, border_style = _LEVEL_STYLE[f.level]
68
+ label = Text(_LEVEL_LABEL[f.level], style=label_style)
69
+
70
+ # Location suffix
71
+ loc = ""
72
+ if f.line:
73
+ loc = f" [dim]line {f.line}[/dim]"
74
+ elif f.file:
75
+ loc = f" [dim]{f.file}[/dim]"
76
+
77
+ rule_text = Text.from_markup(f"{f.rule_id}{loc}")
78
+
79
+ # Message + context
80
+ detail = Text(f.message)
81
+ if f.context:
82
+ detail.append("\n")
83
+ detail.append(f.context, style="dim")
84
+
85
+ table.add_row(label, rule_text, detail)
86
+
87
+ # Panel border color: red if errors, yellow if only warnings, blue if only info
88
+ if errors:
89
+ border = "red"
90
+ elif warnings:
91
+ border = "yellow"
92
+ else:
93
+ border = "blue"
94
+
95
+ # Summary subtitle
96
+ parts = []
97
+ if errors:
98
+ parts.append(f"[red]{errors} error{'s' if errors != 1 else ''}[/red]")
99
+ if warnings:
100
+ parts.append(
101
+ f"[yellow]{warnings} warning{'s' if warnings != 1 else ''}[/yellow]"
102
+ )
103
+ if infos:
104
+ parts.append(f"[blue]{infos} info[/blue]")
105
+ subtitle = " · ".join(parts)
106
+
107
+ panel = Panel(
108
+ table,
109
+ title=f"[bold]{file}[/bold] — {len(findings)} issues",
110
+ subtitle=subtitle,
111
+ border_style=border,
112
+ padding=(0, 1),
113
+ )
114
+ console.print(panel)
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # JSON output
119
+ # ---------------------------------------------------------------------------
120
+
121
+
122
+ def format_json(findings: list[Finding], file: str) -> str:
123
+ """Return findings as JSON string."""
124
+ data = {
125
+ "file": file,
126
+ "total": len(findings),
127
+ "errors": sum(1 for f in findings if f.level == "error"),
128
+ "warnings": sum(1 for f in findings if f.level == "warning"),
129
+ "infos": sum(1 for f in findings if f.level == "info"),
130
+ "findings": [
131
+ {
132
+ "level": f.level,
133
+ "rule_id": f.rule_id,
134
+ "message": f.message,
135
+ "file": f.file,
136
+ "line": f.line,
137
+ "context": f.context,
138
+ }
139
+ for f in findings
140
+ ],
141
+ }
142
+ return json.dumps(data, indent=2)
143
+
144
+
145
+ def format_findings(
146
+ findings: list[Finding],
147
+ label: str,
148
+ fmt: str | None = None,
149
+ color: bool = True,
150
+ ) -> None:
151
+ """Dispatch findings to the appropriate formatter.
152
+
153
+ Args:
154
+ findings: List of findings to output.
155
+ label: File/paper label for the output header.
156
+ fmt: Output format — "terminal" (default) or "json".
157
+ color: Whether to use colored terminal output.
158
+ """
159
+ fmt = fmt or "terminal"
160
+ if fmt == "json":
161
+ print(format_json(findings, label))
162
+ else:
163
+ format_terminal(findings, label, color=color)
@@ -0,0 +1,260 @@
1
+ """Pydantic models for vLLM structured output schemas.
2
+
3
+ Every string field has a generous ``max_length`` (chars) set at ~2x natural
4
+ output length. This tells vLLM's constrained decoder how much space each
5
+ field gets, so the JSON structure completes within ``max_tokens``. The limits
6
+ are ceilings — the model writes naturally below them.
7
+
8
+ Conservative token math: at worst-case 3 chars/token, the total constrained
9
+ response for the largest schema (ClaimVerdict) is ~192 tokens, leaving 3904
10
+ of 4096 max_tokens for thinking.
11
+
12
+ To get the JSON schema dict for vLLM::
13
+
14
+ from sciwrite_lint.schemas import ClaimVerdict, vllm_schema
15
+ schema = vllm_schema(ClaimVerdict)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections import OrderedDict
21
+ from typing import Any, Literal
22
+
23
+ from pydantic import BaseModel, Field
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Citation purpose categories — single source of truth
27
+ # ---------------------------------------------------------------------------
28
+
29
+ # Canonical ordered dict: purpose → (short description, verification question).
30
+ # Every consumer (prompts, schemas, weights, verify questions) derives from this.
31
+ CITATION_PURPOSES: OrderedDict[str, tuple[str, str]] = OrderedDict(
32
+ [
33
+ (
34
+ "evidence",
35
+ (
36
+ "The sentence states a specific, quantitative or empirical finding and cites this source as proof. Remove the citation and the claim loses its support",
37
+ "Does this source contain data, findings, or arguments that support the specific factual claim being made?",
38
+ ),
39
+ ),
40
+ (
41
+ "example",
42
+ (
43
+ "The cited work is a case or instance being discussed to illustrate a broader point. The sentence talks ABOUT the cited work as a story or case — it does not credit authorship. E.g. 'the graphene paper was a curiosity before it was a Nobel Prize'",
44
+ "Is this the work being cited as an illustrative example? The source does not need to support the broader claim — it only needs to be the example described.",
45
+ ),
46
+ ),
47
+ (
48
+ "attribution",
49
+ (
50
+ "The sentence names WHO created a term, concept, or idea — the focus is on crediting the person, not explaining what the concept means. E.g. 'X was introduced by [cite]'",
51
+ "Is this source where the term, concept, or idea being attributed was originally introduced?",
52
+ ),
53
+ ),
54
+ (
55
+ "tool",
56
+ (
57
+ "Names a software tool, library, system, or dataset used in this work",
58
+ "Does this source describe or document the tool, system, or dataset being referenced?",
59
+ ),
60
+ ),
61
+ (
62
+ "method",
63
+ (
64
+ "The sentence describes a methodology, algorithm, or approach adopted from this source. The focus is on HOW something is done, not on what a concept means",
65
+ "Does this source describe the methodology or approach being referenced?",
66
+ ),
67
+ ),
68
+ (
69
+ "definition",
70
+ (
71
+ "The sentence explains WHAT a concept means, citing this source as the authority for the definition. Look for 'as defined by', 'the notion of X as', or explicit criteria/thresholds from the source",
72
+ "Does this source contain the definition being cited?",
73
+ ),
74
+ ),
75
+ (
76
+ "contrast",
77
+ (
78
+ "Presents a finding or approach that this paper disagrees with, compares against, or improves upon",
79
+ "Does this source present findings, methods, or claims that the citing paper disagrees with, improves upon, or tests against?",
80
+ ),
81
+ ),
82
+ (
83
+ "context",
84
+ (
85
+ "The citation decorates a general statement — remove it and the sentence still makes the same point. Typical: 'X has been applied to A [cite], B [cite]' or 'there has been growing interest in [cite]'. No specific finding from this source is used",
86
+ "Is this source relevant to the topic being discussed?",
87
+ ),
88
+ ),
89
+ ]
90
+ )
91
+
92
+ # Tuple of valid purpose names (for Literal type construction and validation).
93
+ CITATION_PURPOSE_NAMES: tuple[str, ...] = tuple(CITATION_PURPOSES.keys())
94
+
95
+ # Pre-built dicts for common access patterns.
96
+ PURPOSE_DESCRIPTIONS: dict[str, str] = {k: v[0] for k, v in CITATION_PURPOSES.items()}
97
+ VERIFY_QUESTIONS: dict[str, str] = {k: v[1] for k, v in CITATION_PURPOSES.items()}
98
+
99
+ # Type alias — Literal can't be built dynamically, so we spell it out once here.
100
+ CitationPurposeLiteral = Literal[
101
+ "evidence",
102
+ "example",
103
+ "attribution",
104
+ "tool",
105
+ "method",
106
+ "definition",
107
+ "contrast",
108
+ "context",
109
+ ]
110
+
111
+
112
+ def vllm_schema(model: type[BaseModel]) -> dict[str, Any]:
113
+ """Generate a JSON schema dict for vLLM's constrained decoder.
114
+
115
+ Inlines ``$ref`` references (vLLM doesn't resolve ``$defs``) and strips
116
+ Pydantic metadata (``title``, ``description``) to keep the schema compact.
117
+ """
118
+ raw = model.model_json_schema()
119
+ defs = raw.pop("$defs", {})
120
+
121
+ def _resolve(obj: Any) -> Any:
122
+ if isinstance(obj, dict):
123
+ if "$ref" in obj:
124
+ ref_name = obj["$ref"].rsplit("/", 1)[-1]
125
+ return _resolve(defs[ref_name])
126
+ return {k: _resolve(v) for k, v in obj.items() if k != "title"}
127
+ if isinstance(obj, list):
128
+ return [_resolve(v) for v in obj]
129
+ return obj
130
+
131
+ return _resolve(raw)
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Claim verification (eval_claims.py)
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ class CitationClassify(BaseModel):
140
+ """Citation purpose classification (sentence-level)."""
141
+
142
+ purpose: CitationPurposeLiteral
143
+ reasoning: str = Field(max_length=400)
144
+
145
+
146
+ class ClaimVerdict(BaseModel):
147
+ """Claim-vs-source verification result."""
148
+
149
+ verdict: Literal[
150
+ "SUPPORTS",
151
+ "PARTIALLY_SUPPORTS",
152
+ "NOT_SUPPORTED",
153
+ "CANNOT_DETERMINE",
154
+ ]
155
+ confidence: float = Field(ge=0.0, le=1.0)
156
+ relevant_quote: str = Field(max_length=500)
157
+ explanation: str = Field(max_length=500)
158
+
159
+
160
+ class NarrowContext(BaseModel):
161
+ """Extracted sentence(s) for a citation from a paragraph."""
162
+
163
+ sentences: str = Field(max_length=400)
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Cross-section consistency (checks/cross_section_consistency.py)
168
+ # ---------------------------------------------------------------------------
169
+
170
+
171
+ class Contradiction(BaseModel):
172
+ """A single contradiction between two sections."""
173
+
174
+ type: str = Field(max_length=100)
175
+ section_a_says: str = Field(max_length=400)
176
+ section_b_says: str = Field(max_length=400)
177
+ explanation: str = Field(max_length=400)
178
+ is_genuine: bool = Field(
179
+ description="true ONLY if the two passages actively disagree"
180
+ )
181
+
182
+
183
+ class ConsistencyResult(BaseModel):
184
+ """Cross-section consistency check result."""
185
+
186
+ contradictions: list[Contradiction]
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Structure promises (checks/structure_promises.py)
191
+ # ---------------------------------------------------------------------------
192
+
193
+
194
+ class ContribCount(BaseModel):
195
+ """Contribution count mismatch check result."""
196
+
197
+ claimed_count: int
198
+ listed_count: int
199
+ mismatch: bool
200
+ explanation: str = Field(max_length=400)
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # Claim taxonomy (claims.py)
205
+ # ---------------------------------------------------------------------------
206
+
207
+
208
+ class ClaimClassification(BaseModel):
209
+ """5-dimension claim taxonomy classification."""
210
+
211
+ type: Literal["prediction", "explanation", "reproduction", "synthesis"]
212
+ specificity: Literal["quantified", "directional", "vague"]
213
+ testability: Literal["falsifiable", "unfalsifiable", "tautological"]
214
+ support: Literal["severe_test", "weak_test", "no_test", "post_hoc"]
215
+ scope: Literal["cross_domain", "within_domain", "within_paper"]
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Laudan problem-solving (scoring/contribution.py)
220
+ # ---------------------------------------------------------------------------
221
+
222
+
223
+ class LaudanProblemSolving(BaseModel):
224
+ """Laudan problem-solving effectiveness score."""
225
+
226
+ num_problems_solved: int = Field(ge=0)
227
+ num_acknowledged: int = Field(ge=0)
228
+ num_unacknowledged: int = Field(ge=0)
229
+ score: float = Field(ge=0.0, le=1.0)
230
+
231
+
232
+ # ---------------------------------------------------------------------------
233
+ # Venue matching (references/matching.py)
234
+ # ---------------------------------------------------------------------------
235
+
236
+
237
+ class VenueMatch(BaseModel):
238
+ """Venue name confirmation result."""
239
+
240
+ same_venue: bool
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Full-paper consistency (checks/full_paper_consistency.py)
245
+ # ---------------------------------------------------------------------------
246
+
247
+
248
+ class FullPaperIssue(BaseModel):
249
+ """A single issue found by a full-paper consistency check."""
250
+
251
+ description: str = Field(max_length=300)
252
+ evidence: str = Field(max_length=300)
253
+ location: str = Field(max_length=100)
254
+ is_genuine: bool = Field(description="true ONLY for clear, unambiguous issues")
255
+
256
+
257
+ class FullPaperIssueList(BaseModel):
258
+ """Result from a full-paper consistency check."""
259
+
260
+ issues: list[FullPaperIssue]
@@ -0,0 +1 @@
1
+ """Scoring: SciLint Score computation, chain verification, contribution axes, and calibration."""