paperlint 3.1.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CLAUDE.md +18 -97
  2. package/CONTRIBUTING.md +7 -21
  3. package/README.md +11 -7
  4. package/dist/adapters/references/clock.io.d.ts +5 -0
  5. package/dist/adapters/references/clock.io.d.ts.map +1 -0
  6. package/dist/adapters/references/clock.io.js +7 -0
  7. package/dist/adapters/references/clock.io.js.map +1 -0
  8. package/dist/adapters/references/index.d.ts +24 -2
  9. package/dist/adapters/references/index.d.ts.map +1 -1
  10. package/dist/adapters/references/index.js +125 -15
  11. package/dist/adapters/references/index.js.map +1 -1
  12. package/dist/build.d.ts +4 -0
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +10 -3
  15. package/dist/build.js.map +1 -1
  16. package/dist/domain/lookup-cache.d.ts +75 -0
  17. package/dist/domain/lookup-cache.d.ts.map +1 -0
  18. package/dist/domain/lookup-cache.js +137 -0
  19. package/dist/domain/lookup-cache.js.map +1 -0
  20. package/dist/ports/check-references.d.ts +12 -2
  21. package/dist/ports/check-references.d.ts.map +1 -1
  22. package/dist/ports/check-references.js +0 -5
  23. package/dist/ports/check-references.js.map +1 -1
  24. package/dist/references.d.ts +11 -3
  25. package/dist/references.d.ts.map +1 -1
  26. package/dist/references.js +63 -15
  27. package/dist/references.js.map +1 -1
  28. package/docs/prior-art/README.md +11 -10
  29. package/docs/prior-art/citation-checkers.md +174 -0
  30. package/docs/references.md +126 -0
  31. package/docs/testing.md +118 -0
  32. package/docs/toolchain.md +6 -4
  33. package/package.json +1 -1
  34. package/skills/paper-pipeline/references/artifact-checklist.md +7 -7
  35. package/skills/render-paper/checkers-requirements.txt +30 -19
  36. package/skills/render-paper/ensure-checkers.sh +20 -4
  37. package/skills/verify-citations/SKILL.md +26 -10
  38. package/skills/verify-citations/SKILL.md.spec.ts +25 -9
  39. package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +28 -10
  40. package/skills/verify-citations/scripts/bib-authors.mjs +5 -6
  41. package/skills/verify-citations/scripts/verify-cites.mjs +285 -140
  42. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +321 -20
  43. package/skills/verify-citations/scripts/verify-cites.test.mjs +4 -2
  44. package/src/adapters/references/clock.io.ts +8 -0
  45. package/src/adapters/references/index.test.ts +341 -9
  46. package/src/adapters/references/index.ts +198 -29
  47. package/src/build.ts +10 -3
  48. package/src/domain/lookup-cache.test.ts +178 -0
  49. package/src/domain/lookup-cache.ts +202 -0
  50. package/src/ports/check-references.ts +16 -2
  51. package/src/references.test.ts +160 -10
  52. 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:9973bd8fef7e0927 compiled from skills/verify-citations/SKILL.md.spec.ts -->
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 `scripts/.cite-cache.json` (gitignored) so re-runs are
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. Run the scripts by hand (below) to read a single verdict in full.
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. We deliberately do not depend on
183
- it: its install fights modern setuptools (the `bibtexparser` wheel dies with `AttributeError:
184
- install_layout`), and its *output* replaces our entries with DBLP's very long official booktitles — wrong for
185
- a page-limited paper. We needed the **comparison**, not the rewrite, and that is one fetch with zero deps.
186
- Note also that live DBLP beat rebiber's bundled dump: `raji2021benchmark` was found only by the live query.
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 \`scripts/.cite-cache.json\` (gitignored) so re-runs are
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. Run the scripts by hand (below) to read a single verdict in full.
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. We deliberately do not depend on
199
- it: its install fights modern setuptools (the \`bibtexparser\` wheel dies with \`AttributeError:
200
- install_layout\`), and its *output* replaces our entries with DBLP's very long official booktitles — wrong for
201
- a page-limited paper. We needed the **comparison**, not the rewrite, and that is one fetch with zero deps.
202
- Note also that live DBLP beat rebiber's bundled dump: \`raji2021benchmark\` was found only by the live query.
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": "crossref",
119
+ "matched_db": "arxiv",
116
120
  "matched_title": "Attention Is All You Need",
117
- "reason": "confirmed by crossref — "Attention Is All You Need"",
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": "crossref",
230
- "matched_title": "",
231
- "reason": "confirmed by crossref",
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. Two reasons, both practical: its install needs a Python toolchain
32
- * that fights modern setuptools (bibtexparser wheel fails with `AttributeError:
33
- * install_layout`; the workaround is vendoring a tarball), and its output REPLACES entries
34
- * with DBLP's very long official booktitles, which is wrong for a page-limited paper. What
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) {