paperlint 3.1.1 → 3.1.3
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.
- package/CLAUDE.md +18 -97
- package/CONTRIBUTING.md +7 -21
- package/README.md +11 -7
- package/dist/adapters/references/clock.io.d.ts +5 -0
- package/dist/adapters/references/clock.io.d.ts.map +1 -0
- package/dist/adapters/references/clock.io.js +7 -0
- package/dist/adapters/references/clock.io.js.map +1 -0
- package/dist/adapters/references/index.d.ts +24 -2
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +125 -15
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/build.d.ts +4 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +10 -3
- package/dist/build.js.map +1 -1
- package/dist/domain/lookup-cache.d.ts +75 -0
- package/dist/domain/lookup-cache.d.ts.map +1 -0
- package/dist/domain/lookup-cache.js +137 -0
- package/dist/domain/lookup-cache.js.map +1 -0
- package/dist/ports/check-references.d.ts +12 -2
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/check-references.js +0 -5
- package/dist/ports/check-references.js.map +1 -1
- package/dist/references.d.ts +11 -3
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +63 -15
- package/dist/references.js.map +1 -1
- package/docs/prior-art/README.md +11 -10
- package/docs/prior-art/citation-checkers.md +174 -0
- package/docs/references.md +126 -0
- package/docs/testing.md +118 -0
- package/docs/toolchain.md +6 -4
- package/package.json +1 -1
- package/skills/paper-pipeline/references/artifact-checklist.md +7 -7
- package/skills/render-paper/checkers-requirements.txt +30 -19
- package/skills/render-paper/ensure-checkers.sh +20 -4
- package/skills/verify-citations/SKILL.md +26 -10
- package/skills/verify-citations/SKILL.md.spec.ts +25 -9
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +28 -10
- package/skills/verify-citations/scripts/bib-authors.mjs +5 -6
- package/skills/verify-citations/scripts/verify-cites.mjs +296 -141
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +356 -20
- package/skills/verify-citations/scripts/verify-cites.test.mjs +18 -2
- package/src/adapters/references/clock.io.ts +8 -0
- package/src/adapters/references/index.test.ts +341 -9
- package/src/adapters/references/index.ts +198 -29
- package/src/build.ts +10 -3
- package/src/domain/lookup-cache.test.ts +178 -0
- package/src/domain/lookup-cache.ts +202 -0
- package/src/ports/check-references.ts +16 -2
- package/src/references.test.ts +160 -10
- package/src/references.ts +93 -17
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Verifying a bibliography against reality — is this solved?
|
|
2
|
+
|
|
3
|
+
**Question this file answers:** could `paperlint`'s "does every `.bib` entry name a real work, with the
|
|
4
|
+
right authors" check depend on an existing, maintained package instead of shelling out to Crossref /
|
|
5
|
+
Semantic Scholar / arXiv / DBLP itself?
|
|
6
|
+
|
|
7
|
+
**Verdict: no dependable dependency exists, but two API-level facts we were not using would cut our
|
|
8
|
+
own runtime by roughly 5–8×.** Depend on nothing; keep the in-repo checker; add Semantic Scholar's
|
|
9
|
+
`/paper/batch`, OpenAlex's `|`-joined `filter=doi:`, and the Crossref polite pool's raised concurrency.
|
|
10
|
+
Everything found in the "verify existence + author list against a real record" niche is either (a) a
|
|
11
|
+
research prototype released alongside one paper, (b) a solo weekend project pushed in the last few
|
|
12
|
+
months with single-digit-to-low-hundred commits and no PyPI/npm listing, or (c) a Zotero/LaTeX tool
|
|
13
|
+
that solves an adjacent problem (parsing, key generation, unused-entry detection) and does not touch
|
|
14
|
+
external databases at all. None ship a bulk/batch mode faster than calling the same three-to-five
|
|
15
|
+
free APIs ourselves would already be, and every one of them is Python — none is an npm package, so
|
|
16
|
+
"depend on it" would mean shelling out to a Python subprocess from a Node 22 tool. That includes
|
|
17
|
+
`rebiber` itself: this file corrects our earlier read of it (below) — the maintained line is GitHub
|
|
18
|
+
1.4, not the 2021 PyPI 1.1.3 our toolchain currently installs — but even 1.4 doesn't clear the bar,
|
|
19
|
+
because its bundled venue data is ML/NLP/CV/IR only.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Comparison table
|
|
24
|
+
|
|
25
|
+
Checked 2026-09-27, via GitHub READMEs/WebFetch summaries unless marked otherwise. Star/commit counts
|
|
26
|
+
and "last activity" are as reported by the fetch, not independently re-verified against the live repo
|
|
27
|
+
— treat anything below as **[B]** (read a secondary description) rather than **[A]** (read the
|
|
28
|
+
primary source myself), except where a direct quote is given.
|
|
29
|
+
|
|
30
|
+
| tool | verifies | online/offline | install | license | maintenance signal | batch/bulk | verdict |
|
|
31
|
+
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| **rebiber 1.4** (yuchenlin, GitHub HEAD — **not** the PyPI package) | rewrites entry to DBLP/ACL-anthology record (existence + authors + venue, by _replacing_ the entry); `--dry-run --report` now previews without rewriting, `-s True` keeps short venue names, `--live-lookup` falls back to a live DBLP search on a local-data miss | offline-first against a bundled, **monthly-refreshed** local DBLP/anthology snapshot; only `--live-lookup` goes to the network | **GitHub only, and its own README says so**: _"Install from GitHub only. We do not publish or support PyPI (`pip install rebiber` is a 2021 1.1.3 release)."_ | not stated in the fetched excerpt | active — 1.4 migrates to `bibtexparser 2`, raises the Python floor to 3.10 | one bib file per run; the local-snapshot design makes that run fast regardless | **narrower than it looks, not simply rejected** — see the callout right below the table |
|
|
33
|
+
| **hallucite** (se-uhd) | title + authors primarily, DOI/venue/year via 5 DBs (offline DBLP mirror first, then CrossRef/DOI/arXiv/OpenAlex/S2); LLM only for human-in-the-loop triage of what's left unresolved | **offline-first**: local DBLP mirror built from Dagstuhl monthly snapshot (~3.8 GB, ~5 min build), only escalates to the network for misses | `mise install` + `mise run fetch-dblp-dump` + `mise run build-dblp`; ships as a Claude Code / Codex CLI plugin, not a pip/npm package | not stated in README | 83 commits, active CI, no version tag seen | offline pass over the whole PDF's refs is effectively bulk (SQLite lookups); network fallback is per-entry | closest in spirit to our DBLP pass — the offline-mirror idea is real and reusable _as an idea_, but it's a Python-only agent-skill bundle, not an importable package, and license is unstated |
|
|
34
|
+
| **clibib** (from arXiv:2604.03159) | deterministic **identifier resolution** — given a DOI/arXiv-id/ISBN/PMID it returns exactly one record via the Zotero Translation Server (falls back to CrossRef on an empty `/search`); title match via token-Jaccard ≥0.3 | online | PyPI, MIT, "≈230 lines of Python, 2 runtime deps (`requests`, `python-dotenv`)" | MIT | released alongside a Sept-2026 arXiv paper; no independent release history yet | not documented as batched — one identifier per call | narrower than what we need: it fixes/retrieves a record given a _known-good_ id, not "does this claimed entry match a real one" — a retrieval tool, not a verifier |
|
|
35
|
+
| **bibverify** (Hylouis233) | DOI/PMID/PMCID/arXiv + title/authors/year/venue/pages across **9 providers** (Crossref, OpenAlex, S2, PubMed, Europe PMC, CORE, DBLP, arXiv, bioRxiv) | online, with an **expiring SQLite cache of successful responses only** (TTL 168h default) | `pip`, `uv tool install`, Docker; npm wrapper (`npx @hylouis233/bibverify`) "planned v0.4.0", not shipped | MIT | v0.3.0, 51 commits, 100 stars, CI across OS/Python matrix — the most conventionally "maintained"-looking of the group | shared connection pool + retry/backoff/`Retry-After`, `delay_between_requests` (default 0.5s); README's own benchmark is explicitly disclaimed as "not a complete scientific evaluation" | the best-engineered of the bunch, but pre-1.0, single-maintainer, npm channel not live yet |
|
|
36
|
+
| **citeproof** (xiehuanyi) | existence + author/year/venue/DOI/arXiv-id match across Crossref, DBLP, S2, OpenAlex, arXiv; verdicts `verified / metadata_mismatch / inconclusive / likely_hallucinated` | online only | `pip install git+…`, not on PyPI | MIT | **4 commits visible** — early/inactive | `--workers` flag (default 3); README benchmarks 1 worker ≈47s vs 3 workers ≈4s for its test set, and warns raising workers past API throughput does nothing | too new/thin to trust; but its own worker benchmark independently confirms our finding that concurrency, not CPU, is the lever |
|
|
37
|
+
| **reference-validator** (bet-lab) | DOI vs Crossref, arXiv detection, "dual validation" flagging local-vs-API conflicts; also touches DBLP/PubMed/Zenodo/DataCite/OpenAlex | online | `uv tool install git+…` | Apache-2.0 | 60 commits, no date surfaced | `--workers` (default 10), `--delay` (default 1.0s) | same shape as the others: solo project, git-only install |
|
|
38
|
+
| **Bibtex-Verifier** (Ritesh778 / merfanian forks) | Crossref + OpenAlex + Semantic Scholar; web-app framing ("catch errors & hallucinated references") | online | web app / local Flask-style app, not a library | not confirmed | multiple parallel forks (Ritesh778, merfanian, Slowist-Lee) — a sign of "everyone reinvents this small thing," not of one canonical maintained project | not documented | a UI tool for a human pasting one `.bib`, wrong shape for a CI dependency |
|
|
39
|
+
| **VeriBib** (Ferry-Li) | cross-references against **Google Scholar** to catch fabrications | online, scrapes Google Scholar | GitHub only | not confirmed | not confirmed | not documented | Google Scholar has no public API and scraping it is a ban-prone, unstable foundation — worse than what we have |
|
|
40
|
+
| **Better BibTeX** (Zotero plugin) | _nothing external_ — it generates stable citation keys and exports formats from a Zotero library the user already curated | offline (operates on your own library) | Zotero plugin | AGPL-3.0 | mature, years of releases, widely used | n/a | **not a verifier at all** — it assumes your Zotero entry is already correct and just keeps the citation key stable across edits. Wrong tool for this problem, included because it's the obvious "isn't this solved by reference-manager software" question |
|
|
41
|
+
| **checkcites** (CTAN/LuaTeX) | unused/undefined references between `.bib` and `.aux`/`.bcf` — a **local consistency** check, no external database at all | offline | TeX Live (bundled) | mature CTAN package | stable, part of TeX Live | n/a | answers a different question ("did I cite everything I defined"), not "is this a real paper" |
|
|
42
|
+
| **GPTZero / Paperpile "citation checker"** | existence + hallucination flagging, marketed at authors/reviewers after the NeurIPS-2025 incident (GPTZero found 53/4,841 accepted papers with fabricated cites) | online, hosted SaaS | web upload only, no API/CLI for automation | proprietary | commercial product, actively marketed | unknown, not exposed for programmatic bulk use | not integrable into a CLI linter at all — no API surface to call from Node |
|
|
43
|
+
|
|
44
|
+
### 🔴 Correction to our own prior verdict on rebiber — and a live bug it exposes
|
|
45
|
+
|
|
46
|
+
Our original rejection of rebiber (setuptools-broken install, DBLP-snapshot miss, forced rewrite to
|
|
47
|
+
long booktitles) was measured against **PyPI `rebiber` 1.1.3, released 2021-09-08**. That is not the
|
|
48
|
+
tool's current state, and — checked while writing this file — **it is not even the artifact rebiber's
|
|
49
|
+
own maintainer wants installed.** Its README says outright:
|
|
50
|
+
|
|
51
|
+
> Install from GitHub only. We do not publish or support PyPI (`pip install rebiber` is a 2021 1.1.3
|
|
52
|
+
> release).
|
|
53
|
+
|
|
54
|
+
`paperlint`'s toolchain currently pins `rebiber>=1.1.3` and installs it **from PyPI** — meaning it is
|
|
55
|
+
running exactly the release the upstream maintainer disowns, five point-releases and a bibtexparser
|
|
56
|
+
major-version migration behind. Filed as **paperlint#114**.
|
|
57
|
+
|
|
58
|
+
GitHub HEAD (1.4) fixes two of our three original complaints in principle: `--dry-run --report` can
|
|
59
|
+
preview matches without rewriting the entry (no forced long booktitle unless asked for), and
|
|
60
|
+
`-s True` keeps short venue abbreviations; `--live-lookup` adds a live-DBLP fallback for the
|
|
61
|
+
local-snapshot-miss case that burned us before. It does **not** fix the shape of the underlying
|
|
62
|
+
problem for us: its bundled venue data is **ML/NLP/CV/IR only** — no CCS, USENIX Security, S&P/Oakland,
|
|
63
|
+
ICSE, or FSE — so any reference to a security or software-engineering venue falls straight through to
|
|
64
|
+
`--live-lookup` (a live, unbatched DBLP query, the exact bottleneck this file is about) or is left
|
|
65
|
+
unmatched. **Verdict on rebiber specifically: worth re-pointing our toolchain at GitHub 1.4 instead of
|
|
66
|
+
the disowned PyPI 1.1.3 regardless (that's a straight bug-fix, tracked in #114), but not worth adopting
|
|
67
|
+
as the existence/author checker itself** — its offline-snapshot idea is sound (same shape as
|
|
68
|
+
hallucite's, below), its venue coverage is not ours, and it still rewrites rather than reports unless
|
|
69
|
+
`--dry-run` is remembered on every invocation.
|
|
70
|
+
|
|
71
|
+
## What "solved" would require, and why nothing here clears the bar
|
|
72
|
+
|
|
73
|
+
A dependency worth taking has to be (1) **on a registry** (npm ideally, or a PyPI package with a clean
|
|
74
|
+
wheel — not `pip install git+…`), (2) **maintained** past a single paper's release, (3) **licensed**
|
|
75
|
+
clearly, and (4) **faster or no slower** than calling the free APIs directly. Every project above fails
|
|
76
|
+
at least one of these:
|
|
77
|
+
|
|
78
|
+
- **rebiber, citeproof, clibib, reference-validator, bibverify's Python line** — git-only or PyPI-only
|
|
79
|
+
install, none published to npm. A Node 22 linter would be shelling out to a Python venv it has to
|
|
80
|
+
provision itself — the same failure class that sank `rebiber` (its `bibtexparser` wheel breaking on
|
|
81
|
+
modern `setuptools`), just relocated to a different Python package.
|
|
82
|
+
- **hallucite** is the one idea genuinely worth stealing rather than depending on: an **offline DBLP
|
|
83
|
+
mirror**, rebuilt monthly from the Dagstuhl CC0 dump (`dblp.xml.gz`, updated on every DBLP rebuild —
|
|
84
|
+
<https://dblp.org/faq/1474679.html>), turns "one DBLP query per reference, rate-limited" into "one
|
|
85
|
+
3.8 GB SQLite build, then instant local lookups." That is a real fix for the exact bottleneck named
|
|
86
|
+
in our own script's docstring ("DBLP rate limits at >1 req/s"). It is not something to `npm install`;
|
|
87
|
+
it is a pattern (`fetch dump → build local index → query locally → escalate misses`) our own script
|
|
88
|
+
could adopt without taking hallucite as a dependency.
|
|
89
|
+
- **Everything web-UI-shaped** (Bibtex-Verifier forks, GPTZero, Paperpile) has no CLI/API a build step
|
|
90
|
+
can call.
|
|
91
|
+
- **Better BibTeX and checkcites** are not verifiers against reality at all — they were checked because
|
|
92
|
+
"doesn't Zotero/LaTeX already solve this" is the natural next question, and the honest answer is no:
|
|
93
|
+
BBT trusts whatever is already in your Zotero library, and checkcites only compares your `.bib`
|
|
94
|
+
against your own `.tex`, never against an external database.
|
|
95
|
+
- **No maintained project publishes a batch/bulk verification API of its own.** The closest thing —
|
|
96
|
+
citeproof's and reference-validator's `--workers` flags — is just client-side concurrency against the
|
|
97
|
+
_same_ public APIs we already call, with no batching at the HTTP level. Their own numbers (citeproof:
|
|
98
|
+
1 worker ≈47s → 3 workers ≈4s on its test set, extra workers "no faster") independently confirm what
|
|
99
|
+
our own 3.5–7 min runtime already suggests: **the bottleneck is round trips, not our per-request
|
|
100
|
+
logic**, which points straight at the API-level fixes below rather than at swapping tools.
|
|
101
|
+
|
|
102
|
+
## API-level speedups we are not using, ranked by expected gain
|
|
103
|
+
|
|
104
|
+
1. **Semantic Scholar `/graph/v1/paper/batch`** — up to **500 mixed identifiers (DOI/arXiv/PMID/…) in
|
|
105
|
+
one POST**, replacing up to 500 of our per-entry calls with one round trip
|
|
106
|
+
(<https://api.semanticscholar.org/api-docs/graph#tag/Paper-Data/operation/post_graph_get_papers>).
|
|
107
|
+
For a 27-reference file this alone turns Semantic Scholar's contribution from ~27 sequential
|
|
108
|
+
requests into 1. **Highest expected gain of the three**, because it removes an entire per-entry
|
|
109
|
+
network round-trip rather than just widening its allowed rate.
|
|
110
|
+
|
|
111
|
+
2. **DBLP: switch from live per-entry queries to an offline mirror**, the hallucite pattern above.
|
|
112
|
+
DBLP's live API has no published numeric rate limit — the FAQ only asks callers to "tone down"
|
|
113
|
+
scripts and offers a self-hosted SPARQL/dump route for anyone needing volume
|
|
114
|
+
(<https://dblp.org/faq/Am+I+allowed+to+crawl+the+dblp+website.html>) — which is exactly the
|
|
115
|
+
unpredictability our docstring already blames for the 3.5–7 min runtime ("DBLP rate limits at >1
|
|
116
|
+
req/s, timeouts, retries"). A monthly dump (`https://dblp.org/xml/release/`, or the Dagstuhl-hosted
|
|
117
|
+
snapshot) turns every DBLP author-sequence comparison into a local SQLite lookup. **Second-highest
|
|
118
|
+
gain** because it's specifically the part our own docstring names as the slow one, but it costs a
|
|
119
|
+
~3.8 GB local artifact and a monthly refresh job — a real added complexity, not free.
|
|
120
|
+
|
|
121
|
+
3. **OpenAlex: `filter=doi:<doi1>|<doi2>|…`**, joining **up to 50–100 DOIs per call** with `|` (OR)
|
|
122
|
+
inside one `filter=` parameter (confirmed by OpenAlex's own blog post,
|
|
123
|
+
<https://blog.openalex.org/fetch-multiple-dois-in-one-openalex-api-request/>, and its help docs).
|
|
124
|
+
For a paper with ~27 references this collapses OpenAlex's entire contribution into 1 request instead
|
|
125
|
+
of up to 27. Ranked third only because our current script does not query OpenAlex at all today per
|
|
126
|
+
the task description (Crossref + S2 + arXiv + DBLP) — so this is a net-new win, not a replacement of
|
|
127
|
+
an existing slow path, and adding a fourth provider is itself a small scope increase to weigh.
|
|
128
|
+
|
|
129
|
+
4. **Crossref: send `mailto=` for the polite pool, and prefer `/works/{doi}` (single-record) over
|
|
130
|
+
`/works?query=` (list) lookups.** Crossref's Dec-2025 rate-limit revision documents the public pool
|
|
131
|
+
at 5 req/s (single-record) / 1 req/s (list) with concurrency 1, versus the polite pool at **10 req/s
|
|
132
|
+
/ 3 req/s with concurrency 3** (<https://www.crossref.org/blog/announcing-changes-to-rest-api-rate-limits/>).
|
|
133
|
+
Our script already sends `mailto` (see its docstring: "THE ADDRESS IS THE CONSUMER'S, NOT OURS"), so
|
|
134
|
+
this is confirmation the polite pool is already in effect, not a new lever — but the single-record
|
|
135
|
+
vs. list distinction is: a citation with a known DOI should hit `/works/{doi}` (5–10 req/s) rather
|
|
136
|
+
than the title-search `/works?query.bibliographic=` (1–3 req/s) fallback path, and it's worth
|
|
137
|
+
auditing which of our 27 references fall through to the slower list endpoint today.
|
|
138
|
+
|
|
139
|
+
Combined, (1)+(3) should remove most of the _count_ of round trips for entries that carry a DOI or
|
|
140
|
+
arXiv id (the common case), and (2) removes the specific bottleneck our own docstring names for
|
|
141
|
+
authors-vs-venue checks. None of these require a new dependency — they are call-shape changes to the
|
|
142
|
+
same four APIs the script already knows how to speak to (plus adding OpenAlex, which it does not
|
|
143
|
+
currently query).
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Status
|
|
148
|
+
|
|
149
|
+
Written 2026-09-27 from WebSearch/WebFetch passes over GitHub READMEs, arXiv:2604.03159, and the
|
|
150
|
+
Crossref/OpenAlex/Semantic Scholar/DBLP official docs — no tool below was cloned and run locally, so
|
|
151
|
+
every non-quoted maintenance/star claim is **[B]**, and the "closest to solved" claim (hallucite) rests
|
|
152
|
+
on its README's self-description, not an independent audit of its DBLP-mirror code.
|
|
153
|
+
|
|
154
|
+
Sources:
|
|
155
|
+
[rebiber](https://github.com/yuchenlin/rebiber) ·
|
|
156
|
+
[rebiber README (raw, checked for the 1.4 / PyPI-disowned correction)](https://raw.githubusercontent.com/yuchenlin/rebiber/main/README.md) ·
|
|
157
|
+
[hallucite](https://github.com/se-uhd/hallucite) ·
|
|
158
|
+
[BibTeX Citation Hallucinations in Scientific Publishing Agents (arXiv:2604.03159)](https://arxiv.org/abs/2604.03159) ·
|
|
159
|
+
[bibverify](https://github.com/Hylouis233/bibverify) ·
|
|
160
|
+
[citeproof](https://github.com/xiehuanyi/citeproof) ·
|
|
161
|
+
[reference-validator](https://github.com/bet-lab/reference-validator) ·
|
|
162
|
+
[Bibtex-Verifier](https://github.com/merfanian/Bibtex-Verifier) ·
|
|
163
|
+
[VeriBib](https://github.com/Ferry-Li/VeriBib) ·
|
|
164
|
+
[Better BibTeX for Zotero](https://retorque.re/zotero-better-bibtex/) ·
|
|
165
|
+
[checkcites (CTAN)](https://ctan.org/tex-archive/support/checkcites) ·
|
|
166
|
+
[Paperpile — a new tool to fight hallucinations in preprints](https://paperpile.com/blog/citation-checker-hallucinations/) ·
|
|
167
|
+
[Semantic Scholar Graph API — batch endpoint](https://api.semanticscholar.org/api-docs/graph) ·
|
|
168
|
+
[OpenAlex blog — fetch multiple DOIs in one request](https://blog.openalex.org/fetch-multiple-dois-in-one-openalex-api-request/) ·
|
|
169
|
+
[OpenAlex — Filter docs](https://help.openalex.org/api/filtering/) ·
|
|
170
|
+
[Crossref — Announcing changes to REST API rate limits](https://www.crossref.org/blog/announcing-changes-to-rest-api-rate-limits/) ·
|
|
171
|
+
[Crossref — Access and authentication (polite pool)](https://www.crossref.org/documentation/retrieve-metadata/rest-api/access-and-authentication/) ·
|
|
172
|
+
[DBLP — How can I download the whole dblp dataset?](https://dblp.org/faq/1474679.html) ·
|
|
173
|
+
[DBLP — Am I allowed to crawl the dblp website?](https://dblp.org/faq/Am+I+allowed+to+crawl+the+dblp+website.html) ·
|
|
174
|
+
[Crossref — Public data files and snapshots](https://www.crossref.org/documentation/retrieve-metadata/bulk-downloads/)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# The reference check
|
|
2
|
+
|
|
3
|
+
`paperlint build` checks every entry of a paper's bibliography against the services that know
|
|
4
|
+
about published work, after the PDF is built. `paperlint lint` then reports what the check found,
|
|
5
|
+
offline. This page explains how the check works, the file it keeps in your paper's folder, and
|
|
6
|
+
what happens in CI and without a network.
|
|
7
|
+
|
|
8
|
+
## What is checked, and where
|
|
9
|
+
|
|
10
|
+
For each entry in the bibliography (the `filecontents` block inside `paper.tex`, else `refs.bib`):
|
|
11
|
+
|
|
12
|
+
| question | asked of |
|
|
13
|
+
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
14
|
+
| does the cited work exist, and does its title match? | Crossref, OpenAlex, Semantic Scholar, arXiv |
|
|
15
|
+
| does a DOI that no registry knows really not exist? | doi.org, the DOI authority |
|
|
16
|
+
| for an entry that claims a published venue, are the authors the published version's, not the preprint's? | DBLP |
|
|
17
|
+
|
|
18
|
+
CVE identifiers are not checked by `build`: a bibliography has no CVE field, so none is extracted.
|
|
19
|
+
The verify-citations script checks a `cve` against NVD when it is given citations as JSON (see the
|
|
20
|
+
skill), and that is the only place NVD is asked.
|
|
21
|
+
|
|
22
|
+
A work that is simply not found is `unresolvable`, never a failure: plenty of real work is not
|
|
23
|
+
indexed. Only a positive disproof fails an entry, for example a DOI that resolves to a different
|
|
24
|
+
paper, or one the DOI authority says does not exist.
|
|
25
|
+
|
|
26
|
+
The verdicts land in `<paper>/_build/references.json`. `paperlint lint` reads that file and reports:
|
|
27
|
+
|
|
28
|
+
| rule | when |
|
|
29
|
+
| -------------------- | ----------------------------------------------------------------- |
|
|
30
|
+
| `paper/cite-exists` | an entry's identifier provably does not resolve to the work cited |
|
|
31
|
+
| `paper/author-list` | an entry's authors are the preprint's under a published venue |
|
|
32
|
+
| `paper/refs-fresh` | the bibliography changed since the build checked it — build again |
|
|
33
|
+
| `paper/refs-checked` | no build has checked the references, or the last one could not |
|
|
34
|
+
|
|
35
|
+
The step never fails the build. Without a network the PDF is still built and lint warns that the
|
|
36
|
+
references were not checked.
|
|
37
|
+
|
|
38
|
+
## `repro/references-cache.json` — commit it
|
|
39
|
+
|
|
40
|
+
Asking five services about every entry is slow. DBLP asks clients to pause between requests, and a
|
|
41
|
+
slow service is retried. Before this cache existed, a 27-reference paper spent 217 seconds on the
|
|
42
|
+
check, on every build, and a 51-reference paper did not finish inside a 12-minute CI job
|
|
43
|
+
([#107](https://github.com/zernie/paperlint/issues/107)).
|
|
44
|
+
|
|
45
|
+
So the build keeps every answer it receives in `<paper>/repro/references-cache.json`, and the next
|
|
46
|
+
build asks only what that file cannot answer:
|
|
47
|
+
|
|
48
|
+
- An unchanged bibliography whose answers are under 30 days old asks nothing and makes no network request at all, not even the check
|
|
49
|
+
for whether the services can be reached.
|
|
50
|
+
- Adding or editing an entry asks only about that entry. Answers are stored per identifier (DOI,
|
|
51
|
+
arXiv id, or title), so changing an entry's DOI or title is what makes it be asked again.
|
|
52
|
+
- A request that failed is not an answer and is not stored, so the next build asks it again.
|
|
53
|
+
- "Does not exist" from doi.org is not stored either. A DOI cited before the authority has
|
|
54
|
+
registered it would otherwise stay "does not exist" in the file forever; it is asked again on each
|
|
55
|
+
build until the authority confirms it.
|
|
56
|
+
|
|
57
|
+
## When a service refuses
|
|
58
|
+
|
|
59
|
+
Free services rate-limit. From a CI runner or a shared network, Semantic Scholar answers `429`,
|
|
60
|
+
DBLP answers an HTML page where JSON was asked, a registry stops answering. The check handles this
|
|
61
|
+
within one build:
|
|
62
|
+
|
|
63
|
+
- **A service that refuses stops being asked for the rest of the build.** The first request to
|
|
64
|
+
each service goes alone; once a service refuses (a `429` or `5xx`, HTML instead of JSON, a
|
|
65
|
+
timeout, no connection), no new request goes to it until the next build, and no retries or pauses
|
|
66
|
+
are spent on it. A service that refuses from the start costs one request. One that starts refusing
|
|
67
|
+
later costs at most the requests already in flight at that moment — six, the number of entries
|
|
68
|
+
checked at once.
|
|
69
|
+
- **The entries it would have answered say so.** An entry no registry confirmed carries
|
|
70
|
+
`not asked: <service>: <reason>` in its reason. An entry whose authors DBLP could not check is
|
|
71
|
+
`authors: unchecked`, which `paper/refs-checked` reports and lint never treats as a pass.
|
|
72
|
+
- **Once one registry confirms a work, the others are not asked.** A confirmation settles the
|
|
73
|
+
verdict, so the remaining registries (and doi.org) could only cost requests.
|
|
74
|
+
- **DBLP's pace** — it asks clients to pause between queries — is kept between queries DBLP
|
|
75
|
+
actually answered, and not after a cached answer or once DBLP has refused.
|
|
76
|
+
|
|
77
|
+
Measured 2026-09-27 from a sandboxed runner where Semantic Scholar, OpenAlex and Crossref answered
|
|
78
|
+
`429` and DBLP answered HTML: a 30-reference paper built in 5.6 s cold and 5.4 s warm (the check
|
|
79
|
+
used to take 475 s and 450 s). Its entries were recorded, with the refusals named, and each later
|
|
80
|
+
build fills in more of the cache as the services let it.
|
|
81
|
+
|
|
82
|
+
**Commit this file.** It sits in `repro/`, beside the paper's other reproduction files, and not in
|
|
83
|
+
`_build/`, which is build output. Committed, it makes CI and every other clone start warm: a CI job
|
|
84
|
+
that builds an unchanged paper spends no time on the check.
|
|
85
|
+
|
|
86
|
+
It holds what the services answered, each answer with the day it was fetched, and never a verdict.
|
|
87
|
+
The verdicts are worked out again from those answers on every build, so an improvement to the
|
|
88
|
+
checker reaches every paper at once, instead of being frozen behind a stored "passed".
|
|
89
|
+
|
|
90
|
+
### Refreshing it
|
|
91
|
+
|
|
92
|
+
An answer is used for 30 days after the date in its `fetched` field; after that the next build asks
|
|
93
|
+
again and re-dates it. This is what lets a work that a service had not indexed yet ("no record")
|
|
94
|
+
be found once it is. An answer that could not be refreshed because the service refused stays, and is
|
|
95
|
+
asked again on the next build. To ask sooner:
|
|
96
|
+
|
|
97
|
+
- **one entry**: delete its lines from the file (the keys name the service and the identifier, and
|
|
98
|
+
DBLP answers carry the title they were asked for);
|
|
99
|
+
- **everything**: delete the file.
|
|
100
|
+
|
|
101
|
+
The next build asks again and writes the new answers.
|
|
102
|
+
|
|
103
|
+
### A file that does not read
|
|
104
|
+
|
|
105
|
+
The build refuses a cache file it cannot read (bad JSON, another schema version, a field of the
|
|
106
|
+
wrong type) and says which part is wrong. It does not overwrite the file, and does not run the
|
|
107
|
+
check over half a cache: the references are recorded as not checked, and lint says so. Fix the
|
|
108
|
+
file, or delete it.
|
|
109
|
+
|
|
110
|
+
## CI
|
|
111
|
+
|
|
112
|
+
| the paper has | a CI build |
|
|
113
|
+
| --------------------------------- | ------------------------------------------------------------------ |
|
|
114
|
+
| a committed, up-to-date cache | asks nothing, needs no network for the check |
|
|
115
|
+
| a cache missing a few new entries | asks about those entries only |
|
|
116
|
+
| no cache committed | asks about every entry, every run: commit the file after one build |
|
|
117
|
+
|
|
118
|
+
A CI run writes new answers into its own checkout, which is thrown away. Build locally and commit
|
|
119
|
+
the file to keep the answers.
|
|
120
|
+
|
|
121
|
+
## Running a checker by hand
|
|
122
|
+
|
|
123
|
+
The two checkers are scripts in the `verify-citations` skill and can be run on their own to read
|
|
124
|
+
one verdict in full: `verify-cites.mjs` (existence and title) and `bib-authors.mjs` (authors). Run
|
|
125
|
+
from the command line, `verify-cites.mjs` keeps its own cache in `~/.cache/paperlint/cite-cache.json`
|
|
126
|
+
(`VERIFY_CITES_CACHE` overrides); it never reads or writes the paper's committed cache.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
The one map of the kinds of test in this repository: what each answers, where it lives, what it
|
|
4
|
+
may touch, and how to run it. The rules that apply to every kind are at the end.
|
|
5
|
+
|
|
6
|
+
## Which kind do I need?
|
|
7
|
+
|
|
8
|
+
| you are testing | kind |
|
|
9
|
+
| --------------------------------------------------------------------------------------------- | --------------- |
|
|
10
|
+
| a pure function: a parser, a decision, a formatter | **unit** |
|
|
11
|
+
| a use case or an adapter that reaches the disk, a process, the network or the clock | **integration** |
|
|
12
|
+
| what an agent sees or does: a hook's decision, a skill's contract | **harness** |
|
|
13
|
+
| the package as someone else gets it: the tarball installed elsewhere, a real TeX build, a PDF | **e2e** |
|
|
14
|
+
|
|
15
|
+
When in doubt between unit and integration: if the function takes a port (`Files`,
|
|
16
|
+
`CheckReferences`, `RunProcess`, …) or calls `fetch`, it is integration.
|
|
17
|
+
|
|
18
|
+
## Unit — vitest, pure functions
|
|
19
|
+
|
|
20
|
+
- **Answers:** does this function return the right value for this input?
|
|
21
|
+
- **Lives:** beside the module, `<module>.test.ts` for a `.ts` module, `<module>.test.mjs` for a
|
|
22
|
+
`.mjs` one (TypeScript has no types to check a JS module against, and `allowJs` was measured to
|
|
23
|
+
break a dozen existing `@ts-expect-error` imports).
|
|
24
|
+
- **May touch:** nothing. No disk, no network, no process, no clock.
|
|
25
|
+
- **Example:** `src/domain/lookup-cache.test.ts` — the reference cache's parse, serialize and key
|
|
26
|
+
functions, each refusal compared as a whole value.
|
|
27
|
+
|
|
28
|
+
## Integration — vitest, with fakes through the ports
|
|
29
|
+
|
|
30
|
+
- **Answers:** does this use case or adapter do the right thing with the world it talks to?
|
|
31
|
+
- **Lives:** beside the module, named like a unit test.
|
|
32
|
+
- **May touch:** a temp directory (`useTempDir` and `writeTree` in `test/support.mjs`), a child
|
|
33
|
+
process of this repository's own scripts (`runNode`). Never the real network.
|
|
34
|
+
- **How fakes get in:** through the seams the code already has.
|
|
35
|
+
- a port the use case takes: `memoryFiles` and `scriptedProcess` in `src/adapters/memory/`, a
|
|
36
|
+
`CheckReferences` function, a `MeasureGeometry`;
|
|
37
|
+
- `fetch`, stubbed with `vi.stubGlobal` and routed by URL, which also records every request made;
|
|
38
|
+
- a clock or a pause, passed in (`referencesChecker({ today })`) or run under `vi.useFakeTimers`.
|
|
39
|
+
- **Example:** `src/adapters/references/index.test.ts` — the reference checker over a fake
|
|
40
|
+
`fetch`: a cold run stores every answer, a warm run makes no request at all (the recorded list of
|
|
41
|
+
requests is empty), an edited entry asks only for itself.
|
|
42
|
+
|
|
43
|
+
## Harness — vigiles, the agent-facing surface only
|
|
44
|
+
|
|
45
|
+
- **Answers:** what does an agent see or cause? A hook's allow or deny on a given input, what a
|
|
46
|
+
skill's contract promises.
|
|
47
|
+
- **Lives:** `<surface>.harness.mjs` beside what it tests; it imports `runHook`, `runHarnessTest`
|
|
48
|
+
or `runEval` from vigiles. Harnesses written before this rule that import none of the three are
|
|
49
|
+
frozen in `scripts/harness-api.frozen.json`, which only shrinks (`scripts/harness-api.test.ts`,
|
|
50
|
+
#77). A new test of anything else is a unit or integration test.
|
|
51
|
+
- **Asserts through** `lib/check.mjs`: `const check = createChecker();` then
|
|
52
|
+
`check(label, cond, detail)`. A failure prints the label and the detail, every call is counted.
|
|
53
|
+
Do not define a local `check`.
|
|
54
|
+
- **Example:** `hooks/hooks.harness.mjs`.
|
|
55
|
+
- **Skills are tested through vigiles**, not a bespoke runner: a home-grown runner here once printed
|
|
56
|
+
byte-identical "clean" verdicts for three skills that had never loaded.
|
|
57
|
+
|
|
58
|
+
## End-to-end — `test/e2e/*.mjs`
|
|
59
|
+
|
|
60
|
+
- **Answers:** does the package work once it is somewhere else? The tarball installed into another
|
|
61
|
+
tree, a real `pdflatex` build, the PDF's fonts measured.
|
|
62
|
+
- **May touch:** everything a user's machine has, except the network: a citation service is a
|
|
63
|
+
counting fake `fetch` even here (`test/e2e/build.mjs`).
|
|
64
|
+
- **What each run proves, and when a change owes one:** [`e2e.md`](e2e.md).
|
|
65
|
+
|
|
66
|
+
## Running
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx vitest run <file or folder> # unit and integration tests
|
|
70
|
+
npx vigiles test <file> # one harness
|
|
71
|
+
npm test # everything: vitest, then every harness
|
|
72
|
+
npm run coverage # the same under c8, failing below the thresholds in .c8rc.json
|
|
73
|
+
npm run test:e2e # the end-to-end runs
|
|
74
|
+
npm run check # every gate CI runs, and the list of what it does not reproduce
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
While editing, run the tests you touched, `npx tsc --noEmit`, `npx eslint <files>` and
|
|
78
|
+
`npm run fmt:check`. **Before every push, run `npm run check` and read its exit code** — the
|
|
79
|
+
coverage gate cannot be judged from one file.
|
|
80
|
+
|
|
81
|
+
⚠️ **Not `vigiles test .`**, and never drop `--min=1` from `npm test`. Measured 2026-09-11:
|
|
82
|
+
|
|
83
|
+
| form | what happens | exit |
|
|
84
|
+
| ---------------------- | --------------------------------------------------------------------------- | ----- |
|
|
85
|
+
| `vigiles test .` | `.` is read as a FILE — `ERR_UNSUPPORTED_DIR_IMPORT`, uncaught, runner dies | **0** |
|
|
86
|
+
| `vigiles test` | `No **/*.harness.{mjs,cjs,js,mts,cts,ts} files found.` | **0** |
|
|
87
|
+
| `vigiles test --min=1` | names the empty match and fails | **1** |
|
|
88
|
+
|
|
89
|
+
`--min=1` is the only thing between "every test passed" and "no test ran". vitest exits 1 when no
|
|
90
|
+
file matches, and it transpiles without type-checking, so `npm run check` runs
|
|
91
|
+
`tsc -p tsconfig.test.json` as its own gate.
|
|
92
|
+
|
|
93
|
+
## Rules for every kind
|
|
94
|
+
|
|
95
|
+
1. **Red first.** Run a new test on the code before the change and watch it fail at its own
|
|
96
|
+
assertion. A test that has only ever been green is indistinguishable from one that cannot fail.
|
|
97
|
+
Never add a file that patches source to prove a test can fail.
|
|
98
|
+
2. **Assert the whole value** — `assert.deepEqual`, `expect(x).toEqual(…)` on the entire result,
|
|
99
|
+
not a substring or one field. A substring passes on output that is wrong everywhere else. The
|
|
100
|
+
exception is prose whose wording is not the subject; say so in a comment.
|
|
101
|
+
3. **Test what the code does, never what its source says.** A test that reads a source file (a
|
|
102
|
+
`.mjs`, a `SKILL.md`, a README, a config) and asserts it contains some text restates the file:
|
|
103
|
+
rewording turns it red while a real regression stays green. Assert a return value, a run's output
|
|
104
|
+
and exit code, what landed on disk, the requests made. Text is the subject only when it is the
|
|
105
|
+
output — a message the program prints, a file it generates.
|
|
106
|
+
4. **No test touches the real network.** A service is a fake `fetch` or a fake port. A test that
|
|
107
|
+
passes only while Crossref or DBLP answers is not a test of this package.
|
|
108
|
+
5. **Coverage is 100% on lines, statements, functions and branches**, enforced by `npm run coverage`
|
|
109
|
+
in `npm run check` and CI. c8 reads `NODE_V8_COVERAGE`, so a CLI a test spawns is measured too,
|
|
110
|
+
unless the test hands the child a fresh `env` without it. vitest runs in the `threads` pool with
|
|
111
|
+
native `import` (`vitest.config.ts` says why), and the run preloads `test/coverage-src.mjs`, which
|
|
112
|
+
answers every import of `dist/*.js` with `src/*.ts`, so no module is measured twice.
|
|
113
|
+
6. **No `c8 ignore`, and no guard deleted to reach 100%.** An effect a test cannot reach — a race, a
|
|
114
|
+
permission root is never denied, a broken install, a 270 MB download — is made injectable and the
|
|
115
|
+
test passes a fake. A precondition about an argument becomes the signature. A guard whose input can
|
|
116
|
+
really occur stays, with the test that reaches it. `scripts/coverage-config.test.ts` fails on any
|
|
117
|
+
coverage-ignore comment and pins `.c8rc.json`'s `exclude` list.
|
|
118
|
+
7. **Say what an assertion guards** in a comment directly above it (`// Guards: …`).
|
package/docs/toolchain.md
CHANGED
|
@@ -141,12 +141,14 @@ bash node_modules/paperlint/skills/render-paper/ensure-checkers.sh
|
|
|
141
141
|
|
|
142
142
|
```
|
|
143
143
|
✅ aclpubcheck
|
|
144
|
-
✅ rebiber
|
|
144
|
+
✅ rebiber 1.4.0
|
|
145
145
|
✅ jinja2
|
|
146
146
|
✅ textidote (/opt/textidote/textidote.jar)
|
|
147
147
|
✅ all checkers are installed AND run
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
It checks that the tools RUN, not that pip exited zero
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
It checks that the tools RUN, not that pip exited zero: aclpubcheck's checker module is imported
|
|
151
|
+
(which pulls in numpy and pandas) and its `python3 -m aclpubcheck` entry point must answer, and
|
|
152
|
+
rebiber must convert a known arXiv entry to its official record. Both come from their GitHub
|
|
153
|
+
sources pinned to a commit — each project's own instruction, and why is recorded in
|
|
154
|
+
`checkers-requirements.txt`.
|
package/package.json
CHANGED
|
@@ -77,17 +77,17 @@ unreleased. Absence and unfindability fail the same way for a reader.
|
|
|
77
77
|
|
|
78
78
|
## Running `aclpubcheck` locally
|
|
79
79
|
|
|
80
|
-
The official ACL format checker.
|
|
81
|
-
|
|
80
|
+
The official ACL format checker. Install it, pinned to its GitHub source with rebiber beside it,
|
|
81
|
+
with the package's one command, which then proves it runs:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
mkdir -p stub/rebiber # rebiber is used ONLY by the optional author-name check
|
|
87
|
-
printf 'def construct_bib_db(*a, **k): return {}\n' > stub/rebiber/__init__.py
|
|
88
|
-
PYTHONPATH=stub python3 -m aclpubcheck --paper_type long <pdf>
|
|
84
|
+
bash node_modules/paperlint/skills/render-paper/ensure-checkers.sh
|
|
85
|
+
python3 -m aclpubcheck --paper_type long <pdf>
|
|
89
86
|
```
|
|
90
87
|
|
|
88
|
+
(The recipe that stood here cloned the repository and stubbed rebiber out, because the PyPI release
|
|
89
|
+
did not install. The pinned commit installs, and `ensure-checkers.sh` records why.)
|
|
90
|
+
|
|
91
91
|
🔴 **Run it on a build WITHOUT line numbers.** Against a `[review]`-mode PDF it reports one margin
|
|
92
92
|
error per line number — about 2,000 of them, all spurious, because it is written for camera-ready
|
|
93
93
|
files. A checker whose output is 100% false positives gets muted the same day.
|
|
@@ -8,25 +8,26 @@
|
|
|
8
8
|
|
|
9
9
|
jinja2>=3.0 # {{macro}} substitution in paper_numbers.py
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# 0, `python -m aclpubcheck.formatchecker --help` runs.
|
|
16
|
-
bibtexparser<2
|
|
11
|
+
# bibtexparser is pulled in by rebiber (below), not by aclpubcheck: aclpubcheck's own code never
|
|
12
|
+
# imports it. rebiber 1.4 moved to bibtexparser 2 (`bibtexparser>=2,<3` in its pyproject), and
|
|
13
|
+
# 2.0.1 is a stable release with a prebuilt wheel. The old pin `bibtexparser<2` existed only
|
|
14
|
+
# for rebiber 1.1.3; it would now make the file unsatisfiable, so it is gone.
|
|
17
15
|
|
|
18
|
-
# 🔴 aclpubcheck IS INSTALLED SEPARATELY, WITH `--no-deps`, AND IT IS
|
|
19
|
-
#
|
|
20
|
-
#
|
|
16
|
+
# 🔴 aclpubcheck IS INSTALLED SEPARATELY, WITH `--no-deps`, BY ensure-checkers.sh, AND IT IS
|
|
17
|
+
# DELIBERATELY NOT LISTED HERE (a requirements file cannot say `--no-deps` for one line).
|
|
18
|
+
# It comes from its GitHub source pinned to a commit, not from PyPI: upstream says to install "from
|
|
19
|
+
# the GitHub repository (DIFFERENT from `pypi`)", and PyPI's 0.2.0 is a snapshot of 2022-10-10. The
|
|
20
|
+
# pin is 237bee3 of 2026-09-11, whose own setup.py pins rebiber to the SAME commit as below —
|
|
21
|
+
# measured 2026-09-27: both resolve together, `pip check` names no rebiber conflict.
|
|
22
|
+
# Why `--no-deps`: it still declares `tsv`, which has NO PREBUILT WHEEL AT ALL
|
|
23
|
+
# (`pip download --only-binary :all: tsv` → "from versions: none"), and whose source does not build
|
|
21
24
|
# under a modern setuptools:
|
|
22
25
|
# AttributeError: install_layout. Did you mean: 'install_platlib'?
|
|
23
|
-
# That one line alone made installing the WHOLE file fail, and that's why
|
|
26
|
+
# That one line alone once made installing the WHOLE file fail, and that's why a paper went a month
|
|
24
27
|
# without an official format check.
|
|
25
|
-
# Verified, not assumed: `tsv` is imported NOWHERE in aclpubcheck's source
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# So what follows is aclpubcheck's OWN dependencies, taken verbatim from the package's metadata,
|
|
29
|
-
# minus tsv. A list, rather than `aclpubcheck` on one line, so the next unrepresentable dependency
|
|
28
|
+
# Verified, not assumed, on the pinned commit: `tsv` is imported NOWHERE in aclpubcheck's source,
|
|
29
|
+
# and neither is `setuptools`, which it also declares.
|
|
30
|
+
# So what follows is aclpubcheck's OWN dependencies, from its setup.py, minus tsv and setuptools. A list, rather than `aclpubcheck` on one line, so the next unrepresentable dependency
|
|
30
31
|
# reads as a missing line rather than an unverified assumption.
|
|
31
32
|
# 🔴 THE UPPER BOUNDS ARE REMOVED, AND THAT IS A MEASUREMENT, NOT CARELESSNESS. The first edition
|
|
32
33
|
# of this list copied aclpubcheck's bounds verbatim — and turned red on CI, where Python is newer:
|
|
@@ -48,8 +49,18 @@ pylatexenc>=2.10
|
|
|
48
49
|
termcolor>=2.0.1
|
|
49
50
|
tqdm>=4.64.1
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
# 🔴 rebiber FROM ITS GITHUB SOURCE, PINNED TO A COMMIT — NOT FROM PyPI. The upstream README says
|
|
53
|
+
# so itself: "Install from GitHub only. We do not publish or support PyPI (`pip install rebiber` is
|
|
54
|
+
# a 2021 1.1.3 release)." The `rebiber>=1.1.3` that stood here installed exactly that 2021 release
|
|
55
|
+
# (#114). The pin is 1.4.0, commit b917e36 of 2026-09-08 ("Support bibtexparser 2 parsing and
|
|
56
|
+
# writing APIs"); upstream has no 1.4 tag, so a commit is the only fixed name.
|
|
57
|
+
# Measured 2026-09-27 in a clean Python 3.11 venv, under this file's `--only-binary :all:`: installs
|
|
58
|
+
# in 36 s (pip builds it from source with hatchling, the one allowed build), `rebiber -v` runs,
|
|
59
|
+
# and a `--dry-run` over a three-entry .bib leaves the file untouched. It carries 409 MB of venue
|
|
60
|
+
# data. aclpubcheck declares `rebiber>=1.1.3,<2`, which 1.4.0 satisfies.
|
|
61
|
+
rebiber @ git+https://github.com/yuchenlin/rebiber@b917e36ec94084a84025eb06781b5aac96d1cedd
|
|
52
62
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
63
|
+
# Since the pinned commit the package has a `__main__.py`, so `python3 -m aclpubcheck <pdf>` runs
|
|
64
|
+
# the format checker (measured 2026-09-27 on its example PDF: 10 errors, 0 warnings, exit 0). The
|
|
65
|
+
# PyPI 0.2.0 that stood here had none, and `python3 -m aclpubcheck` failed with "cannot be directly
|
|
66
|
+
# executed"; `python3 -m aclpubcheck.formatchecker` works on both.
|
|
@@ -27,8 +27,10 @@ echo "== python dependencies =="
|
|
|
27
27
|
# bibtexparser looked like the guilty one.
|
|
28
28
|
pip install -q --only-binary :all: -r "$HERE/checkers-requirements.txt"
|
|
29
29
|
# The checker itself — WITHOUT its dependencies: they are listed in requirements.txt by name, minus
|
|
30
|
-
# the unbuildable `tsv`, which it does not import.
|
|
31
|
-
|
|
30
|
+
# the unbuildable `tsv`, which it does not import. From its GitHub source at a fixed commit, not
|
|
31
|
+
# PyPI — both reasons are in the header of requirements.txt.
|
|
32
|
+
ACLPUBCHECK="aclpubcheck @ git+https://github.com/acl-org/aclpubcheck@237bee3a554f2d2fcda69cd0cf1edf4168e3d339"
|
|
33
|
+
pip install -q --no-deps "$ACLPUBCHECK"
|
|
32
34
|
|
|
33
35
|
echo "== TeXtidote (a jar, not a pip package) =="
|
|
34
36
|
if [ -f "$JAR" ]; then
|
|
@@ -55,10 +57,24 @@ fail=0
|
|
|
55
57
|
# "numpy.dtype size changed"). That is, the acceptance check that stood here let through exactly
|
|
56
58
|
# the failure it was written for. `import aclpubcheck.formatchecker` pulls in numpy and pandas on
|
|
57
59
|
# lines 15 and 20 of the checker itself and therefore fails honestly.
|
|
60
|
+
# And the entry point, which the PyPI release did not have: `python3 -m aclpubcheck` must answer.
|
|
58
61
|
python3 -c "import aclpubcheck.formatchecker" >/dev/null 2>&1 \
|
|
62
|
+
&& python3 -m aclpubcheck --help >/dev/null 2>&1 \
|
|
59
63
|
&& echo " ✅ aclpubcheck" || { echo " ❌ aclpubcheck is installed, but does not run"; fail=1; }
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
# 🔴 RUN IT, NOT IMPORT IT. rebiber is used as a command, and an import proves neither the entry
|
|
65
|
+
# point, nor that it loads its bundled venue data, nor that it parses a .bib (bibtexparser 2 since
|
|
66
|
+
# 1.4). So it is given the arXiv entry of a paper its data knows (NeurIPS 2017) with --dry-run
|
|
67
|
+
# (nothing is written) and must report that entry CONVERTED to the official record — measured
|
|
68
|
+
# 2026-09-27 on the pinned commit: `converted (official): 1`, no network.
|
|
69
|
+
reb_tmp="$(mktemp -d)"
|
|
70
|
+
printf '@article{vaswani2017attention,\n title = {Attention Is All You Need},\n author = {Vaswani, Ashish and Shazeer, Noam and Parmar, Niki and Uszkoreit, Jakob and Jones, Llion and Gomez, Aidan N. and Kaiser, Lukasz and Polosukhin, Illia},\n journal = {arXiv preprint arXiv:1706.03762},\n year = {2017}\n}\n' > "$reb_tmp/in.bib"
|
|
71
|
+
reb_out="$(rebiber -i "$reb_tmp/in.bib" --dry-run 2>&1 || true)"
|
|
72
|
+
if grep -q "converted (official): 1" <<<"$reb_out"; then
|
|
73
|
+
echo " ✅ rebiber $(rebiber -v 2>/dev/null)"
|
|
74
|
+
else
|
|
75
|
+
echo " ❌ rebiber does not convert a known entry"; fail=1
|
|
76
|
+
fi
|
|
77
|
+
rm -rf "$reb_tmp"
|
|
62
78
|
python3 -c "import jinja2" >/dev/null 2>&1 \
|
|
63
79
|
&& echo " ✅ jinja2" || { echo " ❌ jinja2 does not import"; fail=1; }
|
|
64
80
|
java -jar "$JAR" --version >/dev/null 2>&1 \
|