fix(openalex): canonicalize arXiv DOI to bare arXiv id (DQ-5 arXiv half)
OpenAlex returns arXiv works under the arXiv DOI (10.48550/arXiv.<id>); _normalize_doi (DQ-5) left it as 10.48550/arxiv.<id>, but the corpus keys arXiv papers on the BARE id (2305.10988 / math/0603097). So re-ingesting an arXiv paper by its bare id missed INSERT ... ON CONFLICT (id) and tripped papers_openalex_id_key — the arXiv half of DQ-5, explicitly deferred there and surfaced again driving the R-C .tex re-ingest. Add _canonical_id() (normalize DOI, then strip the 10.48550/arxiv. prefix) and use it in _map_paper. Regression tests (modern + legacy arXiv DOI, regular DOI unchanged, fetch_paper bare id). Confirmed live: re-ingesting 2305.10988 now UPDATEs in place. Full suite 370 passed; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,27 @@ def _normalize_doi(doi: str) -> str:
|
||||
return s
|
||||
|
||||
|
||||
# arXiv works are registered under a DOI of the form 10.48550/arXiv.<id>.
|
||||
_ARXIV_DOI_PREFIX = "10.48550/arxiv."
|
||||
|
||||
|
||||
def _canonical_id(doi: str) -> str:
|
||||
"""Return the canonical ``papers.id`` for an OpenAlex ``doi`` value.
|
||||
|
||||
Builds on :func:`_normalize_doi` (bare, lower-cased DOI), then strips the
|
||||
arXiv DOI prefix ``10.48550/arxiv.`` so an arXiv work's id is the BARE arXiv
|
||||
id (``2305.10988`` / ``math/0603097``) — the form the corpus is keyed on —
|
||||
not the arXiv DOI. Without this, re-ingesting an arXiv paper by its bare id
|
||||
misses ``INSERT … ON CONFLICT (id)`` and trips ``papers_openalex_id_key``.
|
||||
This is the arXiv half of DQ-5 (the DOI half was fixed there; this was
|
||||
explicitly deferred and surfaced again driving the R-C .tex re-ingest).
|
||||
"""
|
||||
normalized = _normalize_doi(doi)
|
||||
if normalized.startswith(_ARXIV_DOI_PREFIX):
|
||||
return normalized[len(_ARXIV_DOI_PREFIX) :]
|
||||
return normalized
|
||||
|
||||
|
||||
def _map_paper(data: dict[str, Any]) -> Paper:
|
||||
authors: list[str] = [
|
||||
a.get("author", {}).get("display_name") or "" for a in data.get("authorships", [])
|
||||
@@ -112,7 +133,7 @@ def _map_paper(data: dict[str, Any]) -> Paper:
|
||||
abstract = _reconstruct_abstract(data.get("abstract_inverted_index"))
|
||||
doi = data.get("doi")
|
||||
return Paper(
|
||||
id=_normalize_doi(doi) if doi else (data.get("id") or ""),
|
||||
id=_canonical_id(doi) if doi else (data.get("id") or ""),
|
||||
title=data.get("title") or "",
|
||||
openalex_id=data.get("id"),
|
||||
authors=authors,
|
||||
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
|
||||
from codex.models import Citation, Paper
|
||||
from codex.sources import openalex
|
||||
from codex.sources.openalex import _normalize_doi, _resolve_id
|
||||
from codex.sources.openalex import _canonical_id, _normalize_doi, _resolve_id
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
@@ -97,6 +97,42 @@ def test_normalize_doi_already_bare_is_idempotent() -> None:
|
||||
assert _normalize_doi("10.1007/s00454-019-00132-8") == "10.1007/s00454-019-00132-8"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _canonical_id — arXiv DOI → bare arXiv id (arXiv half of DQ-5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_canonical_id_strips_arxiv_doi_modern() -> None:
|
||||
# arXiv works carry the arXiv DOI; the canonical id is the bare arXiv id.
|
||||
assert _canonical_id("https://doi.org/10.48550/arXiv.2305.10988") == "2305.10988"
|
||||
|
||||
|
||||
def test_canonical_id_strips_arxiv_doi_legacy() -> None:
|
||||
assert _canonical_id("https://doi.org/10.48550/arXiv.math/0603097") == "math/0603097"
|
||||
|
||||
|
||||
def test_canonical_id_leaves_regular_doi_bare() -> None:
|
||||
# A normal journal DOI is only normalized (bare + lower-case), not stripped.
|
||||
assert _canonical_id("https://doi.org/10.1007/S00454-019-00132-8") == (
|
||||
"10.1007/s00454-019-00132-8"
|
||||
)
|
||||
|
||||
|
||||
def test_fetch_paper_arxiv_doi_yields_bare_id(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""An arXiv work maps to the BARE arXiv id (not the arXiv DOI), so re-ingesting
|
||||
by arXiv id hits ON CONFLICT (id) instead of tripping papers_openalex_id_key."""
|
||||
work = {**_SAMPLE_WORK, "doi": "https://doi.org/10.48550/arXiv.2305.10988"}
|
||||
|
||||
def mock_get(url: str, params: dict[str, str] | None = None) -> httpx.Response:
|
||||
return httpx.Response(200, json=work)
|
||||
|
||||
monkeypatch.setattr(openalex, "_get", mock_get)
|
||||
paper = openalex.fetch_paper("2305.10988")
|
||||
|
||||
assert paper is not None
|
||||
assert paper.id == "2305.10988"
|
||||
|
||||
|
||||
def test_map_paper_falls_back_to_openalex_id_without_doi(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user