Bib-manage

SkillFiles & storage

ALWAYS use when a manuscript's BIBLIOGRAPHY is the thing being fixed, or when a cite's PDF turns out to be wrong, 'link my bib entries to the PDFs', 'which sources am I missing a PDF for', 'audit my sources.bib', 'my bib entry has no file field', 'fetch the Federal Register PDFs', 'why can't pincite find this source'. WRONG FILE: 'is this PDF the published version or the preprint', 'this PDF is a book review, not the book', 'the file field points at the wrong paper', 'the PDF doesn't match the citation'. METADATA: 'reconcile my bib against Paperpile', 'these entries were extracted by a model, check them', 'fix the metadata in sources.bib', 'my bib has no DOIs and the fields look wrong', 'pull the real author/journal/volume from my library', 'where did this bib entry's metadata come from'. PAGE RANGES: 'does my bib have page ranges', 'my bib only has start pages', 'backfill the page ranges', 'pull the pages from the DOI', 'add DOIs to my bibliography'. Use proactively before any pincite run, pincite resolves footnotes through `file = {...}` and silently skips entries that lack one. NEGATIVE ROUTING: the page number inside a footnote is pincite, but whether that footnote's PDF is the version of record or the wrong document is THIS skill, mid-pincite-run included; whether a cited source says what the footnote claims is source-verify; Bluebook FORM is bluebook or bluebook-audit; finding or downloading a paper is fetch-paper or paperpile.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Bib-manage skill

What this skill tells your AI

The instructions your AI receives, as published by edwinhu/workflows in skills/bib-manage/SKILL.md and read by ahel’s review.

What this skill carries — grep references/ for any subject the names below miss: !d=${CLAUDE_SKILL_DIR}; command -v skill-toc >/dev/null 2>&1 && exec skill-toc "$d"; s=$HOME/.claude/skills/plugin-utils/bin/skill-toc; [ -x "$s" ] && exec "$s" "$d"; echo "(skill-toc unavailable: references and scripts are NOT listed here — install the plugin-utils plugin, or start a new session so its bin/ reaches PATH)"

Owns the bibliography as an index: every entry pointing at the right PDF, and that PDF being the version of record.

pincite CONSUMES this index — it resolves footnote → source through file = {...} and through nothing else. So the two failures this skill exists to prevent are both silent in pincite: an entry with no file is a pincite that is never attempted, and an entry pointing at a preprint is a pincite that is confidently wrong.

audit    entries with/without file, files missing on disk, entries without a
         DOI, PDFs no entry claims          <- run this first
link     citekey -> PDF, written back as file = {...}
version  is each entry's PDF the published version, or a preprint/proof?
doi      backfill missing DOIs from Crossref, verified against the bib's pages,
         and the PAGE RANGE that same verified record already carries
reconcile
         rewrite the entry's OWN FIELDS from a better source:
         Paperpile > Crossref > the extracted value. Never `file`, never a key
fedreg   fetch the Federal Register PDFs the entries cite

CLI

B="${CLAUDE_SKILL_DIR}/scripts/bibman.py"

python3 "$B" audit   --root .                 # start here
python3 "$B" link    --root . --dry-run       # inspect the mapping first
python3 "$B" link    --root . --backup        # then write
python3 "$B" version --root .
python3 "$B" doi     --root . --dry-run --only aggarwal2015
python3 "$B" reconcile --root . --dry-run            # read the full set FIRST
python3 "$B" reconcile --root . --backup --fields doi,pages
python3 "$B" fedreg  --root . --dry-run

Every path is a flag — --bib, --pdf-dir, --fedreg-dir — defaulting to the layout pincite uses, so the tool works on any manuscript. --only scopes version and doi to named citekeys; --dry-run reports without writing; --backup leaves a .bak; --strict makes version exit 1 on any entry that is not the version of record.

--fields scopes reconcile to a subset of author,title,journal,volume,pages, year,doi,publisher; --pp-index points at the Paperpile CLI's index cache.

audit, link and version never touch the network. doi and fedreg do. reconcile reads Paperpile locally and reaches Crossref only for what Paperpile does not cover.

Facts

  • The link threshold depends on the YEAR, and that is the whole of its accuracy. Two matchers run in order: PDF stem == citekey (48 of 79 on the test bibliography), then Paperpile-style Author Year - Title.pdf on first-author surname plus title similarity (the other 31). Same year, the year corroborates and 0.55 is enough. DIFFERENT year — a working paper against its published version, Lund 2017lund2018 — demand ≥ 0.95, because only the title separates two papers by one author. At 0.55 with the year ignored, alexander2010 ("Interim News and the Role of Proxy Voting Advice") claims the PDF "The role of advisory services in proxy voting" at 0.52. That is a wrong file in the bib, which is the one input pincite trusts absolutely — a mis-link there sends a footnote to another paper and can verify CONFIRMED while doing it.

  • Never force a match; REPORT what stays unmapped. On the test bibliography four PDFs stay unclaimed and every one is correct: Alexander et al. 2009 (0.52), Cremers and Romano 2011 whose filename is truncated mid-title by the filesystem (...proxy voting on co ... ion plans..., best match 0.89 against a 2007 entry), and two whose filenames are not Author Year - Title at all. An unmapped PDF costs a minute of human attention. A forced one costs a wrong citation nobody looks at again.

  • Paperpile writes SURNAMES ONLY, so the first-author surname is the FIRST token. Barzuza et al., Bebchuk and Hirst, Rock. Taking the last token — the correct habit for a bibtex author field — reads "Barzuza et al." as an author named "al." and unmapped 14 of 83 PDFs here before it was caught.

  • A 6-digit pages is an Elsevier ARTICLE NUMBER, not a page. J. Fin. Econ. 154 (2024) 103810 is paginated 1–N in its own PDF and IS the version of record. Range-testing it reports every such entry as a preprint — shu2024 and cabezon2025 are both genuinely published and both false alarms without this exception.

  • A publisher's own PDF can be an ADVANCE-ACCESS PROOF. OUP served iliev2021 as pages 1–49 instead of 5581+, v 00 n 0, internal name RFS-OP-….tex — authenticated, current, and useless for a pincite. Flag when the range test fails AND page 1 carries no nonzero volume designation. A successful fetch never means the pagination problem is solved.

  • Do NOT identify the version of record by matching the journal name against page 1. Bib journal names are Bluebook abbreviations ("Rev. Fin. Stud.") that never appear in a masthead ("The Review of Financial Studies"). That test flagged 46 of 61 entries and is worthless. The derived printed-page range against the bib's start page is the test that works: 5 of 79 flagged, all five real preprints.

  • The page derivation is pincite's, imported not copied. pages_of, page_numbers and page_offset are pure functions and pincite does nothing at import time. Copying them lets the two skills' notion of "what page is this" drift, and a version verdict that disagreed with pincite's own would be worse than no verdict.

  • A bibliographic title query returns the SSRN PREPRINT DOI. Measured on aggarwal2015: title alone returns 10.2139/ssrn.2120797, 10.2139/ssrn.1688993, 10.2139/ssrn.2023480 — the top three hits are all SSRN, none carries a page. Query with title + journal + volume + pages, then VERIFY the returned record's start page equals the bib's pages before accepting: that yields 10.1111/jofi.12284, page 2309–2346 against the bib's 2309. A DOI that fails the page check is reported, never written — it is usually the preprint of the very work the entry means.

  • The verified Crossref record already carries the END page; doi writes it. The match test was always record start page == bib start page, so the range that passes it is the same work's own span — 10.1111/jofi.12284 returns 2309-2346 against a bib holding 2309. Written as pages = {2309--2346}. Four guards, and each blocks a different way of destroying a value that was already right: @article only (a @misc press release's pages = {1} is not an article span); a bare start page only, so a Bluebook pincite someone recorded there survives — on OPV easterbrook1991 (66-67) and jackson2019 (849, 851--52) are the only range-like values in 197 entries and BOTH are pincites, not spans; never an ARTICLE_NUMBER; and the Crossref range must START at the bib's page, re-checked at write time so the DOI test and the range test can never disagree. Measured on OPV: 55 of the 64 bare start pages were eligible, 30 resolved, and the 25 that did not are law reviews, which Crossref indexes thinly.

  • An end page turns version from one test into three. With only a start page the derived span can be asked one question, lo <= start <= hi. With the end page it answers whether the span's BOTH endpoints fall inside, and whether the PDF's page count can hold the printed span at all — n < span is impossible for a version of record, and n > span + 2 (beyond a cover sheet) is a different artifact. aggarwal2015 is a 57-page PDF claiming a 38-page span, ertimur2018 64 pages for 21, hayne2019 59 for 43; each now fails on three counts instead of one, with no header parsing. Entries with no end page keep the start-only path unchanged — on OPV that is still most of them, and all 77 verdicts were byte-identical before and after the backfill.

  • reconcile's precedence is Paperpile > Crossref > the extracted value, and it is implemented as source ORDER, not a merge. An entry confidently matched in Paperpile is never queried against Crossref, so the stronger source cannot be contradicted by the weaker one. On OPV — a bib whose header reads Auto-extracted from sources.md via Gemini Vertex Batch, where every field was written by a model reading a markdown list and no entry carried a DOI — Paperpile covered 33 of 197, Crossref 24, and 140 had neither. Every change prints as citekey.field: old -> new [source]; --dry-run writes nothing and is byte-identical run to run.

  • A CONFIDENT Paperpile match needs the surname, the year AND the title, and an entry with no author needs the year EXACTLY. Surname equal, |Δyear| ≤ 1, title ≥ 0.90 same-year / ≥ 0.95 across a year — the same asymmetry link uses. With NO author there is nothing to corroborate with, and 1.00-vs-0.97 is not enough: secproxyadvice2019 (the SEC's own 2019 release, 84 Fed. Reg. 66,518) matched a 2020 COMMENT LETTER about it at 0.98, whose title is the release's title with RE: in front. Requiring Δyear == 0 there kills it; a document with no author has a fixed date and no working-paper drift to forgive. Two candidates within 0.02 of each other are AMBIGUOUS — reported, never picked, because a wrong library match rewrites author, title, journal and DOI at once.

  • Crossref's start-page verification is @article ONLY, because a pincite is shaped exactly like a page range. Nothing in the VALUE 66-67 says whether it is pages 66 to 67 of an article or a Bluebook pincite into a book. Reading easterbrook1991's 66 as a start page made Crossref accept 10.1111/j.1468-0319.1991.tb00155.x, titled "Discussion Papers", because it happens to begin on page 66 — a proposal to overwrite the title of The Economic Structure of Corporate Law. The entry TYPE is what separates them, and range_blocked already said so. This guard lives in crossref_pick, so doi gets it too.

  • A Paperpile record can be the SSRN PREPRINT of a published article, and taking its fields REGRESSES the citation. choi2009 (S. Cal. L. Rev. 82:649) and bebchuk2019a (B.U. L. Rev. 99:721) are both published; both library records carry journal = SSRN Electron. J. and a 10.2139/ssrn.* DOI. Precedence orders sources by reliability — it does not authorise replacing a real journal name with a preprint server's placeholder. When the source record is SSRN-shaped and the entry ALREADY names a journal, journal, volume, pages, year, publisher and doi are all refused and reported. A genuine working paper has no journal to protect, so it is unaffected.

  • Precedence does not authorise a THINNER rendering of a name. Paperpile answered J Fisch and A Hamdani and S D Solomon for fisch2019's Jill E. Fisch and Assaf Hamdani and Steven Davidoff Solomon, and Crossref answered VICENTE CUÑAT and MIREIA GINE and MARIA GUADALUPE for Vicente Cuñat and Mireia Giné and Maria Guadalupe — initials for given names, and full caps with the accents gone. Both are refused and reported. Note that str.isupper() on the whole author string is ALWAYS False: the and separators are lowercase, so the test must run on the names alone.

  • reconcile never touches file and never touches a citekey. file is not in RECONCILE_FIELDS and is refused by name in field_blocked — the PDF mapping belongs to link, which owns the matcher and its thresholds. The keys are load-bearing for something outside this repo: paper/typst/opv-body.typ references entries through #ref(<key>) and that project's scripts/resolve_refs.py resolves supra numbers through them, so a renamed key silently breaks the manuscript. After writing, the file is re-parsed and the key LIST compared to what it was; a mismatch exits non-zero pointing at the .bak.

  • federalregister.gov bot-blocks every request and lands on unblock.federalregister.gov. Fetch from govinfo's link service, https://www.govinfo.gov/link/fr/<vol>/<page>?link-type=pdf, into --fedreg-dir as fedreg-<vol>-<page>.pdf. Pre-2000 volumes return the WHOLE ISSUE, 200+ MB — trim with qpdf --empty --pages IN a-b -- OUT. The SEC's own release PDF of the same document has different pagination, so it is the wrong source for a Fed. Reg. pincite.

Red flags — STOP

About toWhy wrongDo instead
Run pincite on a bib you have not auditedEntries with no file are skipped silently; here that was 118 of 197audit first
Lower the 0.95 cross-year threshold to map "just one more" PDFAt 0.55 alexander2010 claims the wrong paper at 0.52Leave it unmapped and link it by hand
Hand-add a file to make an unmapped PDF go awayThe unmapped list is the output, not a failureRead the reason it prints first
Report a 6-digit pages entry as a preprintIt is an Elsevier article number; the PDF is the version of recordTrust the article-number skip
Check the version of record by finding the journal name on page 1Bluebook abbreviations never match mastheads — 46 of 61 flaggedRange-test the derived pages against the bib's start page
Accept a Crossref DOI because the title matchedTitle-only returns the SSRN preprint; 6 of 7 test entriesRequire the start page to equal the bib's pages
Write a page range over a pages that already holds oneIt is a Bluebook pincite, not a span — both such values on OPV wereLeave it; doi reports it as SKIPPED
Give a @misc or @book entry an article spanpages = {1} on a press release is not page 1 of an articleThe @article guard; read the SKIPPED list
Trust a freshly fetched publisher PDFIt can be an advance-access proof paginated 1–NRe-run version on anything fetched
Use the SEC release PDF for a Fed. Reg. citeDifferent pagination from the one the cite usesfedreg via govinfo link/fr/<vol>/<page>
Run link without --dry-run on a bib you have not inspectedIt writes into 197 entries--dry-run first, then --backup
Run reconcile without reading the full --dry-run setIt rewrote 163 fields across 47 entries on OPVRead every line, then --backup; --fields to scope
Take a Paperpile record's journal when the entry already names one and the record is SSRNSSRN Electron. J. over B.U. L. Rev. is a regression, not a correctionThe SSRN guard; read the CONFLICTS list
Accept a Paperpile match on a no-author entry a year apartThe SEC's 2019 release matched a 2020 comment letter at 0.98Δyear == 0 when there is no surname to corroborate
Let Crossref verify a non-@article by its start page66-67 is a pincite, and "Discussion Papers" also starts on page 66@article only — the type is what separates them
Add file to RECONCILE_FIELDS "since it is a field too"The PDF mapping is link's, matcher and thresholds includedLeave it; field_blocked refuses it by name

Signals

GitHub stars
21
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
bib-manage
Source
github.com/edwinhu/workflows