Skip to content

Download MeSH by configured year, read registry numbers from MeSH XML, and fail the build when MeSH lags UMLS - #1144

Open
cbizon wants to merge 13 commits into
mainfrom
configurable-mesh-year
Open

cbizon wants to merge 13 commits into
mainfrom
configurable-mesh-year

Conversation

@cbizon

@cbizon cbizon commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

NLM stopped updating the top-level mesh.nt.gz after MeSH 2024, so Babel's MeSH had fallen behind its UMLS: with UMLS 2026AA, 1,878 MSH codes on 2,162 CUIs had no MeSH record, mostly new Supplementary Concept Records such as foliglurax MESH:C000730045 and befotertinib MESH:C000731667. This PR pins the MeSH release with a new mesh_year in config.yaml, beside umls_version, and downloads that year's RDF dump. Per-year RDF has carried no registry numbers since MeSH 2025, so CAS, UNII and NCBITaxon registry numbers now come from the MeSH XML.

So that this can't happen silently again, the build now fails early when mesh_year lags umls_version, and each release archives the UMLS/MeSH skew report and prints its count in the drafted notes.

The calls worth overruling

  • The skew limit is 100 missing codes, and it blocks MeSH ownership. rule check_umls_mesh_skew raises when more UMLS MSH codes than umls_mesh_skew_max_missing_codes have no record in the dump. It leaves out the U "(MeSH Category)" headings UMLS adds itself, and the SUPPRESS=O/E rows check_mrconso_line() drops everywhere else (2026AA has none for MSH). Against UMLS 2026AA, MeSH 2026 misses 0, MeSH 2025 829 and MeSH 2024 1,856 (table and script). Stop the same CURIE leading cliques in two compendia, and fail the build when it does #1124's umls_mesh_ownership waits for the check, so the five MeSH-owning pipelines stop before building compendia.
  • Registry numbers come from XML, kept to records in the RDF dump. pull_mesh_registry() stream-parses each record's preferred-concept RegistryNumber from desc{year}.gz / supp{year}.gz (new get_mesh_xml rule). The current year's XML is updated daily while the RDF is a snapshot, so on 2026-10-21 it had registry numbers for 100 SCRs mesh2026.nt lacks; only records with a label in MESH/labels are kept, so the unfiltered NCBI_MESH concord can't pull a label-less MeSH ID into a taxon clique. (Filtering it by taxon/ids/MESH instead would also drop existing links.) Against the old RDF registry (measured on 3a96a82), 158,927 pairs match; of the 2,327 only the RDF had, all but 11 are on records NLM has since deleted.
  • No fallback. If NLM hasn't published the configured year, get_mesh and get_mesh_xml fail rather than fall back to the stale top-level file. The dump is hashed as it downloads and checked against NLM's .sha1; a network test checks the year is published.
  • Per-year IRIs are normalized on download. normalize_mesh_nt() rewrites year-versioned record IRIs (http://id.nlm.nih.gov/mesh/2026/D002839) to the unversioned form, so the existing SPARQL and get_mesh_id_from_iri() work unchanged, and raises if the versioned prefix never appears.

Details: Ingestion.md, Source data and Architecture.md, Build controls.

Also here

  • Registry concord metadata names the XML files actually downloaded (recorded in MESH/xml_sources.txt, since current vs. archived directory is only known at download time) and the RDF dump they are filtered to.
  • archive_build.py requires reports/umls_mesh_skew.json, and draft_release_notes.py prints its count and reports MeSH version bumps; for the first release after this one, whose baseline has no mesh_year, it drafts a to-do to name the old version by hand.
  • config.yaml and the ingestion doc point at https://nlmpubs.nlm.nih.gov/projects/mesh/rdf/ for checking whether a new MeSH year is out. Downloads stay on FTP here; Download MeSH over HTTPS from NLM's nlmpubs site instead of FTP #1161, stacked on this PR, moves them to HTTPS.

What it does not do

Registry numbers on records already in the dump can still change from one day's XML to the next, so two downloads of the current mesh_year may give different concords until NLM archives the year.

Effect on #1124's MeSH ownership

Replaying #1124's rule on ids/MESH rebuilt from MeSH 2026 (output): MeSH 2026 adds about 1,700 records and drops about 4,000 NLM deleted; the rule moves 46 more CUIs than on the 2024 dump, and all 949 resolvable MeSH duplicate leaders are still predicted resolved. The first cluster build confirms this (#1158).

Verification

On 41b6dc2 the unit tests (730-odd) pass and ruff, snakefmt and rumdl are clean; fixtures are copied verbatim from mesh2026.nt, desc2026/supp2026 and 2026AA MRCONSO.RRF, and the download's checksum handling is tested against a mocked FTP server. On 41b6dc2 get_mesh and get_mesh_xml ran through Snakemake against NLM, and pull_mesh_registry() ran on the real XML. On c6a79c8 the full 2026 dump loaded through the existing Mesh queries, and check_umls_mesh_skew passed with 0 missing codes on the local 2026AA and MeSH 2026 downloads and raised on the MeSH 2025 report.

Nothing blocks this PR.

🤖 Generated with Claude Code

- Add mesh_year to config.yaml (next to umls_version) and download
  /online/mesh/rdf/{year}/mesh{year}.nt.gz. NLM stopped updating the top-level
  mesh.nt.gz after MeSH 2024, so with UMLS 2026AA we were missing ~1,900 UMLS MSH
  codes (mostly new SCRs). mesh{year}-dups.nt is the pre-dedup version of the same
  data per the per-year readme, so it is not downloaded.
- Fail fast if the configured year is not published; no fallback to the stale
  top-level file. The .gz is verified against NLM's .sha1.
- Per-year dumps use year-versioned record IRIs (mesh/2026/D002839).
  normalize_mesh_nt() rewrites them to the unversioned form during decompression so
  the existing SPARQL and get_mesh_id_from_iri() work unchanged, and raises if the
  expected prefix never appears (wrong year or changed IRI scheme).
- Per-year RDF dropped meshv:registryNumber from MeSH 2025 on (mesh2024.nt: ~384k
  triples; 2025/2026: none), which would have silently emptied the mesh_cas,
  mesh_unii and NCBI_MESH concords. pull_mesh_registry() now stream-parses the
  preferred concept's RegistryNumber from desc{year}.gz/supp{year}.gz (new
  get_mesh_xml rule) and raises if it finds none. Against the old RDF registry,
  158,927 pairs match; nearly all old-only pairs are on records NLM has deleted.
- Add reports/umls_mesh_skew.json (in all_reports): counts MRCONSO MSH codes with no
  record in mesh.nt, so UMLS/MeSH version skew is visible. With 2026AA + MeSH 2026
  only the 22 UMLS-only U "(MeSH Category)" codes remain. Informational only.
- Report MeSH in release-note version bumps, record the MeSH year/URL in the
  NCBI_MESH concord metadata, and pass mesh_year through the pipeline test fixture.
- Tests use fixtures copied verbatim from mesh2026.nt, desc2026/supp2026 XML and
  2026AA MRCONSO; a network test checks the configured year is on the FTP site.
- Docs: MeSH source data, IRI normalization and registry sourcing in
  docs/sources/MESH/Ingestion.md; mesh_year/umls_version pairing in RunningBabel.md.
gaurav and others added 2 commits October 21, 2026 02:30
2026jul22's ids/MESH came from the stale top-level mesh.nt (MeSH 2024).
With this PR's per-year download, the replay over MeSH 2026 ids/MESH and
2026jul22's ids/UMLS moves 46 more CUIs (33 protein -> chemicals), the
46 records two pipelines' ids/MESH share are unchanged (so the duplicate
leader allowlist still covers them), and all 949 resolvable MeSH leaders
are still predicted resolved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gaurav gaurav added this to the Critical milestone Oct 21, 2026
gaurav and others added 2 commits October 21, 2026 02:40
version_bumps() only wrote a bullet when both configs had a key, so the
first release with mesh_year (whose baseline config.yaml predates it)
would have drafted no MeSH line at all, though MeSH moved from the stale
2024 dump to 2026. It now writes a visible to-do asking for the old
version by hand, for this key and any added later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Oct 21, 2026
Regenerating 2026jul22.md/.csv with every pipeline's ids/MESH rebuilt
from MeSH 2026 (#1144) gives identical output, so the proposed owners
carry over. Also say get_mesh reuses an older mesh.nt without
--forcerun.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gaurav and others added 4 commits October 21, 2026 13:23
https://nlmpubs.nlm.nih.gov/projects/mesh/ serves the same files under the
same paths as ftp://ftp.nlm.nih.gov/online/mesh/, and is the site NLM's
download page links to. Its rdf/ directory lists one directory per MeSH year,
so a browser is the quickest check for a release newer than mesh_year.

We still download over FTP here; #1161 moves the downloads to HTTPS. Say so in
config.yaml and the MeSH ingestion doc, so that an FTP failure before #1161
lands leads straight to the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skew report gains umls_msh_codes_missing_from_mesh_excluding_categories:
the U "(MeSH Category)" headings UMLS adds to its MSH source are missing from
every MeSH dump, so a limit on the raw count would only be offset by a
constant. docs/sources/MESH/umls-mesh-skew/scripts/measure_skew.py runs the
build's report against each year's per-year RDF dump. Against UMLS 2026AA,
excluding U: MeSH 2026 misses 0 codes, MeSH 2025 829, MeSH 2024 1,856. That is
the evidence for the build limit added next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UMLS/MeSH skew report was informational, and nothing read it. Now rule
check_umls_mesh_skew raises when more MSH codes than
umls_mesh_skew_max_missing_codes (100) are missing from the MeSH dump, not
counting the U category headings. 100 sits far from both measured cases:
0 with matched versions, 829 with MeSH a year behind UMLS 2026AA.

rule umls_mesh_ownership takes the check's output as an input, so the five
MeSH-owning pipelines stop before they build compendia on a skewed dump; only
their ids/MESH rules run first. The check is a separate rule from the report,
as with assert_no_unexpected_duplicate_clique_leaders, because Snakemake
deletes the outputs of a job that raises and the JSON is what explains the
failure. all_reports now requires the check rather than the bare report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
archive_build.py now requires reports/umls_mesh_skew.json, which every build
since 2026jul22 writes; the releases already archived predate it.
draft_release_notes.py prints its count under the version bumps, beside the
UMLS and MeSH versions it depends on, and stays silent for an older build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gaurav gaurav changed the title Download MeSH by configured year and read registry numbers from MeSH XML Download MeSH by configured year, read registry numbers from MeSH XML, and fail the build when MeSH lags UMLS Oct 21, 2026
MeSH is NLM's own spelling. docs/sources/README.md now says a source may be
named for itself rather than its CURIE prefix where the two are spelled
differently, as PubMed (prefix PMID) already was. The CURIE prefix stays MESH
everywhere else: src/prefixes.py, babel_downloads/MESH, ids/MESH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Oct 21, 2026
#1144 renamed docs/sources/MESH to MeSH. Git's merge carried this branch's
edits to existing files across but left the four files this branch adds under
the old directory, which on a case-sensitive filesystem (the cluster, CI)
would sit beside MeSH/ as a second directory. Move them, and point the
references to them at the new path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gaurav and others added 2 commits October 21, 2026 19:55
read_umls_mesh_codes() now skips the rows check_mrconso_line() rejects
(SUPPRESS=O/E), as every other MRCONSO reader in Babel does. An obsolete
atom for a deleted descriptor is missing from every MeSH dump, so counting
it would shift check_umls_mesh_skew's limit by a constant. UMLS 2026AA has
no such MSH rows (all are SUPPRESS=N or Y), so the measured skew table in
docs/sources/MeSH/umls-mesh-skew/ is unchanged.

Also: reuse MESH_IRI_PREFIX and get_mesh_id_from_iri() from the MeSH
handler instead of a second, differently-shaped prefix constant, and fix
the generate_umls_mesh_skew_report() docstring, which still called the
report informational after check_umls_mesh_skew made it a build failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The current year's MeSH XML (MESH_FILES/xmlmesh) is updated daily, while
the per-year RDF dump is a snapshot. On 2026-10-21 the XML had registry
numbers for 100 SCRs that mesh2026.nt does not contain. The chemical
concords filter by their ids file, but taxon's NCBI_MESH concord does not,
so a txid on such a record would have put a MeSH ID with no label or type
into a taxon clique. pull_mesh_registry() now keeps only records with a
label in babel_downloads/MESH/labels and logs how many it skipped.

pull_mesh_registry() also takes its desc/supp XML and labels paths as
arguments instead of finding them with make_local_name(), so the rules'
declared inputs are the files it reads and it can be tested on fixtures.

Concord metadata for NCBI_MESH, mesh_cas and mesh_unii now names the XML
files the registry numbers came from (recorded by get_mesh_xml in
MESH/xml_sources.txt, since whether they came from the current or the
archived directory is only known at download time) plus the RDF dump they
are filtered to. NCBI_MESH previously named only the RDF dump, which has
had no registryNumber triples since MeSH 2025.

Download changes:
- fetch_mesh_nt() hashes the RDF dump while it downloads, instead of
  reading the ~120 MB file once more just to hash it, and reports a
  missing .sha1 as a missing checksum file rather than an unpublished
  mesh_year.
- mesh_xml_dir() is renamed fetch_mesh_xml_dir(), since it lists a remote
  FTP directory, and now closes its connection when login or nlst fails.
- measure_skew.py downloads through fetch_mesh_nt() instead of its own
  unchecked urllib download, so its numbers come from the build's path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants