Skip to content

Add EMAPA as an anatomy source - #781

Merged
gaurav merged 41 commits into
mainfrom
add-emapa-actual
Jul 30, 2026
Merged

Add EMAPA as an anatomy source#781
gaurav merged 41 commits into
mainfrom
add-emapa-actual

Conversation

@gaurav

@gaurav gaurav commented May 20, 2026

Copy link
Copy Markdown
Collaborator

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.md records a full babel-clique-diff between 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.

compendium before after diff
AnatomicalEntity.txt 145,743 147,565 +1,822
Cell.txt 9,197 9,197 0
CellularComponent.txt 9,469 9,469 0
GrossAnatomicalStructure.txt 10,708 12,639 +1,931

Totals 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" or EMAPA:35868 "tissue" as biolink:GrossAnatomicalStructure, everything else as biolink: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:

declared by EMAPA final, in the compendia
biolink:AnatomicalEntity 4,090 2,787
biolink:GrossAnatomicalStructure 3,988 5,291

The totals match at 8,078, so the ~1,300-term shift is retyping — an EMAPA term called an anatomical entity landing in GrossAnatomicalStructure.txt because 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, no skos:exactMatch/closeMatch/broadMatch/narrowMatch. Every EMAPA↔UBERON equivalence comes from the other direction: UBERON asserts 4,356 hasDbXref triples 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 main today.

  • CL:0000166 "chromaffin cell" shared a biolink:Cell clique with UBERON: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 a biolink:CellularComponent clique led by GO:0042600 "egg chorion" — a mammalian extraembryonic membrane merged with the acellular envelope of an insect egg. Cause: UMLS:C0008503 eq GO:0042600 in 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" and EMAPA:16112 "chorion" were dragged into those mistyped cliques and then dropped entirely, because EMAPA is not a permitted prefix for biolink:Cell or biolink: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 term EMAPA terms it xrefs
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.

  • Confirm that collapsing e.g. "spinal ganglion", "dorsal root ganglion" and "tail dorsal root ganglion" into one clique with UBERON:0000044 is 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 because EMAPA:16164 is 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".

  • Confirm that no taxon is preferable to a mouse taxon here.

Both decisions are recorded in docs/sources/EMAPA/README.md.

Implementation notes

For code review; none of this changes the answers above.

  • EMAPA is a part_of partonomy rather than an is_a hierarchy, so build_sets() and get_subclasses_of() now take a hierarchy_predicate (defaulting to rdfs:subClassOf). A subClassOf walk from EMAPA:0 reaches 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.txt is applied through the concord_pair_filter hook glom_from_files() already exposes, so it needed no new plumbing.

Test plan

🤖 Generated with Claude Code

Comment thread docs/sources/EMAPA/README.md Outdated
@gaurav

gaurav commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up after rebasing onto the updated add-emapa (#742): the shared clique-building skeleton was refactored out of anatomy into a new module src/createcompendia/cliques.py (commit 154f3788).

anatomy.compute_cliques_for_impact_report is now a thin wrapper over cliques.glom_from_files(...), which runs the common load ids → glom, load concords → filter → drop overused xrefs → glom loop parameterized by three hooks:

  • concord_pair_filter(parts, infile, dicts) — per-pair keep/drop, with access to the clique state built so far
  • overused_xref_remover(pairs, infile) — per-file remove_overused_xrefs variant
  • glom_kwargs — extra kwargs forwarded to every glom call (e.g. disease's close={MONDO: ...})

build_compendia still calls the wrapper, so the impact report's reglom stays in lockstep with the real build (no drift).

This AddingNewSources.md was written before that refactor, so two sections describe the compute hook as anatomy-only logic and will be slightly stale once rebased:

  • §3 "Add a compendium-building hook" — should describe a new type's compute_cliques_for_impact_report as a thin wrapper supplying its hooks to cliques.glom_from_files, rather than reimplementing the glom loop.
  • §Future "Roll out the compute hook to other semantic types" — can note that the skeleton already exists in cliques.py, so rolling out to disease/taxon/process is now adding a thin wrapper + a SEMANTIC_TYPE_CONFIG entry (and, ideally, routing each type's real build_compendia through the same wrapper, or adding a sync test).

The CLAUDE.md update in #742 (commit 25174094, "extending the report to a new semantic type" section) has ready-to-lift wording for both.

gaurav added a commit that referenced this pull request Jun 28, 2026
- 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>
gaurav added a commit that referenced this pull request Jun 28, 2026
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>
gaurav added a commit that referenced this pull request Jun 28, 2026
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>
gaurav added a commit that referenced this pull request Jun 28, 2026
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>
gaurav added a commit that referenced this pull request Jun 29, 2026
…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)
Base automatically changed from add-emapa to main June 29, 2026 20:09
gaurav added a commit that referenced this pull request Jun 30, 2026
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>
@gaurav gaurav added this to the Babel v1.19 milestone Jul 10, 2026
@cbizon

cbizon commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Can this be merged?

gaurav and others added 4 commits July 19, 2026 14:34
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>
@gaurav
gaurav force-pushed the add-emapa-actual branch from 7f2180e to a05580f Compare July 19, 2026 18:34
@gaurav gaurav changed the title Add EMAPA as an anatomy source Add EMAPA as an anatomy source (based on babel-1.18) Jul 19, 2026
@gaurav
gaurav changed the base branch from main to babel-1.18 July 19, 2026 18:35
gaurav and others added 3 commits July 19, 2026 15:29
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>
Comment thread docs/sources/EMAPA/impact-report/new-cliques.csv Outdated
Comment thread docs/sources/EMAPA/on-addition/clique-diff.summary.json
Comment thread input_data/anatomy_badxrefs.txt
Comment thread src/babel_utils.py

@SkyeAv SkyeAv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

gaurav added a commit that referenced this pull request Jul 29, 2026
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>
gaurav added a commit that referenced this pull request Jul 29, 2026
…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)
gaurav and others added 6 commits July 30, 2026 03:28
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>
@gaurav gaurav modified the milestones: Babel v1.19, Babel v1.18 Jul 30, 2026
@gaurav
gaurav merged commit 0a4db27 into main Jul 30, 2026
5 checks passed
@gaurav
gaurav deleted the add-emapa-actual branch July 30, 2026 19:50
@github-project-automation github-project-automation Bot moved this from In progress to Done in Babel sprints Jul 30, 2026
gaurav added a commit that referenced this pull request Jul 30, 2026
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% |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request new data source A new data source to include in Babel Priority: High

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants