Reference-PDF collection and plagiarism scanning for research papers.
Use cases:
- Reviewing a paper draft before submission: download every cited reference as a PDF, then shingle-match your paper's prose against them to find passages tracking too closely to source text.
- Tracking a long bibliography: auto-categorize references by status (downloaded / fetchable / pre-arXiv / skip) in a per-paper markdown tracker, with optional per-paper heuristics for books, software, and suspect titles.
- Checking for self-plagiarism across a research program: given multiple paper directories, detect overlapping passages.
Stdlib-only at runtime (uses pdftotext from poppler for PDF text extraction). Optional extras add paraphrase detection — refscan[semantic-lite] (NumPy only, no torch) or refscan[semantic] (sentence-transformers); everything else needs no Python dependencies.
One CLI, sixteen subcommands (full reference in Commands). The highlights:
check— ⭐ one-shot pre-submission pass: bib sanity + plagiarism scan (+ optional verify) → a single PASS / WARN / FAIL verdict;--html/--json/--sarifwrite shareable or CI-native reportsverify— flag likely-fabricated, metadata-drifted, or retracted references against arXiv, Semantic Scholar, OpenAlex + Crossref (all fields)scan/semscan— match your prose against the papers you cite: exact shingle matching (confidence-ranked) plus semantic paraphrase detectionclaims— offline "citation needed" pass: flags sentences with numbers, attribution phrases, or strong claims but no\citecite— generate a clean BibTeX entry from a DOI or arXiv ID;--addappends it to your bib (dedupe-aware)fix— auto-apply the safe correctionsverifyimplies (missing DOIs, drifted years);--upgrade-preprintsre-points arXiv-preprint citations at their published version; preview by default
Plus the supporting commands: fetch (download cited PDFs), doctor (environment self-check), init, track, sanity-stats, refstats, watch, overlap, and the maintainer-only release.
Cross-cutting:
- Auto-detected layout — finds your
.biband.texfor common layouts with no config; override viarefscan.jsonor--bib/--sections(details) - Per-paper heuristics — book/software/suspect-title markers in
refscan.json - Robust extraction — mtime-cached
pdftotext, letter-spacing repair, generic-phrase filtering - Safe & polite — bib-key path-traversal protection, arXiv/S2 rate-limit etiquette (optional
REFSCAN_S2_API_KEY) - Stdlib-only runtime, Python 3.10+, 311 tests, CI on 3.10–3.13
Recommended (uv tool — works from any directory regardless of active venv):
git clone https://github.com/rktreddy/refscan
uv tool install --editable refscanAlternative (pip — installs into current Python env):
git clone https://github.com/rktreddy/refscan
pip install -e refscanBoth are editable installs — code edits in refscan/src/refscan/ take effect immediately, no reinstall needed.
Runtime dependency: pdftotext (poppler). On macOS: brew install poppler. On Debian/Ubuntu: apt install poppler-utils.
Optional: semantic paraphrase detection (semscan) needs an extra embedding backend — install commands in the semscan command section. Everything else runs on the dependency-free base install.
The prose-analysis commands (scan, semscan, claims, watch, overlap) need .tex sources — but the reference-integrity half of refscan is manuscript-format-agnostic: it reads only your BibTeX file. If you write in Word or Google Docs and manage references with Zotero, Mendeley, or EndNote, export your library as BibTeX and you can still check it:
mkdir mypaper && cp ~/Downloads/exported.bib mypaper/references.bib
refscan verify mypaper # flag fabricated + retracted references
refscan fix mypaper # preview safe metadata corrections
refscan refstats mypaper # recency / self-citation balance
refscan cite 10.1038/... # generate a clean entry for the next paper(Zotero: File → Export Library → BibTeX. Mendeley and EndNote have equivalent exports.) Native .docx parsing isn't currently planned — if you'd use it, say so in an issue.
This is the default layout; it's configurable (see Custom paper layouts).
paper_X/
├── paper/
│ ├── references.bib
│ └── sections/
│ ├── introduction.tex
│ ├── method.tex
│ └── ...
├── refscan.json # optional per-paper categorization heuristics
└── literature/ # created by `refscan init`
├── refs/ # downloaded reference PDFs, named {BIBKEY}.pdf
├── pdf_text_cache/ # extracted text, keyed by PDF mtime
├── reference_tracking.md # generated by `refscan track`
└── plagiarism_findings.md # generated by `refscan scan`
refscan auto-detects common layouts: if paper/references.bib / paper/sections/ aren't there, it searches the paper root and paper/ for a .bib (preferring references.bib) and for .tex files — so flat single-file papers usually need no config. Commands print a note when something was auto-detected.
To be explicit (or when auto-detection can't decide — e.g. multiple .bib files), point refscan at the real locations in refscan.json at the paper-dir root, or with per-command flags. Precedence: flag > refscan.json > auto-detection > default.
For a flat, single-file paper (references.bib and one paper.tex at the root):
{
"bib": "references.bib",
"sections": "paper.tex"
}Then the usual commands just work: refscan init . && refscan scan . && refscan verify .
Configurable keys (all optional, all relative to the paper dir):
| key / flag | meaning | default |
|---|---|---|
bib / --bib |
path to references.bib |
paper/references.bib |
sections / --sections |
a directory, a single .tex file, or a glob (e.g. chapters/*.tex) |
paper/sections |
main_tex |
extra .tex scanned for \cite |
paper/main.tex |
literature |
refscan's workspace directory | literature |
--bib is accepted by fetch/track/scan/verify/sanity-stats; --sections by scan/sanity-stats/watch. Papers using the default layout need no config and are unaffected.
One-shot integrity check — the recommended entry point. Runs sanity-stats and scan (and verify with --verify; uses the network), writes the individual reports, and ends with a single PASS / WARN / FAIL verdict. Exits non-zero on FAIL (a bib error, a fabricated reference, or a retracted one), so it drops straight into CI or a pre-commit hook. --html writes a self-contained, shareable literature/report.html combining verdict, color-coded findings, and side-by-side paper-vs-source context; --json writes a machine-readable report.json; --sarif writes SARIF 2.1.0 for inline GitHub PR annotations.
refscan check: my-paper
bib: .../paper/references.bib
sections: 3 .tex file(s)
refs: 34 PDF(s) in .../literature/refs
results:
sanity: 0 error(s), 2 warning(s) (35 entries, 22 cited)
scan: 1 finding(s), top confidence 0.41 (34 refs indexed)
verify: skipped (pass --verify to check refs against arXiv/S2; uses network)
✅ PASS reports in .../literature/
Scaffold literature/refs/, literature/pdf_text_cache/, write an initial reference_tracking.md, and (if absent) a refscan.json config template.
Resolve and download a PDF for each bib entry into literature/refs/{BIBKEY}.pdf, trying in order: explicit arXiv ID → arXiv title/author search → Semantic Scholar (unless --no-s2) → OpenAlex → Unpaywall (when the entry has a DOI). Downloads are validated as real PDFs (landing-page HTML is never saved), resolution is rate-limited per API guidelines, and downloads run in parallel (--workers, default 5). Interactive terminals get a live progress bar; piped/CI output stays line-per-entry. Bib keys with path separators or .. are skipped as unsafe-key, so a reference can never write outside literature/refs/.
Generate a clean BibTeX entry from a DOI or arXiv ID and print it to stdout. Accepts bare DOIs, doi.org URLs, and arXiv IDs in any common form (1706.03762, arXiv: prefix, abs/pdf URLs; version suffixes stripped). DOIs resolve via Crossref with an OpenAlex fallback; arXiv IDs via the arXiv API. Entry type is inferred (@article / @inproceedings / @misc + eprint for arXiv-only work) and citation keys follow the vaswani2017attention pattern with collision suffixes. With --add, new entries are appended to the bib resolved from --paper-dir (default .); an identifier already in the bib prints its existing key instead of duplicating it. Exit codes: 0 = resolved (or already present), 1 = not found / unrecognized, 2 = source unreachable.
refscan cite 10.1038/s41586-020-2649-2 # print an entry
refscan cite arXiv:1706.03762 --add --paper-dir . # append to this paper's bibOffline "citation needed" pass. Splits your section files into sentences (citation-aware — \cite commands are tracked before LaTeX stripping) and scores each against claim signals: percentages and multipliers ("90%", "3x faster"), attribution phrases ("prior work has shown", "it is well known", "studies show"), comparatives ("outperforms", "better than"), and universals ("always", "the first to"). Sentences that already carry a citation are skipped, and first-person statements about your own results ("we show in Table 2 that...") are downweighted — contributions don't need citations, claims about the world do. Output: literature/uncited_claims.md, grouped by section with line numbers and the signals that fired. Advisory by design: findings are suggestions (common-knowledge and covered-nearby cases are expected), so the exit code is always 0 and it never gates check. Tune sensitivity with --min-score (default 2).
Environment self-check — run it after installing, or when something doesn't work. Verifies Python version, pdftotext, which semantic backend semscan would use (including the broken-install case), reachability of the four metadata sources (skip with --no-network), and whether the optional env vars are set. Pass a paper_dir to also diagnose layout resolution (bib / sections / reference PDFs) for that paper. Exits 1 if anything fails outright; warnings (optional features, offline) don't fail. Fast and side-effect-free — it never downloads models.
✓ python Python 3.11.5
✓ pdftotext pdftotext version 26.03.0
✓ semantic backends semscan will use model2vec
⚠ REFSCAN_CONTACT_EMAIL not set
✓ arXiv reachable https://export.arxiv.org/api/query
...
7 ok, 3 warning(s), 0 failure(s)
Regenerate literature/reference_tracking.md based on what's currently in literature/refs/. Each entry is bucketed as downloaded / fetchable / pre-arXiv / skip-book / skip-software / verify-exists.
Categorization uses general BibTeX signals out of the box — @book/@inbook → skip-book, @software → skip-software, pre-2000 → pre-arXiv. Papers with their own conventions can add heuristics in an optional refscan.json at the paper-dir root (all lists are lowercase substring/key matches, all optional):
{
"book_title_markers": ["nonlinear dynamics and chaos"],
"software_keys": ["jax", "pytorch"],
"software_title_markers": ["tensorrt"],
"suspect_title_markers": ["coupled oscillator networks for"]
}suspect_title_markers flags 2015+ entries whose titles match into a verify-exists bucket — a coarse offline pre-filter. For robust fabrication detection, use refscan verify, which checks every entry against arXiv + Semantic Scholar.
The same refscan.json also holds layout keys (bib, sections, …) — see Custom paper layouts.
Extract text from every reference PDF, then shingle-match (sliding N-word windows) against each LaTeX section in paper/sections/. Write a findings report ranked by confidence score — a 0–1 metric combining run length, non-stopword density, and phrase rarity across the cited reference corpus. The report opens with a Top N concerning matches table for quick triage, followed by all findings grouped by reference. Default shingle size is 6 words.
Active-drafting helper. Polls paper/sections/*.tex at the configured interval and re-runs the plagiarism scan whenever any file is saved, printing a compact terminal summary (top-N matches by confidence). Press Ctrl-C to stop. Defaults: poll every 1s, show top 5.
Detect shared n-word passages across two or more papers. Useful as a self-plagiarism check for a research program.
Bib hygiene report. Surfaces undefined cites, unused entries, duplicate keys, duplicate titles, missing required fields, suspicious years, and stub authors. Exits with code 1 on any errors, 0 otherwise — useful in CI. Output: literature/sanity_report.md.
Reference-balance stats — the presentation signals reviewers complain about (distinct from integrity). Reports recency (% of references within the last 5 / 10 years, median year, range), an optional self-citation share when you pass your surname(s) via --author, and a by-year histogram. Bib-only, no network. Output: literature/reference_stats.md.
refscan semscan <paper_dir> [--backend B] [--threshold T] [--min-words N] [--model NAME] [--out PATH]
Semantic / near-duplicate scan — catches paraphrase the exact-shingle scan misses (same meaning, different words). Compares sentence embeddings between your prose and each reference and flags pairs above a cosine threshold (default 0.75). Output: literature/semantic_findings.md.
Needs one of two optional backends; the base package stays dependency-free. Add one to your existing install (refscan isn't on PyPI, so install from the cloned repo):
# light backend — model2vec (NumPy only, no torch, ~100 MB):
uv tool install --editable refscan --with model2vec # if you installed via uv tool
pip install -e 'refscan[semantic-lite]' # if you installed via pip -e
# full backend — sentence-transformers (torch, ~2 GB, best fidelity):
uv tool install --editable refscan --with sentence-transformers
pip install -e 'refscan[semantic]'Choosing / switching the backend. It's one command with a --backend flag — not two commands. semscan auto-detects whichever backend is installed (preferring the higher-fidelity sentence-transformers if both are present); a backend only works if its package is installed.
refscan semscan <paper_dir> # auto: best installed backend
refscan semscan <paper_dir> --backend model2vec # force the light backend
refscan semscan <paper_dir> --backend sentence-transformers # force the full backend
refscan semscan <paper_dir> --threshold 0.83 # stricter — fewer topical hitsThe first run downloads a small model. semscan is slower than scan, so it's best as a final pre-submission pass; treat its output as a review list (technical definitions and properly-attributed summaries are expected to score high). Without a backend installed, the command exits with an install hint. In auto mode, if a backend is installed but fails to load (e.g. a torch/NumPy version conflict), semscan transparently falls back to a working one.
model2vec is the recommended backend — lighter, no torch, and works everywhere. The
sentence-transformersbackend needs a recent PyTorch (≥ 2.4); on Intel macOS PyTorch is capped at 2.2.2, so use model2vec there.
Check each bib entry against arXiv, Semantic Scholar, OpenAlex, and Crossref — together they cover preprints, journals, proceedings, and books across all fields, so real non-arXiv papers are rarely false-flagged. Retracted papers (via OpenAlex) get a dedicated 🚨 section, and check --verify treats them as a FAIL. Verdicts per entry: verified, metadata-drift (right paper, wrong author/year), weak-match, not-found (likely fabricated), skipped (book/software/no-title), or api-error (lookup failed — not treated as fabricated). Entries citing an arXiv preprint whose published version now exists (found via the arXiv record's author-linked DOI) get an advisory 📰 section listing venue, year, and DOI — apply the upgrade with fix --upgrade-preprints. Output: literature/verification_report.md. Results are cached keyed by title/author/year, so a corrected entry re-checks automatically without --refresh; api-error results are never cached.
API keys & etiquette: none required — OpenAlex + Crossref work anonymously and keep verify/fetch working across fields. Two optional env vars improve coverage: a contact email joins the polite pools and enables Unpaywall (refscan ships no default email and skips Unpaywall without one), and a free Semantic Scholar key avoids S2's aggressive unauthenticated throttling:
export REFSCAN_CONTACT_EMAIL=you@example.com
export REFSCAN_S2_API_KEY=<your-key>Closes the loop on verify: applies the safe corrections that verify's matches imply. For each entry whose best match is confident (high title overlap), it adds a missing DOI and corrects a drifted year (only when the author also matches). Titles and author lists are never rewritten — too easy to clobber a correct entry with an API variant.
With --upgrade-preprints, entries that cite an arXiv preprint whose published version verify found are additionally re-pointed at it: DOI → published DOI, journal → the venue, year → publication year, and @misc entries become @article. The eprint/archivePrefix fields are kept (citing both is standard). Volume/pages aren't available from this pass — refscan cite <doi> generates a complete entry when you want one.
Safe by default: it previews the proposed edits and changes nothing. Re-run with --apply to write them — a references.bib.bak backup is made first, and surrounding formatting is preserved.
proposed fixes (2):
he2015: doi (none) → 10.1109/CVPR.2016.90 [crossref: add missing DOI]
he2015: year 2015 → 2016 [crossref: year disagrees with matched record]
(preview only — re-run with --apply to write these; a .bak backup is made first)
For shipping new versions of refscan itself. Validates environment, runs tests, bumps version in pyproject.toml and __init__.py in lockstep (plus this README's pre-commit/action version pins), commits, tags v{new_version}, and (with --push) pushes to origin and creates a GitHub Release with the CHANGELOG section as notes (needs gh; skipped gracefully without it). Requires editable install + clean working tree on main + a CHANGELOG section pre-written for the new version. Use --dry-run to preview.
A practical end-to-end flow. <paper> is your paper directory (often just .); refscan auto-detects the bib and .tex for common layouts, or set them in refscan.json.
1. Set up & collect references
refscan init <paper> # scaffold literature/ (refs, cache, refscan.json template)
refscan fetch <paper> # auto-download PDFs from arXiv / S2 / OpenAlex / Unpaywall
refscan track <paper> # see what's downloaded vs. still missing
# Drop any still-missing PDFs into literature/refs/{BIBKEY}.pdf by hand.2. Verify the references are real (and not retracted) — the AI-era check
refscan verify <paper> # arXiv + S2 + OpenAlex + Crossref; flags fabricated + 🚨 retracted
refscan fix <paper> # preview safe metadata fixes (DOIs, drifted years)
refscan fix <paper> --apply # write them (a references.bib.bak backup is made first)3. Check the prose against your sources
refscan scan <paper> # exact: verbatim / lightly-edited copying, confidence-ranked
refscan semscan <paper> # semantic: paraphrase (needs refscan[semantic-lite|semantic])4. One-shot verdict + a shareable report
refscan check <paper> --verify --html # sanity + scan + verify → PASS/WARN/FAIL + report.html
# exits non-zero on FAIL — drop it into CI or a pre-commit hook (see below)5. (optional) Sanity-check the bibliography's shape
refscan sanity-stats <paper> # hygiene: undefined cites, dupes, missing fields
refscan refstats <paper> --author Doe # recency, median age, self-citation shareEverything writes a markdown report to literature/; check is the recommended single entry point and bundles the read-only checks with one verdict.
Each finding shows a run of consecutive words shared between your paper and one cited reference. Not every finding is plagiarism:
- Technical terminology ("neural ordinary differential equation") is expected.
- Quoted theorem statements with attribution are fine.
- Author/venue clusters are noise.
- Concern-worthy: a ≥10-word run tracking the source's sentence structure closely. These deserve a rewrite.
- pdftotext letter-spacing artifacts. Section titles rendered with letter-spaced fonts extract as
d i f f e r e n t i a b l e.refscanrejoins long single-letter runs (4+ in a row); for partial-word patterns likeD IFFERENTIABLE A RCHITECTURE S EARCH, the verify command falls back to substring matching against the whitespace-collapsed text, which catches them. Body-text shingle matching may still miss letter-spaced text — but this is rare in body content. - Only considers cited references. Does not catch accidental overlap with uncited work. For that, use a paid service like iThenticate or Crossref Similarity Check.
- No support for BibLaTeX advanced features (
\addbibresource, cross-references, string substitutions). Plain BibTeX only. - Books and pre-arxiv papers often unavailable. The auto-fetcher cannot download paywalled content or material not on arXiv/S2. You'll need to grab those manually.
FileNotFoundError: .../paper/references.bib, or "no section .tex files found".
Your paper isn't in the default paper/{references.bib,sections} layout. Point refscan at the real locations via refscan.json or the --bib/--sections flags — see Custom paper layouts.
pdftotext: command not found, or every reference shows as "failed" in the scan report.
The poppler runtime dependency is missing. Install it: brew install poppler (macOS) or apt install poppler-utils (Debian/Ubuntu).
refscan verify marks a real paper as "not found".
verify checks arXiv + Semantic Scholar + OpenAlex + Crossref, so genuine papers in any field are usually found. A real not-found most often means a metadata typo in your bib, or a niche/very recent work — fix the bib (refscan fetch/verify show the closest match) and re-run with --refresh, and check Google Scholar before treating it as fabricated.
refscan verify reports api-error.
The arXiv/Semantic Scholar request itself failed (network blip, timeout). This is not a fabrication signal — api-error results are never cached, so just re-run.
Semantic Scholar returns 429 (rate limit) after a few requests.
The unauthenticated endpoint throttles aggressively. Get a free key at https://www.semanticscholar.org/product/api and export REFSCAN_S2_API_KEY=<your-key>, then re-run (--refresh for verify).
refscan fetch downloaded nothing for an entry, or reports not-found.
The reference isn't openly available on arXiv/Semantic Scholar (paywalled or not indexed). Grab the PDF manually and drop it in literature/refs/{BIBKEY}.pdf — the filename must match the bib key exactly.
refscan fetch reports unsafe-key for an entry.
That bib key contains path separators or .. and was skipped to prevent writing outside literature/refs/. Rename the key in references.bib to plain characters.
The scan flags lots of matches that look like normal terminology.
That's expected — shared technical terms aren't plagiarism. Findings are ranked by confidence score; review the top of the report first. Use --no-filter to see raw matches, or raise --min-run to require longer shared runs.
cd refscan
pip install -e ".[dev]" # or: uv pip install -e ".[dev]"
pytest311 tests covering bib parsing and path-safety, text processing, shingle/scan logic, semantic-scan matching, the uncited-claim detector, fetch + the source chain (arXiv/S2/OpenAlex/Crossref/Unpaywall), cite (identifier parsing, key generation, BibTeX formatting, dedupe), the doctor environment checks, verify (verdicts + caching + retraction + published-version detection), bib auto-fix incl. preprint upgrades, sanity checks, reference-balance stats, tracking/config, layout resolution + auto-detection, the check verdict + HTML/JSON/SARIF reports, color output, cross-paper overlap, and the release flow. Lint with ruff check.
Terminal output is colorized when stdout is a TTY; it stays plain when piped or when NO_COLOR is set (and you can force it with FORCE_COLOR=1).
Keep your bibliography honest automatically.
pre-commit — add to your paper repo's .pre-commit-config.yaml:
repos:
- repo: https://github.com/rktreddy/refscan
rev: v0.25.0 # use the latest release tag
hooks:
- id: refscan-sanity # fast, offline; blocks a commit on bib errors
# - id: refscan-check # also runs the plagiarism scan (needs pdftotext)GitHub Actions — refscan ships a composite action; in your paper repo's workflow:
- uses: rktreddy/refscan@v0.25.0 # use the latest release tag
with:
paper-dir: .
extra-args: "--verify" # optional: also check refs against arXiv/S2/OpenAlex/CrossrefBoth exit non-zero on a FAIL (bib error, fabricated or retracted reference), so they gate the commit/PR. For inline PR annotations, run refscan check . --verify --sarif and upload literature/report.sarif with github/codeql-action/upload-sarif.
See CHANGELOG.md. Releases follow Semantic Versioning. Pre-1.0 — interfaces may still change.
A personal project, maintained on a best-effort basis. Issues and pull requests are welcome (see CONTRIBUTING.md), but reviews may be slow. Provided as-is under MIT — use it freely. Security reports: see SECURITY.md.