refaudit 0.1.0__tar.gz

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.
@@ -0,0 +1,3 @@
1
+ # Every change needs review from a code owner before it can reach main.
2
+ # Branch protection enforces this; see CONTRIBUTING.md.
3
+ * @jeevanp03
@@ -0,0 +1,29 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+ - run: pip install -e ".[dev]"
25
+ # The suite is offline by design: it uses fake resolvers and never makes a
26
+ # network call, so CI cannot be flaked by a rate-limited upstream.
27
+ - run: pytest -q
28
+ - run: ruff check .
29
+ - run: mypy src --ignore-missing-imports
@@ -0,0 +1,39 @@
1
+ name: release
2
+
3
+ # Publishes to PyPI via Trusted Publishing (OIDC). There is deliberately no API
4
+ # token stored in this repository: PyPI verifies the workflow's identity
5
+ # directly, so there is no long-lived credential to leak or rotate.
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+ - run: pip install build
22
+ - run: python -m build
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment: pypi # add required reviewers here to gate releases
32
+ permissions:
33
+ id-token: write # required for Trusted Publishing
34
+ steps:
35
+ - uses: actions/download-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ refaudit-out/
12
+ .DS_Store
@@ -0,0 +1,50 @@
1
+ # Contributing
2
+
3
+ `main` is protected. Nobody pushes to it directly — changes arrive by pull
4
+ request with a review from a code owner (see `.github/CODEOWNERS`).
5
+
6
+ ```bash
7
+ git checkout -b your-change
8
+ # ...
9
+ git push -u origin your-change
10
+ gh pr create --fill
11
+ ```
12
+
13
+ CI runs the test suite on Python 3.10–3.13 plus `ruff` and `mypy`, and must be
14
+ green before merge.
15
+
16
+ ## Running the tests
17
+
18
+ ```bash
19
+ pip install -e ".[dev]"
20
+ pytest -q
21
+ ```
22
+
23
+ The suite is **offline by design**: it exercises the checker through fake
24
+ resolvers and never makes a network call. This is deliberate — the services this
25
+ tool talks to rate-limit aggressively, and a test suite that depended on them
26
+ would fail for reasons that have nothing to do with the change under review.
27
+
28
+ If you add a resolver, add it to `AVAILABLE` in `src/refaudit/resolvers/__init__.py`
29
+ and give it a `RateSpec` whose `rationale` cites the service's own documented
30
+ limit. Please do not guess a rate.
31
+
32
+ ## The invariant to preserve
33
+
34
+ **"We could not check" and "this is wrong" must never collapse into each other.**
35
+
36
+ A resolver returns `Found`, `NotFound` or `Unavailable`, and only `Found` may
37
+ produce a negative verdict. `UNVERIFIED` results are reported separately from
38
+ findings and are never cached. If you are tempted to simplify this, read the
39
+ tests in `tests/test_checker.py` under "the central guarantee" first — they
40
+ exist because both failure modes happened during development.
41
+
42
+ ## Releasing
43
+
44
+ 1. Bump `version` in `pyproject.toml` and `__version__` in `src/refaudit/__init__.py`.
45
+ 2. Merge to `main`.
46
+ 3. Publish a GitHub release with a tag like `v0.1.1`.
47
+
48
+ The `release` workflow builds and publishes to PyPI using Trusted Publishing.
49
+ There is no API token in this repository; PyPI verifies the workflow's identity
50
+ over OIDC, so there is no long-lived credential to leak.
refaudit-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SHARE Lab, University of Waterloo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.5
2
+ Name: refaudit
3
+ Version: 0.1.0
4
+ Summary: Verify bibliography entries against Crossref, arXiv and OpenAlex before you submit.
5
+ Project-URL: Homepage, https://github.com/uw-share-lab/refaudit
6
+ Project-URL: Issues, https://github.com/uw-share-lab/refaudit/issues
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: arxiv,bibtex,citations,crossref,openalex,research-integrity
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Text Processing :: Markup :: LaTeX
13
+ Requires-Python: >=3.10
14
+ Provides-Extra: dev
15
+ Requires-Dist: mypy>=1.5; extra == 'dev'
16
+ Requires-Dist: pytest>=7; extra == 'dev'
17
+ Requires-Dist: ruff>=0.4; extra == 'dev'
18
+ Provides-Extra: xml
19
+ Requires-Dist: defusedxml>=0.7; extra == 'xml'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # refaudit
23
+
24
+ Verify the entries in a `.bib` against Crossref, arXiv and OpenAlex, and report
25
+ the ones a human needs to look at.
26
+
27
+ Written for venues that run automated checks for hallucinated or malformed
28
+ references. The failure it is built to catch is not a missing DOI — it is a DOI
29
+ that resolves to **a different paper than the entry claims**. That one is
30
+ invisible when you read the reference list, because the title and authors look
31
+ fine and only the identifier is wrong.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install git+https://github.com/uw-share-lab/refaudit.git
37
+ ```
38
+
39
+ Python 3.10+. No runtime dependencies.
40
+
41
+ > PyPI publishing is wired up (Trusted Publishing, see `.github/workflows/release.yml`)
42
+ > but no release has been cut yet, so `pip install refaudit` will not work until
43
+ > the first tagged release. Use the command above in the meantime.
44
+
45
+ ## Quick start
46
+
47
+ ```bash
48
+ # every entry in the file
49
+ refaudit refs.bib --email you@uwaterloo.ca
50
+
51
+ # only the entries actually cited in the paper, which is usually what you want
52
+ refaudit refs.bib --email you@uwaterloo.ca --tex paper/sections --only-cited
53
+ ```
54
+
55
+ `--email` is required. Crossref and OpenAlex give identified callers a separate,
56
+ more reliable request pool, and it is the courtesy their documentation asks for.
57
+ Set it once instead of typing it each time:
58
+
59
+ ```bash
60
+ export REFAUDIT_EMAIL=you@uwaterloo.ca
61
+ ```
62
+
63
+ Results are written to `refaudit-out/` as `reference_check.txt` (readable) and
64
+ `reference_check.csv` (sortable), and printed to stdout.
65
+
66
+ ### Working from Overleaf
67
+
68
+ Download the `.bib` (Menu → Download → Source, or just the file), then point
69
+ `--tex` at the unzipped `sections/` directory so `--only-cited` can tell which
70
+ keys actually reach the PDF:
71
+
72
+ ```bash
73
+ refaudit sample-base.bib --email you@uwaterloo.ca --tex sections/ --only-cited
74
+ ```
75
+
76
+ ## What it reports
77
+
78
+ | Verdict | Meaning |
79
+ |---|---|
80
+ | `TITLE_MISMATCH` | an identifier resolved to a different paper — check first |
81
+ | `DEAD_DOI` | the DOI is not registered |
82
+ | `AUTHOR_MISMATCH` | titles agree, first author does not |
83
+ | `YEAR_MISMATCH` | titles agree, year is off by more than one |
84
+ | `NOT_FOUND` | no identifier, and no title match anywhere |
85
+ | `UNVERIFIED` | **a source was unreachable — this says nothing about the entry** |
86
+ | `SKIPPED` | `@misc`/`@online` with no identifier; nothing to check against |
87
+ | `OK` | resolved and consistent |
88
+
89
+ Exit status is `1` if there is at least one finding, `0` if not, `2` on a usage
90
+ error — so it drops into CI or a pre-submission script.
91
+
92
+ ## Options
93
+
94
+ | Flag | Effect |
95
+ |---|---|
96
+ | `--email` | contact address sent to the APIs (or `REFAUDIT_EMAIL`). Required. |
97
+ | `--tex PATH` | LaTeX file or directory, used to work out which keys are cited |
98
+ | `--only-cited` | check only cited keys; requires `--tex` |
99
+ | `--resolvers` | comma-separated subset of `crossref:doi`, `arxiv:id`, `openalex`, `crossref:title` |
100
+ | `--out DIR` | output directory (default `refaudit-out`) |
101
+ | `--cache PATH` / `--no-cache` | cache location, or disable it |
102
+ | `--ttl-days N` | how long cached results stay valid (default 90) |
103
+ | `--timeout N` | per-request timeout in seconds (default 20) |
104
+ | `--title-match N` | similarity at or above which two titles are the same work (default 0.75) |
105
+ | `--quiet` | suppress per-entry progress, print only the summary |
106
+
107
+ A run over a few hundred references takes minutes, because it is deliberately
108
+ paced. Successful lookups are cached, so it is safe to interrupt with Ctrl-C and
109
+ re-run — it picks up where it stopped.
110
+
111
+ ## The one design rule
112
+
113
+ **"We could not check" and "this is wrong" are different answers and never
114
+ collapse into each other.**
115
+
116
+ This sounds obvious and is easy to get wrong. arXiv rate-limits whole networks;
117
+ when that happens, a naive checker either silently passes the entry (false
118
+ comfort, the worse failure) or falls back to a title search, finds something
119
+ loosely related, and reports a mismatch (false alarm, which trains you to ignore
120
+ it). Both are worse than saying "I could not check this one."
121
+
122
+ So every resolver returns exactly one of `Found`, `NotFound`, or `Unavailable`,
123
+ and only `Found` can produce a negative verdict. `UNVERIFIED` results are listed
124
+ separately from findings and are never cached, so a transient outage does not
125
+ get baked into later runs.
126
+
127
+ Relatedly, evidence is weighted by strength: a DOI that Crossref does not
128
+ recognise is a finding, but a title search returning something different is only
129
+ a finding when there was no identifier to go on. Otherwise every arXiv-only
130
+ workshop paper that Crossref does not index would be flagged.
131
+
132
+ ## Rate limiting
133
+
134
+ Each source declares the limit its own documentation specifies, next to the code
135
+ that calls it:
136
+
137
+ | Source | Rate used | Why |
138
+ |---|---|---|
139
+ | Crossref | 2/s, then whatever the response headers say | Crossref publishes `X-Rate-Limit-Limit` / `-Interval`; the client reads and obeys them |
140
+ | arXiv | 1 per 3s | [arXiv's terms of use](https://info.arxiv.org/help/api/tou.html) specify exactly this |
141
+ | OpenAlex | 3/s | [documented](https://docs.openalex.org/how-to-use-the-api/rate-limits-and-authentication) ceiling is 10/s and 100k/day; we use a third of it |
142
+
143
+ A `429` is treated as instruction rather than noise: `Retry-After` is honoured,
144
+ the token bucket is permanently halved, and after repeated refusals a circuit
145
+ breaker stops asking that host so the rest of the run still finishes. Retries use
146
+ exponential backoff with full jitter.
147
+
148
+ `--email` is required because Crossref and OpenAlex give identified callers a
149
+ separate, more reliable pool, and it is the courtesy their docs ask for.
150
+
151
+ ## Security
152
+
153
+ - **HTTPS only.** Plain-`http` URLs are refused, not silently upgraded.
154
+ - **Bounded redirects**, kept on https, so a redirect cannot downgrade transport
155
+ or forward the `mailto` identifier somewhere unexpected.
156
+ - **Response size cap and per-request timeouts** on every call.
157
+ - **XML is parsed with entity declarations refused**, blocking billion-laughs and
158
+ XXE. Uses `defusedxml` when installed, otherwise a hardened stdlib path; both
159
+ raise the same exception type so callers cannot miss one.
160
+ - **DOIs and arXiv ids are validated against a pattern before being interpolated
161
+ into a request path**, so a malformed field cannot steer the URL.
162
+ - **No credentials in code.** Optional API keys come from the environment and are
163
+ sent as headers, never query parameters, so they stay out of logs.
164
+ - **No runtime dependencies.** This gets installed in a hurry near a deadline,
165
+ often on a machine someone else administers; that is the wrong moment to widen
166
+ the supply chain.
167
+
168
+ ## Troubleshooting
169
+
170
+ **Lots of `UNVERIFIED` results.** A source refused your network. arXiv in
171
+ particular rate-limits by IP and will 429 an entire institution or VPN
172
+ regardless of your own pace. These are not findings — the entries were simply
173
+ not checked. Try again later, from a different network, or lean on OpenAlex,
174
+ which indexes arXiv identifiers too:
175
+
176
+ ```bash
177
+ refaudit refs.bib --email you@uwaterloo.ca --resolvers crossref:doi,openalex
178
+ ```
179
+
180
+ **A correct entry is flagged `NOT_FOUND`.** Workshop papers, theses and
181
+ tech reports are often in no citation index. If the entry has no DOI and no
182
+ arXiv id there is nothing to verify it against; confirm it by hand and move on.
183
+
184
+ **A correct entry is flagged `TITLE_MISMATCH`.** This one is worth taking
185
+ seriously: it means the DOI or arXiv id in your `.bib` resolves to a different
186
+ paper. Usually the identifier was copied from the wrong row, or generated rather
187
+ than looked up. Check the `found` line in the report against what you meant to
188
+ cite.
189
+
190
+ **Everything is `SKIPPED`.** `@misc` and `@online` entries with no identifier
191
+ cannot be checked. That is expected for datasets, blog posts and software.
192
+
193
+ ## Library use
194
+
195
+ ```python
196
+ from refaudit import Checker, default_resolvers, parse_file
197
+
198
+ entries = parse_file("refs.bib")
199
+ checker = Checker(default_resolvers("you@university.edu"))
200
+
201
+ for result in checker.check_all(entries):
202
+ if result.verdict.is_finding:
203
+ print(result.key, result.verdict.value, result.note)
204
+ ```
205
+
206
+ `Resolver` is a `Protocol`: implement `name`, `rate`, `can_handle` and `resolve`
207
+ to add a source, and pass it to `Checker` alongside the built-ins.
208
+
209
+ ## Development
210
+
211
+ ```bash
212
+ pip install -e ".[dev]"
213
+ pytest # offline: the suite uses fake resolvers and never hits a network
214
+ ruff check .
215
+ mypy src
216
+ ```
217
+
218
+ `main` is protected: changes go through a pull request with a code-owner review,
219
+ and CI must pass on Python 3.10-3.13. See [CONTRIBUTING.md](CONTRIBUTING.md).
220
+
221
+ ## Licence
222
+
223
+ MIT.
@@ -0,0 +1,202 @@
1
+ # refaudit
2
+
3
+ Verify the entries in a `.bib` against Crossref, arXiv and OpenAlex, and report
4
+ the ones a human needs to look at.
5
+
6
+ Written for venues that run automated checks for hallucinated or malformed
7
+ references. The failure it is built to catch is not a missing DOI — it is a DOI
8
+ that resolves to **a different paper than the entry claims**. That one is
9
+ invisible when you read the reference list, because the title and authors look
10
+ fine and only the identifier is wrong.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install git+https://github.com/uw-share-lab/refaudit.git
16
+ ```
17
+
18
+ Python 3.10+. No runtime dependencies.
19
+
20
+ > PyPI publishing is wired up (Trusted Publishing, see `.github/workflows/release.yml`)
21
+ > but no release has been cut yet, so `pip install refaudit` will not work until
22
+ > the first tagged release. Use the command above in the meantime.
23
+
24
+ ## Quick start
25
+
26
+ ```bash
27
+ # every entry in the file
28
+ refaudit refs.bib --email you@uwaterloo.ca
29
+
30
+ # only the entries actually cited in the paper, which is usually what you want
31
+ refaudit refs.bib --email you@uwaterloo.ca --tex paper/sections --only-cited
32
+ ```
33
+
34
+ `--email` is required. Crossref and OpenAlex give identified callers a separate,
35
+ more reliable request pool, and it is the courtesy their documentation asks for.
36
+ Set it once instead of typing it each time:
37
+
38
+ ```bash
39
+ export REFAUDIT_EMAIL=you@uwaterloo.ca
40
+ ```
41
+
42
+ Results are written to `refaudit-out/` as `reference_check.txt` (readable) and
43
+ `reference_check.csv` (sortable), and printed to stdout.
44
+
45
+ ### Working from Overleaf
46
+
47
+ Download the `.bib` (Menu → Download → Source, or just the file), then point
48
+ `--tex` at the unzipped `sections/` directory so `--only-cited` can tell which
49
+ keys actually reach the PDF:
50
+
51
+ ```bash
52
+ refaudit sample-base.bib --email you@uwaterloo.ca --tex sections/ --only-cited
53
+ ```
54
+
55
+ ## What it reports
56
+
57
+ | Verdict | Meaning |
58
+ |---|---|
59
+ | `TITLE_MISMATCH` | an identifier resolved to a different paper — check first |
60
+ | `DEAD_DOI` | the DOI is not registered |
61
+ | `AUTHOR_MISMATCH` | titles agree, first author does not |
62
+ | `YEAR_MISMATCH` | titles agree, year is off by more than one |
63
+ | `NOT_FOUND` | no identifier, and no title match anywhere |
64
+ | `UNVERIFIED` | **a source was unreachable — this says nothing about the entry** |
65
+ | `SKIPPED` | `@misc`/`@online` with no identifier; nothing to check against |
66
+ | `OK` | resolved and consistent |
67
+
68
+ Exit status is `1` if there is at least one finding, `0` if not, `2` on a usage
69
+ error — so it drops into CI or a pre-submission script.
70
+
71
+ ## Options
72
+
73
+ | Flag | Effect |
74
+ |---|---|
75
+ | `--email` | contact address sent to the APIs (or `REFAUDIT_EMAIL`). Required. |
76
+ | `--tex PATH` | LaTeX file or directory, used to work out which keys are cited |
77
+ | `--only-cited` | check only cited keys; requires `--tex` |
78
+ | `--resolvers` | comma-separated subset of `crossref:doi`, `arxiv:id`, `openalex`, `crossref:title` |
79
+ | `--out DIR` | output directory (default `refaudit-out`) |
80
+ | `--cache PATH` / `--no-cache` | cache location, or disable it |
81
+ | `--ttl-days N` | how long cached results stay valid (default 90) |
82
+ | `--timeout N` | per-request timeout in seconds (default 20) |
83
+ | `--title-match N` | similarity at or above which two titles are the same work (default 0.75) |
84
+ | `--quiet` | suppress per-entry progress, print only the summary |
85
+
86
+ A run over a few hundred references takes minutes, because it is deliberately
87
+ paced. Successful lookups are cached, so it is safe to interrupt with Ctrl-C and
88
+ re-run — it picks up where it stopped.
89
+
90
+ ## The one design rule
91
+
92
+ **"We could not check" and "this is wrong" are different answers and never
93
+ collapse into each other.**
94
+
95
+ This sounds obvious and is easy to get wrong. arXiv rate-limits whole networks;
96
+ when that happens, a naive checker either silently passes the entry (false
97
+ comfort, the worse failure) or falls back to a title search, finds something
98
+ loosely related, and reports a mismatch (false alarm, which trains you to ignore
99
+ it). Both are worse than saying "I could not check this one."
100
+
101
+ So every resolver returns exactly one of `Found`, `NotFound`, or `Unavailable`,
102
+ and only `Found` can produce a negative verdict. `UNVERIFIED` results are listed
103
+ separately from findings and are never cached, so a transient outage does not
104
+ get baked into later runs.
105
+
106
+ Relatedly, evidence is weighted by strength: a DOI that Crossref does not
107
+ recognise is a finding, but a title search returning something different is only
108
+ a finding when there was no identifier to go on. Otherwise every arXiv-only
109
+ workshop paper that Crossref does not index would be flagged.
110
+
111
+ ## Rate limiting
112
+
113
+ Each source declares the limit its own documentation specifies, next to the code
114
+ that calls it:
115
+
116
+ | Source | Rate used | Why |
117
+ |---|---|---|
118
+ | Crossref | 2/s, then whatever the response headers say | Crossref publishes `X-Rate-Limit-Limit` / `-Interval`; the client reads and obeys them |
119
+ | arXiv | 1 per 3s | [arXiv's terms of use](https://info.arxiv.org/help/api/tou.html) specify exactly this |
120
+ | OpenAlex | 3/s | [documented](https://docs.openalex.org/how-to-use-the-api/rate-limits-and-authentication) ceiling is 10/s and 100k/day; we use a third of it |
121
+
122
+ A `429` is treated as instruction rather than noise: `Retry-After` is honoured,
123
+ the token bucket is permanently halved, and after repeated refusals a circuit
124
+ breaker stops asking that host so the rest of the run still finishes. Retries use
125
+ exponential backoff with full jitter.
126
+
127
+ `--email` is required because Crossref and OpenAlex give identified callers a
128
+ separate, more reliable pool, and it is the courtesy their docs ask for.
129
+
130
+ ## Security
131
+
132
+ - **HTTPS only.** Plain-`http` URLs are refused, not silently upgraded.
133
+ - **Bounded redirects**, kept on https, so a redirect cannot downgrade transport
134
+ or forward the `mailto` identifier somewhere unexpected.
135
+ - **Response size cap and per-request timeouts** on every call.
136
+ - **XML is parsed with entity declarations refused**, blocking billion-laughs and
137
+ XXE. Uses `defusedxml` when installed, otherwise a hardened stdlib path; both
138
+ raise the same exception type so callers cannot miss one.
139
+ - **DOIs and arXiv ids are validated against a pattern before being interpolated
140
+ into a request path**, so a malformed field cannot steer the URL.
141
+ - **No credentials in code.** Optional API keys come from the environment and are
142
+ sent as headers, never query parameters, so they stay out of logs.
143
+ - **No runtime dependencies.** This gets installed in a hurry near a deadline,
144
+ often on a machine someone else administers; that is the wrong moment to widen
145
+ the supply chain.
146
+
147
+ ## Troubleshooting
148
+
149
+ **Lots of `UNVERIFIED` results.** A source refused your network. arXiv in
150
+ particular rate-limits by IP and will 429 an entire institution or VPN
151
+ regardless of your own pace. These are not findings — the entries were simply
152
+ not checked. Try again later, from a different network, or lean on OpenAlex,
153
+ which indexes arXiv identifiers too:
154
+
155
+ ```bash
156
+ refaudit refs.bib --email you@uwaterloo.ca --resolvers crossref:doi,openalex
157
+ ```
158
+
159
+ **A correct entry is flagged `NOT_FOUND`.** Workshop papers, theses and
160
+ tech reports are often in no citation index. If the entry has no DOI and no
161
+ arXiv id there is nothing to verify it against; confirm it by hand and move on.
162
+
163
+ **A correct entry is flagged `TITLE_MISMATCH`.** This one is worth taking
164
+ seriously: it means the DOI or arXiv id in your `.bib` resolves to a different
165
+ paper. Usually the identifier was copied from the wrong row, or generated rather
166
+ than looked up. Check the `found` line in the report against what you meant to
167
+ cite.
168
+
169
+ **Everything is `SKIPPED`.** `@misc` and `@online` entries with no identifier
170
+ cannot be checked. That is expected for datasets, blog posts and software.
171
+
172
+ ## Library use
173
+
174
+ ```python
175
+ from refaudit import Checker, default_resolvers, parse_file
176
+
177
+ entries = parse_file("refs.bib")
178
+ checker = Checker(default_resolvers("you@university.edu"))
179
+
180
+ for result in checker.check_all(entries):
181
+ if result.verdict.is_finding:
182
+ print(result.key, result.verdict.value, result.note)
183
+ ```
184
+
185
+ `Resolver` is a `Protocol`: implement `name`, `rate`, `can_handle` and `resolve`
186
+ to add a source, and pass it to `Checker` alongside the built-ins.
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ pip install -e ".[dev]"
192
+ pytest # offline: the suite uses fake resolvers and never hits a network
193
+ ruff check .
194
+ mypy src
195
+ ```
196
+
197
+ `main` is protected: changes go through a pull request with a code-owner review,
198
+ and CI must pass on Python 3.10-3.13. See [CONTRIBUTING.md](CONTRIBUTING.md).
199
+
200
+ ## Licence
201
+
202
+ MIT.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.20"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "refaudit"
7
+ version = "0.1.0"
8
+ description = "Verify bibliography entries against Crossref, arXiv and OpenAlex before you submit."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ keywords = ["bibtex", "citations", "crossref", "openalex", "arxiv", "research-integrity"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Intended Audience :: Science/Research",
16
+ "Topic :: Text Processing :: Markup :: LaTeX",
17
+ ]
18
+ # No runtime dependencies on purpose: this gets installed in a hurry, close to a
19
+ # deadline, often on a machine someone else administers. defusedxml is used if
20
+ # present but a hardened stdlib parser is built in.
21
+ dependencies = []
22
+
23
+ [project.optional-dependencies]
24
+ xml = ["defusedxml>=0.7"]
25
+ dev = ["pytest>=7", "mypy>=1.5", "ruff>=0.4"]
26
+
27
+ [project.scripts]
28
+ refaudit = "refaudit.cli:main"
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/uw-share-lab/refaudit"
32
+ Issues = "https://github.com/uw-share-lab/refaudit/issues"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/refaudit"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ addopts = "-q"
40
+
41
+ [tool.ruff]
42
+ line-length = 100
43
+ target-version = "py310"
44
+
45
+ [tool.mypy]
46
+ python_version = "3.10"
47
+ warn_unused_ignores = true
48
+ disallow_untyped_defs = false
@@ -0,0 +1,39 @@
1
+ """refaudit -- verify bibliography entries against external indexes.
2
+
3
+ Built for the case where a venue runs an automated check for hallucinated or
4
+ malformed references and a false entry costs you a desk reject.
5
+
6
+ from refaudit import Checker, parse_file, default_resolvers
7
+
8
+ entries = parse_file("refs.bib")
9
+ checker = Checker(default_resolvers("you@example.org"))
10
+ for result in checker.check_all(entries):
11
+ print(result.key, result.verdict.value)
12
+ """
13
+
14
+ from .bibtex import cited_keys, find_tex, parse_file, parse_string
15
+ from .cache import Cache
16
+ from .checker import Checker, Thresholds
17
+ from .models import CheckResult, Entry, Found, NotFound, Record, Unavailable, Verdict
18
+ from .resolvers import default_resolvers
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ __all__ = [
23
+ "Cache",
24
+ "CheckResult",
25
+ "Checker",
26
+ "Entry",
27
+ "Found",
28
+ "NotFound",
29
+ "Record",
30
+ "Thresholds",
31
+ "Unavailable",
32
+ "Verdict",
33
+ "__version__",
34
+ "cited_keys",
35
+ "default_resolvers",
36
+ "find_tex",
37
+ "parse_file",
38
+ "parse_string",
39
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())