577 citations carried a bare PMID with no title, so the audit built to catch a fabricated title could not see them: the third dropped denominator in this project
also called citation audit blind spot · untitled citations · title backfill · dropped denominator · bare PMID census
An audit built to catch invented citations came back reassuring - four suspicious out of roughly 8,700 - but it could only grade a citation that had a title stored beside it. 577 citations stored nothing but a bare reference number, so for those the checker confirmed only that the number pointed at a paper, never that it pointed at the right one.
The same thing, for a scientist
`analyses/citation-title-overlap-distribution` scored 8,661 distinct (entry, PMID) pairs and reported 4 below a 0.50 weighted-title-overlap, 0 below the 0.18 `--strict` threshold. **Its denominator excluded every citation with no stored title, and it did not say so.** Census over the node corpus: | | count | |---|---:| | citation-like records | 16,440 | | with a PMID | 12,690 | | **PMID but no stored title** | **577** | | ...and no authors either | **575** | | entries affected | 187 | `verify-citations.py` compares stored title against PubMed and stored authors against PubMed. **With neither field present it verifies only that the PMID resolves** - which an unrelated paper does. Two PMIDs in this project have already turned out to be entirely unrelated papers, so this is a live failure mode, not a hypothetical. **Two `pmid` fields did not contain PMIDs at all** (2 of 12,694): the free-text string `"unverified this cycle"`, and eleven PMIDs comma-packed into one string. Both sat inside `status: verified` entries and both were silently skipped by every checker. **Remediation, and its limits.** All 388 distinct untitled PMIDs resolve - **zero fabrications**. Titles, authors, venue and year were backfilled from PubMed esummary into 575 records across 187 entries, each tagged `title_provenance` recording that **backfilling is not verification**: it establishes the PMID points at a real paper, not at the right one. A topical screen of the backfilled titles against their citing entry's vocabulary left 4 zero-overlap cases out of 456, and all four are false positives on reading.
Why the reassuring number was the wrong number
The audit's own docstring asked the right question - what does the rest of the distribution look like? - because a checker whose pass mark sits just below a real fabrication is only safe if little of the corpus sits near that mark. It answered for the corpus it could see.
8,661 pairs scored, 4 suspicious reads as 0.05% of citations are questionable. 577 citations were never scored, because scoring requires a title to compare.
Those 577 are not a random sample of the corpus. They are systematically the citations somebody entered in a hurry, as a bare number - exactly the population where a copy-paste of the wrong PMID is most likely, and exactly the population the audit excluded. The exclusion was silent: nothing in the output named it.
This is the third instance of the same shape in this project, and the first two are already written down:
verify-accessions.pyreported 400 present / 399 checked / exit 0 because a fetch failure fell out of the denominator - the run reported clean because the bad case left the measurement.verify-citations.pyscored a fabricated title at 0.1979 against a 0.18 threshold and reported title mismatches 0 - the wrong answer graded as close enough.- This one: the audit built in response to (2) inherited the same defect, and 577 citations sat outside its denominator while it reported a clean rate.
The recurring lesson is not that these checks are bad. It is that every clean headline number in this project needs its denominator printed next to it, and that an audit written to check an instrument is itself an instrument.
What the two malformed pmid fields were, and why they are worse than they look
Only 2 of 12,694 pmid fields fail a bare-digits match, so the rate is negligible. The reason they matter is that both were invisible rather than wrong.
interface-psip1-pwwp-h3k36me-nucleosome stored "pmid": "unverified this cycle" on a cryo-EM structure row, inside an entry marked status: verified. A human-readable admission of doubt was placed in a machine-read field, where it became neither - the checker skipped it as unparseable and the entry kept its verified status. Checked live: PDB 9XYC is real - released 2026-03-18, ELECTRON MICROSCOPY, 3.5 A, substrate string matching what the entry claims. Its RCSB primary citation carries no PubMed id and no DOI, i.e. the structure paper is not yet indexed, which is the true reason no PMID could be given. The field now records the PDB accession as the resolving identifier and states why no PMID exists.
ana-menin-interactome-enumeration-and-bounded-rejections packed eleven PMIDs into one comma-joined string. A comma-joined value is not a PMID, so eleven identifiers sat unread inside the one field whose entire purpose is to be read. All eleven were fetched and all eleven resolve, and each matches the rejection reason recorded beside it - mesenchymal stem cells, murine behaviour, parathyroid tumours, neuroendocrine profiling, TGF-beta, reviews, incidental MEN1 panels. The scholarship was sound; only the schema was wrong. Now stored as a pmids array.
Both defects share a property worth generalising: a field that a checker cannot parse is more dangerous than a field a checker marks wrong, because the second produces an alert and the first produces silence that reads as a pass.
The backfill, and the sentence that must travel with it
575 records across 187 entries now carry a title, authors, venue and year fetched from PubMed. Every one is tagged:
BACKFILLING IS NOT VERIFICATION: it confirms the PMID resolves to a real paper, not that this is the right paper for the claim. That still requires reading.
The tag exists to stop a future audit reporting a fake improvement. If the overlap distribution is re-run now, these 575 will score ~1.0 by construction - their titles came from the same source the checker compares against - and the corpus-wide clean rate will rise for no epistemic reason whatsoever. Any future run must exclude title_provenance-tagged records or report them separately. This entry is partly here to make that circularity impossible to forget.
What the backfill did genuinely buy: it converts 575 citations from unconstrained to constrained going forward. A later edit that corrupts one of these titles will now be caught, and a reader can see what is being cited without a round trip to PubMed. It also established, as a real result, that none of the 388 distinct PMIDs is fabricated - a live possibility before the check, given this project's history.
The topical screen and its two false-positive modes. Scoring each backfilled title against the citing entry's vocabulary initially flagged 45 zero-overlap cases, of which essentially all were artefacts of the screen:
- 119 backfilled citations live in enumeration and rejection ledgers, which cite papers because they were searched and rejected. Off-topic is the point; flagging them measures the screen, not the graph.
- Exact-token matching cannot see
cbxin agene-cbx8entry. PMID 21047797, Recognition and specificity determinants of the human cbx chromodomains, scored zero againstgene-cbx8.
With ledgers excluded and prefix-tolerant matching, 4 of 456 scored zero, and all four are false positives on reading - most plainly PMID 9277049, Subcellular localization of the MEN, MLL/MEN and truncated MLL proteins, flagged against a chromatin-bound-versus-soluble localization entry. The screen found nothing. That is the honest result, and it is reported as a null rather than dressed up.
The larger blind spot found while fixing the first: 663 PMIDs the checker never walked to
Patching verify-citations.py meant reading collect(), which turned out to be the bigger finding.
It walked only evidence[].citation and evidence[].counter_evidence[].citation. 663 PMID-bearing objects sit outside those two paths - every one of them inside a data block, where beats store structured result tables that carry their own citations: gene-parp1's combination_results, gene-rad51's locus_level_measurement, drug-chidamide's menin_combination_evidence, gene-ctcf's patient_break_mapping, and 180-odd more.
Those citations had never been compared against PubMed by any gate in this project. Not because they lacked a title - the first defect - but because nothing ever looked at them. The 577 were unscoreable; these 663 were unreachable, which is worse, and the two populations overlap heavily since a bare-PMID row is exactly what a data table holds.
collect() now walks the whole node: a citation is anything with a pmid key, wherever it sits. Distinct PMIDs under the gate went from 2,343 to 2,404.
The widened gate found two real defects on its first run, both in collab-pieters-interfant, both in a data block, both previously invisible:
- PMID 17658395 stored 102 of 162 characters - the truncation cut ": an observational study and a multicentre randomised trial", which is the study design, from the citation for the Interfant-99 protocol.
- PMID 41505662 stored 70 of 123 characters, losing "Long-Term Outcomes, Toxicity, and Pharmacokinetics".
Both are the listing-truncation trap, which weighted overlap structurally cannot see - a prefix shares all of its words with the full title, so it scores ~1.0. The dedicated prefix check existed and reported 0 for as long as it was pointed at the wrong half of the corpus. Two independent blind spots had to coincide for these to survive, and they did.
Both checkers now tested against synthetic defects, because a check that has nothing to catch is untested
The validator now errors on a pmid field that is not bare digits, with a hint to use a pmids array when several are packed in. It passed immediately - because the only two offenders had already been fixed an hour earlier.
A check that passes because there is nothing to catch has not been tested, and that is the entire subject of this entry. So both malformed forms were re-injected synthetically into a temporary node and the validator was re-run:
x .data.rows[0].pmid is not a PMID: "unverified this cycle"
x .data.rows[1].pmid is not a PMID: "111, 222, 333" (several PMIDs in one field - use a `pmids` array)
Both fire. This is the same discipline that caught the previous hardening of verify-accessions.py being committed broken: that fix was verified by monkeypatching fetch() to raise, after an earlier version was committed while crashing with a NameError and reported as working because the wrapper's exit code was read instead of the traceback.
| citation like records | 16440 |
|---|---|
| with pmid | 12690 |
| pmid without title | 577 |
| pmid without title or authors | 575 |
| entries affected | 187 |
| distinct untitled pmids | 388 |
| unresolved pmids | 0 |
| malformed pmid fields | 2 |
| records backfilled | 575 |
| pmids unreachable by collector | 663 |
| truncations found by widened gate | 2 |
topical screen — the numbers behind the chart
{
"excluded_as_ledgers": 119,
"screened": 456,
"zero_overlap": 4,
"zero_overlap_true_positives": 0
}verifier state after — the numbers behind the chart
{
"distinct_pmids": 2343,
"title_mismatches": 0,
"author_mismatches": 0,
"doi_mismatches": 0,
"not_found": 0
}prior instances of this shape
[ "verify-accessions.py dropped a fetch failure from its denominator and exited 0", "verify-citations.py scored a fabricated title at 0.1979 against a 0.18 threshold" ]
distinct pmids under gate
{
"before": 2343,
"after": 2404
}checkers hardened
[ "verify-citations.py collect() now walks the whole node, not just evidence citations", "validate-graph.mjs now errors on a pmid field that is not bare digits; tested synthetically" ]
- Do not cite '4 suspicious out of 8,661' from the overlap audit without the denominator correction: 577 citations were outside that measurement, and they were the ones most likely to be wrong.
- Do not re-run the overlap distribution and report the improved clean rate. The 575 backfilled titles came from PubMed and will score ~1.0 against PubMed by construction. Exclude title_provenance-tagged records or report them separately.
- Do not read 'zero fabrications among 388 PMIDs' as 'the citations are correct'. It means every number points at a real paper. Whether it points at the right paper for the claim is unmeasured for all 575.
- Do not read 'verify-citations.py --strict is clean' as 'every citation is checked' without asking what collect() walks. It missed 663 PMIDs until 2026-09-20 and found two real truncations within one run of being widened.
- How many of the 575 backfilled citations are the RIGHT paper for their claim? The backfill cannot answer this and the topical screen is weak evidence. Only reading can, and 575 is a reading beat, not a coordinator task.
- Should verify-citations.py FAIL on a PMID with no stored title, rather than skipping it? That would have surfaced this in one run, at the cost of failing the gate on 577 records the day it was introduced.
- Should the validator reject a pmid field that is not bare digits? Two rows is a small prize, but both were silent, and silence is the failure mode that costs most here.
- Do the EDGE and FIGURE files carry citations nothing walks? collect() now covers all of node JSON, but the same reasoning that found 663 unreachable PMIDs in data blocks applies to any file the collector does not open.
- How many other checks in this project are pointed at a subset of what they claim to cover? The prefix check was correct, tested and reporting 0 while two truncations sat in a region it never saw. That is a property of the corpus walk, not of the check.
- E1strong confidence 4 of 5: Replicated in peer-reviewed work by more than one group.analysis
577 of 12,690 PMID-bearing citation records in this graph stored no title, and 575 of those stored no authors either, so verify-citations.py could verify only that their PMID resolved and not that it resolved to the cited paper; the title-overlap audit that reported 4 suspicious citations out of 8,661 excluded all of them from its denominator without saying so.
citation-title-overlap-distribution: scoring every stored citation title against PubMed, and the census of citations that could not be scored at all · Oncofusion knowledge graph (internal analysis) 2026What this evidence cannot show
Counted 2026-09-20 by walking every node JSON and treating any object with a pmid, doi, url or nct key as a citation-like record, which over-counts slightly because some such objects are database rows rather than bibliographic citations. The 577/575 split is counted only over objects carrying a pmid, so it is not affected. **The audit's exclusion was a silent omission, not a coding error** - it skipped empty titles deliberately, because an empty title cannot be scored; the defect is that the resulting rate was reported as if corpus-wide.
Evidence that argues against this (1)
- THE REMEDIATION FOUND NOTHING WRONG, which bounds how alarming this should be. All 388 distinct untitled PMIDs resolve to real papers, none is fabricated, and a topical screen left 4 zero-overlap cases out of 456 that are all false positives on reading. So the blind spot was real but, on this evidence, empty. The argument for caring is about the guarantee rather than the outcome: a clean result obtained after the fact does not retroactively justify a headline rate computed over a partial denominator. Oncofusion knowledge graph (internal analysis) 2026
- E2strong confidence 4 of 5: Replicated in peer-reviewed work by more than one group.database
Two of 12,694 pmid fields contain values that are not PMIDs - the free-text string 'unverified this cycle' on a cryo-EM row in a status:verified entry, and eleven PMIDs comma-packed into a single string - and both were silently skipped by every citation checker rather than being reported as malformed.
RCSB PDB entry 9XYC: Pol II-DSIF-SPT6-PAF1c-TFIIS-IWS1-ELOF1-LEDGF-nucleosome, cryo-EM 3.5 A, released 2026-03-18 · RCSB Protein Data Bank 2026What this evidence cannot show
Malformedness was defined as failing a bare-digits match after stripping whitespace, which would not catch a syntactically valid PMID that is simply the wrong number - the more common and more dangerous error. **This check bounds schema validity, not correctness.** PDB 9XYC was verified live against data.rcsb.org on 2026-09-20; the absence of a PubMed id in its primary citation is read from the RCSB record and could change once the structure paper is indexed.
Evidence that argues against this (1)
- NEITHER MALFORMED FIELD CONCEALED AN ERROR. PDB 9XYC is real and matches the entry's stored substrate, method and resolution exactly, and all eleven packed PMIDs resolve and match their recorded rejection reasons. The 'unverified this cycle' string was an honest admission by whoever wrote it - the defect is that it was written into a machine-read field, where honesty became silence. Oncofusion knowledge graph (internal analysis) 2026
- E3strong confidence 4 of 5: Replicated in peer-reviewed work by more than one group.analysis
verify-citations.py collected citations only from evidence[].citation and evidence[].counter_evidence[].citation, leaving 663 PMID-bearing objects inside node data blocks unreachable by any citation gate in this project; widening the collector to walk the whole node raised the distinct PMIDs under the gate from 2,343 to 2,404 and immediately surfaced two truncated titles in collab-pieters-interfant that had been invisible to both the overlap check and the dedicated prefix check.
citation-title-overlap-distribution: scoring every stored citation title against PubMed, and the census of citations that could not be scored at all · Oncofusion knowledge graph (internal analysis) 2026What this evidence cannot show
Counted 2026-09-20 by object identity against the set collect() reaches, over node JSON only - edge and figure files were not walked and could hold citations nothing checks. 'Unreachable' means unreachable by verify-citations.py; a human reading the entry would see these citations perfectly well. The 663 and the 577 OVERLAP HEAVILY rather than summing, because a bare-PMID row is exactly what a data table holds - they are two views of one population, not two populations.
Evidence that argues against this (1)
- THE WIDENED GATE STILL REPORTS ZERO TITLE, AUTHOR AND DOI MISMATCHES across all 2,404 distinct PMIDs, so the unreachable region was not concealing wrong papers - only two truncations. As with the first defect, the case for caring is about the guarantee rather than the harvest. It is also worth stating plainly that the two truncations were found by a check that ALREADY EXISTED and had been reporting 0; it was aimed at the wrong half of the corpus, which is the same category of error as the partial denominator rather than a new one. Oncofusion knowledge graph (internal analysis) 2026
- related to →What this beat looked at and did not write: rejected candidates, with counts for both halves of every search
THE FABRICATION THAT STARTED THIS: the invented title for PMID 25822366 is what prompted the overlap audit, whose own denominator then turned out to be partial.
HELD ELEVEN PMIDS PACKED INTO ONE FIELD, unread by every checker; all eleven verified and re-stored as an array.
STORED 'unverified this cycle' IN A PMID FIELD inside a status:verified entry; PDB 9XYC verified live and recorded as the resolving identifier instead.
THE SAME FAMILY OF ERROR, A NEW VARIETY: those were about what a check WALKS (a dropped denominator, an unreachable region); this is about which SLICE a sample represents. A measurement can cover its denominator honestly and still generalise from the wrong part.
ANOTHER INSTANCE, AND THIS ONE IS MINE: the first run of this enumeration reported 2 of 18 partners as absent from the graph, because IntAct names them hoxa9_mouse_gene and meis1_mouse_gene and my normaliser left HOXA9MOUSEGENE -- while gene-hoxa9 and gene-meis1 both exist. A normaliser that invents a gap is worse than no normaliser, and the fix moved the count from 16/18 to 18/18.
methodological sibling: that audit's denominator leaked, so this one prints sample + unsampled == pool and states the Wilson bound on its zero finding
A FOURTH INSTANCE OF THE INSTRUMENT'S OWN BLIND SPOT: this staleness detector matched only the literal string 'data/graph/nodes' and so missed every script using os.path.join(ROOT, 'data', 'graph', 'nodes') -- including the one analysis whose drift prompted the exercise. The first report said 13 of 22 where the truth is 25 of 36. A detector that finds the sloppy spelling and misses the idiomatic one undercounts precisely the careful code.