Skip to content

Stop a catch-all namespace from shadowing specific fallback mappings - #559

Open
kevinschaper wants to merge 1 commit into
masterfrom
fix/catch-all-prefix-shadowing
Open

Stop a catch-all namespace from shadowing specific fallback mappings#559
kevinschaper wants to merge 1 commit into
masterfrom
fix/catch-all-prefix-shadowing

Conversation

@kevinschaper

Copy link
Copy Markdown
Collaborator

Fixes #558.

Problem

contract() consults the default prefix maps only when prefix_maps returns zero matches:

curie_list = contract_uri(uri, prefix_maps)
if len(curie_list) == 0:
    if fallback:
        curie_list = contract_uri(uri, default_curie_maps)

The JSON-LD context carries OBO -> http://purl.obolibrary.org/obo/, a catch-all that matches every OBO IRI. So curie_list is never empty for one, the fallback never runs, and obo_context's per-ontology mappings are unreachable:

>>> PrefixManager().contract("http://purl.obolibrary.org/obo/DDPHENO_0000001")
'OBO:DDPHENO_0000001'      # expected 'DDPHENO:0000001'

165 OBO idspaces are affected, including DDPHENO, FBbt, WBbt, EMAPA, ZFA, XAO, CHR, ZFS, OBA and FBdv. The MONARCH -> https://monarchinitiative.org/ catch-all hides BNODE, ISBN, ISBN-10, ISBN-13 and OMIA-breed the same way.

Latent until 2.7.0: TsvSource.set_prefix_map used to be destructive, and the transformer calls source.set_prefix_map({}) when the caller supplies no map, so the map was wiped to five entries and every OBO IRI fell through to the fallback and contracted correctly. Making it additive (#548) restored the map, and with it the dormant catch-all.

Fix

Consider both tiers and keep the most specific match — the candidate that consumed the longest IRI prefix — with prefix_maps winning ties so a caller-supplied map stays canonical. No OBO-specific special-casing; fallback=False is untouched.

prefixcommons.contract_uri already prefers the shortest CURIE when both mappings are in scope, so this only changes which tiers get compared, not how a single tier resolves.

Blast radius

Probed every namespace in all three contexts (488 URIs), patched vs unpatched PrefixManager.contract:

change n
OBO: catch-all → specific idspace 165
MONARCH: catch-all → specific (BNODE, ISBN, ISBN-10, ISBN-13, OMIA-breed) 5
other 1 — WIKIDATA:Property:x vs WIKIDATA_PROPERTY:x, which is nondeterministic on master anyway

Everything that resolves correctly today is unchanged: HP:, MONDO:, GO:, RO:, BFO:, CHEBI:, UBERON:, HGNC: via identifiers.org, and all biolink: vocab terms. IRIs beneath the OBO namespace that no per-ontology mapping covers still contract to the catch-all — OBO:fbbt#has_function_in, OBO:go/extensions/ro_0002092 — because monarch_context supplies OBO in the fallback tier.

Test plan

  • 20 new cases: tests/unit/test_kgx_utils.py covers catch-all vs fallback, prefix_maps winning a tie against an equally specific fallback mapping, and fallback=False still yielding OBO:DDPHENO_0000001. tests/unit/test_prefix_manager.py covers the eight regressed idspaces, four controls, and a relation IRI that must keep OBO:.
  • 11 of the new cases fail on master, all pass with the fix.
  • Full unit suite: 393 passed, 19 skipped, 1 failed. The failure is test_sink/test_jsonl_sink.py::test_write_jsonl2, which fails identically on unpatched master — pre-existing and unrelated.

Downstream

This is what untyped ~52k nodes in kg-phenio from the 2026-06-03 release on: its transform looks node categories up by CURIE prefix, and OBO isn't an ontology, so DDPHENO/FBbt/EMAPA/WBbt/ZFA/XAO/CHR/ZFS terms have been shipping as biolink:NamedThing.

https://claude.ai/code/session_01BzdpULg2jTEP8wN3jBNzrN

contract() consulted the default prefix maps only when prefix_maps returned
zero matches. The JSON-LD context carries OBO -> http://purl.obolibrary.org/obo/,
a catch-all that matches every OBO IRI, so the fallback never ran and
obo_context's per-ontology mappings were unreachable: DDPHENO, FBbt, WBbt,
EMAPA, ZFA, XAO, CHR, ZFS, OBA and FBdv all contracted to OBO:DDPHENO_0000001
rather than DDPHENO:0000001. The MONARCH catch-all hid BNODE, ISBN, ISBN-10,
ISBN-13 and OMIA-breed the same way.

Consider both tiers and keep the most specific match -- the candidate that
consumed the longest IRI prefix -- with prefix_maps winning ties so a
caller-supplied map stays canonical. fallback=False is unchanged.

This was latent until 2.7.0. TsvSource.set_prefix_map used to be destructive,
so the transformer's set_prefix_map({}) wiped the map down to five entries and
every OBO IRI fell through to the fallback and contracted correctly. Making it
additive (#548) restored the map, and with it the dormant catch-all.

Closes #558.

Claude-Session: https://claude.ai/code/session_01BzdpULg2jTEP8wN3jBNzrN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Catch-all namespace in prefix_maps shadows specific mappings in the fallback maps (OBO:DDPHENO_0000001 instead of DDPHENO:0000001)

1 participant