Yesterday’s dry run showed a CIViC re-run moving 885 endpoints onto other entities and writing 4,960 relations a second time. The approved fix is simple: before asking the cache, ClinGen, Node Normalizer or the name match anything, the resolver asks mods_kg_entity_source whether this source already stores the record’s external id, and if so it reuses that entity. Against prod, read only, the current CIViC nightly has 6,229 endpoints. 5,997 are already stored and now stay put, and 232 are new, carrying 440 relations. The same check found the bug’s older footprint: 375 RTX-KG2 ids are already stored on two entities, a Reactome pathway on both its own node and a GO process. The resolver refuses those rather than choosing, so an RTX-KG2 re-run waits on a decision about them.

The ClinGen question was “why do these three fail?”, and the registry answered it in its own error body: Unknown reference: ENST00000275493.2. All three CIViC expressions sit on Ensembl 75 transcript versions the registry does not hold, and it reports a missing reference as an internal error rather than a 4xx. EGFR L858R on the same old transcript fails identically, and the current version resolves, so the variant was never the problem. None of the three would ever get a CAID anyway: the two EGFR exon deletions are unregistered on the current RefSeq, and the LRP1B one is over the registry’s 10 kb limit. With identity sticky they are not asked again, and the one new HGVS hint in the nightly resolves cleanly.

The gene-taxon question had a surprise in it. Node Normalizer already returns a taxon for every gene, so recording one costs a backfill, not a new data source. With it, 1,013 of PharmGKB’s 1,086 synthetic genes would find a unique human gene. But GP-KG’s genes are not all human. DrugBank’s targets include bacterial gyrA and the penicillin-binding proteins, so “human only” would refuse matches that are probably right. What the match needs is the record’s own species, where the source states one.

The refresh question is where the data argued hardest. I counted the stored CIViC rows the current nightly no longer emits, and got 3,859 with main’s connector and 140 with the one in PR #725. #725 renamed CIViC’s stored predicates on prod this morning, and main’s connector still builds the old names. A replace run from the wrong checkout would have retracted 31% of CIViC as “withdrawn by the source”. So a refresh needs two things before it may retire anything: it must know which connector version wrote the rows it compares against, and what it retires must go somewhere it can be read back from. “Not emitted” is a fact about the stream, not about the source.

With the three decisions made, the history table went in, and its first real dry run caught a hole in my own guard. A ClinVar refresh against the live fetch from yesterday wanted to retire 1,633 of ClinVar’s 1,644 stored rows. ClinVar had not withdrawn them. The naming versions matched, but the connector walks only the patient’s variants by default, while the stored rows came from a walk over every linked variant. So a stream can differ from the stored rows in scope as well as in naming, and in both cases a missing row says something about the stream, not the source. Retirement now considers only rows whose subject the stream still covers. Rerun over the full scope, it inserts the 36 new ClinVar associations and 11 reclassifications, and retires exactly the 14 dropped classifications the issue had measured. The gene-taxon backfill is ready too: 121,736 of 124,385 gene entities get a species from Node Normalizer, 40,857 of them human. Neither has been applied.

By evening both had been applied, after the merge. The backfill wrote a species onto 121,736 gene entities. The ClinVar refresh then added 47 rows and retired 14 into the new history table, and the check I cared most about was the as-of rebuild. Taking the live rows from before the refresh and the history rows open at that moment gave back exactly the 1,644 rows I had backed up beforehand, so the knowledge-time design reconstructs a past graph rather than approximating one. The last piece was the generic ingest for ClinVar’s 24 relations on conditions it had never stored. On a real source this time, 836 endpoints stayed on their stored entities and only the 20 new ones were placed, which is the property this whole issue was about. Fifteen of those twenty are generic “gene-related disorder” labels with no trait id, so they landed as synthetic entities, the same as ClinVar’s existing unmapped conditions. Giving them real ids is someone else’s audit (#706), not something to improvise inside an ingest.