bibcheck · diff
git:20260515.605c1c3 to git:20260727.37a3f77
255 added, 110 removed. Audit A to A.
---
name: bibcheck
- description: Audit an existing .bib file entry-by-entry against ground truth (DOI, Semantic Scholar, OpenAlex, arXiv) to catch the silent citation errors that pass spell-check but kill peer review — wrong year, mis-cited authors, wrong journal/volume, swapped title-author pairs, fabricated DOIs, and outright hallucinated entries (a real concern with LLM-assisted drafting). Spawns one narrow-focus subagent per entry so each citation gets full attention rather than the gradient-decay that single-pass audits suffer at scale. Use whenever the user says "/bibcheck", "audit my .bib", "verify references", "check my bibliography for errors", "any fake citations", "pre-submission bib audit", "did Claude hallucinate any of these", or before shipping a manuscript / R&R to Marketing Science, JMR, JCR, Management Science. Read-only and audit-only — never adds, edits, or removes entries; produces a PASS/WARN/FAIL report and a `corrected.bib` the user can diff and move into place themselves. Strictly distinct from `/cite` (which ADDS one entry to a `.bib` from a DOI/arXiv/title) — `/bibcheck` is the verification mirror image.
- argument-hint: "[.bib file path] (defaults to references.bib / refs.bib / bibliography.bib in cwd)"
- allowed-tools: ["Read", "Grep", "Glob", "Write", "Bash", "Task", "Monitor", "WebFetch", "mcp__semantic-scholar__get_paper_details", "mcp__semantic-scholar__search_papers", "mcp__openalex__openalex_search_entities", "mcp__arxiv__get_abstract", "mcp__arxiv__search_papers", "mcp__zotero__zotero_item_metadata", "mcp__zotero__zotero_search_items"]
- effort: medium
+ description: Audit an existing .bib file entry by entry against canonical metadata (Crossref, OpenAlex, arXiv, Zotero) to catch silent citation errors that pass spell-check and kill peer review, including wrong years, mis-cited authors, wrong journal or volume, swapped title-author pairs, dead DOIs, and hallucinated entries from LLM-assisted drafting. TRIGGER on "/bibcheck", "audit my .bib", "verify my references", "check my bibliography", "are any of these citations fake", "did Claude hallucinate a citation", "pre-submission bib audit", or before shipping a manuscript or R&R to Marketing Science, JMR, JCR, or Management Science. Read-only: writes a report and a separate corrected.bib, never touches the source.
+ allowed-tools: Read, Write, Glob, Grep, Bash, Task, Agent, Monitor, WebFetch, mcp__zotero__zotero_search_items, mcp__zotero__zotero_get_item_metadata
---
- # bibcheck — Per-Entry Bibliography Audit
+ # bibcheck
- Audit-only sister skill to `/cite`. Given a `.bib` file, spawn one subagent per entry to verify the entry against canonical sources (DOI metadata, Semantic Scholar, OpenAlex, arXiv, Zotero). Report PASS / WARN / FAIL per entry with a one-line diagnostic, group by severity, and write a dated report plus a `corrected.bib` the user can review and merge themselves. Never overwrites the source `.bib`.
+ Given a `.bib`, spawn subagents over batches of 5 entries to verify each entry against canonical
+ metadata, then emit a PASS/WARN/FAIL report and a `corrected.bib` the user reviews and merges. The
+ source `.bib` is never modified.
- **Why per-entry parallelism.** One agent asked to audit 80 entries in a single pass drifts: early entries get careful treatment, late entries get pattern-matched. Spawning one subagent per entry gives each citation a full attention budget and removes the late-batch quality cliff. This mirrors the structure of `/seven-pass-review` (parallel lenses) and `/audit-reproducibility` (per-claim checking).
+ One agent asked to audit 200 entries in a single pass drifts: early entries get careful treatment,
+ late entries get pattern-matched. Batches of 5 stay short enough that the cliff never arrives, and
+ the batch contract in phase 1 polices the same drift inside each batch.
- **The LLM-fabrication concern.** Hallucinated citations have a recognizable signature: plausible author names + plausible title + a DOI that 404s + no Semantic Scholar / OpenAlex / arXiv match anywhere. `/bibcheck` is built to surface exactly this pattern.
+ Per-entry verification against canonical metadata is adapted from Scott Cunningham's
+ [MixtapeTools](https://github.com/scunning1975/MixtapeTools).
- ## When to use
+ Hallucinated citations have a recognizable signature: plausible authors, plausible title, a DOI
+ that 404s, and no match in Crossref, OpenAlex, or arXiv. Surfacing that pattern is the point.
- - **Pre-submission**: before MKSCI / JMR / JCR / MS submission or resubmission, especially for drafts where any text was AI-assisted.
- - **Inherited `.bib`**: when picking up a coauthor's or RA's bibliography and you don't know its provenance.
- - **Post-Zotero-export sanity check**: even Better BibTeX can carry forward bad upstream metadata.
- - **After a large `/litreview` batch**: verify that everything the lit-review skill staged into `.bib` actually exists.
+ ## Inputs and scoping
- ## Inputs
+ Take a `.bib` path as the argument. With no argument, Glob in this order and ask if ambiguous:
- - `$0` — path to a `.bib`. If omitted, Glob the cwd for `references.bib`, `refs.bib`, `bibliography.bib`, then any `*.bib`. Overleaf projects typically live under `<OVERLEAF_ROOT>/<PROJECT_SUBDIR>/`. If multiple `.bib` files exist, grep the main `.tex` for `\bibliography{}` / `\addbibresource{}` to pick the right one; ask the user if still ambiguous.
+ 1. `references.bib`, `refs.bib`, `bibliography.bib`, then `*.bib` in the cwd.
+ 2. `~/Library/CloudStorage/Dropbox*/Apps/Overleaf/*/*.bib` (this setup assumes Overleaf projects sync here via Dropbox; adjust to your machine).
- ## Workflow
+ With several `.bib` files in one project, grep the `.tex` for `\bibliography{}` / `\addbibresource{}`
+ to pick the one actually loaded. Skip anything under `_archive/` or `Old/` unless asked.
- ### Phase 0: Parse the .bib
+ Scope before dispatching. Zotero-exported libraries carry far more entries than the manuscript
+ cites (one real project here: 345 entries, 170 cited), and auditing an uncited entry is wasted work.
+ Collect the cited keys and offer to restrict to them:
- 1. Read the full `.bib`.
- 2. Split into entries on `^@\w+\{` boundaries. Capture entry type (`article`, `book`, `incollection`, `misc`, `inproceedings`, `unpublished`), citation key, and every `field = {value}` pair (handle braced and quoted forms, escaped braces, multi-line values).
- 3. Build an in-memory list of entries. Note malformed entries (unbalanced braces, missing key, duplicate key) — these go straight to FAIL without dispatching a subagent.
- 4. Create a working directory `<bib_dir>/bibcheck_<YYYYMMDD_HHMM>/` with subfolders `entries/` (one file per entry for subagent input) and `reports/` (one JSON per entry for subagent output).
+ ```bash
+ grep -ohE '\\[a-zA-Z]*[Cc]ite[a-zA-Z]*\*?(\[[^]]*\])*\{[^}]*\}' /abs/path/to/*.tex \
+ | grep -oE '\{[^}]*\}$' | tr -d '{}' | tr ',' '\n' | tr -d ' ' | sort -u
+ ```
- ### Phase 1: Spawn per-entry subagents
+ Report both counts and let the user choose. Default to cited-only above 100 entries.
- For each well-formed entry, launch a `Task` subagent with a focused prompt: "Verify this single BibTeX entry against canonical sources. Return JSON with verdict and per-field findings." Cap concurrent subagents at 8 (`--max-parallel` if the user passes it). Each subagent runs the per-entry checks below and writes one JSON file to `reports/<citekey>.json`. Lower the cap if rate-limited; raise it for very large bibliographies.
+ ## Phase 0: parse
- ### Phase 2: Per-entry checks (run by each subagent)
+ Read the `.bib` in full. Split on `^@\w+\{` boundaries and capture the entry type, citation key, and
+ every `field = {value}` pair (braced and quoted forms, escaped braces, multi-line values). Malformed
+ entries (unbalanced braces, missing key, duplicate key) go straight to FAIL with no subagent.
- In order, stopping early once verdict is determined:
+ Triage by entry type, because the checks that apply differ:
- 1. **DOI resolution.** If the entry has a `doi` field:
- - Call `mcp__semantic-scholar__get_paper_details(paper_id="DOI:<doi>")`. On hit, compare title / author surnames / year / venue / volume / issue / pages against the entry. Discrepancies → field-level WARN; outright mismatches → FAIL.
- - If S2 misses, fall back to `WebFetch("https://api.crossref.org/works/<doi>")` for Crossref ground truth.
- 2. **arXiv resolution.** If the entry looks like a preprint (`eprint = {2403.12345}`, `archivePrefix = {arXiv}`, or `note = {arXiv:...}`):
- - Call `mcp__arxiv__get_abstract(paper_id="<id>")` to confirm existence and pull canonical metadata.
- - Cross-check whether the same paper now has a published version with a DOI (use `mcp__semantic-scholar__search_papers(query="<title>")`) — if so, emit a WARN suggesting the published `@article` entry instead of the preprint `@misc`.
- 3. **Title-only search (no DOI, no arXiv).** Call `mcp__semantic-scholar__search_papers(query="<title>", limit=5)`. Fuzzy-match against the entry's title (case-insensitive, normalize punctuation, accept ≥ 0.85 cosine on token-set). On a confident hit, compare authors / year / venue. On no plausible match, also try `mcp__openalex__openalex_search_entities(entity_type="works", query="<title>")` as a second opinion.
- 4. **Fabrication test.** If (a) `doi` is present but DOI doesn't resolve in S2 OR Crossref, AND (b) title search returns no fuzzy match in S2 OR OpenAlex OR arXiv → FAIL with diagnostic `fabricated? — DOI does not resolve and no metadata source recognises the title`. This is the LLM-hallucination signal.
- 5. **Author normalization.** Compare author surnames as the ground-truth source returns them. Flag:
- - Wrong surname (clear mismatch) → FAIL.
- - Initials vs. full first names → no flag (style choice).
- - Missing accents (`Pena` vs. `Peña`), missing tussenvoegsels (`Van Den Bulte` vs. `van den Bulte`) → WARN.
- - Author-list truncation (`& others` or missing trailing authors) → WARN.
- 6. **Year reconciliation.** If the entry year differs from the canonical publication year:
- - If `eprint`/`archivePrefix=arXiv` present and entry year matches arXiv-submission year → WARN ("entry cites preprint year `<y1>`; published version is `<y2>`"). Do not auto-FAIL — citing the preprint year is a legitimate choice.
- - Otherwise → FAIL ("year mismatch: entry says `<y1>`, canonical says `<y2>`").
- 7. **Journal canonicalization.** Compare `journal` against canonical name from S2 / OpenAlex. Marketing-relevant cases worth WARNing on:
- - Abbreviation drift: `J. Mark. Res.` vs. `Journal of Marketing Research`, `Mark. Sci.` vs. `Marketing Science`, `Mgmt. Sci.` vs. `Management Science`, `Quant. Mark. Econ.` vs. `Quantitative Marketing and Economics`.
- - Wrong journal entirely (different ISSN family) → FAIL.
- - Apply the same logic for `booktitle` on `@inproceedings` (`ICML`, `NeurIPS`, `KDD` — full names).
- 8. **Volume / issue / pages.** Compare numeric fields. WARN on a single-field mismatch; FAIL if 2+ disagree (likely a swapped-entry bug — title of paper A glued to the volume/pages of paper B).
- 9. **Zotero cross-check (optional).** If a citation key looks Better-BibTeX-shaped (e.g., `lastname2024Word`), call `mcp__zotero__zotero_search_items(query="<title>", qmode="titleCreatorYear", limit=3)`. If a Zotero hit exists with a different DOI than the entry, WARN. *The Zotero MCP is read-only* — never attempt to write back; surfacing the mismatch is sufficient.
+ - Indexed types (`@article`, `@inproceedings`, `@book`, `@incollection`, `@phdthesis`): full check set.
+ - Non-indexed types (`@misc`, `@online`, `@software`, `@dataset`, and `@unpublished` with no arXiv
+ id): blog posts, docs pages, and repos legitimately have no DOI and appear in no index. Check the
+ `url`/`howpublished` link resolves (`curl -sSI -o /dev/null -w '%{http_code}'`) and that `year`
+ looks sane. Never assign the fabrication verdict to these.
- Each subagent emits:
+ Create `<bib_dir>/bibcheck_<YYYYMMDD_HHMM>/` containing `entries/` (one `.bib` fragment per entry,
+ subagent input) and `reports/` (one `.json` per entry, subagent output), plus `input.bib` as a
+ verbatim copy of the source for provenance.
+ ## Phase 1: dispatch
+
+ Group the well-formed entries into batches of 5, preserving file order; the last batch holds the
+ remainder. Launch one subagent per batch with the `Agent` tool, 8 per message, so 40 entries are in
+ flight per wave. Wait for the wave to return, then send the next 8. 170 cited entries is 34 batch
+ subagents in 5 waves. Respect `--max-parallel N` if the user passes it; drop to 2 when
+ rate-limited.
+
+ Each subagent gets its batch's raw entry texts, the absolute `entries/` paths, one absolute output
+ path `reports/<citekey>.json` per entry, and the check list below. Subagent working directories
+ reset between Bash calls, so every path handed to a subagent must be absolute.
+
+ The batch contract: the subagent MUST return the full per-entry output schema for every one of its
+ entries, each verified independently against the lookup tools, with no verdict inferred from a
+ batchmate. State in the subagent prompt that the failure mode to guard against is skimping on the
+ later entries in the batch, and that entry 5 gets the same treatment as entry 1.
+
+ Give each subagent a 60-second lookup budget per entry. On expiry it writes
+ `{"verdict": "WARN", "diagnostic": "lookup_timeout"}` for that entry and moves to its next one.
+ Raise the budget on a cold cache; `paper.py` disk-caches responses for 30 days, so reruns are fast.
+
+ For a long run, dispatch waves of background subagents with the `Agent` tool and act on their
+ completion notifications, sending the next wave as each one finishes.
+
+ ## Phase 2: per-entry checks
+
+ ### The two lookup tools
+
+ `paper.py` is the resolver from the `reading-papers` skill. Read
+ `~/.claude/skills/reading-papers/SKILL.md` if unfamiliar.
+
+ ```bash
+ ~/.claude/skills/reading-papers/scripts/paper.py resolve "<query>" --json
+ ```
+
+ It returns a JSON array. Interpreting it is the whole game:
+
+ - One element with `title`, `authors`, `year`, `venue`, `is_oa` populated: a confident canonical
+ match.
+ - One element with only `{"doi": ..., "title": null}`: the identifier exists as a string but no
+ index recognizes it. Exit code is still 0, so test the payload, not `$?`.
+ - Several elements carrying only `{doi, title, venue}`: no confident match, just relevance-ranked
+ candidates. Treat as "not found" unless one clears the 0.85 title threshold below.
+
+ It does not return volume, issue, or pages. Crossref does, and Crossref is free and unmetered.
+ Replace `you@example.edu` with your real address in both Crossref calls in this file: the
+ `mailto=` parameter joins Crossref's polite pool, and Crossref asks for a real contact.
+
+ ```bash
+ curl -sS -w '\nHTTP:%{http_code}' "https://api.crossref.org/works/<doi>?mailto=you@example.edu" \
+ | python3 -c 'import sys,json; b=sys.stdin.read(); code=b.rsplit("HTTP:",1)[1].strip()
+ if code!="200": print(json.dumps({"crossref_status":code})); raise SystemExit
+ m=json.loads(b.rsplit("\nHTTP:",1)[0])["message"]
+ print(json.dumps({k:m.get(k) for k in ("DOI","title","container-title","short-container-title","volume","issue","page","issued","type","author","ISSN")},ensure_ascii=False))'
+ ```
+
+ A dead DOI returns HTTP 404 with the non-JSON body `Resource not found.`, so test the status code,
+ never `$?` alone (`--fail` exits 56 here, not the documented 22). Use `WebFetch` on the same URL only
+ if Bash is unavailable; it summarizes through a model and drops fields.
+
+ Cost discipline. OpenAlex is metered: an identifier lookup costs 1 credit, anything routed through
+ `search` (including title search) costs 10. Spend identifiers before strings. Order per entry:
+ Crossref by DOI (free) → `paper.py resolve` by DOI or arXiv id (1 credit) → Crossref
+ `query.bibliographic` (free) → `paper.py resolve` by title (up to 10 credits), reaching the last rung
+ only when the entry has no identifier at all.
+
+ ### The checks, in order, stopping once the verdict is settled
+
+ 1. DOI resolution. With a `doi` field, run the Crossref call above for volume, issue, pages, ISSN,
+ and `issued`, then `paper.py resolve "<doi>" --json` for the authoritative author spellings and
+ venue. Crossref 404 plus `"title": null` from `paper.py` is a dead DOI, so go to check 4.
+
+ 2. arXiv resolution and preprint versus published. When the entry carries `eprint`,
+ `archivePrefix = {arXiv}`, or `arXiv:` in `note`, run
+ `paper.py resolve "<arxiv_id>" --json`. A hit returns `arxiv_id`, `title`, `authors`, `year`,
+ `abstract`, and `doi` (null when arXiv-only). Confirm the title matches the entry. If the entry
+ has no `doi`, resolve the exact title once to see whether a journal version now exists; a single
+ full record with a non-arXiv `venue` means it does, and that is a WARN suggesting the published
+ `@article` in place of the preprint `@misc`. Skip this probe for entries that already carry a
+ published DOI, since it is the expensive rung.
+
+ 3. Title match, last resort only. With no DOI and no arXiv id:
+
+ ```bash
+ curl -sS "https://api.crossref.org/works?query.bibliographic=<urlencoded+title>&rows=5&select=DOI,title,container-title,volume,issue,page,issued,author&mailto=you@example.edu"
+ ```
+
+ Crossref ranks by relevance, and the preprint often outranks the version of record (a real case:
+ the SSRN copy of a Marketing Science paper comes back first). Pick by container: prefer a journal
+ container over `SSRN Electronic Journal`, `arXiv`, `Research Square`, or `SocArXiv`. Confirm the
+ winner with `paper.py resolve "<winning doi>" --json` at 1 credit. Only if Crossref returns
+ nothing plausible, fall back to `paper.py resolve "<title>" --json`.
+
+ Title match rule: lowercase, strip LaTeX braces and accents, strip punctuation, drop the
+ stopwords {a, an, the, of, on, in, for, and, to}, tokenize on whitespace, and compute Jaccard
+ overlap of the token sets. Accept at ≥ 0.85.
+
+ 4. Fabrication test. FAIL with `fabricated? DOI does not resolve and no index recognizes the title`
+ when all of these hold: the entry is an indexed type; a `doi` is present and both Crossref and
+ `paper.py` reject it (or no `doi` is present at all); and the title clears 0.85 against nothing
+ in Crossref, OpenAlex, or arXiv. All three conditions are required. A missing DOI alone is
+ normal for older and non-English work.
+
+ 5. Author normalization. Compare surname sequences after NFKD normalization, combining-mark removal,
+ lowercasing, and punctuation stripping.
+ - Clear surname mismatch, or a different number of named authors where the source lists fewer:
+ FAIL.
+ - Initials against full given names: no flag, that is a style choice.
+ - Accent loss (`Pena` for `Peña`) or a dropped tussenvoegsel (`Van Den Bulte` for
+ `van den Bulte`): WARN. Strip leading particles {van, van der, van den, de, de la, del, della,
+ di, du, von, zu, ter, ten, la, le} before the surname comparison, then compare the particle on
+ its own.
+ - Author-list truncation (`and others`, missing trailing authors): WARN.
+ - Judge accents against `paper.py`/OpenAlex, never against Crossref. Crossref frequently stores
+ the stripped form (it returns `Yildirim` where OpenAlex returns `Yıldırım`), so a Crossref
+ diff alone is not evidence of an error in the entry.
+
+ 6. Year reconciliation against Crossref `issued.date-parts[0][0]`. With an arXiv id present and the
+ entry year matching the arXiv submission year, WARN
+ (`entry cites preprint year <y1>; published version is <y2>`), because citing the preprint year
+ is a legitimate choice. Otherwise FAIL.
+
+ 7. Journal canonicalization. Compare `journal` against Crossref `container-title`. WARN on
+ abbreviation drift (`J. Mark. Res.` for `Journal of Marketing Research`, `Mark. Sci.` for
+ `Marketing Science`, `Mgmt. Sci.` for `Management Science`, `Quant. Mark. Econ.` for
+ `Quantitative Marketing and Economics`). FAIL on a different journal entirely, confirmed by a
+ disjoint `ISSN` set. Apply the same logic to `booktitle` on `@inproceedings` (ICML, NeurIPS, KDD
+ and their full names).
+
+ 8. Volume, issue, and pages against Crossref `volume`, `issue`, `page`. Normalize page ranges before
+ comparing: `831--847`, `831-847`, and `831–847` are the same range, and BibTeX's double hyphen is
+ correct style. WARN on one field disagreeing. FAIL on two or more, which usually means a swapped
+ entry, the title of paper A glued to the volume and pages of paper B.
+
+ 9. Zotero cross-check, optional. For a Better BibTeX shaped key, call
+ `mcp__zotero__zotero_search_items(query="<title>", qmode="titleCreatorYear", limit=3)` and pull
+ `mcp__zotero__zotero_get_item_metadata` on a hit. A Zotero item with a different DOI than the
+ entry is a WARN. Zotero reads work only while the desktop app is open, so any error means
+ `checks_skipped: ["zotero"]`, never a WARN.
+
+ When a Zotero call fails, tell the user directly, in the chat reply and not only in the report:
+ Zotero looks closed, open the Zotero desktop app and this can be re-run to add the library
+ cross-check. Say it once, up front, rather than burying it in per-entry output. A closed Zotero
+ is the usual cause and it is a five-second fix, so it is worth interrupting for. Do not stop the
+ audit over it; the other eight checks do not need Zotero.
+
+ ### Subagent output
+
+ One file per entry at `reports/<citekey>.json`:
+
```json
{
- "citekey": "smith2024paper",
+ "citekey": "liu2022implications",
"verdict": "PASS | WARN | FAIL",
- "diagnostic": "one-line summary",
+ "diagnostic": "one line",
"field_findings": [
- {"field": "year", "status": "FAIL", "entry_value": "2023", "canonical_value": "2024", "source": "doi.org/10.1287/mksc.2024.0123"}
+ {"field": "year", "status": "FAIL", "entry_value": "2023",
+ "canonical_value": "2022", "source": "crossref:10.1287/mksc.2022.1361"}
],
- "canonical_source": "doi | s2 | openalex | arxiv | none",
- "suggested_replacement": "<full corrected BibTeX entry, or null if PASS / unfixable>"
+ "canonical_source": "crossref | openalex | arxiv | zotero | none",
+ "checks_skipped": ["zotero"],
+ "suggested_replacement": "<full corrected BibTeX entry, or null>"
}
```
- ### Phase 3: Aggregate
-
- Read every JSON in `reports/`. Bucket entries by verdict: FAIL > WARN > PASS. Within each bucket, sort by citekey alphabetically (stable, easy to diff across runs).
-
- ### Phase 4: Report
+ ## Phase 3: aggregate and report
- Write `<bib_dir>/bibcheck_<YYYYMMDD_HHMM>/bibcheck_report.md`:
+ Read every JSON in `reports/`. Bucket by FAIL, then WARN, then PASS; sort alphabetically by citekey
+ within each bucket so runs diff cleanly. Write
+ `<bib_dir>/bibcheck_<YYYYMMDD_HHMM>/bibcheck_report.md`:
```markdown
- # Bibliography Audit: <bib filename>
+ # Bibliography audit: <bib filename>
- **Date:** YYYY-MM-DD HH:MM
- **Source:** <full path to .bib>
- **Entries audited:** N (parsed) / M (malformed, skipped)
+ Date: YYYY-MM-DD HH:MM
+ Source: <absolute path>
+ Entries audited: N parsed, M malformed, K uncited and skipped
## Summary
| Verdict | Count |
|---|---|
| PASS | a |
| WARN | b |
| FAIL | c |
- | Possibly fabricated | d (subset of FAIL)|
+ | Possibly fabricated | d (subset of FAIL) |
- ## FAIL (blockers — verify before submission)
- | Citekey | Diagnostic | Canonical source |
+ ## FAIL (verify before submission)
+ | Citekey | Diagnostic | Source |
|---|---|---|
- | smith2023paper | year mismatch: entry 2023, canonical 2024 | doi.org/10.1287/mksc.2024.0123 |
- | jones2024nope | fabricated? DOI 404s, no S2/OpenAlex/arXiv match | none |
- ## WARN (style / preprint-vs-published / minor field drift)
+ ## WARN (preprint status, field drift, style)
| Citekey | Diagnostic |
|---|---|
## PASS
- <one-line per entry>
+ <one line each>
## Per-entry detail
- <expandable section with the JSON for each FAIL and WARN>
+ <the JSON for every FAIL and WARN>
## Next steps
- 1. Review each FAIL row. Open `corrected.bib` for suggested replacements.
- 2. Diff `corrected.bib` against the source `.bib` (`git diff --no-index`).
- 3. After review, move `corrected.bib` into place yourself — this skill does not overwrite the source.
+ 1. Review each FAIL row against corrected.bib.
+ 2. `git diff --no-index <source>.bib corrected.bib`
+ 3. Move corrected.bib into place yourself; this skill never overwrites the source.
```
- Also write `<bib_dir>/bibcheck_<YYYYMMDD_HHMM>/corrected.bib`: a full copy of the source `.bib` with FAIL/WARN entries replaced by their `suggested_replacement` (preserving every PASS entry untouched, preserving comments and entry order). Entries with no confident correction are kept as-is and tagged with a leading `% bibcheck:FAIL — <diagnostic>` comment line.
+ Also write `corrected.bib`: the full source with FAIL and WARN entries replaced by their
+ `suggested_replacement`, PASS entries copied byte for byte, comments and entry order preserved. An
+ entry with no confident correction stays as-is under a leading
+ `% bibcheck:FAIL <diagnostic>` comment line.
- ## Pass / Warn / Fail criteria
+ ## Verdict tolerances
- - **PASS** — DOI or canonical source resolves; title, authors, year, venue, volume/issue/pages all match within style tolerance.
- - **WARN** — at most one of: author accent/tussenvoegsel drift, journal abbreviation drift, preprint-year vs. published-year, single volume/issue/page disagreement, preprint entry where a published version now exists.
- - **FAIL** — any of: DOI doesn't resolve, wrong surname, wrong year (not preprint-explained), wrong journal, two-or-more volume/issue/page disagreements, or the fabrication signature (no source recognises the entry).
+ PASS: a canonical source resolves and title, authors, year, venue, and volume/issue/pages all agree
+ within style tolerance. For a non-indexed `@misc`, PASS means the URL resolves.
- ## Output report shape
+ WARN: at most one of accent or tussenvoegsel drift, journal abbreviation drift, preprint year
+ against published year, a single volume/issue/page disagreement, a preprint entry whose published
+ version now exists, a Zotero DOI mismatch, or `lookup_timeout`.
- Files written to `<bib_dir>/bibcheck_<YYYYMMDD_HHMM>/`:
+ FAIL: a dead DOI, a wrong surname, a wrong year with no preprint explanation, a wrong journal
+ confirmed by ISSN, two or more volume/issue/page disagreements, a malformed or duplicate-key entry,
+ or the fabrication signature.
+ ## Output layout
+
```
- input.bib # verbatim copy of source for provenance
- entries/<key>.bib # one parsed entry per file (subagent inputs)
- reports/<key>.json # one verdict per file (subagent outputs)
- bibcheck_report.md # consolidated human-readable report
- corrected.bib # drop-in candidate replacement (user reviews + merges)
+ <bib_dir>/bibcheck_<YYYYMMDD_HHMM>/
+ input.bib verbatim copy of the source
+ entries/<key>.bib subagent inputs
+ reports/<key>.json subagent outputs
+ bibcheck_report.md consolidated report
+ corrected.bib candidate replacement for the user to diff and merge
```
- ## Relation to /cite (do not duplicate)
+ ## Failure modes
- | Skill | Direction | Mutates `.bib`? | Talks to Zotero? |
- |---|---|---|---|
- | `/cite` | adds ONE entry from a DOI/arXiv/title identifier | yes (appends) | yes (read + write via Web API) |
- | `/bibcheck` | audits ALL entries in an existing `.bib` | no (writes a separate `corrected.bib`) | read-only via MCP for cross-check |
+ `.bib` not found: ask for the path, do not invent one.
- If `/bibcheck` finds an entry that should be replaced and the user wants to do that via `/cite`, hand off the DOI / arXiv ID to `/cite` rather than mutating the `.bib` here.
+ Every entry returns `lookup_timeout`: a network or rate-limit problem. Retry once at
+ `--max-parallel 2`, then surface the error. Do not mark a whole bibliography FAIL on this.
- ## Long batch runs
+ A wave of year FAILs: usually preprint versus published. Widen the arXiv detection in check 2 and
+ leave the year tolerance alone.
- Bibliographies over ~60 entries with `--max-parallel 8` can take several minutes. Use `Bash run_in_background: true` for the dispatch loop and stream subagent completion via `Monitor`. Do not block on a single slow MCP call — set a 30-second per-entry timeout and emit a `WARN: lookup_timeout` rather than hanging.
+ Title match confidence below 0.7 across the board: the bibliography may be non-English or in a venue
+ neither Crossref nor OpenAlex indexes. Say so, ask for one entry known to be correct, and recalibrate
+ against it.
- ## Failure modes
+ OpenAlex credits exhausted (1,000/day anonymous, 10,000 with the key in
+ `~/.claude/secrets/scholar.env`): fall back to Crossref-only checking, which still covers checks 1
+ and 6 through 8, and mark author-accent findings as skipped.
- - **`.bib` not found.** Ask the user for the path; do not invent one.
- - **All entries return FAIL with `lookup_timeout`.** MCP rate-limit or network issue. Pause, retry once with `--max-parallel 2`. If still failing, surface the error to the user rather than silently marking everything FAIL.
- - **Many false-positive FAILs on `year`.** Likely a preprint-vs-published mismatch pattern; widen Phase 2 step 6's preprint detection rather than loosening tolerance.
- - **Match confidence universally below 0.7.** The `.bib` may be a non-English-titled set or a niche venue not indexed by S2/OpenAlex. Flag and ask the user to point at a sample entry known to be correct, then recalibrate.
+ ## Out of scope
- ## Out of scope (do not do these)
+ Do not add entries. Hand a DOI to `mcp__zotero__zotero_add_by_doi`, or use the reading-papers
+ skill for anything else.
- - **Don't add entries.** That's `/cite`. If an entry should be replaced, write the suggestion to `corrected.bib`; do not POST to Zotero.
- - **Don't auto-overwrite the source `.bib`.** Ever. The user diffs and merges themselves.
- - **Don't claim a citation supports a textual claim.** That's a `/review-paper` literature-fidelity check; `/bibcheck` only verifies the entry's metadata matches a real paper.
- - **Don't bulk-reformat.** Entries that PASS are copied verbatim into `corrected.bib`, even if the user's style preference differs.
- - **Don't audit `.bib` files in `_archive/` or `Old/` subdirectories unless explicitly asked** — these are intentionally frozen.
+ Do not overwrite the source `.bib` under any circumstances.
- ## Cross-references
+ Do not claim a citation supports a textual claim. This skill verifies that an entry's metadata
+ matches a real paper and nothing more.
- - `/cite` — adds individual entries; complementary forward direction.
- - `/litreview` — finds papers to cite; pair before `/cite` and `/bibcheck`.
- - `/audit-reproducibility` — same per-claim subagent-fanout structure, applied to numeric claims rather than citations.
- - `/review-paper` / `/seven-pass-review` — broader manuscript reviews; the citation-pass in those skills delegates here.
+ Do not bulk-reformat. A PASS entry is copied verbatim into `corrected.bib` even where its style
+ differs from the rest of the file.