paperlint 3.1.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -156,13 +156,23 @@ export function normalizeArxiv(arxiv) {
156
156
  .trim();
157
157
  }
158
158
 
159
+ /** arXiv registers a DataCite DOI for every paper: `10.48550/arXiv.<id>`. */
160
+ const ARXIV_DOI = /^10\.48550\/arxiv\.(.+)$/i;
161
+
159
162
  /** Shallow copy of a citation with doi/arxiv identifiers normalized. Applied at the
160
163
  * verify boundary so BOTH JSON input and .bib-parsed input are clean before any
161
- * network / authority lookup. */
164
+ * network / authority lookup.
165
+ *
166
+ * An arXiv DOI also yields the arXiv id it names, when the entry has none of its own: without
167
+ * it arXiv is asked only by title, and only for an entry with no DOI, so a registry that maps
168
+ * the DOI to the wrong work goes uncontradicted (OpenAlex did, for SWE-bench, on 2026-09-27). */
162
169
  export function normalizeIdentifiers(citation) {
163
170
  const c = { ...citation };
164
171
  if (c.doi) c.doi = normalizeDoi(c.doi);
165
172
  if (c.arxiv) c.arxiv = normalizeArxiv(c.arxiv);
173
+ // Decided on the NORMALIZED id: a blank field normalizes to "" and must not block the DOI's id.
174
+ const fromDoi = c.doi ? ARXIV_DOI.exec(c.doi)?.[1] : undefined;
175
+ if (!c.arxiv && fromDoi) c.arxiv = fromDoi;
166
176
  return c;
167
177
  }
168
178
 
@@ -271,6 +271,41 @@ test("a corrected arXiv id is asked again even when the DOI is unchanged", async
271
271
  );
272
272
  });
273
273
 
274
+ // Guards: an arXiv DOI (10.48550/arXiv.<id>) names an arXiv id, so arXiv is asked by that id.
275
+ // Measured 2026-09-27 on a real bibliography: OpenAlex maps 10.48550/arXiv.2310.06770 (SWE-bench)
276
+ // to an unrelated work; with Crossref and Semantic Scholar refusing and arXiv asked only by title
277
+ // "when there is no DOI", nothing contradicted it and the paper came out `exists: false`.
278
+ test("an arXiv DOI is asked of arXiv by its id, whose match outweighs a registry's wrong record", async () => {
279
+ const calls = fakeFetch({
280
+ [CR]: () => json(429),
281
+ [OA]: () =>
282
+ json(200, {
283
+ id: "W1",
284
+ display_name: "GardenBench",
285
+ publication_year: 2023,
286
+ }),
287
+ [S2]: () => json(429),
288
+ [AX]: () => text(200, feed(TITLE, 2017)),
289
+ [DOI]: () => json(200, { responseCode: 1 }),
290
+ });
291
+ const v = await verifyCitationLive(
292
+ { id: "c", doi: "10.48550/arXiv.1706.03762", title: TITLE, year: "2017" },
293
+ { cache: {} },
294
+ );
295
+ assert.deepEqual(
296
+ {
297
+ verdict: v.verdict,
298
+ matched: v.matched_db,
299
+ arxiv: calls.filter((u) => u.startsWith(AX)),
300
+ },
301
+ {
302
+ verdict: "true",
303
+ matched: "arxiv",
304
+ arxiv: [`${AX}?id_list=1706.03762&max_results=1`],
305
+ },
306
+ );
307
+ });
308
+
274
309
  // Guards: NVD's "not found" must not be cached. A CVE cited before NVD indexes it would otherwise
275
310
  // stay "fabricated" in the committed cache forever; only positive existence is kept, as for doi.org.
276
311
  test("an NVD not-found answer is not cached, so a later build asks again", async () => {
@@ -738,6 +738,20 @@ console.log("\n[S4] .bib parser edge cases:");
738
738
 
739
739
  // ── Edges of the pure layer: the defensive arms, each with the input that reaches it ──────────
740
740
 
741
+ // Guards: the arXiv id is read out of an arXiv DOI whenever the entry has no USABLE arXiv id, so
742
+ // a blank `arxiv` field (JSON input) does not keep arXiv from being asked.
743
+ test.each([
744
+ [{ doi: "10.48550/arXiv.2310.06770" }, "2310.06770"],
745
+ [{ doi: "10.48550/arXiv.2310.06770", arxiv: " " }, "2310.06770"],
746
+ [
747
+ { doi: "10.48550/arXiv.2310.06770", arxiv: "arXiv:2401.00001" },
748
+ "2401.00001",
749
+ ],
750
+ [{ doi: "10.1145/3548606" }, undefined],
751
+ ])("normalizeIdentifiers(%o).arxiv is %s", (citation, arxiv) => {
752
+ assert.equal(normalizeIdentifiers(citation).arxiv || undefined, arxiv);
753
+ });
754
+
741
755
  test("levenshtein, normalizeTitle and titleIdentity take a missing string as empty", () => {
742
756
  assert.deepEqual(
743
757
  [