Skip to content

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

Description

@kevinschaper

Summary

A catch-all namespace in prefix_maps permanently shadows the more specific mappings in the fallback maps, so OBO ontologies without their own entry in the JSON-LD context contract to OBO:DDPHENO_0000001 instead of DDPHENO:0000001.

from kgx.prefix_manager import PrefixManager

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

Affects at least DDPHENO, FBbt, WBbt, EMAPA, ZFA, XAO, CHR, ZFS, OBA, FBdv — 165 OBO idspaces in total (see blast radius below). HP, MONDO, GO, RO, BFO, CHEBI, UBERON and everything else with its own context entry are unaffected.

Cause

kgx/utils/kgx_utils.py::contract() consults the default 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 context carries OBO -> http://purl.obolibrary.org/obo/, a catch-all that matches every OBO IRI. So curie_list is never empty for an OBO IRI, the fallback never runs, and obo_context's per-ontology mappings (DDPHENO -> …/obo/DDPHENO_, FBbt -> …/obo/FBbt_, …) are unreachable.

prefixcommons.contract_uri already resolves this ambiguity correctly when both mappings are in scope — it prefers the shortest CURIE, so DDPHENO:0000001 beats OBO:DDPHENO_0000001. The two tiers just never meet:

>>> contract_uri(uri, [biolink_context])          # ['OBO:DDPHENO_0000001']
>>> contract_uri(uri, [monarch_context, obo_context])  # ['DDPHENO:0000001']

The same shadowing applies to the MONARCH -> https://monarchinitiative.org/ catch-all, which hides BNODE, ISBN, ISBN-10, ISBN-13 and OMIA-breed.

Why this surfaced in 2.7.0

Not a new defect — 2.6.0 was accidentally immune. TsvSource.set_prefix_map was destructive, and the transformer calls source.set_prefix_map({}) when the caller supplies no map, so the map was wiped down to 5 entries:

kgx 2.6.0, after ObographSource.set_prefix_map({}):
  prefix_map size: 5   ['', 'MONARCH', 'MONARCH_NODE', 'biolink', 'owlstar']
  http://purl.obolibrary.org/obo/DDPHENO_0000001 -> DDPHENO:0000001

With nothing to match, every OBO IRI fell through to the fallback and got the right CURIE. #548 made set_prefix_map additive — correctly, since the wipe was also breaking identifiers.org/hgnc/ and friends — which restored the 631-entry map and with it the catch-all that had been sitting dormant.

Downstream, this untyped ~52k nodes in kg-phenio: 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 shipped as biolink:NamedThing since the 2026-06-03 release.

Proposed fix

Consult 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:

if prefix_maps:
    curie_list = contract_uri(uri, prefix_maps)
    if fallback:
        fallback_list = contract_uri(uri, default_curie_maps)
        if fallback_list:
            curie_list = _most_specific(uri, curie_list, fallback_list)
    if curie_list:
        curie = curie_list[0]

where _most_specific compares the length of the local part (shorter local part == longer IRI prefix consumed == more specific mapping). No OBO-specific special-casing, and fallback=False is untouched.

Alternatives considered:

  • Drop the OBO key from the loaded context. One line, fixes the eight ontologies that matter to phenio, and OBO: survives for both expansion and for contracting IRIs nothing more specific matches (OBO:fbbt#has_function_in), because monarch_context re-supplies the catch-all in the fallback tier. But it hardcodes a special case and leaves the identical MONARCH bug in place.
  • Bump the pinned context. config.yml still points at biolink-model/2.2.5/context.jsonld (628 entries, 2021). The current biolink-model context has 1182 entries, no OBO catch-all at all, and explicit FBbt/EMAPA/WBbt/ZFA/XAO/ZFS/FBdv/WBls mappings — it fixes this at the source and looks overdue for a tool emitting biolink 4.x-shaped data. Much larger blast radius; probably its own PR.

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 (see below)

Everything that resolves correctly today — HP:, MONDO:, RO:, BFO:, HGNC: via identifiers.org, all biolink: vocab terms — is unchanged.

Tests

Have a branch with the fix plus 20 test cases across tests/unit/test_kgx_utils.py (catch-all vs fallback, prefix_maps wins ties, fallback=False unchanged) and tests/unit/test_prefix_manager.py (the eight regressed idspaces, four controls, one relation IRI that must keep OBO:).

  • 11 of the new cases fail on master, all pass with the fix.
  • Full unit suite with the fix: 393 passed, 19 skipped, 1 failed — the failure is test_sink/test_jsonl_sink.py::test_write_jsonl2, which fails identically on unpatched master.

Happy to open the PR.

Related: contraction is not deterministic across runs

Found while measuring the above, independent of this bug and present in 2.6.0 too. prefixcommons.contract_uri collects candidates in a set and filters to the shortest; when two candidates tie on length, contract() takes [0] of an arbitrarily ordered set:

PYTHONHASHSEED=1  https://www.wikidata.org/wiki/Property:P31 -> WIKIDATA:Property:P31
PYTHONHASHSEED=2  https://www.wikidata.org/wiki/Property:P31 -> WIKIDATA_PROPERTY:P31

Same input, same version, different output between runs. Happy to file separately if you'd prefer it tracked on its own.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions