scidex/atlas/citation_validity.py runs a triple-redundant LLM check
on every (claim, PMID) pair to decide whether the cited paper actually
supports the claim. We trust this sweep to catch fabricated PMIDs (a
real failure mode in LLM-generated hypotheses), but we never test it.
This task plants honeypot citations — fabricated PMIDs in
fabricated-but-quarantined claims — periodically, then verifies that
the validity sweep catches them within one cycle. Honeypots that are
not caught indicate the sweep has degraded (model drift, prompt
corruption, throughput cap hiding rows) and trigger a Senate alert.
This is a canary system, not a test suite — it runs in production
against the live sweep.
Effort: deep
scidex/senate/citation_honeypot.py:plant(n=5) -> list[honeypot_id] — generates n fake PMIDs99000000–99999999scidex/atlas/citation_validity.py as "alwayscitation_honeypot table with the synthetic claim textcontradicts).harvest() -> HarvestReport — checks each planted honeypotcontradicts/off_topic within the SLA window (defaultmigrations/20260428_citation_honeypot.sql:CREATE TABLE citation_honeypot (
id TEXT PRIMARY KEY,
fake_pmid TEXT NOT NULL,
claim_text TEXT NOT NULL,
target_artifact_id TEXT, -- optional: synthetic decoy hypothesis
planted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expected_verdict TEXT NOT NULL DEFAULT 'contradicts',
first_caught_at TIMESTAMPTZ,
actual_verdict TEXT,
retired_at TIMESTAMPTZ
);citation_validity.fetch_abstract is patchedlifecycle='honeypot' and/hypotheses,/exchange, dashboard) by default. Add aWHERE lifecycle != 'honeypot' clause where missing.
senate_alerts rowkind='citation_sweep_degraded' and create a Senateretired_at = NOW()); retired rows kept for the trailing 30dtests/test_citation_honeypot.py:scidex/senate/citation_honeypot.py so future maintainerscitation_validity.py and patchfetch_abstract first (under unit test).
scidex/atlas/citation_validity.py — read-path consumer.q-rt-falsifier-of-truth-cron — uses the same canary pattern forWhat was built:
migrations/20260428_citation_honeypot.sql — citation_honeypot table with allscidex/atlas/citation_validity.py — Added:HONEYPOT_PMID_MIN = 99_000_000, HONEYPOT_PMID_MAX = 99_999_999 constantsHONEYPOT_ABSTRACT_PLACEHOLDER deterministic placeholder stringis_honeypot_pmid(pmid) -> bool helperfetch_abstract(pmid) -> Optional[str] wrapper that short-circuits honeypotsweep() and run_synthetic_test() both use fetch_abstract() instead ofget_abstract directlyscidex/senate/citation_honeypot.py — New module with:plant(n=5) -> list[str] — inserts n honeypots with random PMIDs from theharvest() -> HarvestReport — sweeps active honeypots via _triple_evaluate,get_detection_rate_30d() -> dict — metric for dashboard tile_emit_alert() — writes senate_alerts row with alert_type='citation_sweep_degraded'scidex/senate/scheduled_tasks.py — Two new tasks registered:citation-honeypot-plant (daily, plants 1 honeypot)citation-honeypot-harvest (daily, runs harvest cycle)scidex/senate/quality_dashboard.py — New tile:_honeypot_health_metric() — calls get_detection_rate_30d()_render_honeypot_tile(hp) — renders the "Citation-Validity Canary" cardbuild_quality_dashboard() payload as honeypot_healthtests/test_citation_honeypot.py — 23 tests (all pass):Design notes:
citation_honeypot table (no fake hypothesis rows).harvest() drives its own _triple_evaluate calls rather than waiting forsenate_alerts table uses alert_type (not alert_kind) per the live schema.lifecycle='honeypot' column was not needed since honeypots live in their own{
"completion_shas": [
"ecdf740b3c04dd6acf890d34914ab44030f4bc9a"
],
"completion_shas_checked_at": ""
}