// notes · citation audit

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.

LLMOpsClinical AICI/CD app.chadwycke.io · first pass May 2026 · CI gate since June 2026
// 01 · the problem

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.

// 02 · the pipeline

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.

  1. Extract app/scripts/lib/citations.mjs

    Pulls 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.

  2. Verify app/scripts/check-citations.mjs

    Queries the NCBI E-utilities esummary endpoint in polite batches — up to 200 ids per request, rate-limited, transient failures retried. Every PMID must resolve to a live PubMed record.

  3. 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.

  4. Gate exit code 1

    Prints 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.

  5. Re-verify, forever .github/workflows/citation-check.yml

    A 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.

// 03 · first pass

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
// 04 · the gates

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.

// 05 · sme-in-the-loop

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.

// 06 · why it matters

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).