Activate glom's MONDO close-match guard, with concord-reader symmetry tests#888
Open
gaurav wants to merge 13 commits into
Open
Activate glom's MONDO close-match guard, with concord-reader symmetry tests#888gaurav wants to merge 13 commits into
gaurav wants to merge 13 commits into
Conversation
build_compendium() read the MONDO_close concord with close_mondos[x[0]].add(x[1]), keying each MONDO subject to column 2 (the predicate, "oio:closeMatch") instead of column 3 (the close-match object). No clique ever contains the literal predicate string, so glom()'s `close=` guard never matched and has been a silent no-op on every build, letting close (but not exact) matches collapse into exact MONDO cliques. Extract the parse into load_close_mondos(), key on x[2] (matching the (stuff[0], stuff[2]) parsing every other concord in build_compendium already uses), skip blank lines, and raise RuntimeError on any row that is not exactly 3 columns. Activating the guard is a deliberate, SME-reviewable change to disease clique merging: close-but-not-exact matches (notably ~1,219 MEDDRA codes on an add-mpo-base build; the exact count on a pure-main build may differ) no longer merge into exact MONDO cliques. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover load_close_mondos (keys on the object column, skips blanks, rejects malformed rows) and glom's close= guard end-to-end: a close-match MEDDRA term must not collapse into the exact MONDO clique. A companion test documents the original bug -- a predicate-keyed close dict leaves the guard a no-op, so the merge goes through. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 2, 2026
Closed
git blame traces close_mondos[x[0]].add(x[1]) to commit 93a6c89 (2021-05-25, "disease/phenotype") -- the very commit that introduced glom's MONDO close-match guard. The parent commit had no close logic at all, and MONDO_close was already a 3-column concord then, yet the reader keyed on the predicate (x[1]) while the regular-concord loop in the same function correctly keyed on subject+object (stuff[0]/stuff[2]). So the guard has been a silent no-op for its entire ~4-year existence, and no disease build has ever actually enforced it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…parse The regular-concord reader lived inline in build_compendium and keyed on subject+object (stuff[0]/stuff[2]); the close-match reader was a separate parse that keyed on the predicate (x[1]) -- the asymmetry that silently disabled glom's close= guard. Pull the regular reader into a named, documented load_concord_pairs() helper alongside load_close_mondos() so the shared "subject + object, skip the predicate" convention lives in one testable place per reader. Also fixes the malformed RuntimeError in the old inline loop, which passed a tuple of positional args instead of a formatted message, now an f-string matching load_close_mondos. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parametrized tests assert every disease concord reader (load_concord_pairs and load_close_mondos) surfaces the subject (column 1) and object (column 3) and never the predicate (column 2), and that each raises RuntimeError on a non-3-column row. This pins the shared column convention whose violation disabled the close-match guard, so a new reader (or a regression of an old one) that reads the predicate column fails loudly. Also covers load_concord_pairs dropping bad_pairs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gaurav
added a commit
that referenced
this pull request
Jul 2, 2026
The NOTE in compute_cliques_for_impact_report and disjointness.md's "Related work" pointed the deferred close-match-guard fix at the old combined PR #883. That PR was split: the babel-clique-diff tool is now #885 and the close-match guard activation (keying glom's close= map on the object, x[2], rather than the predicate, x[1]) is now #888 (fix-mondo-close-guard). Point both references at the concrete PRs so they don't go stale. No behavior change -- this branch still preserves the predicate-keyed no-op that #888 will fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes how the disease/phenotype pipeline reads the MONDO_close concord so that glom()’s MONDO close-match guard is actually enforced (it was previously a silent no-op due to keying on the predicate column instead of the object column). It also extracts concord parsing into named helpers and adds regression + symmetry tests to prevent future column-index drift.
Changes:
- Fix
MONDO_closeparsing by keying MONDO subjects to close-match objects (column 3), with blank-line skipping and strict 3-column validation. - Extract regular concord parsing into
load_concord_pairs()with strict 3-column validation and bad-pair filtering. - Add unit tests covering the close-match guard end-to-end and parametrized “reader column symmetry” invariants across concord readers.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/createcompendia/diseasephenotype.py |
Adds load_close_mondos() / load_concord_pairs() helpers and wires them into build_compendium() so the MONDO close-match guard can trigger correctly. |
tests/createcompendia/test_diseasephenotype.py |
Adds regression tests proving the guard blocks close-match merges and enforces subject/object column symmetry across concord readers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
PR #886 (MP ingestion) refactored build_compendium() into compute_cliques_for_impact_report() and, in doing so, deliberately carried the MONDO close-match bug forward with a NOTE deferring the fix to this PR. Resolve by keeping all of #886's structure and applying this PR's fix inside it: - Keep compute_cliques_for_impact_report()'s excluded_sources handling, MONDO_close basename discovery, DISEASE_UNIQUE_PREFIXES, OVERUSE_FILTERED_CONCORDS (now including MP), the post-glom HP/MP split, and the logger calls. - Route its two inline parse loops through this branch's load_close_mondos() and load_concord_pairs() helpers, so the close-match reader keys on the object (column 3), not the predicate. - Drop the NOTE deferring the fix; this is that follow-up. - Preserve #886's None-guard: an absent MONDO_close (impact-report CLI, or --source MONDO) still yields an empty close map rather than opening a missing file. Both sides' tests are retained; the test file gains 164 lines and loses none. Reword the now-stale "Related work" bullet in docs/sources/MP/disjointness.md, which described this fix as still deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The full narrative of the mis-keyed close-match reader lives in load_close_mondos; load_concord_pairs now points there and cites the symmetry tests instead of re-telling it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Points readers at the full write-up of the four-year silent no-op in glom's MONDO close-match guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build-vs-build diff isolating the close-match guard activation (#888, issue #912): both compendia built from identical cached disease intermediates, differing only in diseasephenotype.py keying close_mondos on the object (x[2]) vs the predicate (x[1]). Result: 1,218 MEDDRA identifiers drop from Disease.txt (the close-match terms that reached MONDO cliques via the UMLS concord), matching the ~1,219 figure from #883. PhenotypicFeature.txt is essentially unaffected. Indexes the new docs/sources/MONDO entry in docs/sources/README.md and CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 1,218 dropped identifiers are all MEDDRA (low-stakes downstream). The changes that need an SME's eye are the cliques that actually split, moving a real non-MEDDRA identifier out of a MONDO clique. Because glom() drops the whole pairwise concord link when the close-match guard fires, a legitimate UMLS/DOID exact-equivalent riding in the same group as a close-matched MEDDRA is ejected as collateral damage -- hence the many splits pairing a MONDO term with an identically-labelled UMLS/DOID concept (likely regressions), alongside genuine improvements. Lists all 23 in the README and a compact split-cliques.csv with an sme_assessment column. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Marks the two user-confirmed cases (MONDO:0000889 regression, MONDO:0009297 improvement) and the eight identical-label splits as suspected regressions; the remaining 13 (a distinct close concept was separated) stay blank for a genuine SME call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the mechanism (guard blocks the winning MONDO merge; MONDO being a unique_prefix hands the ejected group to a sibling MONDO clique it also had a concord path to) with the oligoastrocytoma -> mixed glioma trace, so an SME understands why a real identifier can land under a different MONDO term. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The guard also splits 14 disease cliques into brand-new PhenotypicFeature cliques: the ejected group carries an HP term and no MONDO, so it re-types as biolink:PhenotypicFeature (these are the +13 phenotype additions the clique-diff tool doesn't surface as rows, since it iterates before-cliques). Adds them to split-cliques.csv (now 37 rows: 23 disease + 14 phenotype) and documents them, noting that a near-identical MONDO/HP label here does NOT imply a regression -- disease and phenotype are meant to live in separate compendia, so the separation is often correct typing; the SME question is whether the moved UMLS/SNOMEDCT belong on the phenotype side. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Isolates the MONDO close-match guard fix from #883 onto a clean,
main-based branch so it can be reviewed on its own. #883 also bundled the babel-clique-diff tool (already merged via #885) and the add-mpo (#790) work; neither is included here.The bug
compute_cliques_for_impact_report()(whichbuild_compendium()calls to do the real production build) read theMONDO_closeconcord withclose_mondos[x[0]].add(x[1]), keying each MONDO subject to column 2 — the predicate,oio:closeMatch— instead of column 3, the close-match object CURIE.Full write-up of the bug and its history: #912.
glom()'sclose=guard (src/babel_utils.py) rejects a merge only when a MONDO CURIE's recorded close-match object also turns up in the same newly-merged set. Because the recorded value was the literal predicate string (which never appears in any clique), the guard never fired — a silent no-op on every build. Close-but-not-exact matches were free to collapse into exact MONDO cliques.How long has this been broken?
git blametracesclose_mondos[x[0]].add(x[1])to commit93a6c898("disease/phenotype", 2021-05-25) — the very same commit that introduced the close-match guard. Its parent had no close-match logic at all, andMONDO_closewas already a 3-column concord (subject predicate object) written byubergraph.build_sets()from that commit. So this was never a regression from a later format change: the reader keyed on the predicate (x[1]) from day one, while the regular-concord loop in the same function already keyed on subject + object (stuff[0]/stuff[2]).The guard has therefore been a silent no-op for its entire ~4-year existence — no disease build has ever actually enforced it, and close-but-not-exact matches have been collapsing into exact MONDO cliques the whole time.
Why it went unnoticed / how to prevent the next one
x[1]for the close reader vsstuff[0]/stuff[2]for every other concord) — a symmetry a reviewer or a linter can't easily see, and no test exercised the guard end-to-end.load_concord_pairs()(regular concords) andload_close_mondos()(MONDO_close) — each with a docstring pinning the column semantics, and a set of concord-reader column-symmetry tests asserts the shared convention across all of them (see the Tests section). Any future concord reader should likewise route through a shared, tested parse rather than re-deriving column indices inline.The fix
Extract the parse into
load_close_mondos()and key onx[2], matching the(stuff[0], stuff[2])parsing every other concord incompute_cliques_for_impact_reportalready uses. Also skip blank lines and raiseRuntimeErroron any row that is not exactly 3 columns, sox[2]is always safe and a malformed file fails loudly.To remove the asymmetry at its root, the regular-concord reader that lived inline in
compute_cliques_for_impact_reportis also pulled out into a namedload_concord_pairs()helper, so both disease concord readers now share one documented "subject + object, skip the predicate" parse (this also fixes a pre-existing malformedRuntimeErrorin that inline loop that passed a tuple of args instead of a message).No change to
src/babel_utils.py— the guard already worked; the bug was in what was fed to it.Impact (SME review)
Activating the guard is a deliberate change to disease clique merging: close-but-not-exact matches no longer merge into exact MONDO cliques.
A build-vs-build clique diff isolating exactly this change (both sides built from identical cached disease intermediates, only
src/createcompendia/diseasephenotype.pydiffers) is committed underdocs/sources/MONDO/mondo-close-match-guard/:Disease.txt, every one a MEDDRA identifier — the close-match MEDDRA terms that were reaching MONDO disease cliques transitively through the UMLS concord, now pulled back out.Disease.txt, 72 moved toPhenotypicFeature.txt; net clique count +20.PhenotypicFeature.txt: +13 cliques (14 new HP-led cliques formed from groups shed by disease cliques, 1 pre-existing absorbed); 0 dropped members. The diff tool emits only 1 row here because these are brand-new cliques with no before-counterpart.The 1,218 figure matches the ~1,219 MEDDRA drop from the earlier combined analysis in #883.
Cliques that actually split — needs SME review
MEDDRA is barely used downstream, so the 1,218 dropped MEDDRA identifiers are low-stakes. The changes worth an SME's attention are the 37 cliques that structurally split, moving a real (non-MEDDRA) identifier out of a MONDO clique. The full list with members is in
docs/sources/MONDO/mondo-close-match-guard/README.mdandsplit-cliques.csv. Three kinds: 19 eject a piece into a brand-newDisease.txtclique, 4 re-attach it to a different existing MONDO clique, and 14 shed a group that re-types into a brand-newPhenotypicFeature.txtclique (these are the +13 phenotype additions the diff tool doesn't emit as rows, since it iterates before-cliques).Mechanism: when the guard fires,
glom()drops the entire pairwise concord link, not just the offending MEDDRA term. So a legitimate UMLS/DOID exact-equivalent that happens to ride in the same group as a close-matched MEDDRA is ejected as collateral damage. That is why many splits pair a MONDO term with an identically-labelled UMLS/DOID concept — a strong regression signal:MONDO:0000889"haemophilus meningitis" splitting offUMLS:C0276028"Haemophilus influenzae meningitis"; the twomultiple endocrine neoplasiaDOID splits (MONDO:0007540→DOID:10017,MONDO:0017169→DOID:3125);toxic oil syndrome,argyria,pyomyositis,secondary polycythemia,macular corneal dystrophy,Spasmus nutanseach splitting off their identically-named UMLS concept.MONDO:0009297"familial renal glucosuria" correctly no longer subsuming the broaderUMLS:C0017980"Glycosuria, Renal".The 4 re-attach cases land the ejected group in a different, already-existing MONDO clique. That happens because
MONDOis aunique_prefix(one MONDO per clique): when a group is linked to two MONDO terms at once, only one wins, and blocking the winning path hands the group to the runner-up. Example:MONDO:0016702"oligoastrocytoma" →MONDO:0003268"mixed glioma" — aUMLS:C0280793group (incl.NCIT:C129323"Oligoastrocytoma, NOS") linked to both; the guard blocks the oligoastrocytoma path, so it lands under mixed glioma instead. These re-attachments can also be regressions (here an oligoastrocytoma NCIT term now sits under mixed glioma). See the README for the full trace.The 14 phenotype cases eject a group carrying an HP term and no MONDO, so it re-types as
biolink:PhenotypicFeature. Here a near-identical MONDO↔HP label does not imply a regression — Babel deliberately keeps disease and phenotype in separate compendia, so pullingHP:0001543"Gastroschisis" out ofMONDO:0009264"gastroschisis" is arguably correct typing; the SME question is whether the UMLS/SNOMEDCT that moved with the HP belong on the phenotype side.split-cliques.csvcarries a first-passsme_assessmentfor the 23 disease splits: the 2 confirmed cases, the 8 identical-label splits markedsuspected regression, and the remaining 13 left blank as genuine SME calls. The 14 phenotype splits are left blank.Tests
tests/createcompendia/test_diseasephenotype.pyadds two groups of tests:MONDO close-match guard —
load_close_mondoskeys on the object column, skips blanks, and rejects malformed rows; glom'sclose=guard blocks a close-match MEDDRA term from collapsing into the exact MONDO clique end-to-end; and a companion test documents the original bug (a predicate-keyed close dict leaves the guard a no-op).Concord-reader column symmetry — parametrized over every disease concord reader (
load_concord_pairsandload_close_mondos), asserting each surfaces the subject (column 1) and object (column 3), never the predicate (column 2), and raisesRuntimeErroron a non-3-column row. This pins the shared column convention whose violation silently disabled the guard, so a new reader (or a regression of an old one) that reads the predicate column fails loudly. Adding a future reader to theCONCORD_READERSlist automatically holds it to the same invariant.uv run pytest tests/createcompendia/test_diseasephenotype.py -q— passesuv run pytest -m unit -q— 251 passeduv run ruff check/ruff format --check— clean🤖 Generated with Claude Code