Add EMAPA as an anatomy source - #781
Conversation
|
Heads-up after rebasing onto the updated
This
The CLAUDE.md update in #742 (commit |
- CLAUDE.md: the registration table is `PIPELINE_CONFIG`, not the old `SEMANTIC_TYPE_CONFIG`; and the EMAPA worked example lives in PR #781. - src/model/source.py: drop the obsolete rebase note pointing at the old `SEMANTIC_TYPE_CONFIG` / `--semantic-types` naming, which the merged CLI no longer uses (it is `PIPELINE_CONFIG` / `diffs_by_pipeline` / `--pipelines`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the new-source onboarding guide into #742 so it is reviewed alongside the source-impact report tool it documents (and to resolve the dangling reference at CLAUDE.md, which already linked to docs/AddingNewSources.md). The guide was written in the stacked EMAPA PR (#781), where it was authored against an earlier revision of this tool; reconciled here to match the current code: - SEMANTIC_TYPE_CONFIG → PIPELINE_CONFIG (renamed in this PR). - Survival columns are judged on the clique's assigned biolink type, not each identifier's own declared type (the review changed this). - new-cliques/new-xrefs first column is `pipeline`, not `semantic_type`. - ExpandedClique field is `preexisting_source_curies`, not `promoted_source_curies`. - new-xrefs `status` value is `from_other_source`, not `made_real`. - CURIE→URL example uses the in-repo get_biolink_prefix_map() helper rather than a manual urllib fetch of the prefix map. EMAPA worked-example mentions remain as forward-references to #781. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt #781's restructure of "Adding a new data source": replace the inline how-to (now covered by docs/AddingNewSources.md) with a pointer plus the two gotchas the report exists to catch (type every ids file; generate + commit the report). Keep PIPELINE_CONFIG (not the pre-rename SEMANTIC_TYPE_CONFIG). Add the Snakemake --unlock LockException tip and the per-target sizing note, and a Documentation convention for linking CURIEs to OBO PURLs that points at the existing get_biolink_prefix_map() helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
General (non-EMAPA) running guidance from #781: a per-target memory/time breakdown (the README's 500 GB applies only to the heaviest builds; anatomy builds on a laptop in ~25 min) and a "Common build issues" list (stale Snakemake lock, UberGraph transient failures, unset UMLS_API_KEY, --rerun-incomplete). Placed as subsections of "Building Compendia" so the cross-link to AddingNewSources.md resolves within this PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y/cliques refactor) (#742) Adds the **source-impact report** tool used to evaluate how a new identifier source changes Babel's cliques, plus the anatomy/cliques refactor it depends on. Split out from the EMAPA work (#781) so the tooling can be reviewed independently of any single source. The shared clique-diffing and source-discovery primitives it consumes have already been merged (via #877). Given a source's intermediate `ids`/`concords` files, the tool discovers every pipeline the source touches, runs a synthetic re-glom **with and without** the source, diffs the resulting cliques into **pure-new / expanded / merged** buckets, predicts which identifiers survive the Biolink Model's per-class prefix filtering, and renders a markdown + JSON report plus deterministically-sorted CSV/TSV detail files for SME review. ## What's here - **New `source-impact-report` CLI** (`src/cli/source_impact_report.py`): pipeline discovery, synthetic before/after diff, downstream Biolink prefix-survival prediction, and an optional `--mode both --remote-url <build>` comparison against a previous build's compendia. Sample sizes are configurable via `--sample-limit`, `--pure-new-sample-limit`, and `--expanded-sample-limit`. The label-enrichment prefix list is derived from `config.yaml`'s `<pipeline>_prefixes`, so adding a new source to config automatically enriches the report samples. - **New report renderers** (`src/reports/source_impact.py`, `src/reports/source_impact_details.py`): the human-readable markdown/JSON and the full `new-cliques.csv` / `modified-cliques.{csv,json}` / `new-xrefs.tsv` detail files. The detail files use `preexisting` (not `promoted`) to describe source CURIEs that were already present in cliques via another source's xref. - **New `src/model/cliques.py`**: the shared `glom_from_files()` clique-building skeleton, parameterized by `concord_pair_filter` / `overused_xref_remover` / `glom_kwargs` hooks. - **`anatomy.py` refactor**: both the real build and the impact report now route through `compute_cliques_for_impact_report` → `cliques.glom_from_files`, so the report's re-glom provably matches the real build. Clique typing is factored into `classify_anatomy_clique`, and `anatomy_unique_prefixes` moves to `config.yaml`. - **Snakemake convenience rule** `report_source_impact` (`src/snakefiles/reports.snakefile`) wrapping the CLI, with its roots derived from config so they stay consistent. - **Packaging**: registers the `source-impact-report` entry point and packages `src` in `pyproject.toml`. - **Tests** (all `unit`-marked, offline): `tests/test_source_impact_cli.py` drives the CLI end-to-end on a synthetic two-dataset fixture (current-Babel set vs. the source being added); plus `tests/model/test_cliques.py`, `tests/test_source_impact_details.py`, and `tests/test_source_impact_survival.py`. All test functions carry docstrings and inline section headers per the updated test-documentation convention. - **`CLAUDE.md`**: adds the "configuration over constants" / "document every configuration value" / "keep related settings together" conventions and documents the source-impact workflow, the `cliques.glom_from_files` no-drift rule, and the `babel_pipeline` vs `biolink_type` distinction (the last from `main`). - **`docs/AddingNewSources.md`**: full onboarding guide for the source-impact tool, using EMAPA as a forward-referenced worked example. Includes a note that updating `<pipeline>_prefixes` in `config.yaml` (step 5) is also what makes the new source's labels visible in the rendered report. The EMAPA source itself is in the stacked follow-up **#781**. ## Review round (addressed in this branch) - Biolink-prefix survival (`would_be_added` / `needs_biolink_registration`) is judged against the **clique's** assigned biolink type — the single `node_type` `NodeFactory.create_node()` filters every member against — not each identifier's own declared type. - `glom_from_files()` skips blank/malformed concord lines before indexing `parts[2]`. - `report_source_impact` derives `--intermediate-root` / `--compendia-root` / `--downloads-root` from config so inputs and outputs can't desync. - Remote-comparison mode no longer inflates the "current only" count when a remote compendium file fails to download; streaming HTTP responses are now properly closed via context manager. - `PIPELINE_CONFIG` uses the named Biolink class constants from `src/categories.py` instead of hard-coded `biolink:` strings, and derives compendium filenames from them. - New-xref `status` renamed to `added` / `from_other_source` to avoid implying before/after novelty the writer doesn't compute. - Label loading streams each prefix file once and keeps only the CURIEs the report will actually render, avoiding multi-GB in-memory dicts for large prefixes (UMLS, MESH). ## Test plan - [x] `uv run ruff check` / `uv run ruff format --check` / `uv run rumdl check` — clean - [x] `uv run pytest -m unit -q` — **236 passed** - [x] Synthetic CLI test covers the pure-new / expanded / merged / promotion-only paths, the survival columns, detail-file determinism, and markdown-lint hygiene - [ ] SME spot-check of a real run's `new-cliques.csv` / `modified-cliques.csv` / `new-xrefs.tsv` (done against EMAPA in #781) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add docs/sources/MP/disjointness.md explaining the post-glom split, why unique_prefixes/concord-dropping are insufficient, and the measured impact (added/split/moved/deleted) from babel-clique-diff comparing the overlap-allowed build to the disjoint build. Commit the clique-diff CSV + summary JSON under docs/sources/MP/disjointness/. Update the MP and HP READMEs: MP/HP are now disjoint, so an MP clique carries only the Mammalia taxon and an HP clique only Homo sapiens (correcting the earlier "mixed cliques carry both taxa" note). Cross-link prior PRs (#790, #300, #883, #742/#781). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Can this be merged? |
EMAPA is a part_of partonomy, not an rdfs:subClassOf hierarchy, so the get_subclasses_* family needs to walk a caller-supplied predicate. Also guard build_sets() against a silently-ignored hierarchy_predicate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the Mouse Developmental Anatomy Ontology through anatomy ID and concord generation: collect terms by part_of traversal from the EMAPA root, type organ/tissue descendants as biolink:GrossAnatomicalStructure and the rest as biolink:AnatomicalEntity, and export xref concords. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lessons from running anatomy end to end: never let two Snakemake runs share a working directory, which targets are laptop-sized, the config-vs-constant split, and why the get_subclasses_* closure returns hundreds of megabytes of JSON. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build_sets() iterated a dict whose insertion order follows arbitrary SPARQL row order (the queries carry no ORDER BY), and within each subject a set of strings, whose iteration order varies per process under hash randomization. The concord files it wrote therefore differed between builds of identical code and data. That is not cosmetic. glom's unique_prefixes keeps whichever CURIE of a restricted prefix it sees first, so the row order decides clique membership -- and the loser is dropped from the compendia entirely when it has no ids-file row to seed a clique of its own. Adding EMAPA to anatomy_unique_prefixes made this reachable for the 122 UBERON terms that xref more than one EMAPA term: one build seated the deprecated, label-less EMAPA:35459 in UBERON:0005185's clique and stranded the live EMAPA:28061 "medullary collecting duct" as a singleton; the next build did the reverse. Sorting both levels makes the concord byte-identical across runs. This is the single point every source's concord generation routes through, so the guarantee is pipeline-wide rather than EMAPA-specific. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuilt from scratch on babel-1.18 (the old report and its detail files were deleted first, so every file here comes from the new build) and after the build_sets() ordering fix, so it is now reproducible. Headline numbers are unchanged from the previous report: 8,078 EMAPA identifiers, 0 outgoing xref rows, 175,115 pre-existing cliques. The small movements are the 122 UBERON terms that xref more than one EMAPA term, whose winner was previously drawn at random; all 101 identifiers that changed side between the two reports are in that competing set and none outside it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three things learned rebuilding the EMAPA impact report, each of which cost real time and none of which is visible from the code alone. AddingNewSources.md, step 3: adding a prefix to a pipeline's unique_prefixes is not free. glom() keeps the first CURIE of a restricted prefix it sees and refuses the rest, and a refused CURIE with no ids-file row to fall back on vanishes from the compendia entirely -- EMAPA put 263 CURIEs into contests over 122 UBERON cliques. Since the winner is whichever row is written first, concord row order decides clique membership, which is why build_sets() now sorts. AddingNewSources.md, validating: delete a report fully before regenerating so every file provably comes from the new build; beware an intermediate tree written by the pipeline-test fixtures rather than a build, since they share the same stable paths; and when diffing two reports, check the churn is confined to a population you can explain rather than trusting summary totals, which can hide offsetting changes (a net -1 clique concealed 62 identifiers leaving and 61 entering). RunningBabel.md, common build issues: deleting intermediates does not force a rebuild. The per-pipeline `<pipeline>_done` sentinel is the target's own output, so with it present Snakemake reports "Nothing to be done" and exits 0 having rebuilt nothing -- which makes a code fix look like it had no effect when it was never run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SkyeAv
left a comment
There was a problem hiding this comment.
Summary
Good overall PR. Love some ideas like the new impact reports and bad xrefs. I think before we merge it's worth looking into some of the architectural patterns and standards this sets because I'm not sure all of them yet scale to all sources (i.e., the new-cliques/xref files).
- see comments for specifics
Recommendation
Merge with minor revisions.
These rode along with the cross-repo link fixes and belong to none of them: a Finder .DS_Store, an Apple Numbers working copy of drugchemical.tsv that nothing references, and an EMAPA source-impact detail CSV that #781 had already decided not to commit (docs/sources/EMAPA/impact-report.md says so in as many words), so it arrives here without the report that would explain it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The detail-file note claimed neither the modified-clique nor the new-xref records were committed, but new-xrefs.tsv is in the tree — and the repo convention (docs/tools/SourceImpactReport.md) is to commit it. Restore the generated link line for it, matching what source-impact-report emits and what docs/sources/MP/impact-report.md carries, and leave the note covering the one file it is actually true of, with that file's own size rather than the pair's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he locked ruff (#973) Documentation fixes found while working out a clean division of labour between Babel's docs and NodeNorm's, plus a test that keeps the cross-repo links from rotting again. Companion to NCATSTranslator/NodeNormalization#403. ## Corrections **`preferred_name` is used by NodeNorm.** `DataFormats.md` said it was "not currently used by NodeNorm, but will be in the future". NodeNorm has consumed it since its #299 and returns it as the label of the normalized clique. The entry now says so, and keeps the more useful warning that the preferred name is not necessarily the clique leader's label. **Synonym files and GeneProtein conflation.** The note that synonym files are built with GeneProtein conflation turned off is still true of the per-type files, but v1.13 added a separate `synonyms/GeneProteinConflated.txt.gz` so NameRes can be loaded with that conflation on. Both are now described. **`Architecture.md` listed a clique-level `descriptions` field.** `write_compendium()` does not emit one — descriptions are per-identifier, in the `d` key, sorted shortest first. Replaced with a description of what the `identifiers` list actually carries, and clarified `taxa` as the union of the per-identifier `t` values. **Removed a pointer to `docs/debugging/Conflation.md`,** which does not exist. ## Cross-references `Conflation.md` explains what NodeNorm does with the conflation files but never says how a caller turns a conflation on, so it now points at NodeNorm's API documentation for the flags. `Deployment.md` step 3.1 says to "split the Babel outputs into smaller files to improve load times" without saying what the sizes are or why it matters. That is documented in detail on the NodeNorm side, so this links there rather than growing a second copy that can drift. ## Link hygiene, and a test that enforces it Several cross-repo links pointed at `master`, which none of these repositories uses any more: `README.md`, `docs/Babel.ipynb`, `docs/Deployment.md`, and a provenance URL reported to users from `src/snakefiles/diseasephenotype.snakefile`. They resolve today only through GitHub's branch-rename redirect. `CITATION.cff` still named `TranslatorSRI/Babel` as `repository-code`, which is the URL Zenodo reads. Two release notes linked `[May 2024](TranslatorMay2024)` with no `.md`, so they never resolved at all. `tests/test_docs_links.py` now enforces four rules across every tracked Markdown file, and — for the URL rules — every tracked file that can carry a GitHub link (snakefiles, `CITATION.cff`, notebooks, YAML, Python): - relative links resolve on disk - heading anchors match a real heading - no link is pinned to the `master` branch of an NCATSTranslator repository - no link names the pre-rename `TranslatorSRI` org for Babel, NameRes or NodeNorm Both bans are deliberately scoped by name: biolink-model really does default to `master`, and `TranslatorSRI/babel-validation` really is still under that org, so a blanket ban would push someone to "fix" correct links into 404s. Everything is offline — nothing fetches a URL, because a test that fails when GitHub is slow is a test people learn to ignore. It is marked `unit`, so per-PR CI runs it, and it discovers files through `git ls-files` rather than walking the repo, which keeps it out of `babel_downloads/` and `babel_outputs/`. Ported from NameResolution's test of the same name (NameResolution#262, NodeNormalization#403); the three copies should be kept in sync. ## CI runs the ruff in `uv.lock` now The formatting job installed whatever ruff was newest rather than the project's, so it was checking the repo with a version nobody has locally — 0.16.0 in CI against 0.15.21 in the lockfile. That is how this PR, which touches no Python at all, came to fail on three Markdown files: ruff 0.16 began formatting the Python inside fenced blocks. `version-file: "uv.lock"` makes the job run the locked version, so a formatter upgrade arrives as a lockfile change that can be reformatted for in the same PR instead of as a red X on an unrelated one. #981 has since landed the other half of this — `extend-exclude = ["*.md"]`, so ruff leaves the fragments in our docs alone — and `CONTRIBUTING.md` now says where a bump's reformatting is due. While in the file: `astral-sh/ruff-action` v3 → v4.1.0, which runs on node24 and so drops the "Node.js 20 is deprecated" warning. It is pinned exactly because releases from v4 on are immutable — there is no moving `v4` tag, and `@v4` does not resolve. The `actions/setup-python` step is gone: ruff is a standalone binary and never used the Python it installed. That was the repository's only use of the action, so **this supersedes #966** (Dependabot bumping it 6 → 7), which can be closed. ## Also removed Three files committed by accident at some point, belonging to none of the above: `.DS_Store` (now gitignored), `input_data/manual_concords/drugchemical.numbers` (an Apple Numbers working copy of `drugchemical.tsv` that nothing references), and `docs/sources/EMAPA/impact-report/modified-cliques.csv`, which #781 had already decided not to commit. ## Deliberately not changed `DataFormats.md` says there are "only two conflation files". I initially had this down as wrong because `DrugChemicalSmaller.txt.gz` appears in two release notes — but it is a SapBERT training-data export (`src/exporters/sapbert.py`), not a conflation file. The existing text is correct. `Conflation.md` documents NodeNorm's query-time behaviour, which looks at first like it belongs in the other repo. Leaving it: it is where readers actually ask the question, and NodeNorm now restates the contract from its own side. ## Verification - `uv run pytest -m unit` — 406 passed (402 before; the four new link tests are the difference) - `ruff check`, `ruff format --check`, `snakefmt --check`, `rumdl check .` — all clean, re-run against ruff 0.16.0 after merging main - `uvx ruff@0.16.0 format --check .` and the locked 0.15.21 agreed at 191 files before #981 raised the lock to 0.16.0 ## Follow-ups filed #971 (three documented paths that do not exist: `docs/pipelines/`, `docs/sources/EMAPA/*.md`, and the `docs/debugging/Conflation.md` walkthrough removed here — the walkthrough still seems worth writing), #972 (the "General releases" section of `releases/README.md` is an empty TODO, which matters because that index is effectively the provenance table behind NodeNorm's `/status` `babel_version`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A source contributes thousands of pure-new cliques (3,753 for EMAPA, 14,750 for MP), almost all of them clean single-identifier cliques, so committing the full list added megabytes that no SME read and that go stale on the next build. Rename the file to new-cliques-top-100.csv and cap it at NEW_CLIQUES_TOP_N rows, deriving the filename from the constant so both move together. The cap is only safe because the writer now ranks before truncating: identifiers the Biolink prefix filter would drop come first, then the largest cliques, then CURIE order for a stable diff. A blind rows[:N] over the old CURIE sort would have kept an arbitrary EMAPA:16* prefix and could have hidden the survival failures the report exists to surface. test_new_cliques_csv_cap_keeps_unsurvivable_and_largest_rows guards that property; the pipeline test additionally asserts the file is capped for EMAPA. The Snakemake rule and the tests take the name from NEW_CLIQUES_CSV rather than repeating a literal. src/reports/source_impact.py keeps its own literal because source_impact_details imports from it, so importing back would be circular. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iles Applies the new ranking and cap to the two already-committed files offline — neither report needed re-running, since the transformation is a sort plus a slice over the existing rows. 1.9 MB down to 21 KB. EMAPA's retained rows are unchanged in content: all 3,753 of its pure-new cliques are singletons with clean survival flags, so the new key degenerates to the old CURIE order. MP now leads with its two 3-member and 70 2-member cliques instead of a lexicographic slice, which is the point of the ranking. Also updates the generated link line in both reports, and the EMAPA note about uncommitted detail files to cover the cap as well as modified-cliques.csv. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the pattern so the next source addition follows it rather than
re-deriving it: commit the capped file, never the full list, and keep the
ranking that makes the cap safe.
- docs/AddingNewSources.md gains a "Why the new-cliques file is capped"
subsection, and the clique-diff commit convention now points at a ranked
top-N slice as the middle ground between committing a huge CSV and
gitignoring it entirely.
- docs/tools/SourceImpactReport.md and src/tools/source_impact_report/CLAUDE.md
name NEW_CLIQUES_TOP_N as the single knob and spell out the property to
preserve if the cap or ranking changes.
- docs/sources/CLAUDE.md gets the cross-cutting version, since it governs
committed artifacts under docs/sources/. It also states the counterpart rule:
new-xrefs.tsv stays uncapped, because it is the one detail file not derivable
from anything else committed.
Fixes pre-existing drift while in these paragraphs: three places said
equivalent_ids is a comma-joined quoted field and gave that as the reason a real
CSV parser is needed. It is pipe-joined (PIPE in source_impact_details.py); the
genuinely quoted field is preferred_label, because ontology labels contain
commas ("nephric duct, mesonephric portion"). The warning was right, its stated
reason was not — and anyone who followed it and split equivalent_ids on "," got
a single field back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
new-xrefs.tsv was the last uncapped committed detail file: 4,336 rows / 464 KB for EMAPA, one row per concord row touching a source CURIE. A slice would be the wrong reduction, because what a reviewer needs from a source's xrefs is which join pathways it opens, and there are far fewer pathways than rows — all 4,336 of EMAPA's xrefs are a single pathway (UBERON asserting xref to EMAPA), and MP's 87 are five. So summarize_xref_groups groups rows by predicate, canonical prefix pair, asserting concord file and status, and new-xrefs-summary.csv commits every pathway with its total plus ten example rows each. The prefix pair is sorted, matching how src/metadata/provenance.py already keys its metadata counts, so a pathway does not appear twice because two files wrote it in opposite directions. asserted_by and status stay in the group key, because MP asserting a mapping to HP is a bridge the addition introduces while HP asserting one to MP may predate it entirely — same sorted pair, different facts, and MP's summary has both rows. Examples are spread evenly across each group rather than taken from the front, so they span the identifier range instead of being the ten lowest IDs. Also here: - All six detail files are now CSV, for consistency; there is no TSV any more. - The full new-cliques.csv and new-xrefs.csv are always written locally and gitignored, so "regenerate the full table for SME review" is a copy rather than a rebuild. The previous commit had left the full clique list unobtainable. write_new_cliques_csv takes a limit rather than duplicating its ranking. - Section 3 of the markdown gains a join-pathway table. Its existing counts come from the source's own concord file only, so EMAPA's report claimed "0 cross-reference rows" while 4,336 UBERON-asserted xrefs were the entire reason EMAPA joined any existing clique. - The concord tree is scanned once per report instead of twice: the label-needed set, the markdown table and both xref files now share the scanned rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grouping and sampling are a transformation over rows that were already committed, labels included, so no report re-run was needed; the conversion imported the production summarize_xref_groups and writer so the artifacts cannot drift from what the tool emits, and asserted that the per-pathway counts account for every original row. 464 KB -> 1.4 KB for EMAPA (one pathway, ten examples) and 8 KB -> 2.8 KB for MP (five pathways, 27 examples — every group is under the example budget, so it keeps all of its rows). MP's summary is the case that shows why asserted_by is part of the group key: HP <-> MP appears twice, as 9 xrefs asserted by HP (from_other_source) and 2 asserted by MP (added). Section 3's join-pathway table is added by hand to both reports to match what the tool now generates, and MP/disjointness.md is repointed at the summary — an absent pathway row answers "no EFO->MP rows remain" more directly than the full table did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ntion Extends the "commit the reduction, never the full table" convention with the second reduction shape and the escape hatch for the question a reduction cannot answer. - docs/AddingNewSources.md: the detail-file inventory now names which two files are committed and which four are local, plus "Why the xrefs are aggregated rather than capped" and a "Sharing a full table with an SME" section — regenerate, upload, link read-only from the PR, never commit. The xref-audit passage said to sample a few rows per target prefix by hand; that grouping is now precomputed, so it points at the pathway table and warns that section 3's totals cover only source-asserted rows. The blank-labels warning now says to fix labels before committing a report, since the labels are what make the example rows judgeable. - docs/sources/CLAUDE.md: "Aggregate when rows are not the unit of interest", generalising the two rules worth keeping — canonicalise a grouping key only where the variation is an artifact, and spread examples across the group. - docs/tools/SourceImpactReport.md: a six-file table, and why each reduction takes the shape it does. - src/tools/source_impact_report/CLAUDE.md: the properties to preserve if the grouping changes, and the asserted_by wart — it is a concord file path, so a nested file like UNICHEM/UNICHEM_7 never equals source UNICHEM and its pathways are always mislabelled from_other_source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This PR adds some functionality to [Babel 1.18](https://github.com/NCATSTranslator/Babel/releases/tag/v1.18) (approx [`babel-1.18`](#937)). The goal is to fix some bugs in the Babel v1.18 run (2026jul15) without redownloading and regenerating the full build, just the bits that need changing. For this reason, I haven't updated any of the included versions. Includes the following PRs -- please review and merge them **before** this one, otherwise they'll be closed automatically when this PR is merged: * #781 | **Metric** | **2025sep1** | **2026jul22** | **Absolute change** | **Percent change** | | ----------------------------------- | ------------ | ----------- | ------------------- | ------------------ | | **All CURIEs** | 688983999 | 605864191 | \-83119808 | \-12.1% | | **All cliques (approx)** | 490293340 | 388490111 | \-101803229 | \-20.8% | | **AnatomicalEntity CURIEs** | 249584 | 252287 | 2703 | 1.1% | | **BiologicalProcess CURIEs** | 67929 | 65256 | \-2673 | \-3.9% | | **Cell CURIEs** | 13175 | 13952 | 777 | 5.9% | | **CellLine CURIEs** | 38810 | 38896 | 86 | 0.2% | | **CellularComponent CURIEs** | 14696 | 14818 | 122 | 0.8% | | **ChemicalEntity CURIEs** | 4046131 | 518554 | \-3527577 | \-87.2% | | **ChemicalMixture CURIEs** | 530 | 609 | 79 | 14.9% | | **ComplexMolecularMixture CURIEs** | 276 | 1470 | 1194 | 432.6% | | **Disease CURIEs** | 632330 | 639398 | 7068 | 1.1% | | **Drug CURIEs** | 360925 | 358459 | \-2466 | \-0.7% | | **Food CURIEs** | 0 | 932 | 932 | NEW | | **Gene CURIEs** | 79427652 | 88740328 | 9312676 | 11.7% | | **GeneFamily CURIEs** | 28050 | 28463 | 413 | 1.5% | | **GrossAnatomicalStructure CURIEs** | 15709 | 25867 | 10158 | 64.7% | | **MacromolecularComplex CURIEs** | 1258 | 20579 | 19321 | 1535.9% | | **MolecularActivity CURIEs** | 206636 | 213714 | 7078 | 3.4% | | **MolecularMixture CURIEs** | 21879355 | 23892388 | 2013033 | 9.2% | | **OrganismTaxon CURIEs** | 3543867 | 3745133 | 201266 | 5.7% | | **Pathway CURIEs** | 53125 | 53772 | 647 | 1.2% | | **PhenotypicFeature CURIEs** | 483108 | 103707 | \-379401 | \-78.5% | | **Polypeptide CURIEs** | 166 | 5 | \-161 | \-97.0% | | **Protein CURIEs** | 275514857 | 170218499 | \-105296358 | \-38.2% | | **Publication CURIEs** | 79773973 | 83969833 | 4195860 | 5.3% | | **SmallMolecule CURIEs** | 221734011 | 231673258 | 9939247 | 4.5% | | **umls CURIEs** | 897846 | 1274014 | 376168 | 41.9% |
Summary
Adds EMAPA (Mouse Developmental Anatomy Ontology) as an anatomy identifier source (#733), contributing 8,078 identifiers to the anatomy compendia. Source documentation, the generated impact report and a full build-vs-build clique diff are under
docs/sources/EMAPA/.Two things for an SME to weigh in on are at the bottom; everything above them is evidence for those two decisions.
What this does to existing cliques: nothing
docs/sources/EMAPA/clique-diff.mdrecords a fullbabel-clique-diffbetween anatomy built without and with EMAPA, from identical cached intermediates on the same commit. It closes the gap the impact report cannot see (#895): before-cliques that split, lose members, or disappear.No pre-existing clique changes in any way. Zero change rows: nothing splits, nothing is retyped, nothing loses a member, and no identifier is dropped from any of the four anatomy compendia. Every difference is a brand-new EMAPA-only clique.
AnatomicalEntity.txtCell.txtCellularComponent.txtGrossAnatomicalStructure.txtTotals go 175,117 → 178,870; the +3,753 is exactly the pure-new clique count the impact report computes independently.
How EMAPA is represented in Babel
Every EMAPA term is typed at extraction: terms at or below
EMAPA:35949"organ" orEMAPA:35868"tissue" asbiolink:GrossAnatomicalStructure, everything else asbiolink:AnatomicalEntity.That declared type is only a starting point — the clique's type wins, and EMAPA is placed last in the type-precedence order, behind GO, CL and UBERON. So EMAPA can add typing to a clique that had none, but can never override an existing ontology's judgement:
biolink:AnatomicalEntitybiolink:GrossAnatomicalStructureThe totals match at 8,078, so the ~1,300-term shift is retyping — an EMAPA term called an anatomical entity landing in
GrossAnatomicalStructure.txtbecause a UBERON member of its clique is typed that way — not loss. No EMAPA identifier is dropped.How EMAPA joins to the rest of anatomy. EMAPA asserts no outgoing mapping triples at all in UberGraph — no
hasDbXref, noskos:exactMatch/closeMatch/broadMatch/narrowMatch. Every EMAPA↔UBERON equivalence comes from the other direction: UBERON asserts 4,356hasDbXreftriples pointing at EMAPA. So the identifiers EMAPA contributes are the substance of this ingest, and the mappings that place them into cliques are UBERON's.Fixes two cliques that merged a gross structure with a cell type
Found by scanning for identifiers in a compendium they had no business being in — an unexpected Biolink type is the cheapest signal that a merge went wrong. Both conflations predate this PR and ship on
maintoday.CL:0000166"chromaffin cell" shared abiolink:Cellclique withUBERON:0001236"adrenal medulla" — the cell type merged with the structure it sits in. Cause:UBERON:0001236 hasDbXref MESH:D019439, and MESH:D019439 is "Chromaffin Cells".UBERON:0003124"chorion membrane" sat in abiolink:CellularComponentclique led byGO:0042600"egg chorion" — a mammalian extraembryonic membrane merged with the acellular envelope of an insect egg. Cause:UMLS:C0008503 eq GO:0042600in the UMLS concord.Neither is fixable at the prefix level (MESH and GO are legitimate anatomy xref targets), so both pairs are now listed individually in
input_data/anatomy_badxrefs.txt.This also rescues two EMAPA terms the branch was silently losing.
EMAPA:18428"adrenal medulla" andEMAPA:16112"chorion" were dragged into those mistyped cliques and then dropped entirely, because EMAPA is not a permitted prefix forbiolink:Cellorbiolink:CellularComponent. A clique diff cannot see this class of loss — a dropped identifier appears on neither side.SME sign-off required: two judgement calls
1. EMAPA identifiers are not forced to be unique within a clique
122 UBERON terms cross-reference more than one EMAPA term (263 EMAPA CURIEs); for 116 of them, more than one target is a live, labelled EMAPA term. Full list, one row per mapping, in
docs/sources/EMAPA/multi-emapa-xrefs.csv.Reading that list, these are overwhelmingly genuine 1:n mappings — a mouse developmental partonomy naming a structure at finer or stage-specific granularity than the adult UBERON term:
UBERON:0000044"dorsal root ganglion"EMAPA:16667"spinal ganglion",EMAPA:16668"dorsal root ganglion",EMAPA:18372"tail dorsal root ganglion"UBERON:0001017"central nervous system"EMAPA:16470"central nervous system",EMAPA:16754"tail central nervous system"UBERON:0001049"neural tube"EMAPA:16164"neural tube",EMAPA:16757"tail neural tube"Restricting EMAPA to one identifier per clique would block all 122 merges, and would have silently withdrawn three already-published identifiers from the compendia.
UBERON:0000044is acceptable. Restricting EMAPA does not produce a better clique — it produces an arbitrary winner plus singletons, and can silently delete identifiers. If these mappings should not be equivalences at all, the fix is to filter the offending UBERON xrefs, not to restrict the prefix.2. EMAPA terms carry no taxon
We considered attaching
NCBITaxon:10090"Mus musculus" to every EMAPA term, since EMAPA is by definition a mouse ontology, and decided against it. The tempting precedent is MP, where the taxon cleanly separates MP (mammal) from HP (human). EMAPA is not analogous: of the 8,078 EMAPA CURIEs in the built compendia, 4,345 share a clique with a UBERON term, and UBERON is a multi-species ontology. Marking those cliques mouse-specific would be incorrect —UBERON:0001049"neural tube" is not a mouse structure just becauseEMAPA:16164is one of its cross-references. A taxon attaches to the clique, not to the EMAPA identifier within it, so there is no way to say "mouse, but only for the EMAPA member".Both decisions are recorded in
docs/sources/EMAPA/README.md.Implementation notes
For code review; none of this changes the answers above.
part_ofpartonomy rather than an is_a hierarchy, sobuild_sets()andget_subclasses_of()now take ahierarchy_predicate(defaulting tordfs:subClassOf). AsubClassOfwalk fromEMAPA:0reaches only two terms.build_sets()now sorts its output. It previously iterated a set of strings, so identical code and data produced different concord row orders — and therefore different cliques — between runs (unique_prefixes tie-break picks the lexicographically smallest CURIE, not the valid one #945). EMAPA ended up unrestricted so this decides no EMAPA clique, but UBERON and GO are restricted and the guarantee makes any such tie-break reproducible.input_data/anatomy_badxrefs.txtis applied through theconcord_pair_filterhookglom_from_files()already exposes, so it needed no new plumbing.Test plan
ruff check/ruff format/snakefmt/rumdl— all cleanuv run pytest -m unit -q— 421 passeduv run pytest tests/pipeline -q --all— 56 passed, 3 skipped, 2 xfailed. Both xfails are pre-existingKNOWN_DUPLICATESentries (MESH → Pipeline tests reveal MeSH duplication between different compendia #730, UMLS → Pipeline tests reveal UMLS duplication between different compendia #729); EMAPA passes.anatomybuild, after deleting the previous report entirely so every file provably comes from the new build🤖 Generated with Claude Code