Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

citeverify

Cross-checks the references in a bibliography against eight free bibliographic indexes, and reports whether each one resolves to an indexed record.

Reads BibTeX, RIS, CSL-JSON, LaTeX .bbl, Word .docx, PDF and plain text. Covers journals, preprints, books, CS conference proceedings, biomedical literature and datasets.

Install

pip install citeverify

Requires Python 3.9 or later. The core has no third-party dependencies.

CLI

citeverify check refs.bib                      # BibTeX
citeverify check library.ris                   # Zotero / EndNote / Mendeley export
citeverify check refs.json                     # CSL-JSON
citeverify check paper.docx                    # reads the reference section
citeverify check paper.pdf                     # needs: pip install 'citeverify[pdf]'
citeverify check refs.txt                      # plain text, one per line
citeverify check refs.txt --json out.json      # structured results
citeverify check refs.txt --cache .cv.json     # memoise source responses
citeverify check refs.txt --strict             # exit 1 on any suspicious verdict
citeverify parse refs.txt                      # parse only, no network calls
citeverify check - < refs.txt                  # read stdin
Flag Default Effect
--format detected Force an input format: bibtex, ris, csl-json, latex, docx, pdf, document, text
--mailto unset Sent as a contact address to OpenAlex and Crossref, which routes the request to their polite pool
--cache PATH unset JSON memo of source responses, keyed by source and identifier
--delay 0.12 Seconds between calls that hit the network. Cached lookups do not sleep
--verified-threshold 0.60 Title score at or above which a candidate can be verified
--partial-threshold 0.35 Title score at or above which a candidate is partial
--year-tolerance 1 Permitted absolute difference between cited and indexed year
--strict off Exit 1 if any reference is suspicious. error verdicts do not trigger it

Exit codes: 0 completed, 1 suspicious references found under --strict, 2 usage or input error.

Input formats

Detected from the extension, or from the content when reading stdin. Override with --format.

Format Extension Notes
BibTeX .bib Brace-matched, so nested braces in titles survive. LaTeX accents ({\'E}mile) are folded to Unicode so they match indexed records
RIS .ris Zotero, Mendeley, EndNote, Web of Science, Scopus. Soft-wrapped fields are rejoined
CSL-JSON .json Zotero and Pandoc. Accepts a bare array or a wrapped export
LaTeX .bbl, .tex Splits on \newblock, which delimits author/title/venue explicitly and beats sentence segmentation
Word .docx Paragraph boundaries are explicit, so hanging-indent bibliographies survive intact
PDF .pdf Requires pip install 'citeverify[pdf]'
Document any Finds the References heading in a longer text and parses only what follows, stopping at Appendix
Plain text any One reference per line. The heuristic fallback

Structured formats set parse_confidence to high because field boundaries are given rather than inferred. That matters: a mis-segmented title is the main cause of a false unverified.

Verdicts

Verdict Condition
verified Title score at or above the verified threshold, year within tolerance, author overlap present or uncheckable
year_mismatch Title score at or above the verified threshold, year outside tolerance
author_mismatch Title score at or above the verified threshold, no surname overlap with the indexed record
partial Title score between the partial and verified thresholds
unverified No candidate above the partial threshold, or no title or identifier to search on
error Every configured source failed to respond

Result.suspicious is true for year_mismatch, author_mismatch, partial and unverified. It is false for error.

Resolution semantics

Transport failures are separated from negative results. If every source raises, the verdict is error and suspicious is false. Failed lookups are not written to the cache, so they are retried on the next run.

Missing author data does not lower a verdict. author_overlap is tri-state: true, false, or None when either side lists no authors. None is treated as passing.

Candidate selection uses title score alone. Author and year are recorded as corroborating evidence and applied in classify, outside candidate ranking. A reference to a real work carrying an incorrect year therefore resolves to that work and returns year_mismatch rather than unverified.

Title scoring is Jaccard overlap of content tokens. Stopwords and tokens of two characters or fewer are dropped before comparison.

Containment is a guarded escape hatch, not a free pass. If either normalised title contains the other the score is 1.0, but only when the contained title carries at least 4 content tokens and accounts for at least 70% of the longer one. Without that guard, containment is exactly the shape a fabricated citation takes: an invented title opening with a real, short one. "Quantum entanglement" is contained in "Quantum Entanglement in Recurrent Transformer Memory Lattices", and scoring that 1.0 reports a fabrication as verified. Pairs that fail the guard fall through to Jaccard and are scored on the evidence actually shared.

An exact identifier outranks any title score. When a citation and a candidate carry the same DOI, PMID, arXiv id or ISBN, the match is identity rather than similarity and scores 1.0 regardless of title. A reference that cites by PMID alone, with no title, still verifies.

Python API

from citeverify import Cache, parse_references, summarize, verify_all

refs = parse_references(open("references.txt").read())
results = verify_all(refs, cache=Cache(".cv.json"))

summarize(results)          # {'verified': 4, 'unverified': 1, ...}
[r for r in results if r.suspicious]

verify() and verify_all() accept dicts directly, bypassing the parser:

verify({"title": "...", "authors": "...", "year": 2017, "doi": "10.1/x"})

Recognised keys: title, authors, year, doi, arxiv_id, isbn, pmid, entry_type, venue. authors accepts a string or a list. entry_type (book, inproceedings, dataset, ...) steers source ordering.

Thresholds

from citeverify import Thresholds, verify_all

verify_all(refs, thresholds=Thresholds(verified=0.7, partial=0.4, year_tolerance=0))

Thresholds validates 0 <= partial <= verified <= 1 and year_tolerance >= 0 at construction.

Transport

All network access goes through one callable with signature (url, headers) -> str. No other module performs I/O.

from citeverify import OpenAlexSource, verify

def fetcher(url, headers):
    return proxy.get(url, headers=headers).text

verify(citation, sources=[OpenAlexSource(fetcher=fetcher)])

Sources are tried in order and the search stops at the first verified result. Each source exposes last_error after a failed call.

default_sources() returns all eight, none of which require an API key:

Source Covers
OpenAlex Broadest general index, queried first
Crossref DOI-registered publisher records
arXiv Preprints
Europe PMC Life sciences and everything in PubMed; the only source that resolves a bare PMID
Semantic Scholar Computer science, and preprints that never acquired a DOI
DBLP CS conference proceedings, patchily covered by DOI-centric indexes
Open Library Books, which have no DOI and are the largest blind spot in DOI-based checking
DataCite Datasets, software and theses

sources_for(citation) reorders that list by what the reference looks like, so a book reaches Open Library first rather than after seven journal databases have each spent a request failing to find it. Since verification stops at the first confirmation, this turns eight requests into one. verify() applies it automatically.

Parser

parse_references splits on newlines, rejoins soft-wrapped entries, and strips list markers ([1], 1., (1)). Each entry yields a Citation with parse_confidence set to:

  • high: title found via a quoted string, or via an author/parenthesised-year/title split
  • low: title inferred by sentence segmentation. Recorded in notes

DOIs match 10.\d{4,9}/.... arXiv ids match both the NNNN.NNNNN and the older archive/NNNNNNN forms. A parenthesised year is preferred over a bare four-digit number, which may be a volume or page range.

Limits

  • Title matching is lexical and does not model meaning.
  • Coverage is bounded by the eight indexes. Standards, grey literature, and much non-English and pre-1970 work remain thinly indexed, so unverified on those often reflects absence from the index rather than fabrication.
  • The free-text parser is heuristic. Structured input (.bib, .ris, CSL-JSON) is read exactly and should be preferred when available; it is also what sets parse_confidence to high.
  • PDF extraction is only as good as the PDF. Two-column layouts and ligature-heavy fonts still produce broken reference strings. Export a .bib where you can.
  • The tool determines whether a matching record exists. It does not evaluate whether a citation supports the claim it is attached to.

Development

pip install -e ".[dev]"
pytest

76 tests. No network access or API key required; sources are exercised through injected fetchers.

Licence

MIT.

About

Check whether the references in a bibliography actually exist. Cross-checks every reference against OpenAlex, Crossref and arXiv.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages