Check BibTeX entries for completeness, DOI resolution, and broken links. Verify required fields per entry type (article, book, inproceedings), resolve and validate DOIs via the CrossRef API, check URL accessibility, and flag duplicate entries, missing abstracts, and inconsistent formatting. Use when preparing a manuscript bibliography for journal submission, auditing a shared .bib file before a project milestone, after merging bibliographies from multiple sources, when citations render incorrectly, or as a CI check on version-controlled .bib files.
Check BibTeX bibliography entries for completeness, accuracy, and consistency. This skill covers verifying required fields per entry type, resolving DOIs via the CrossRef API, checking URL accessibility, detecting duplicate entries, and producing a structured validation report that flags issues by severity. It ensures that .bib files are publication-ready before rendering.
Do NOT use for agent-memory or documentation cross-references — see
repair-broken-references. This skill
validates bibliographic entries and DOI resolution; the name collision with
"references" in the link-checking sense is the only thing the two share. For the
reachability and budget of a Claude Code memory directory, see
verify-memory-integrity.
basic, standard, strict; default: standard)TRUE)TRUE)required_packages <- c("RefManageR", "httr2", "curl")
missing <- required_packages[!vapply(required_packages, requireNamespace,
logical(1), quietly = TRUE)]
if (length(missing) > 0) install.packages(missing)
library(RefManageR)
Expected: All packages load without errors.
On failure: If httr2 is unavailable, install it with install.packages("httr2").
For systems without curl headers: sudo apt install libcurl4-openssl-dev.
bib <- RefManageR::ReadBib("references.bib", check = FALSE)
message(sprintf("Loaded %d entries from references.bib", length(bib)))
# Inventory entry types
entry_types <- vapply(bib, function(x) tolower(attr(x, "bibtype")), character(1))
type_counts <- sort(table(entry_types), decreasing = TRUE)
message("Entry types:")
for (type in names(type_counts)) {
message(sprintf(" %s: %d", type, type_counts[[type]]))
}
Expected: Summary of entry types (article, book, inproceedings, etc.) and total
count matching the number of @type{ blocks in the file.
On failure: Parsing errors indicate malformed BibTeX. Check for unmatched braces, missing commas between fields, or invalid UTF-8 characters.
# BibTeX required fields by entry type
required_fields <- list(
article = c("author", "title", "journal", "year"),
book = c("author", "title", "publisher", "year"),
inproceedings = c("author", "title", "booktitle", "year"),
incollection = c("author", "title", "booktitle", "publisher", "year"),
phdthesis = c("author", "title", "school", "year"),
mastersthesis = c("author", "title", "school", "year"),
techreport = c("author", "title", "institution", "year"),
misc = c("author", "title", "year"),
unpublished = c("author", "title", "note")
)
validate_fields <- function(bib) {
issues <- list()
for (i in seq_along(bib)) {
key <- names(bib)[i]
entry_type <- tolower(attr(bib[[i]], "bibtype"))
req <- required_fields[[entry_type]]
if (is.null(req)) {
issues[[length(issues) + 1]] <- list(
key = key, severity = "warning",
message = sprintf("Unknown entry type: %s", entry_type)
)
next
}
for (field in req) {
value <- bib[[i]][[field]]
if (is.null(value) || !nzchar(trimws(as.character(value)))) {
issues[[length(issues) + 1]] <- list(
key = key, severity = "error",
message = sprintf("Missing required field: %s (type: %s)", field, entry_type)
)
}
}
}
issues
}
field_issues <- validate_fields(bib)
message(sprintf("Field validation: %d issues found", length(field_issues)))
Expected: A list of issues where required fields are missing. Zero issues for a well-maintained bibliography.
On failure: This step runs locally and should not fail. If it does, check that the .bib file parsed correctly in Step 2.
validate_dois <- function(bib, email = NULL) {
issues <- list()
# Set polite API headers
headers <- list(`User-Agent` = "R-bibliography-validator/1.0")
if (!is.null(email)) {
headers[["mailto"]] <- email
}
for (i in seq_along(bib)) {
key <- names(bib)[i]
doi <- bib[[i]]$doi
if (is.null(doi) || !nzchar(doi)) {
issues[[length(issues) + 1]] <- list(
key = key, severity = "info",
message = "No DOI present"
)
next
}
# Normalize DOI
doi <- gsub("^https?://doi\\.org/", "", doi)
doi <- gsub("^doi:", "", doi, ignore.case = TRUE)
doi <- trimws(doi)
# Resolve via CrossRef
tryCatch({
resp <- httr2::request(sprintf("https://api.crossref.org/works/%s", doi)) |>
httr2::req_headers(!!!headers) |>
httr2::req_timeout(10) |>
httr2::req_perform()
if (httr2::resp_status(resp) != 200) {
issues[[length(issues) + 1]] <- list(
key = key, severity = "error",
message = sprintf("DOI does not resolve: %s (HTTP %d)", doi,
httr2::resp_status(resp))
)
}
}, error = function(e) {
issues[[length(issues) + 1]] <<- list(
key = key, severity = "warning",
message = sprintf("DOI check failed for %s: %s", doi, e$message)
)
})
Sys.sleep(0.5) # Rate limiting
}
issues
}
# Only run online checks if requested
doi_issues <- validate_dois(bib, email = "your.email@example.com")
message(sprintf("DOI validation: %d issues found", length(doi_issues)))
Expected: Each DOI resolves successfully (HTTP 200 from CrossRef). Entries without DOIs are flagged as informational.
On failure: Network errors or rate limiting produce warnings rather than hard
failures. Set the email parameter for higher rate limits from CrossRef's polite pool.
validate_urls <- function(bib) {
issues <- list()
for (i in seq_along(bib)) {
key <- names(bib)[i]
url <- bib[[i]]$url
if (is.null(url) || !nzchar(url)) next
tryCatch({
resp <- httr2::request(url) |>
httr2::req_method("HEAD") |>
httr2::req_timeout(10) |>
httr2::req_error(is_error = function(resp) FALSE) |>
httr2::req_perform()
status <- httr2::resp_status(resp)
if (status >= 400) {
issues[[length(issues) + 1]] <- list(
key = key, severity = "warning",
message = sprintf("URL returned HTTP %d: %s", status, url)
)
}
}, error = function(e) {
issues[[length(issues) + 1]] <<- list(
key = key, severity = "warning",
message = sprintf("URL unreachable: %s (%s)", url, e$message)
)
})
Sys.sleep(0.3)
}
issues
}
url_issues <- validate_urls(bib)
message(sprintf("URL validation: %d issues found", length(url_issues)))
Expected: All URLs return HTTP 200 (or 301/302 redirects). Broken links flagged.
On failure: Some servers block HEAD requests. Retry with GET for failed HEAD checks. Timeout errors are common for slow academic servers.
detect_duplicates <- function(bib) {
issues <- list()
# Check for duplicate DOIs
dois <- vapply(bib, function(x) {
d <- x$doi
if (is.null(d)) NA_character_ else tolower(trimws(d))
}, character(1))
doi_table <- table(dois[!is.na(dois)])
dup_dois <- names(doi_table[doi_table > 1])
for (d in dup_dois) {
keys <- names(bib)[which(dois == d)]
issues[[length(issues) + 1]] <- list(
key = paste(keys, collapse = ", "), severity = "error",
message = sprintf("Duplicate DOI %s in entries: %s", d,
paste(keys, collapse = ", "))
)
}
# Check for duplicate titles (fuzzy)
titles <- vapply(bib, function(x) {
t <- x$title
if (is.null(t)) NA_character_ else tolower(gsub("[^a-z0-9 ]", "", tolower(t)))
}, character(1))
seen <- character(0)
for (i in seq_along(titles)) {
if (is.na(titles[i])) next
for (j in seen) {
if (identical(titles[i], titles[as.integer(j)])) {
issues[[length(issues) + 1]] <- list(
key = sprintf("%s, %s", names(bib)[as.integer(j)], names(bib)[i]),
severity = "warning",
message = sprintf("Possible duplicate titles: '%s'",
substr(bib[[i]]$title, 1, 60))
)
}
}
seen <- c(seen, as.character(i))
}
issues
}
dup_issues <- detect_duplicates(bib)
message(sprintf("Duplicate detection: %d issues found", length(dup_issues)))
Expected: Zero duplicates for a clean bibliography. Any detected duplicates are flagged with the specific entry keys involved.
generate_report <- function(all_issues, bib, output_file = NULL) {
errors <- Filter(function(x) x$severity == "error", all_issues)
warnings <- Filter(function(x) x$severity == "warning", all_issues)
infos <- Filter(function(x) x$severity == "info", all_issues)
lines <- c(
"# Bibliography Validation Report",
"",
sprintf("**File**: references.bib"),
sprintf("**Entries**: %d", length(bib)),
sprintf("**Date**: %s", Sys.Date()),
"",
sprintf("## Summary: %d errors, %d warnings, %d info",
length(errors), length(warnings), length(infos)),
""
)
if (length(errors) > 0) {
lines <- c(lines, "## Errors", "")
for (issue in errors) {
lines <- c(lines, sprintf("- **[%s]** %s", issue$key, issue$message))
}
lines <- c(lines, "")
}
if (length(warnings) > 0) {
lines <- c(lines, "## Warnings", "")
for (issue in warnings) {
lines <- c(lines, sprintf("- **[%s]** %s", issue$key, issue$message))
}
lines <- c(lines, "")
}
report_text <- paste(lines, collapse = "\n")
if (!is.null(output_file)) {
writeLines(report_text, output_file)
message(sprintf("Report written to %s", output_file))
}
cat(report_text)
invisible(all_issues)
}
all_issues <- c(field_issues, doi_issues, url_issues, dup_issues)
generate_report(all_issues, bib, output_file = "validation-report.md")
Expected: A structured markdown report listing all issues grouped by severity.
10.1234/...,
https://doi.org/10.1234/..., or doi:10.1234/.... Normalize before comparingemail parameter to join the polite pool for higher limits@online where BibTeX uses @misc.
The validator should handle bothand, not commas: BibTeX splits an author
or editor field on the word and surrounded by spaces and not enclosed in
braces. A comma delimits parts inside a single name (von Last, First or
von Last, Jr, First), so a comma-joined list collapses silently into one
author rather than several, and a trailing comma makes BibTeX complain that a
name ends with a commaauthor field is dissected into First/von/Last like a personal
name, and any internal and splits it into two people. Wrap it in its own
braces: author = {{National Aeronautics and Space Administration}}Donald E. Knuth in one entry and D. E. Knuth
in another alphabetize as two different authors. Settle on one form per person,
or write D[onald] E. Knuth, which BibTeX alphabetizes as if the brackets were
absentJournal of the American Chemical Society, another writes J. Am. Chem. Soc., and nothing above notices, because
Step 6 compares DOIs and article titles and never looks at the journal field. The
CrossRef record fetched in Step 4 carries the journal's ISSN and container-title,
and often its short-container-title (J. Am. Chem. Soc. for this journal), so
group entries by ISSN and flag any group whose journal values disagree. Flag
rather than normalize: ISO 4 governs title-word abbreviation through the LTWA
maintained by the ISSN International Centre, but NLM/MEDLINE strips the punctuation
from that assignment, so PubMed writes J Am Chem Soc for the same journal. Pick the
form the target journal's style requirestitle (an array), the family names in
author, and issued.date-parts. Normalize both sides first, or LaTeX escapes
and Unicode accents will manufacture mismatchespublished-online and published-print are separate fields that
can fall in different years. 10.1093/bioinformatics/btz848 is online 2019 and
print 2020, with issued following the online date. short-container-title is
sometimes an empty array, so a missing journal abbreviation is not a mismatch,
and a record paginated by article number can carry article-number with no
page field at allmanage-bibliography - fix issues found by this validator (dedup, add fields)format-citations - format validated entries into styled citationsrepair-broken-references - the correct destination for broken internal links,
dead URLs, stale imports, and orphaned files in documentation or agent memoryverify-memory-integrity - read-only reachability and budget check for a Claude
Code memory directory; not a bibliography tool despite the shared word../reporting/format-apa-report - APA reports require complete, validated references../r-packages/write-vignette - vignettes with citations need valid .bib entriesEdit PDFs with natural-language instructions using the nano-pdf CLI.
Control Sonos speakers (discover/status/play/volume/group).
Terminal Spotify playback/search via spogo (preferred) or spotify_player.
Capture frames or clips from RTSP/ONVIF cameras.
CLI to manage emails via IMAP/SMTP. Use `himalaya` to list, read, write, reply, forward, search, and organize emails from the terminal. Supports multiple accounts and message composition with MML (MIME Meta Language).
Monitor blogs and RSS/Atom feeds for updates using the blogwatcher CLI.
Category:tools