Richard opened with a fair question: hadn’t we already federated every edge under Biolink? We had decided to, on #86 in May. A manifest of “bridges” existed, declaring which source predicates mean the same thing. But nothing outside its own tests imported it, and the training export still sent only the mapped Biolink predicate. He also pushed on my recommendation for decision card D-Q4-01. Qualifying every relation by its source, he said, keeps distinctions but stops like meanings merging across graphs, and merging them was the point of an umbrella. He was right. B put provenance where meaning belonged. The revision, B′, makes the bridge the unit of meaning and treats the source qualifier as the honest default for anything no one has yet said is equivalent to something else.

The first thing I checked was whether the manifest described the data at all. It didn’t. Seventeen of its 32 members (outside RTX-KG2) named no row in mods_kg_relation, because the seed had been written from predicate_mapper.py’s dictionary keys rather than from what the ingest stored. CIViC is stored as Predictive_Sensitivity/Response, not predictive_sensitivity; OncoKB as oncokb_LEVEL_1, not sensitive. The enzyme, carrier and transporter bridges, the ones the whole federation decision was about, pointed at rows PrimeKG’s connector never wrote. So Phase 1 starts from a snapshot of what is stored: 175 pairs, 19,478,158 rows, from a 19-second read-only GROUP BY. The test now fails any member that names nothing in it. Run against the old seed, that test reports all seventeen.

Verifying the Biolink side meant fetching the 4.4.2 schema, and fifteen of the predicate strings we store turn out not to be slots in it, about 313K rows. Some have real counterparts under other names: contraindicated in, associated with sensitivity to. has target exists but is declared disease to gene. None of that changes what the model sees, but it matters before anyone calls the graph Biolink.

The export change is opt-in, so Arm A still reproduces. On Arm A’s exact settings, 29 relation types become 82. Two things came out of building it that would otherwise have bitten Run #2. Thirteen of the new types have five rows or fewer, and the split guaranteed train coverage for entities but not for relations, so a type could land only in test and be scored with an untrained relation. Bridged splits now move one group of each into train. The second was in the eval: it excludes biolink:interacts_with from its headline average by name. A bridged export renames drug–drug and protein–protein interactions, so they would quietly have rejoined that average. That one is logged on #592 rather than fixed here, because it belongs to the eval lane.

Two smaller things. I almost let protein–protein interactions keep the bare name biolink:interacts_with once drug–drug moved into its own bridge. A test caught it: a familiar name must mean the same rows it meant before, or a reader who knows the old name is misled. And I ran pytest twice before reading this repo’s rule that DATABASE_URL must be pinned, because the .env walk-up resolves it to production. Neither run opened a connection, since nothing they collected requested the database. Every run after that used a throwaway database on a dedicated local server, dropped at the end.

What remains is Richard’s call: a table of seven proposed merges on #628, with my read on each, and the granularity questions it settles. The ClinVar question, 24 classes as 24 types, is now a matter of counts, not of guesses.