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,248 @@
|
|
|
1
|
+
"""LaTeX parsing utilities.
|
|
2
|
+
|
|
3
|
+
All functions are pure: they take strings and return results with no side effects.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def strip_comments(text: str) -> str:
|
|
12
|
+
r"""Remove LaTeX comments (lines starting with % or trailing % comments).
|
|
13
|
+
|
|
14
|
+
Preserves \% (escaped percent signs).
|
|
15
|
+
"""
|
|
16
|
+
lines = []
|
|
17
|
+
for line in text.split("\n"):
|
|
18
|
+
result = []
|
|
19
|
+
i = 0
|
|
20
|
+
while i < len(line):
|
|
21
|
+
if line[i] == "%" and (i == 0 or line[i - 1] != "\\"):
|
|
22
|
+
break
|
|
23
|
+
result.append(line[i])
|
|
24
|
+
i += 1
|
|
25
|
+
lines.append("".join(result))
|
|
26
|
+
return "\n".join(lines)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_VERBATIM_ENVS = ("lstlisting", "verbatim", "minted", "Verbatim", "alltt")
|
|
30
|
+
|
|
31
|
+
_VERBATIM_RE = re.compile(
|
|
32
|
+
r"\\begin\{(" + "|".join(re.escape(e) for e in _VERBATIM_ENVS) + r")\}"
|
|
33
|
+
r".*?"
|
|
34
|
+
r"\\end\{\1\}",
|
|
35
|
+
re.DOTALL,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# \verb|...| (any single-char delimiter, e.g. \verb+...+, \verb|...|)
|
|
39
|
+
_VERB_INLINE_RE = re.compile(r"\\verb(.)(.*?)\1")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def strip_verbatim(text: str) -> str:
|
|
43
|
+
r"""Replace content of verbatim-like environments and \verb|...| with blanks.
|
|
44
|
+
|
|
45
|
+
Preserves line count so that line numbers in downstream results stay correct.
|
|
46
|
+
Strips: lstlisting, verbatim, minted, Verbatim, alltt environments and
|
|
47
|
+
inline \verb|...| commands.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def _blank_lines(m: re.Match[str]) -> str:
|
|
51
|
+
return "\n" * m.group(0).count("\n")
|
|
52
|
+
|
|
53
|
+
text = _VERBATIM_RE.sub(_blank_lines, text)
|
|
54
|
+
text = _VERB_INLINE_RE.sub("", text)
|
|
55
|
+
return text
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def extract_body(text: str) -> str:
|
|
59
|
+
r"""Extract content between \begin{document} and \end{document}."""
|
|
60
|
+
start = text.find("\\begin{document}")
|
|
61
|
+
end = text.find("\\end{document}")
|
|
62
|
+
if start == -1 or end == -1:
|
|
63
|
+
return text
|
|
64
|
+
start = text.index("\n", start) + 1
|
|
65
|
+
return text[start:end]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_bibliography(text: str) -> str:
|
|
69
|
+
r"""Extract content inside \begin{thebibliography}...\end{thebibliography}."""
|
|
70
|
+
match = re.search(
|
|
71
|
+
r"\\begin\{thebibliography\}.*?\n(.*?)\\end\{thebibliography\}",
|
|
72
|
+
text,
|
|
73
|
+
re.DOTALL,
|
|
74
|
+
)
|
|
75
|
+
return match.group(1) if match else ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def body_without_bibliography(text: str) -> str:
|
|
79
|
+
"""Return document body with bibliography section removed."""
|
|
80
|
+
body = extract_body(text)
|
|
81
|
+
body = re.sub(
|
|
82
|
+
r"\\begin\{thebibliography\}.*?\\end\{thebibliography\}",
|
|
83
|
+
"",
|
|
84
|
+
body,
|
|
85
|
+
flags=re.DOTALL,
|
|
86
|
+
)
|
|
87
|
+
return body
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find_all_commands(text: str, cmd: str) -> list[tuple[int, str]]:
|
|
91
|
+
r"""Find all \cmd{arg} occurrences. Returns (line_number, arg) pairs.
|
|
92
|
+
|
|
93
|
+
Handles arguments that span multiple lines. Line numbers are 1-indexed.
|
|
94
|
+
"""
|
|
95
|
+
results = []
|
|
96
|
+
pattern = re.compile(r"\\%s\{" % re.escape(cmd))
|
|
97
|
+
for m in pattern.finditer(text):
|
|
98
|
+
line_no = text[: m.start()].count("\n") + 1
|
|
99
|
+
start = m.end()
|
|
100
|
+
depth = 1
|
|
101
|
+
pos = start
|
|
102
|
+
while pos < len(text) and depth > 0:
|
|
103
|
+
if text[pos] == "{":
|
|
104
|
+
depth += 1
|
|
105
|
+
elif text[pos] == "}":
|
|
106
|
+
depth -= 1
|
|
107
|
+
pos += 1
|
|
108
|
+
if depth == 0:
|
|
109
|
+
arg = text[start : pos - 1]
|
|
110
|
+
results.append((line_no, arg))
|
|
111
|
+
return results
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def find_all_cite_keys(text: str) -> list[tuple[int, str]]:
|
|
115
|
+
r"""Find all citation keys from \cite, \citep, \citet, \citeyearpar, \citeunverified.
|
|
116
|
+
|
|
117
|
+
Returns (line_number, key) pairs. Multi-key citations like
|
|
118
|
+
\cite{a,b} produce separate entries for each key.
|
|
119
|
+
Skips citations inside verbatim-like environments (lstlisting, verbatim, etc.).
|
|
120
|
+
"""
|
|
121
|
+
text = strip_verbatim(text)
|
|
122
|
+
results = []
|
|
123
|
+
pattern = re.compile(r"\\cite(?:unverified|[tp]|yearpar)?\{([^}]+)\}")
|
|
124
|
+
for line_no, line in enumerate(text.split("\n"), 1):
|
|
125
|
+
for m in pattern.finditer(line):
|
|
126
|
+
for key in m.group(1).split(","):
|
|
127
|
+
key = key.strip()
|
|
128
|
+
if key:
|
|
129
|
+
results.append((line_no, key))
|
|
130
|
+
return results
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def find_unverified_cite_keys(text: str) -> set[str]:
|
|
134
|
+
r"""Find citation keys used with \citeunverified{...}."""
|
|
135
|
+
keys = set()
|
|
136
|
+
pattern = re.compile(r"\\citeunverified\{([^}]+)\}")
|
|
137
|
+
for m in pattern.finditer(text):
|
|
138
|
+
for key in m.group(1).split(","):
|
|
139
|
+
key = key.strip()
|
|
140
|
+
if key:
|
|
141
|
+
keys.add(key)
|
|
142
|
+
return keys
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def find_bare_cite_keys(text: str) -> set[str]:
|
|
146
|
+
r"""Find citation keys used with regular \cite (not \citeunverified)."""
|
|
147
|
+
all_keys = set()
|
|
148
|
+
unverified = find_unverified_cite_keys(text)
|
|
149
|
+
|
|
150
|
+
pattern = re.compile(r"\\cite(?:[tp]|yearpar)?\{([^}]+)\}")
|
|
151
|
+
for m in pattern.finditer(text):
|
|
152
|
+
for key in m.group(1).split(","):
|
|
153
|
+
key = key.strip()
|
|
154
|
+
if key:
|
|
155
|
+
all_keys.add(key)
|
|
156
|
+
|
|
157
|
+
return all_keys - unverified
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def word_count(text: str) -> int:
|
|
161
|
+
"""Count words in text, excluding LaTeX commands and braces."""
|
|
162
|
+
cleaned = re.sub(r"\\[a-zA-Z]+\*?", "", text)
|
|
163
|
+
cleaned = re.sub(r"[{}~\\]", " ", cleaned)
|
|
164
|
+
cleaned = re.sub(r"https?://\S+", "", cleaned)
|
|
165
|
+
return len(cleaned.split())
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def split_sentences(text: str) -> list[tuple[int, str]]:
|
|
169
|
+
"""Split text into sentences with line numbers.
|
|
170
|
+
|
|
171
|
+
Handles common abbreviations and LaTeX constructs.
|
|
172
|
+
Returns (line_number, sentence_text) pairs.
|
|
173
|
+
"""
|
|
174
|
+
line_map = []
|
|
175
|
+
line_no = 1
|
|
176
|
+
for ch in text:
|
|
177
|
+
line_map.append(line_no)
|
|
178
|
+
if ch == "\n":
|
|
179
|
+
line_no += 1
|
|
180
|
+
|
|
181
|
+
cleaned = re.sub(
|
|
182
|
+
r"\\begin\{(?:tikzpicture|tabular|figure|table|equation|align)\}.*?"
|
|
183
|
+
r"\\end\{(?:tikzpicture|tabular|figure|table|equation|align)\}",
|
|
184
|
+
"",
|
|
185
|
+
text,
|
|
186
|
+
flags=re.DOTALL,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
protected = cleaned
|
|
190
|
+
for abbrev in [
|
|
191
|
+
"et al.",
|
|
192
|
+
"e.g.",
|
|
193
|
+
"i.e.",
|
|
194
|
+
"cf.",
|
|
195
|
+
"vs.",
|
|
196
|
+
"Dr.",
|
|
197
|
+
"Mr.",
|
|
198
|
+
"Mrs.",
|
|
199
|
+
"Prof.",
|
|
200
|
+
"Fig.",
|
|
201
|
+
"Eq.",
|
|
202
|
+
"Ref.",
|
|
203
|
+
"Sec.",
|
|
204
|
+
"Vol.",
|
|
205
|
+
"No.",
|
|
206
|
+
"U.S.",
|
|
207
|
+
"U.K.",
|
|
208
|
+
"N.J.",
|
|
209
|
+
]:
|
|
210
|
+
protected = protected.replace(abbrev, abbrev.replace(".", "\xb7"))
|
|
211
|
+
|
|
212
|
+
parts = re.split(r"(?<=[.!?])\s+(?=[A-Z\\])", protected)
|
|
213
|
+
|
|
214
|
+
results = []
|
|
215
|
+
pos = 0
|
|
216
|
+
for part in parts:
|
|
217
|
+
part = part.replace("\xb7", ".").strip()
|
|
218
|
+
if not part:
|
|
219
|
+
continue
|
|
220
|
+
idx = text.find(part[:30], pos) if len(part) >= 30 else text.find(part, pos)
|
|
221
|
+
if idx >= 0 and idx < len(line_map):
|
|
222
|
+
results.append((line_map[idx], part))
|
|
223
|
+
pos = idx + len(part)
|
|
224
|
+
elif results:
|
|
225
|
+
results.append((results[-1][0], part))
|
|
226
|
+
else:
|
|
227
|
+
results.append((1, part))
|
|
228
|
+
|
|
229
|
+
return results
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def is_in_environment(text: str, pos: int, env_name: str) -> bool:
|
|
233
|
+
r"""Check if a character position is inside a \begin{env}...\end{env}."""
|
|
234
|
+
before = text[:pos]
|
|
235
|
+
opens = len(re.findall(r"\\begin\{%s\}" % re.escape(env_name), before))
|
|
236
|
+
closes = len(re.findall(r"\\end\{%s\}" % re.escape(env_name), before))
|
|
237
|
+
return opens > closes
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def extract_author_block(text: str) -> str:
|
|
241
|
+
r"""Extract content of \author{...} command, handling nested braces."""
|
|
242
|
+
results = find_all_commands(text, "author")
|
|
243
|
+
return results[0][1] if results else ""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def line_number_at(text: str, pos: int) -> int:
|
|
247
|
+
"""Return the 1-indexed line number for a character position."""
|
|
248
|
+
return text[:pos].count("\n") + 1
|