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
|
@@ -4,7 +4,7 @@ description: Verify every citation is a real work with correct metadata, and tha
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:3a7e10662d0084c4 compiled from skills/verify-citations/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# verify-citations — every cite real, the delta explicit
|
|
10
10
|
|
|
@@ -133,17 +133,26 @@ node scripts/verify-cites.mjs cites.json --offline # no network (everything de
|
|
|
133
133
|
```
|
|
134
134
|
It prints per-cite `{id, verdict, reason, matched_db, matched_title?, flags?}` + a summary, and **exits 1 iff
|
|
135
135
|
any `false` (fabrication)** — `unresolvable` alone is advisory and does NOT fail the gate, matching the
|
|
136
|
-
narrowed-false philosophy. API responses are cached to
|
|
137
|
-
cheap and deterministic
|
|
136
|
+
narrowed-false philosophy. API responses are cached to `~/.cache/paperlint/cite-cache.json`
|
|
137
|
+
(`VERIFY_CITES_CACHE` overrides) so re-runs are cheap and deterministic — outside the installed package,
|
|
138
|
+
which `npm ci` wipes.
|
|
138
139
|
|
|
139
|
-
## 5a. Both checks run on every `paperlint build`
|
|
140
|
+
## 5a. Both checks run on every `paperlint build` — from a committed cache
|
|
140
141
|
|
|
141
142
|
`npx paperlint build` runs `verify-cites` and `bib-authors` over the paper's bibliography after the PDF is
|
|
142
143
|
built and records the verdicts, with the SHA-256 of the bibliography it checked, in
|
|
143
144
|
`<paper>/_build/references.json`. `paperlint lint` reads that record offline: `paper/cite-exists` and
|
|
144
145
|
`paper/author-list` report a failing entry on its own line, `paper/refs-fresh` says when the bibliography
|
|
145
146
|
changed since, and `paper/refs-checked` warns when nothing was recorded or the build had no network. The
|
|
146
|
-
step never fails the build.
|
|
147
|
+
step never fails the build.
|
|
148
|
+
|
|
149
|
+
What the services ANSWERED is kept in `<paper>/repro/references-cache.json` — **commit it**. A build asks
|
|
150
|
+
only the questions that file cannot answer (a new or edited entry, or an answer older than 30 days), so an
|
|
151
|
+
unchanged bibliography builds with no network at all, in CI too, until its answers age out. It holds responses, never verdicts: the verdicts are derived again on
|
|
152
|
+
every build. A service that refuses (429, HTML instead of JSON, a timeout) is not asked again for the rest of the
|
|
153
|
+
build (the requests already in flight still land); the entries it would have answered say `not asked: <service>: <reason>`, and DBLP's become
|
|
154
|
+
`authors: unchecked`. Answers are asked again after 30 days; to refresh one sooner, delete its entry, or delete the file. Details:
|
|
155
|
+
`docs/references.md` in the package. Run the scripts by hand (below) to read a single verdict in full.
|
|
147
156
|
|
|
148
157
|
## 5b. The author-list gate — `scripts/bib-authors.mjs`
|
|
149
158
|
|
|
@@ -179,11 +188,18 @@ difference is real and not a metadata glitch. Entries that themselves declare a
|
|
|
179
188
|
scope (a preprint entry is allowed to carry preprint metadata).
|
|
180
189
|
|
|
181
190
|
⚠️ **Discovery credit, and why it is NOT a dependency.** The class was found by running **rebiber**
|
|
182
|
-
(`yuchenlin/rebiber`), which rewrites bib entries into their DBLP records.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
(`yuchenlin/rebiber`), which rewrites bib entries into their DBLP records. Re-measured 2026-09-27 on rebiber
|
|
192
|
+
1.4.0 (GitHub commit `b917e36`, the pin in `render-paper/checkers-requirements.txt`; the PyPI package is
|
|
193
|
+
a 2021 release upstream disowns):
|
|
194
|
+
- **Install: no longer broken, but heavy.** It installs in 36 s in a clean venv (bibtexparser 2.0.1 wheel,
|
|
195
|
+
rebiber built from source) — and carries **409 MB** of bundled venue data.
|
|
196
|
+
- **Rewrites: now optional.** `--dry-run --report` previews and leaves the file untouched; `-s True` shortens
|
|
197
|
+
venues (NAACL 2019 becomes `Proc. of NAACL-HLT` instead of a 160-character booktitle).
|
|
198
|
+
- **Bundled data still misses what live DBLP finds.** `raji2021benchmark` stayed an arXiv entry, local
|
|
199
|
+
index and `--live-lookup` alike, as it did in August.
|
|
200
|
+
|
|
201
|
+
So the reason not to depend on it is now scope, not breakage: we need the **comparison** of an author list
|
|
202
|
+
against the published record, not a rewrite of the entry, and that is one DBLP query with zero dependencies.
|
|
187
203
|
|
|
188
204
|
🔴 **Two design decisions that are load-bearing, both learned by measurement the same day:**
|
|
189
205
|
1. **Compare surname SEQUENCES, nothing else.** A first cut reported 13 differences of which **11 were
|
|
@@ -149,17 +149,26 @@ node scripts/verify-cites.mjs cites.json --offline # no network (everything de
|
|
|
149
149
|
\`\`\`
|
|
150
150
|
It prints per-cite \`{id, verdict, reason, matched_db, matched_title?, flags?}\` + a summary, and **exits 1 iff
|
|
151
151
|
any \`false\` (fabrication)** — \`unresolvable\` alone is advisory and does NOT fail the gate, matching the
|
|
152
|
-
narrowed-false philosophy. API responses are cached to
|
|
153
|
-
cheap and deterministic
|
|
152
|
+
narrowed-false philosophy. API responses are cached to \`~/.cache/paperlint/cite-cache.json\`
|
|
153
|
+
(\`VERIFY_CITES_CACHE\` overrides) so re-runs are cheap and deterministic — outside the installed package,
|
|
154
|
+
which \`npm ci\` wipes.
|
|
154
155
|
|
|
155
|
-
## 5a. Both checks run on every \`paperlint build\`
|
|
156
|
+
## 5a. Both checks run on every \`paperlint build\` — from a committed cache
|
|
156
157
|
|
|
157
158
|
\`npx paperlint build\` runs \`verify-cites\` and \`bib-authors\` over the paper's bibliography after the PDF is
|
|
158
159
|
built and records the verdicts, with the SHA-256 of the bibliography it checked, in
|
|
159
160
|
\`<paper>/_build/references.json\`. \`paperlint lint\` reads that record offline: \`paper/cite-exists\` and
|
|
160
161
|
\`paper/author-list\` report a failing entry on its own line, \`paper/refs-fresh\` says when the bibliography
|
|
161
162
|
changed since, and \`paper/refs-checked\` warns when nothing was recorded or the build had no network. The
|
|
162
|
-
step never fails the build.
|
|
163
|
+
step never fails the build.
|
|
164
|
+
|
|
165
|
+
What the services ANSWERED is kept in \`<paper>/repro/references-cache.json\` — **commit it**. A build asks
|
|
166
|
+
only the questions that file cannot answer (a new or edited entry, or an answer older than 30 days), so an
|
|
167
|
+
unchanged bibliography builds with no network at all, in CI too, until its answers age out. It holds responses, never verdicts: the verdicts are derived again on
|
|
168
|
+
every build. A service that refuses (429, HTML instead of JSON, a timeout) is not asked again for the rest of the
|
|
169
|
+
build (the requests already in flight still land); the entries it would have answered say \`not asked: <service>: <reason>\`, and DBLP's become
|
|
170
|
+
\`authors: unchecked\`. Answers are asked again after 30 days; to refresh one sooner, delete its entry, or delete the file. Details:
|
|
171
|
+
\`docs/references.md\` in the package. Run the scripts by hand (below) to read a single verdict in full.
|
|
163
172
|
|
|
164
173
|
## 5b. The author-list gate — \`scripts/bib-authors.mjs\`
|
|
165
174
|
|
|
@@ -195,11 +204,18 @@ difference is real and not a metadata glitch. Entries that themselves declare a
|
|
|
195
204
|
scope (a preprint entry is allowed to carry preprint metadata).
|
|
196
205
|
|
|
197
206
|
⚠️ **Discovery credit, and why it is NOT a dependency.** The class was found by running **rebiber**
|
|
198
|
-
(\`yuchenlin/rebiber\`), which rewrites bib entries into their DBLP records.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
207
|
+
(\`yuchenlin/rebiber\`), which rewrites bib entries into their DBLP records. Re-measured 2026-09-27 on rebiber
|
|
208
|
+
1.4.0 (GitHub commit \`b917e36\`, the pin in \`render-paper/checkers-requirements.txt\`; the PyPI package is
|
|
209
|
+
a 2021 release upstream disowns):
|
|
210
|
+
- **Install: no longer broken, but heavy.** It installs in 36 s in a clean venv (bibtexparser 2.0.1 wheel,
|
|
211
|
+
rebiber built from source) — and carries **409 MB** of bundled venue data.
|
|
212
|
+
- **Rewrites: now optional.** \`--dry-run --report\` previews and leaves the file untouched; \`-s True\` shortens
|
|
213
|
+
venues (NAACL 2019 becomes \`Proc. of NAACL-HLT\` instead of a 160-character booktitle).
|
|
214
|
+
- **Bundled data still misses what live DBLP finds.** \`raji2021benchmark\` stayed an arXiv entry, local
|
|
215
|
+
index and \`--live-lookup\` alike, as it did in August.
|
|
216
|
+
|
|
217
|
+
So the reason not to depend on it is now scope, not breakage: we need the **comparison** of an author list
|
|
218
|
+
against the published record, not a rewrite of the entry, and that is one DBLP query with zero dependencies.
|
|
203
219
|
|
|
204
220
|
🔴 **Two design decisions that are load-bearing, both learned by measurement the same day:**
|
|
205
221
|
1. **Compare surname SEQUENCES, nothing else.** A first cut reported 13 differences of which **11 were
|
|
@@ -34,6 +34,9 @@ exports[`CVEs: found by count, found by list, not found, NVD down, and a malform
|
|
|
34
34
|
"id": "CVE-2024-0004",
|
|
35
35
|
"matched_db": null,
|
|
36
36
|
"reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
|
|
37
|
+
"refused": [
|
|
38
|
+
"nvd: http 500",
|
|
39
|
+
],
|
|
37
40
|
"verdict": "unresolvable",
|
|
38
41
|
},
|
|
39
42
|
{
|
|
@@ -62,6 +65,10 @@ exports[`S2 and arXiv unreachable for an arXiv id 1`] = `
|
|
|
62
65
|
"id": "x",
|
|
63
66
|
"matched_db": null,
|
|
64
67
|
"reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
|
|
68
|
+
"refused": [
|
|
69
|
+
"semantic_scholar: http 500",
|
|
70
|
+
"arxiv: http 500",
|
|
71
|
+
],
|
|
65
72
|
"verdict": "unresolvable",
|
|
66
73
|
},
|
|
67
74
|
}
|
|
@@ -71,9 +78,6 @@ exports[`a DOI every registry resolves to the claimed paper, and the authority k
|
|
|
71
78
|
{
|
|
72
79
|
"calls": [
|
|
73
80
|
"https://api.crossref.org/works/10.1%2Fx",
|
|
74
|
-
"https://api.openalex.org/works/doi:10.1%2Fx",
|
|
75
|
-
"https://api.semanticscholar.org/graph/v1/paper/DOI%3A10.1%2Fx?fields=title,year",
|
|
76
|
-
"https://doi.org/api/handles/10.1/x",
|
|
77
81
|
],
|
|
78
82
|
"result": {
|
|
79
83
|
"id": "v",
|
|
@@ -112,9 +116,9 @@ exports[`a title only: every registry's search endpoint, hits and all 1`] = `
|
|
|
112
116
|
],
|
|
113
117
|
"result": {
|
|
114
118
|
"id": "t",
|
|
115
|
-
"matched_db": "
|
|
119
|
+
"matched_db": "arxiv",
|
|
116
120
|
"matched_title": "Attention Is All You Need",
|
|
117
|
-
"reason": "confirmed by
|
|
121
|
+
"reason": "confirmed by arxiv — "Attention Is All You Need"",
|
|
118
122
|
"verdict": "true",
|
|
119
123
|
},
|
|
120
124
|
}
|
|
@@ -132,6 +136,12 @@ exports[`a title search that cannot reach any registry is unresolvable 1`] = `
|
|
|
132
136
|
"id": "d",
|
|
133
137
|
"matched_db": null,
|
|
134
138
|
"reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
|
|
139
|
+
"refused": [
|
|
140
|
+
"crossref: http 500",
|
|
141
|
+
"openalex: http 502",
|
|
142
|
+
"semantic_scholar: timeout",
|
|
143
|
+
"arxiv: http 503",
|
|
144
|
+
],
|
|
135
145
|
"verdict": "unresolvable",
|
|
136
146
|
},
|
|
137
147
|
}
|
|
@@ -175,13 +185,16 @@ exports[`an arXiv id: Semantic Scholar by arXiv id, the arXiv API by id_list 1`]
|
|
|
175
185
|
"https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
|
|
176
186
|
"https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
|
|
177
187
|
"https://api.semanticscholar.org/graph/v1/paper/arXiv%3A1706.03762?fields=title,year",
|
|
178
|
-
"https://export.arxiv.org/api/query?id_list=1706.03762&max_results=1",
|
|
179
188
|
],
|
|
180
189
|
"result": {
|
|
181
190
|
"id": "a",
|
|
182
191
|
"matched_db": "semantic_scholar",
|
|
183
192
|
"matched_title": "Attention Is All You Need",
|
|
184
193
|
"reason": "confirmed by semantic_scholar — "Attention Is All You Need"",
|
|
194
|
+
"refused": [
|
|
195
|
+
"crossref: Error: unrouted request: https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
|
|
196
|
+
"openalex: Error: unrouted request: https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
|
|
197
|
+
],
|
|
185
198
|
"verdict": "true",
|
|
186
199
|
},
|
|
187
200
|
}
|
|
@@ -199,6 +212,12 @@ exports[`every registry down (429, 503, 403, a thrown error): unresolvable, neve
|
|
|
199
212
|
"id": "u",
|
|
200
213
|
"matched_db": null,
|
|
201
214
|
"reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
|
|
215
|
+
"refused": [
|
|
216
|
+
"crossref: http 429",
|
|
217
|
+
"openalex: http 503",
|
|
218
|
+
"semantic_scholar: http 403",
|
|
219
|
+
"doi_authority: TypeError: fetch failed",
|
|
220
|
+
],
|
|
202
221
|
"verdict": "unresolvable",
|
|
203
222
|
},
|
|
204
223
|
}
|
|
@@ -226,10 +245,9 @@ exports[`records with fields missing: no title, a subtitle, a year only in \`pub
|
|
|
226
245
|
],
|
|
227
246
|
"result": {
|
|
228
247
|
"id": "m",
|
|
229
|
-
"matched_db":
|
|
230
|
-
"
|
|
231
|
-
"
|
|
232
|
-
"verdict": "true",
|
|
248
|
+
"matched_db": null,
|
|
249
|
+
"reason": "DOI_MISMATCH: identifier resolves via crossref to an UNRELATED paper "" (claimed "Attention Is All You Need") — DOI misdirection",
|
|
250
|
+
"verdict": "false",
|
|
233
251
|
},
|
|
234
252
|
}
|
|
235
253
|
`;
|
|
@@ -28,11 +28,10 @@
|
|
|
28
28
|
*
|
|
29
29
|
* ⚠️ Discovery credit and a deliberate NON-dependency: this class was found by running
|
|
30
30
|
* `rebiber` (yuchenlin/rebiber), which rewrites bib entries to their DBLP records. We do
|
|
31
|
-
* NOT depend on it here.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* we actually needed was the comparison, and that is one HTTPS call with no dependencies.
|
|
31
|
+
* NOT depend on it here. Re-measured 2026-09-27 on 1.4.0 (GitHub b917e36): it installs cleanly
|
|
32
|
+
* now but carries 409 MB of venue data, its bundled data still misses entries live DBLP finds,
|
|
33
|
+
* and what it does is REWRITE an entry — we need the comparison of an author list against the
|
|
34
|
+
* published record, and that is one HTTPS call with no dependencies.
|
|
36
35
|
*
|
|
37
36
|
* 🔴 The normalisation below is load-bearing, not tidiness. A first cut of this comparison
|
|
38
37
|
* reported 13 mismatches on our bibliography, of which ELEVEN were "Last, First" vs
|
|
@@ -279,7 +278,7 @@ export function surnames(authorField) {
|
|
|
279
278
|
}
|
|
280
279
|
|
|
281
280
|
// Only called for an entry that has an author (checkAuthors filters on it first).
|
|
282
|
-
const truncated = (authorField) => /\band\s+others\b/i.test(authorField);
|
|
281
|
+
export const truncated = (authorField) => /\band\s+others\b/i.test(authorField);
|
|
283
282
|
|
|
284
283
|
/** Is the entry claiming a PUBLISHED venue (as opposed to a preprint)? */
|
|
285
284
|
export function claimsPublished(e) {
|