Skip to content

feat: server-side citation verification against source text - #25

Open
amal66 wants to merge 1 commit into
upstream-mainfrom
upstream-pr/citation-verification
Open

feat: server-side citation verification against source text#25
amal66 wants to merge 1 commit into
upstream-mainfrom
upstream-pr/citation-verification

Conversation

@amal66

@amal66 amal66 commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Hallucinated citations are the failure mode that gets a small firm sanctioned. This PR adds server-side verification of assistant citations: after the model's <CITATIONS> block is parsed, every document quote is checked against the actual extracted text of the cited document before the citation is streamed to the client or persisted.

Each quote is located in the source with progressively more tolerant matching — exact substring, then whitespace/case-normalized, then punctuation-tolerant — and gets a per-quote verification record plus char offsets into the extracted text. Each citation gets an aggregate verification_status:

  • verified — quote is character-identical to the source
  • repaired — quote was found under tolerant matching but drifted; the exact source excerpt is swapped into the displayed quote so drifted text is never presented as the source's words
  • unverified — no match; the model's text is preserved but must be treated as untrusted

Cross-page quotes ([[PAGE_BREAK]] sentinel) are split and each segment verified independently. Case-law citations pass through untouched — their existence is already verified via CourtListener and is never re-marked here.

Changes

  • backend/src/lib/chat/verifyCitations.ts (new) — quote location + verification + annotation/batch verification logic
  • backend/src/lib/chat/verifyCitations.test.ts (new) — 16 focused unit tests (exact/drifted/fabricated quotes, unreadable sources, cross-page quotes, case-citation passthrough, aggregation)
  • backend/src/lib/chat/streaming.ts — verify final citations before the citations SSE frame; source text is fetched at most once per document per turn (memoized), via readDocumentContent(..., { emitEvents: false }) so no extra events fire
  • backend/src/lib/chat/tools/documentOps.tsnormalizeWithMap gains an optional stripPunctuation mode and is exported (used by tier-3 tolerant matching)
  • backend/src/lib/chat/types.tsCitationVerificationStatus / QuoteVerification types
  • backend/tsconfig.json — exclude *.test.ts / __tests__ from the tsc build so tests don't require test deps at build time

No new runtime dependencies. No frontend changes: the new fields ride along on the existing citation annotations; a missing verification_status is documented as "treat as untrusted", so existing clients are unaffected.

Why

Citation trust is mission-core: the assistant's answer is only usable in legal work if every quote it attributes to a document is actually in that document. This closes the gap server-side, where the source of truth (document bytes in storage) lives — the client never has to be trusted to check.

No new vendors. Negligible compute cost: verification is pure string matching over document text the server already extracts, with source text memoized per document per turn — no extra LLM calls and no network egress (the tests explicitly prove verification only reads bytes handed to it).

Testing

  • npm install && npm run build in backend/ — green on this branch as committed
  • npx vitest run src/lib/chat/verifyCitations.test.ts — 16/16 passed (vitest installed locally with --no-save and not committed; upstream has no test harness yet, so tests run atop the test-harness PR)
  • frontend/ untouched

Provenance

All changes are mechanical ports of code in amal66/mike@origin/main (commit b3166dd); exceptions: none. Sources: apps/api/src/lib/tools/verifyCitations.ts + .test.ts (logic and tests, verbatim modulo import paths and mock/comment path rewrites), apps/api/src/lib/tools/stream.ts (streaming hook, verbatim modulo createCitationAnnotation → upstream's createCitation), apps/api/src/lib/tools/docRead.ts (normalizeWithMap extension, verbatim), packages/core/src/types.ts (the two verification types, inlined verbatim), apps/api/tsconfig.json (test-file build exclusion).

Credits & prior art

  • @Gadoes (Gadoes/dispumike) — independently parallels this work: their fork built citation verification for case-law sources (via legal-database integrations such as CourtListener/EUR-Lex). This PR verifies document quotes against source text server-side — same goal (no hallucinated citations reach a lawyer), different mechanism.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC

Port of the fork's document-quote verification (apps/api/src/lib/tools/
verifyCitations.ts and its wiring) to upstream layout. After the model's
<CITATIONS> block is parsed, each document quote is located in the
document's extracted source text (exact, then whitespace/case-tolerant,
then punctuation-tolerant matching). Quotes get a per-quote verification
record and each citation an aggregate verification_status:
verified | repaired (exact source excerpt swapped in) | unverified.
Case-law citations pass through untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant