Skip to content

PR 6: Cross-source KG merge - #10

Merged
mengerj merged 1 commit into
mainfrom
pr6-cross-source-merge
Jun 30, 2026
Merged

PR 6: Cross-source KG merge#10
mengerj merged 1 commit into
mainfrom
pr6-cross-source-merge

Conversation

@mengerj

@mengerj mengerj commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Scope

PR 6 from the roadmap: merge per-study subgraphs from different sources (CELLxGENE + GEO) into one knowledge graph, deduped by ontology entity ID, so studies link through shared entity nodes. Builds the merger in graph/ (reserved for exactly this since PR 3). Offline-only.

What changed

  • src/parce/graph/merge.py (new, fully mypy-checked):
    • merge_subgraphs(Iterable[KnowledgeGraphOutput]) -> KnowledgeGraphOutput — biological entities dedup by ontology_id; study/dataset/sample nodes deduped by their own ID (cross-source IDs never collide, so this just makes the merge idempotent); all edges preserved, only exact (source, target, relation) duplicates collapse. Order-preserving (first-seen wins).
    • _merge_entity back-fills an unknown/empty entity name from another source; logs a WARNING on an entity_type clash (keeps first).
    • entity_provenance(graph) / cross_source_entities(graph) — derive which studies/sources touch each shared entity.
  • graph/__init__.py exports the new API.
  • main.py — both run and run_geo now persist merge_subgraphs(subgraphs) (was subgraphs[0] with a stale "PR 6" TODO).
  • tests/test_graph_merge.py (20 tests).

Key design decision — provenance is derived from edges, not stored on nodes

The roadmap said "provenance on edges." I read that as keep the edges intact — only entity nodes dedup, so every edge that pointed at a shared entity survives and still records who touched it. entity_provenance traces each entity edge to its owning study (the study itself for GEO study-level edges; via EXTRACTED_FROM for CELLxGENE dataset edges; via HAS_SAMPLE for sample edges) → StudyNode.source gives the repo.

Storing a study-list on BiologicalEntityNode instead would denormalize a relationship the edges already own (drift trap PR 2's "containment is edge-only" rejected) and mutate the frozen extra="forbid" schema. No schema change. Recorded in ARCHITECTURE §4.

Verification

All four CI gates pass locally, including the hermetic run with the repo-root .env moved aside (reproduces the CI runner):

  • ruff check . — clean
  • ruff format --check . — 49 files formatted
  • mypy src/parce — 29 source files, no issues, no new exemptions
  • pytest -m "not integration"188 passed, 13 integration deselected

The cross-source test runs the real CELLxGENE + GEO normalizers (fake resolvers/extractor, fully offline) on two studies sharing lung UBERON:0002048 and human NCBITaxon:9606, then asserts the shared entity is a single node touched by both sources while blood (CELLxGENE-only) stays single-source.

No dependency changes → uv.lock untouched.

Note

The CLI still runs one source per invocation; a single command that fetches multiple sources and merges across them is later orchestration (logical fit: PR 8 export). The merger + tests are the PR 6 deliverable. The PR 5 blocker stands: the live Azure extraction round-trip is still unverified (no AZURE_AI_PROJECT_ENDPOINT in the headless env).

🤖 Generated with Claude Code

Build the KG merger in graph/ that combines per-study subgraphs from
different sources into one knowledge graph, deduped by ontology entity ID.

- graph/merge.py: merge_subgraphs() dedups biological entities by
  ontology_id (back-filling unknown names, logging type clashes), keeps all
  study/dataset/sample nodes (deduped by their own ID -> idempotent), and
  preserves all edges (only exact duplicates collapse).
- Provenance is DERIVED from the retained edges, not stored on nodes:
  entity_provenance()/cross_source_entities() attribute each entity edge to
  its owning study (via EXTRACTED_FROM/HAS_SAMPLE) and that study's source.
  No schema change (respects the frozen extra="forbid" models and PR 2's
  edge-only principle).
- Wire merge_subgraphs into main.py (both paths) and drop the stale
  single-subgraph TODO.
- tests/test_graph_merge.py (20): cross-source case runs the real CELLxGENE
  + GEO normalizers offline on two studies sharing lung (UBERON:0002048) and
  human (NCBITaxon:9606); plus dedup/idempotency/edge-preservation/provenance
  unit tests.

Gates green incl. hermetic no-.env run: ruff check, ruff format --check,
mypy (29 files), 188 unit tests. No dep changes.

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