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 t...
Scanned 9/19/2026
Install to Claude Code
npx -y skills add edwinhu/workflows --skill bib-manage --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Bib Manage?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/edwinhu-bib-manage)More formats (shields.io, HTML) on the badges page.
---
name: bib-manage
description: "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."
---
# Bib-manage
**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
```bash
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 2017` → `lund2018` — 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 to | Why wrong | Do instead |
|---|---|---|
| Run pincite on a bib you have not audited | Entries with no `file` are skipped silently; here that was 118 of 197 | `audit` first |
| Lower the 0.95 cross-year threshold to map "just one more" PDF | At 0.55 `alexander2010` claims the wrong paper at 0.52 | Leave it unmapped and link it by hand |
| Hand-add a `file` to make an unmapped PDF go away | The unmapped list is the output, not a failure | Read the reason it prints first |
| Report a 6-digit `pages` entry as a preprint | It is an Elsevier article number; the PDF is the version of record | Trust the `article-number` skip |
| Check the version of record by finding the journal name on page 1 | Bluebook abbreviations never match mastheads — 46 of 61 flagged | Range-test the derived pages against the bib's start page |
| Accept a Crossref DOI because the title matched | Title-only returns the SSRN preprint; 6 of 7 test entries | Require the start page to equal the bib's `pages` |
| Write a page range over a `pages` that already holds one | It is a Bluebook pincite, not a span — both such values on OPV were | Leave it; `doi` reports it as SKIPPED |
| Give a `@misc` or `@book` entry an article span | `pages = {1}` on a press release is not page 1 of an article | The `@article` guard; read the SKIPPED list |
| Trust a freshly fetched publisher PDF | It can be an advance-access proof paginated 1–N | Re-run `version` on anything fetched |
| Use the SEC release PDF for a `Fed. Reg.` cite | Different pagination from the one the cite uses | `fedreg` via govinfo `link/fr/<vol>/<page>` |
| Run `link` without `--dry-run` on a bib you have not inspected | It writes into 197 entries | `--dry-run` first, then `--backup` |
| Run `reconcile` without reading the full `--dry-run` set | It rewrote 163 fields across 47 entries on OPV | Read every line, then `--backup`; `--fields` to scope |
| Take a Paperpile record's `journal` when the entry already names one and the record is SSRN | `SSRN Electron. J.` over `B.U. L. Rev.` is a regression, not a correction | The SSRN guard; read the CONFLICTS list |
| Accept a Paperpile match on a no-author entry a year apart | The 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 page | `66-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 included | Leave it; `field_blocked` refuses it by name |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!