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,174 @@
1
+ """Cross-paper consistency checks between papers in the same project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from sciwrite_lint.models import CheckResult, Finding
9
+ from sciwrite_lint.tex_parser import (
10
+ body_without_bibliography,
11
+ find_all_cite_keys,
12
+ strip_comments,
13
+ )
14
+
15
+
16
+ def check_citation_direction(
17
+ paper_a_path: Path,
18
+ paper_b_path: Path,
19
+ a_ref_keys: set[str] | None = None,
20
+ b_ref_keys: set[str] | None = None,
21
+ ) -> list[Finding]:
22
+ """Verify citation direction between two papers.
23
+
24
+ Args:
25
+ a_ref_keys: Keys in Paper B that cite Paper A (e.g. {"smith2024example"})
26
+ b_ref_keys: Keys in Paper A that would cite Paper B (forbidden)
27
+ """
28
+ findings = []
29
+ a_ref_keys = a_ref_keys or set()
30
+ b_ref_keys = b_ref_keys or set()
31
+
32
+ text_a = strip_comments(paper_a_path.read_text(encoding="utf-8"))
33
+ text_b = strip_comments(paper_b_path.read_text(encoding="utf-8"))
34
+
35
+ cite_keys_a = {k for _, k in find_all_cite_keys(text_a)}
36
+ cite_keys_b = {k for _, k in find_all_cite_keys(text_b)}
37
+
38
+ # Paper A must NOT cite Paper B
39
+ a_cites_b = cite_keys_a & b_ref_keys
40
+ for key in a_cites_b:
41
+ findings.append(
42
+ Finding(
43
+ level="error",
44
+ rule_id="ref-006",
45
+ message=f"Paper A cites Paper B via \\cite{{{key}}} — forbidden",
46
+ file=paper_a_path.name,
47
+ )
48
+ )
49
+
50
+ # Paper B should cite Paper A
51
+ if a_ref_keys and not (cite_keys_b & a_ref_keys):
52
+ findings.append(
53
+ Finding(
54
+ level="warning",
55
+ rule_id="ref-006",
56
+ message="Paper B does not cite Paper A",
57
+ file=paper_b_path.name,
58
+ )
59
+ )
60
+
61
+ return findings
62
+
63
+
64
+ def check_shared_terminology(
65
+ paper_a_path: Path,
66
+ paper_b_path: Path,
67
+ prohibited_in_a: list[str] | None = None,
68
+ required_in_b: list[str] | None = None,
69
+ shared_modes: list[str] | None = None,
70
+ ) -> list[Finding]:
71
+ """Check terminology consistency between papers."""
72
+ findings = []
73
+
74
+ body_a = body_without_bibliography(
75
+ strip_comments(paper_a_path.read_text(encoding="utf-8"))
76
+ )
77
+ body_b = body_without_bibliography(
78
+ strip_comments(paper_b_path.read_text(encoding="utf-8"))
79
+ )
80
+
81
+ # Check prohibited terms in Paper A
82
+ for term in prohibited_in_a or []:
83
+ matches = [
84
+ m
85
+ for m in re.finditer(re.escape(term), body_a)
86
+ if not _is_in_job_title(body_a, m.start())
87
+ ]
88
+ for m in matches:
89
+ line = body_a[: m.start()].count("\n") + 1
90
+ findings.append(
91
+ Finding(
92
+ level="error",
93
+ rule_id="style-005",
94
+ message=f"'{term}' found in Paper A body",
95
+ file=paper_a_path.name,
96
+ line=line,
97
+ )
98
+ )
99
+
100
+ # Check required terms appear enough in Paper B
101
+ for term in required_in_b or []:
102
+ count = len(re.findall(re.escape(term), body_b))
103
+ if count < 3:
104
+ findings.append(
105
+ Finding(
106
+ level="warning",
107
+ rule_id="con-007",
108
+ message=f"'{term}' appears only {count} times in Paper B (expected more)",
109
+ file=paper_b_path.name,
110
+ )
111
+ )
112
+
113
+ # Check shared mode names are consistent
114
+ for mode_pattern in shared_modes or []:
115
+ in_a = bool(re.search(mode_pattern, body_a, re.IGNORECASE))
116
+ in_b = bool(re.search(mode_pattern, body_b, re.IGNORECASE))
117
+ if in_a and not in_b:
118
+ findings.append(
119
+ Finding(
120
+ level="info",
121
+ rule_id="con-007",
122
+ message=f"Mode '{mode_pattern}' found in Paper A but not Paper B",
123
+ file=paper_b_path.name,
124
+ )
125
+ )
126
+
127
+ return findings
128
+
129
+
130
+ def _is_in_job_title(text: str, pos: int) -> bool:
131
+ """Check if position is within a job title context."""
132
+ start = max(0, pos - 100)
133
+ context = text[start : pos + 50]
134
+ return any(
135
+ x in context.lower()
136
+ for x in [
137
+ "titled",
138
+ "title",
139
+ "advertised as",
140
+ "``",
141
+ "teacher and",
142
+ ]
143
+ )
144
+
145
+
146
+ def run_cross_paper_check(
147
+ paper_a_path: Path,
148
+ paper_b_path: Path,
149
+ a_ref_keys: set[str] | None = None,
150
+ b_ref_keys: set[str] | None = None,
151
+ prohibited_in_a: list[str] | None = None,
152
+ required_in_b: list[str] | None = None,
153
+ shared_modes: list[str] | None = None,
154
+ ) -> CheckResult:
155
+ """Run all cross-paper consistency checks."""
156
+ result = CheckResult(checker="cross_paper", paper="cross")
157
+ result.findings.extend(
158
+ check_citation_direction(
159
+ paper_a_path,
160
+ paper_b_path,
161
+ a_ref_keys,
162
+ b_ref_keys,
163
+ )
164
+ )
165
+ result.findings.extend(
166
+ check_shared_terminology(
167
+ paper_a_path,
168
+ paper_b_path,
169
+ prohibited_in_a,
170
+ required_in_b,
171
+ shared_modes,
172
+ )
173
+ )
174
+ return result