Citation verification: re-fetch, compare, badge the result

Gadoes closes the loop on citations: a user can now click "Verify & read" and the system re-fetches the source URL, checks whether the stored excerpt actually appears in the page, and updates a `verification_status` badge. Four commits, two new tables columns, one new service, one new route.

searchcompliance

Schema (Task 1). Migration 006_citations_verification.sql adds verification_status TEXT NOT NULL DEFAULT 'pending' with a check constraint allowing only pending, verified, unverified, or unavailable. A separate savedCitationIds return value flows out of runLLMStream() so the route layer can back-link citations to their chat message after insert. italaw and ICSID citations are auto-set to verified on save because those sources are static scrapes - no live fetch needed.

Verifier service (Task 2). citationVerifier.ts is 46 lines. It fetches the URL with a 10-second AbortController timeout, strips HTML tags and collapses whitespace, then does a case-insensitive substring check for the excerpt. Returns verified, unverified, or unavailable. Empty excerpts go straight to unavailable without a fetch. The 101-line test covers the happy path, excerpt-not-found, HTTP error, network error, timeout, case-insensitivity, and empty excerpt.

Route (Task 3). POST /citations/:id/verify at routes/citations.ts (40 lines) fetches the citation by ID, returns 422 if there's no excerpt, calls CitationVerifier.verify(), updates verification_status, and returns the updated row. The 137-line test mocks both Supabase and CitationVerifier and covers 404, 422, and all three outcome states.

UI (Task 8). CitationCard.tsx grows a VerificationBadge component with four states: pending (spinner-like indicator), verified, unverified ("Excerpt not found"), unavailable. Clicking "Verify & read" calls the endpoint, updates local state, and hides the button entirely if the citation has no id.

The naming decision from Chunk 4 - keeping liveness_status separate from verification_status - pays off here. No schema collision.

So what Worth a look if distinguishing "a citation exists" from "the citation text is real" matters for your users. This is the piece that makes MCP-grounded responses auditable. Pulling it without Chunks 4-5 makes no sense; with them, the adoption cost is a single column migration plus about 250 lines of backend code. One real constraint: the verifier re-runs the MCP fetch on demand, so it inherits the latency and rate-limit profile of the underlying source. Combining it with the Chunk 17 caching layer is close to mandatory in production.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

5 commits from Gadoes/dispumike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
0ba14c89 feat(chunk20): migration 006, citation DB saving with IDs, verification_status type Gadoes 2026-05-02 ↗ GitHub
1e0939c2 feat(chunk20): CitationVerifier service with unit tests Gadoes 2026-05-02 ↗ GitHub
85e53476 feat(chunk20): POST /citations/:id/verify route with tests Gadoes 2026-05-02 ↗ GitHub
0681ec54 Chunk 20 Task 8: CitationCard verification badge Gadoes 2026-05-02 ↗ GitHub
commit body
- VerificationBadge component shows pending/verified/unverified/unavailable
- handleVerify calls POST /api/citations/:id/verify and updates badge state
- Button hidden when citation has no id
- makeCitation helper defaults to id="cite-default" for pre-Chunk-20 tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
20b226cd Merge Chunks 19-20: Telemetry Dashboard + Citation Verification Gadoes 2026-05-02 ↗ GitHub
commit body
Chunk 19 - MCP Telemetry:
- migration 005_mcp_events.sql
- Telemetry logging in McpClientManager.callTool (latency, cache_hit, success, error_type)
- GET /admin/mcp-telemetry route with per-source aggregation
- Frontend /admin/mcp-telemetry dashboard with auto-refresh

Chunk 20 - Citation Verification:
- migration 006_citations_verification.sql (verification_status column)
- Citation DB saving with IDs before SSE emit; chat_message_id linking
- CitationVerifier service (fetch + HTML text normalize, 10s timeout)
- POST /citations/:id/verify route
- CitationCard VerificationBadge component with live API updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-20.md from inside the repo you want the changes in.

⬇ Download capture-thread-20.md