Skip to content

PR 4: Ontology resolution stage + CELLxGENE organism wiring - #7

Merged
mengerj merged 1 commit into
mainfrom
pr4-ontology-resolver
Jun 25, 2026
Merged

PR 4: Ontology resolution stage + CELLxGENE organism wiring#7
mengerj merged 1 commit into
mainfrom
pr4-ontology-resolver

Conversation

@mengerj

@mengerj mengerj commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Scope

Implements PR 4 — Ontology resolver from the roadmap: the shared
parce/ontology/ resolution stage plus organism wiring into the CELLxGENE
normalizer. Every source's free-text design fields now resolve to the same
ontology IDs — the precondition for cross-source linking (ARCHITECTURE §5).

Split note: the original PR 4 also bundled the canonical-schema change
(replace free-text modality with an EFO assay term + a stored
molecular_layer field). That migration is now PR 4b (new ▶ Next up). The
molecular_layer derivation is implemented here; only its storage on the nodes
is deferred.

What's in it

  • ontology/registry.pyFacet → ontology constant (EFO/OBI/PSI-MS, UBERON,
    MONDO, NCBITaxon, ChEBI, EDAM). Pins ontologies, not term IDs.
  • ontology/ols.pyOlsClient over OLS4 REST (search + is-a ancestors),
    injectable HTTP getter (offline tests), retry-wrapped via sources._retry.
  • ontology/cache.pyResolutionCache: on-disk JSON, atomic writes,
    negative results cached so unresolvable strings aren't re-queried.
  • ontology/layers.pyderive_molecular_layer(): EFO ancestor-label
    anchors, most-specific-first, no-anchor default UNKNOWN. Anchors are
    provisional (unvalidated against live EFO).
  • ontology/resolver.pyOntologyResolver: cache → OLS exact-then-fuzzy →
    optional LLM fallback hook (default None, keeps ontology free of any
    agent/Azure import; PR 5 wires the extraction agent in).
  • models/graph_schema.py — adds the MolecularLayer enum (consumed by PR 4b).
  • normalize/cellxgene.py — grounds the organism string to NCBITaxon at runtime,
    replacing the hardcoded _ORGANISM_ONTOLOGY map. Tissue/disease/assay keep the
    IDs Census already ships. Unresolved organisms are skipped, not emitted
    ungrounded.

Decisions

  • OLS4-only for now; text2term/Zooma deferred to GEO (PR 5) where messy
    characteristics_ch1 strings appear (resolves an ARCHITECTURE §7 open question).
  • LLM fallback is a pluggable callable, not a dependency — dependency
    direction preserved.
  • Anchors keyed by EFO label, not ID (honours "pin ontologies/anchors, not
    IDs"). A marked live-OLS integration test is the validation harness; PR 4b must
    tighten the labels first.

Verification

All four CI gates pass locally, hermetic (worktree has no .env, no network):

  • ruff check . — clean
  • ruff format --check . — 40 files formatted
  • mypy src/parce — clean, 24 files, no new exemptions
  • pytest -m "not integration"121 passed, 9 deselected

New offline unit suites: registry, cache, OLS (fake HTTP), layers, resolver
(fake client + real cache on tmp_path). Live-OLS checks are
@pytest.mark.integration (excluded from CI). No dependency changes.

🤖 Generated with Claude Code

Add the shared `parce/ontology/` stage that grounds free-text design fields to
ontology term IDs, the precondition for cross-source linking (ARCHITECTURE §5):

- registry.py  — Facet → ontology constant (EFO/OBI/PSI-MS, UBERON, MONDO,
  NCBITaxon, ChEBI, EDAM); pins ontologies, never IDs.
- ols.py       — OlsClient over OLS4 REST (search + is-a ancestors), injectable
  HTTP getter, retry-wrapped via sources._retry; CURIE→IRI + double-encoding.
- cache.py     — ResolutionCache: on-disk JSON, atomic, caches negative results.
- layers.py    — derive_molecular_layer(): EFO ancestor-label anchors,
  most-specific-first, UNKNOWN default. Anchors PROVISIONAL (see integration test).
- resolver.py  — OntologyResolver: cache → OLS exact-then-fuzzy → optional LLM
  fallback hook (default None, keeps ontology free of agent/Azure).

Wire into CellxgeneNormalizer: organism string → NCBITaxon at runtime, replacing
the hardcoded _ORGANISM_ONTOLOGY map. Unresolved organisms are skipped, not
emitted ungrounded. Add MolecularLayer enum to models (consumed by PR 4b).

Decisions: OLS4-only for now (text2term/Zooma deferred to GEO/PR 5); LLM fallback
pluggable; anchors keyed by label not ID. Split the original PR 4: the schema
change (store EFO assay term + molecular_layer) is now PR 4b.

Gates: ruff, ruff format, mypy (no new exemptions), 121 unit tests — all green
and hermetic (no .env, no network). No dependency changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mengerj
mengerj marked this pull request as ready for review June 25, 2026 06:11
@mengerj
mengerj merged commit 35a28ce into main Jun 25, 2026
2 checks passed
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.

1 participant