The citation audit.
A pipeline that treats LLM-fabricated evidence as a build failure. What it does, what it caught on its first pass, and why the merge button still belongs to a human.
Plausible is not
the same as real.
Large language models fabricate citations fluently: a real author, a real journal, a volume and page range that parse cleanly — attached to an article that was never written. In most software that is an embarrassment. In clinical education it is a patient-safety defect, because a fabricated reference can smuggle a fabricated clinical threshold into training material, and a clinician can carry it to the bedside.
app.chadwycke.io (opens in new tab) is a clinician-facing procedure-training app for advanced practice providers — 15 modules live, each pairing a training deck with assessments and post-tests. LLMs helped draft the content; none of it reaches a learner unverified. The working rule, from the audit protocol that governs the corpus: no PMID asserted from memory. Every citation must resolve live against the primary record.
Verification is code,
not vigilance.
Citations live inline in the deck HTML as PMID <n> entries in each module's reference list, so the audit runs against the same source files the build publishes — there is no separate bibliography to drift out of sync.
-
Extract
app/scripts/lib/citations.mjsPulls every PMID out of all 15 deck files, de-duplicates, and maps each one back to the modules that cite it. Pure functions, no network — unit-tested against fixture esummary responses, including the error-stub and retraction cases.
-
Verify
app/scripts/check-citations.mjsQueries the NCBI E-utilities
esummaryendpoint in polite batches — up to 200 ids per request, rate-limited, transient failures retried. Every PMID must resolve to a live PubMed record. -
Classify
classifyEsummary()No record, or an error stub, means fail — a typo or a fabrication. A record marked Retracted Publication is flagged separately. Citations older than five years only warn — landmark trials are allowed to age.
-
Gate
exit code 1Prints the report — decks scanned, total and unique PMIDs, failing, retracted — emits a machine-readable summary for CI, and exits nonzero on any dead or retracted citation. A fabricated reference is a failed build.
-
Re-verify, forever
.github/workflows/citation-check.ymlA weekly scheduled run re-audits the whole corpus and opens-or-updates a single tracking issue on failure. A paper retracted after a module ships still gets caught — with no code change.
Around it, standard hygiene: every PR passes deck validation across all 15 modules, a type-check, and the unit suite; every push to main repeats that gate before deploying to Cloudflare Pages, and a Playwright smoke run then checks the deployment it just produced.
What the first
pass caught.
The first full audit pass (May 2026) went deeper than the automated gate: every citation in all 15 modules and their companion assessment documents was verified against PubMed, with title, journal, and year required to match. The deck citations came back clean — roughly ninety unique PMIDs, all real. The fabrications were hiding a layer down, in the material that grades clinicians.
F1
A fabricated citation
A post-test answer key backed a clinical lab threshold with a citation that returns zero PubMed hits. The article does not exist.
F2
A real author, a fake paper
A second citation paired a real researcher with an article they never wrote — that author's actual papers from the cited year cover a different procedure entirely. The most convincing kind of fake.
F3
A hallucinated rule
One module taught an eponymous clinical “rule” as fact — and its post-test graded it as the correct answer. No such rule exists in the literature. It was de-named, re-sourced to a verified citation, and the answer keys were re-synced.
All three were replaced with verified, live-resolving sources before any clinical use. The uncomfortable lesson: a deck-only audit would have missed every one of them. The fabrications sat in the documents that grade clinicians, not the slides that teach them — which is why the audit's scope is the decks and everything that must agree with them.
- inline PMID citations at the gate's first run
- 271
- unique PMIDs — all resolving live
- 85
- fabrications caught pre-publish: 2 citations, 1 rule
- 3
- dead or retracted PMIDs tolerated by the gate
- 0
Audits decay.
Gates don't.
A one-time audit is a snapshot. The pipeline's value is that its checks became permanent: the citation validator fails the build on any dead or retracted PMID, and the weekly re-run catches the failure mode nobody schedules — retractions that land after ship.
The same pattern covers accessibility. A WCAG 2.2 AA audit computed relative-luminance contrast math over 62 foreground/background pairs — 32 in the light theme, 30 in the dark — drawn from the app's stylesheet and component styles. Twelve pairs failed the first run (3 light, 9 dark, two of them blocker-class invisible text). Every failure was remediated, and the contrast table was locked into the palette-change PR as a regression gate, so a future palette can't quietly reintroduce them.
Humans hold the
merge button.
Verification catches fabrication; it cannot adjudicate clinical judgment. That belongs to subject-matter experts, so review is built into the app instead of an email thread. A reviewer signs in with Google and comments on the exact element in question. The /api/intake endpoint verifies the ID token server-side (audience-checked), re-checks the reviewer allowlist on the server — the client-side check is UX only — enforces size caps and a MIME allowlist on attachments, and files a structured GitHub Issue.
A GitHub Action picks up the labeled issue and has an LLM draft the change as a pull request, with a self-rated risk level attached. That rating is advisory only. An earlier version of the workflow auto-merged PRs the model rated low-risk; that path was deliberately removed, because a hallucinated or adversarially induced “low” could land unreviewed clinical content. Merge is now a deterministic human decision — code-owner review is required on every drafted PR, enforced by CODEOWNERS and branch protection, independent of anything the model says about itself.
Why this matters
for clinical AI.
None of this required a frontier lab's budget — a public E-utilities endpoint, a few hundred lines of tested classification logic, and CI wiring. It required refusing two temptations: trusting a model's citations because they look right, and trusting a model's self-assessment because it sounds calibrated. The first pass showed both refusals earning their keep — the fabrications hid where nobody was looking, and the “rule” that never existed had already reached a graded answer key. If a model's output can reach a clinician, verification against ground truth belongs in the build, and a human belongs on the merge. That is the pattern this app runs on, and the one I'd argue for on any clinical-AI team.
The pipeline lives in a private repository — the app serves gated clinical content. Every file path, count, and CI behavior on this page is drawn directly from that repository's code, audit documents, and CI history, re-verified against source on 2026-07-09. The production app is live at app.chadwycke.io (opens in new tab).