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,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