Skip to content

feat(ogar-obo): agnostic xref→baked-address crosswalk adapter - #231

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/ogar-obo-crosswalk
Aug 1, 2026
Merged

feat(ogar-obo): agnostic xref→baked-address crosswalk adapter#231
AdaWorldAPI merged 1 commit into
mainfrom
claude/ogar-obo-crosswalk

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

Agnostic xref → baked-address crosswalk (data-as-config)

Adds ogar-obo::crosswalk — one reverse-index mechanism that resolves any
external code (ICD · FMA · MeSH · SNOMED · OMIM · Orphanet · …) to its baked
OBO address
(classid + 24-bit identity), built from the parsed Bake's
preserved xrefs. No per-consumer hand-rolled table; the same adapter medcare-rs
uses for ICD→MONDO and FMA/DICOM→Uberon, odoo-rs can later use for its own
account codes once its vocabulary is baked.

What it gives consumers

  • Crosswalk::from_bake(&bake) — O(1) reverse index over bake.xrefs.
  • resolve(source, code) — agnostic lookup; resolve_fma(id) — glass-patient
    helix-zone → Uberon; resolve_icd10(code)2-hop GM→WHO rollup (measured:
    MONDO carries the WHO axis densely but no ICD-10-GM, so E11.9 → E11).
  • ANATOMY_CLASSID / DISEASE_CLASSID constants for confirm-the-lane checks.

Discipline

  • Built from the parsed bake, not a retyped literal table (data-as-config maxim).
  • Public OGAR connective tissue — no consumer (medcare-rs) reference; corpus is
    OBO-public.
  • 3 tests (reverse-resolve across sources, GM→WHO rollup incl. can-stay-silent,
    predicate import), clippy-clean.

🤖 Generated with Claude Code

Data-as-config, not a hand-rolled table: a reverse index over the bake's
preserved xrefs resolves ANY external code (ICD · FMA · MeSH · SNOMED · OMIM ·
Orphanet · …) to its baked OBO address (classid + 24-bit identity). One
mechanism, every consumer — medcare-rs resolves ICD→MONDO and FMA/DICOM→Uberon
through it; odoo-rs can resolve its own codes through the same adapter once its
vocabulary is baked (OGAR agnostic connective tissue, never a consumer mirror).

resolve_icd10 carries the 2-hop rollup the MEASURED MONDO coverage requires:
MONDO xrefs the WHO ICD axis but carries no ICD-10-GM, so a German billing code
(E11.9) rolls up its hierarchy to the baked WHO stem (E11) — pure string rollup,
no separate GM table. Honest boundary: an unbaked code returns None (can-stay-
silent test proves it).

reason.rs (the ELK EL-completion port) stays the ORACLE — a reference algorithm
for validating the deterministic path and measuring activation, not production
data. 3 new tests (any-source resolve, ICD rollup, silence); clippy-clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03dd9ade16

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

pub fn resolve_icd10(&self, code: &str) -> Option<Address> {
let mut c = code.trim().to_string();
loop {
if let Some(a) = self.resolve(&XrefSource::Icd, &c) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep ICD editions distinct during ICD-10 resolution

resolve_icd10 queries the shared XrefSource::Icd bucket, while XrefSource::from_prefix maps ICD-9, ICD-10, and ICD-10-CM into that same variant. When editions reuse a literal code—for example, V10 exists in both ICD-9 and ICD-10—the index cannot tell which edition supplied the xref and may return the ICD-9-linked disease for an ICD-10 lookup. Preserve the ICD edition in the indexed source before exposing an ICD-10-specific resolver.

Useful? React with 👍 / 👎.

let mut idx = HashMap::with_capacity(bake.xrefs.len());
for (id, x) in &bake.xrefs {
let addr = Address {
classid: id.namespace().render_classid(0x0000),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return the classid actually written by the bake

When bake was called with a nonzero app_prefix, every emitted row uses that prefix in its classid, but this constructs the returned address with 0x0000. The resulting Address therefore does not identify any row in that bake even though from_bake reports a successful resolution; derive the classid from the corresponding baked row or retain the bake's render prefix.

Useful? React with 👍 / 👎.

other => (tag(other), x.id.clone()),
};
// first writer wins → stable, deterministic (bake.xrefs is sorted by node)
idx.entry(key).or_insert(addr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve every target of non-unique xrefs

When two baked terms carry the same external xref—a valid situation for broad ontology mappings—or_insert silently discards every target after the first. Resolution then depends on the lowest sorted TermId rather than the requested concept's semantics, potentially returning a disease where the same UMLS identifier also maps to a phenotype. Store all matching addresses or make ambiguity explicit instead of selecting an arbitrary target.

Useful? React with 👍 / 👎.

@AdaWorldAPI
AdaWorldAPI merged commit f4892a6 into main Aug 1, 2026
2 checks passed
AdaWorldAPI pushed a commit that referenced this pull request Aug 2, 2026
…_of / phenotypes_of)

`saturate` computes the aggregate EL closure; a consumer resolving ONE entity
(a disease → its anatomy site + phenotypes, e.g. medcare-first-thought's
ontology grounding) needs the per-term walk. These operate directly on
`parse_obo`'s `id -> node` map, applying the same is_a-saturation +
existential-role-propagation rules to a single subject:

  * `ancestors` — transitive is_a closure of a term (self excluded, cycle-safe:
    the start id is pre-seeded into `seen` so a self/loop parent never re-emits
    the query term), deterministic order, depth-capped.
  * `related_via_ancestry` — every `pred` target reachable from the term or its
    is_a ancestors (the subject inherits its ancestors' existential edges).
  * `anatomy_of` / `phenotypes_of` — the Uberon (Mondo→Uberon = HasLocation) /
    HPO (Mondo→Hpo = HasPhenotype) conveniences over that walk, matching the
    crate's namespace-pair `classify`.

Additive over the merged #231 crosswalk/saturate work; no change to the bake,
crosswalk, concept-ids, or `saturate`. 1 new unit test (ancestry + inherited
anatomy + own phenotype + self-exclusion).

Generated by [Claude Code](https://claude.com/claude-code)
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.

2 participants