Skip to content

Sharpen the one genuine GTDB demotion, and record why no rule can find them (#445) - #450

Merged
realmarcin merged 6 commits into
mainfrom
gtdb-demotion-445
Aug 6, 2026
Merged

Sharpen the one genuine GTDB demotion, and record why no rule can find them (#445)#450
realmarcin merged 6 commits into
mainfrom
gtdb-demotion-445

Conversation

@realmarcin

Copy link
Copy Markdown
Contributor

Closes #445.

The question, and the answer

#445 asked whether gtdb_ground.py should detect a demotion — an NCBI clade GTDB keeps but places a rank lower — and prefer the exact equivalent, rather than leaving it to a curator to notice.

Measured over all 159 higher-rank groundings: it should not. Six look sharpenable (every row behind the winning taxon agrees at a finer rank), but only one is a demotion:

grounding why it is not a demotion
Gemmatimonadota ×2, Thermotogota keep their own names in GTDB (is_reclassified: false) — the NCBI taxon is the GTDB phylum. A rule keyed on agreement alone mis-sharpens all three.
Rhodospirillaleso__RF32 looks weak at 7 of 328 rows, but is a 0.704 majority by genomes — the denominator the tool uses.
Ca. Methanophagaleso__Alkanophagales a real reclassification, but no GTDB taxon bears the NCBI clade's name, so there is nothing to sharpen to.
Ca. Parvarchaeotap__Nanoarchaeota the one genuine demotion.

What marks a demotion is that a finer rank still carries the clade's name — and testing that mechanically is exactly where it breaks. Shared stems: Parvarchaeota/Parvarchaeales = 10 chars, Dormiibacterota/Dormibacteria = 5 (NCBI doubles the i), Methanophagales/Methanospirareceae = 7. No threshold separates the two real cases from the false one. So it stays a curator's call, and the durable artifact is the recorded decision, not a heuristic.

The fix

Candidatus Parvarchaeota sharpened from p__Nanoarchaeota to o__Parvarchaeales. Every one of the named-species rows behind the grounding is Parvarchaeales, so the sharper term carries the same 20/20 confidence — sharpening costs nothing here. The phylum term was not wrong, only uninformative: p__Nanoarchaeota also absorbs NCBI Ca. Woesearchaeota, Nanobdellota and Ca. Iainarchaeota, so it said nothing distinguishing this entry, in a record whose Micrarchaeota (ARMAN-1/2) entry is a near neighbour.

Order rather than family: c__Nanoarchaeia does not bear the clade name, and f__Parvarchaeaceae is narrower than the NCBI phylum — o__Parvarchaeales is the most senior GTDB term that still means Parvarchaeota.

Tests

Pin both sharpenings (Parvarchaeales, and #444's Dormibacteria) and the deliberate non-sharpening (Alkanophagales), so a tool re-run cannot quietly undo a decision no rule can re-derive. A fourth test checks that a sharpened term is genuinely inside the taxon it replaced — a sharpening moves down the lineage, never sideways.

Method note

Three measurements were needed to get this right. The first keyed on the genome-level taxid column instead of the name columns the tool uses for higher ranks (yielding meaningless "1 row" agreement); the second read the 1-based header positions as 0-based indices (so "domain" was really phylum). Both are recorded in the test docstring, because the wrong answers were plausible-looking.

Canaried: --apply logs skipping curated NCBITaxon:1462422, applies 0 blocks, leaves the file byte-identical. just qc green.

🤖 Generated with Claude Code

…d them (#445)

#445 asked whether the tool should detect a demotion - an NCBI clade GTDB keeps
but places a rank lower - and prefer the exact equivalent. Measured over all 159
higher-rank groundings, it should not.

Six groundings look sharpenable, in the sense that every row behind the winning
taxon agrees at a finer rank. Only one is a demotion:

* Gemmatimonadota (x2) and Thermotogota keep their own names in GTDB
  (is_reclassified false). The NCBI taxon *is* the GTDB phylum, so sharpening to
  a class would assert what the data does not say. A rule keyed on agreement
  alone mis-sharpens all three.
* Rhodospirillales -> o__RF32 looks weak at 7 of 328 rows but is a 0.704
  majority by genomes, which is the denominator the tool actually uses. My first
  two measurements of this whole question were wrong for related reasons - one
  keyed on the genome-level id column instead of the name columns the tool uses
  for higher ranks, the other used 1-based header positions as 0-based indices.
* Ca. Methanophagales -> o__Alkanophagales is a real reclassification, but no
  GTDB taxon bears the NCBI clade's name, so there is nothing to sharpen to.

What marks a demotion is that a finer rank still carries the clade's name, and
testing that mechanically is where it fails: the shared stem of
Parvarchaeota/Parvarchaeales is 10 characters, of Dormiibacterota/Dormibacteria
5 (NCBI doubles the i), of Methanophagales/Methanospirareceae 7. No threshold
separates the two real cases from the false one, so this stays a curator's call.

Sharpen Ca. Parvarchaeota from p__Nanoarchaeota to o__Parvarchaeales. Every one
of the named-species rows behind it is Parvarchaeales, so the sharper term
carries the same 20/20 confidence, while the phylum also absorbs NCBI Ca.
Woesearchaeota, Nanobdellota and Ca. Iainarchaeota - saying nothing that
distinguishes this entry, in a record whose Micrarchaeota entry is a near
neighbour. Order rather than family: c__Nanoarchaeia does not bear the name and
f__Parvarchaeaceae is narrower than the NCBI phylum.

Pin the two sharpenings and the one deliberate non-sharpening, so a tool re-run
cannot quietly undo a decision that no rule can re-derive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Network integrity findings

Warnings only — a member with no interaction yet, or a participant matched by ontology id rather than by name, or one on a community-level interaction that resolves to no member. Reported, but does not fail the build.

Network Integrity Audit Report
================================================================================

0 error, 55 warning across 27 records with findings
Only error-severity findings fail the build.

ANME_SRB_Anaerobic_Methanotrophic_Syntrophic_Consortia
--------------------------------------------------------------------------------
  [warning] NAME_MISMATCH: Source 'ANME-1' matches no taxonomy entry by name; resolved to 'ANME-1 (anaerobic methanotrophic archaea, clade 1)' by source_id NCBITaxon:588814
  [warning] NAME_MISMATCH: Target 'Desulfofervidus' matches no taxonomy entry by name; resolved to 'Desulfofervidus (sulfate-reducing bacterial partner of ANME-1)' by target_id NCBITaxon:1902583
  [warning] NAME_MISMATCH: Source 'ANME-2a' matches no taxonomy entry by name; resolved to 'ANME-2a (anaerobic methanotrophic archaea, clade 2a)' by source_id NCBITaxon:588816
  [warning] NAME_MISMATCH: Target 'Seep-SRB1' matches no taxonomy entry by name; resolved to 'Seep-SRB1 (sulfate-reducing bacterial partner of ANME-2a)' by target_id NCBITaxon:213119
  [warning] NAME_MISMATCH: Source 'ANME-2c' matches no taxonomy entry by name; resolved to 'ANME-2c (anaerobic methanotrophic archaea, clade 2c)' by source_id NCBITaxon:3386252
  [warning] NAME_MISMATCH: Target 'Seep-SRB2' matches no taxonomy entry by name; resolved to 'Seep-SRB2 (additional sulfate-reducing bacterial partner)' by target_id NCBITaxon:213118

Total: 6 issues (0 error, 6 warning)

Aalborg_East_Full_Scale_EBPR_Community
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Bacteroidetes flanking community members' has no interactions
  [warning] DISCONNECTED: Taxon 'Tetrasphaera-related actinobacterial PAOs' has no interactions

Total: 2 issues (0 error, 2 warning)

Bacteroides_Methanobrevibacter_Gnotobiotic_Mouse_Mutualism
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Desulfovibrio piger comparator' has no interactions

Total: 1 issues (0 error, 1 warning)

BioModels_MODEL2204300001_Kefir_Community_Model
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Lactobacillus kefiri' has no interactions
  [warning] DISCONNECTED: Taxon 'Lactococcus lactis' has no interactions

Total: 2 issues (0 error, 2 warning)

BioModels_MODEL2405300001_Infant_Gut_HMO_SynCom
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Bacteroides ovatus' has no interactions
  [warning] DISCONNECTED: Taxon 'Bacteroides vulgatus' has no interactions
  [warning] DISCONNECTED: Taxon 'Bifidobacterium bifidum' has no interactions
  [warning] DISCONNECTED: Taxon 'Bifidobacterium breve' has no interactions
  [warning] DISCONNECTED: Taxon 'Blautia producta' has no interactions
  [warning] DISCONNECTED: Taxon 'Enterococcus faecalis' has no interactions
  [warning] DISCONNECTED: Taxon 'Escherichia coli K-12' has no interactions
  [warning] DISCONNECTED: Taxon 'Lacticaseibacillus rhamnosus' has no interactions
  [warning] DISCONNECTED: Taxon 'Ruminococcus gnavus' has no interactions
  [warning] DISCONNECTED: Taxon 'Streptococcus thermophilus' has no interactions

Total: 10 issues (0 error, 10 warning)

Crucian_Carp_Gut_Disease_Resistance_SynCom
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Aeromonas hydrophila' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

Drosophila_FiveSpecies_Gnotobiotic_Gut_Microbiota
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Drosophila five-species bacterial microbiota' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

East_River_Floodplain_Core_Microbiome
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'core floodplain bacteria' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_SOURCE: Source taxon 'East River floodplain bacteria' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Ensifer_YF2_Sphingobacterium_Y2_Polyethylene_Degrading_Consortium
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Chryseobacterium sp. MF1' has no interactions

Total: 1 issues (0 error, 1 warning)

GLBRC_UFMP_Fermentation_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Olsenella (Actinobacteriota)' not found in taxonomy section (community-level scope)
  [warning] NAME_MISMATCH: Target 'Clostridium (Firmicutes)' matches no taxonomy entry by name; resolved to 'Clostridium_B sp. (MAG CLOS1)' by target_id NCBITaxon:1485

Total: 2 issues (0 error, 2 warning)

Hanford_300_Area_Unconfined_Aquifer_Community
--------------------------------------------------------------------------------
  [warning] NAME_MISMATCH: Source 'intrusion-associated Actinobacteria' matches no taxonomy entry by name; resolved to 'Actinobacteria/Actinomycetota aquifer bacteria' by source_id NCBITaxon:201174
  [warning] UNKNOWN_TARGET: Target taxon 'Hanford groundwater bacteria' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_SOURCE: Source taxon 'aquifer redox guild bacteria and archaea' not found in taxonomy section (community-level scope)

Total: 3 issues (0 error, 3 warning)

High_Solids_Switchgrass_Methanogenic_Microbiome
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Bacilli' has no interactions

Total: 1 issues (0 error, 1 warning)

KB1_Chlorinated_Ethene_Dechlorinating_Consortium
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Sporomusa spp. in KB-1' has no interactions

Total: 1 issues (0 error, 1 warning)

Legume_Rhizobia_Mars_Simulant_Symbiosis
--------------------------------------------------------------------------------
  [warning] NAME_MISMATCH: Source 'Sinorhizobium spp. (rhizobial symbionts)' matches no taxonomy entry by name; resolved to 'Sinorhizobium meliloti' by source_id NCBITaxon:382
  [warning] UNKNOWN_TARGET: Target taxon 'Medicago truncatula (host legume)' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Lunar_Martian_Simulant_PGPB_Lettuce_SynCom
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Lactuca sativa (lettuce host)' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'Lactuca sativa (lettuce host)' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Lunar_Simulant_Phosphate_Solubilizing_Bacteria_Nicotiana
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Nicotiana benthamiana' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

Model_Lignocellulose_Formaldehyde_Crossfeeding_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'model lignocellulose consortium members' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'model lignocellulose consortium members' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Moss_Microbe_Complex_Regolith_Biofertilizer
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Hypnum plumaeforme (moss host)' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'Hordeum vulgare (barley model crop)' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

ORNL_Clostridium_Desulfovibrio_Geobacter_Trophic_Model
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Desulfovibrio vulgaris Hildenborough and Geobacter sulfurreducens' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'three-species model community' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Oak_Ridge_FRC_Uranium_Nitrate_Groundwater_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'other groundwater bacteria' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

PET_Artificial_FourSpecies_Degradation_Consortium
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'engineered PETase/MHETase and TPA-utilization members' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

PMI_Variovorax_Thermotolerance_Collection
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Variovorax' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'Arabidopsis thaliana' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Rice_Duckweed_Bacillus_SynCom
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Bacillus SynCom' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'Rhizoctonia solani' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Saanich_Inlet_OMZ_Redox_Gradient_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_SOURCE: Source taxon 'Saanich Inlet redox-gradient microorganisms' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

Shewanella_Geobacter_Exoelectrogenic_Biofilm_Community
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'anode-associated biofilm community' not found in taxonomy section (community-level scope)
  [warning] UNKNOWN_TARGET: Target taxon 'anode' not found in taxonomy section (community-level scope)

Total: 2 issues (0 error, 2 warning)

Sulfide_Spring_Autotrophic_CPR_Biofilm
--------------------------------------------------------------------------------
  [warning] DISCONNECTED: Taxon 'Beggiatoa' has no interactions

Total: 1 issues (0 error, 1 warning)

Thermophilic_Lignocellulose_Composting_SynCom_Biosanitization
--------------------------------------------------------------------------------
  [warning] UNKNOWN_TARGET: Target taxon 'Pseudomonas aeruginosa' not found in taxonomy section (community-level scope)

Total: 1 issues (0 error, 1 warning)

The full report is attached to the workflow run as an artifact.

realmarcin and others added 2 commits August 6, 2026 01:14
…rs were wrong

Both reviewers independently refuted this PR's central claim. Parvarchaeota is
not the only genuine demotion - it is one of three (#451):

* Chlorobiota -> p__Bacteroidota, unanimous at c__Chlorobiia (24 named rows, 132
  genomes)
* Ignavibacteriota -> p__Bacteroidota, unanimous at c__Ignavibacteria (3 rows, 7
  genomes)

Both are textbook GTDB demotions of an NCBI phylum into a class, both pass this
PR's own screen, and both incur the harm worse than the case #444 fixed:
GTDB:p__Bacteroidota is the most-shared higher-rank term in the KB, on seven
groundings, so three distinct phylum concepts were collapsing into a term that
distinguished none of them. Sharpened and pinned.

The measurement was also wrong under every definition (#452). "Six sharpenable"
is 8 on raw rows and 20 under the tool's own default, which is the policy every
stored block was built under - and the prose enumerated seven while saying six.
Redone on the default: of 20, fifteen keep their NCBI name in GTDB and must not
be sharpened, and of the five reclassified, two have nothing to sharpen to
(f__CAG-239 is a placeholder, f__Methanospirareceae does not bear the name).
The naive rule would mis-sharpen fifteen, not three, which strengthens the
conclusion while invalidating every number it rested on.

The stem-length argument now has five data points and still holds: demotions sit
at 5, 8, 10 and 14 characters and the non-demotion at 7, so no threshold
separates them.

Rewrite the lineage test, which compared gtdb_id against gtdb_lineage - both
curator-written in the same block, so a wrong sharpening with a matching
hand-written lineage passed. It now goes back to the crosswalk and asserts every
named-species row carries the chosen term at its own rank.

Also: correct the Parvarchaeota note, which gave a false reason for order over
family (the two are coextensive here) and undercounted the absorbed phyla as
three; stop attributing the pin's protection to --apply, which only touches
ungrounded taxa; and replace SKILL.md's stale "two blocks carry it" with the
eight that do, plus a pointer to grep since nothing enforces the list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…could not fail

Round 2 found the screen itself was blind, not merely under-run. It demanded
strict string unanimity at the finer rank, so any clade GTDB had split for
monophyly was invisible - which is how Nitrososphaerota was missed. Its rows
read Nitrososphaeria 57 and Nitrososphaeria_A 8, the same clade split for
monophyly, so re-sweeping with the suffix set aside finds it. Sharpened from
p__Thermoproteota to c__Nitrososphaeria, and the stored fraction now counts only
the unsuffixed name: 552 of 631 genomes where the phylum vote claimed 631 of
631. Less confident and more informative.

The crosswalk test added in round 1 could not fail on the regression it existed
to catch. "Every named row carries X at rank Y" is true of every *ancestor* too,
so reverting the pins to p__Bacteroidota and p__Nanoarchaeota still passed. What
separates a demotion from its own ancestor is the name, so a second test now
asserts the chosen term bears the clade name and the vote's term does not -
verified to fail when a pin is reverted. The crosswalk test is kept and made
suffix-aware, because it catches a different error: an unrelated clade.

Numbers corrected again: 21 sharpenable under the tool default rather than 20,
five demotions rather than three, Ignavibacteriota's stem is 13 not 14, and
Ca. Eiseniibacteriota belongs in the nothing-to-sharpen-to bullet, missing from
a table that claimed to enumerate the reclassified cases.

Also: p__Bacteroidota carries five other groundings, not seven - the count
included the two this PR removes - and it is not the most-shared term either
way, so the superlative is gone. Chlorobiota's class-over-order reason was
unsupported, the two being coextensive here. SKILL.md named Bacteroides ovatus,
which carries no pin; the ninth block is a second Allobosea.

Canaried: all four pins are skipped by --apply, and the only record that moves
is Rifle_Aquifer, on its pre-existing NOT_ATTEMPTED Geobacter entry that main
drifts on identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
realmarcin and others added 2 commits August 6, 2026 02:47
Round 3 falsified the completeness claim for the third time, with the largest
demotion in the KB. Betaproteobacteria (NCBITaxon:28216) grounds to
c__Gammaproteobacteria in two records; GTDB demoted it to the order
Burkholderiales, which holds 41903 of the 41937 named-species genomes - more
than all previously pinned cases combined. Both records are now pinned.

What matters is *why* it was missed twice. It defeats both screens by
construction, not by accident: it is not unanimous (0.999, with 31 genomes in
Enterobacterales and 3 in Pseudomonadales), and GTDB renamed while demoting, so
Betaproteobacteria and Burkholderiales share one letter and the name test scores
it 1 against a floor of 5.

So the docstring no longer claims to enumerate demotions. Three rounds produced
three misses from three different structural blind spots - suffix splits,
sub-unanimity, rename-while-demoting - and the honest conclusion is that a
screen is not what makes one findable. The module records the calls a curator
made and the evidence for each, so a tool re-run cannot undo them silently.

The crosswalk test demanded that every named-species row carry the chosen term,
which is the same unanimity assumption round 3 refuted, and Betaproteobacteria
fails it. It now verifies the block's own stored counts against the crosswalk -
support_genomes, total_genomes and majority_fraction - which is both weaker
about unanimity and stronger about everything else. The name test skips the two
renamed-while-demoted pins explicitly rather than dropping its threshold to
admit them, which would admit anything.

Two notes were wrong. The Nitrososphaeria one had GTDB's suffix convention
backwards: _A marks lineages that are *not* monophyletic with the type, and
those eight rows are Caldarchaeales thermophiles, not Thaumarchaeota - which is
why the block counts only the unsuffixed name. It also cited Crenarchaeota and
Korarchaeota, neither of which appears as an NCBI phylum in the crosswalk.
The Ignavibacteria one called the order narrower than the class when the two are
coextensive under the rows the block counts, contradicting the Chlorobiia note
about the identical fact pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… it ship

The two Betaproteobacteria blocks claimed "4 GTDB taxa under the NCBI taxon"
beside a curation_note naming three orders. Three is right - Burkholderiales,
Enterobacterales, Pseudomonadales - and no denominator yields four.

The reason it shipped is the interesting part: nothing tested mapping_source at
all. Every other field on a pin was checked against the crosswalk, and this one
was prose. The numeric test now also asserts the rank token and the alternative
count, and was verified to fail when the wrong string is put back.

Also stop quoting counts the docstring cannot keep right. Three rounds running,
a headline number in this file has been wrong - six, then twenty, then twenty-one
- while the argument it supports has never depended on the exact figure. The LCP
table now says plainly that it lists the cases that came up rather than claiming
to enumerate, and the non-reclassified group is described rather than counted.

Round 4 also re-derived the whole question independently: re-running the tool's
vote for every higher-rank grounding and diffing against what is stored, exactly
seven differ, and all seven are curated. No eighth demotion is hiding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 5 returned a merge verdict with four non-blocking nits; three are fixed
here and the fourth is filed as #454.

The provenance assertion added in round 4 used a substring match, so a block
reading "11 GTDB taxa" satisfied an expected count of 1. It is a word-boundary
match now.

The docstring used `->` for two different things - a taxon's stored grounding
and the finer term a sharpening would move it to - which made it read as though
Rhodospirillales and Ca. Eiseniibacteriota were grounded at the placeholder
terms rather than at o__RF32 and p__Eisenbacteria. Both are now named
explicitly.

The Parvarchaeales note argued that the phylum term failed to separate this
entry from its Micrarchaeota neighbour. GTDB puts Micrarchaeota in its own
phylum, so p__Nanoarchaeota would in fact have separated them; the note's
load-bearing point - six NCBI phyla collapsing into that one term - stands on
its own and now stands alone.

Filed #454 for the one real gap round 5 found: nothing validates the middle
segments of gtdb_lineage, which matters more now that seven of them are
hand-written pins rather than tool output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@realmarcin

Copy link
Copy Markdown
Contributor Author

Review complete — five rounds

The inner loop ran the full five rounds. Rounds 1–3 each falsified this PR's central claim; rounds 4 and 5 verified the result and returned a merge verdict. Issues filed: #451, #452, #453, #454.

The claim was wrong three times, for three different reasons

The PR opened by asserting Ca. Parvarchaeota was the KB's only genuine GTDB demotion. It is one of six, across seven blocks:

round missed why the screen could not see it
1 Chlorobiota, Ignavibacteriotac__Chlorobiia, c__Ignavibacteria not swept for at all
2 Nitrososphaerotac__Nitrososphaeria rows split Nitrososphaeria / Nitrososphaeria_A; strict string unanimity reads GTDB's polyphyly suffix as disagreement (#453)
3 Betaproteobacteria ×2 records → o__Burkholderiales not unanimous (0.999, 34 stray genomes) and renamed while demoting, so the name test scores it 1 against a floor of 5

Betaproteobacteria is the largest demotion in the KB — 41903 genomes, more than all the others combined — and it defeated both screens by construction.

So the module no longer claims to enumerate demotions. Three rounds, three misses, three distinct structural blind spots is the finding; a screen is not what makes one findable. What ships is the record of the calls a curator made and the evidence for each.

Tests: three that could not fail

  • round 1's compared gtdb_id against gtdb_lineage — both curator-written in the same block, so any descendant passed;
  • round 2's checked the crosswalk but "every row carries X at rank Y" is true of every ancestor, so reverting the pins still passed;
  • round 3's demanded unanimity, which Betaproteobacteria fails at 0.999.

The suite now verifies each pin's stored numberssupport_genomes, total_genomes, majority_fraction — plus the rank token and alternative count in mapping_source, which nothing checked until a block shipped saying "4 GTDB taxa" beside a note naming three. Round 5 mutation-tested all six failure modes; every one fails as required.

Counts

A headline number in the docstring was wrong in three consecutive rounds (six → 20 → 21, actual 23). The argument never depended on the figure, so the precise counts are gone rather than corrected again.

Round 5 verdict

just qc green · 1547 passed, 16 skipped · all validators clean · --apply and --refresh --apply leave every one of the 11 curated pins byte-identical. Round 5 independently re-derived the tool's vote for every higher-rank grounding: exactly seven differ from what is stored, and all seven are curated — no eighth demotion is hiding.

Merging.

@realmarcin
realmarcin merged commit 1c231ad into main Aug 6, 2026
7 checks passed
@realmarcin
realmarcin deleted the gtdb-demotion-445 branch August 6, 2026 12:11
realmarcin added a commit that referenced this pull request Aug 6, 2026
…455)

* Check the middle of gtdb_lineage, not just its head and tail (#454)

The freshness checks compare gtdb_id, gtdb_taxon and the lineage's tail; the
prokaryote-only gate (#365) reads its head. A segment corrupted in between
passed every gate and the whole suite:

    d__Bacteria;p__Bacteroidota;c__Chlorobiia
    d__Archaea;p__Nonsense;c__Chlorobiia        <- indistinguishable

That was tolerable while gtdb_ground.py wrote every lineage from the crosswalk.
#450 made seven of them hand-written curator pins, and the review that caught it
could only do so by asking the crosswalk - which CI has no checkout of, so a
crosswalk-based check would skip exactly where it is needed.

Both new checks are corpus-internal and need no mapping. Ranks must carry a
known prefix and get finer left to right, which is per-record and runs inside
validate-strict as gtdb_lineage_malformed. And because GTDB is a hierarchy, a
taxon must sit under exactly one parent path across every record naming it -
that one needs the whole corpus at once, so it runs in
`just validate-gtdb-all` and in the test suite rather than per file.

The corpus check is a weaker claim than "this lineage matches GTDB" and a much
cheaper one. It cannot catch a corruption that is internally consistent, but it
catches the case that matters: a hand-edited pin drifting from the 720 blocks
the tool wrote.

Measured: 727 blocks, 740 distinct taxa, zero conflicts, zero malformed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address the #455 review: make the two halves of the check agree

The shape check accepted a lineage that skips a rank; the corpus check keys a
taxon by the literal path above it, so the same lineage would place its tail
under a different path than the full chain does - and be reported as a hierarchy
conflict naming the record that is correct. Two halves of one module disagreeing
about what a valid lineage is, and the failure lands on the innocent file.

Requiring contiguity, and a start at d__, turns that into a precise per-record
error against the record that actually has the problem. No crosswalk lineage
skips a rank, so this constrains hand-written pins rather than tool output -
which is the case #454 exists for.

Walk interaction participants too. source_taxon/target_taxon share taxon_term's
range, so a block is schema-valid there, and the sibling #365 gate already walks
them; none exist today, so missing them would have been silent.

The KB test counted taxonomy entries, not blocks - 1032 rather than 727 - so if
`_blocks` ever stopped matching, both checks would return [] for every record
and the test would stay green on an empty corpus. It now counts what the checks
actually walk.

Also: my insertion split the #365 comment from the loop it documents, so it read
as documentation for the new check and claimed something false of it; the
docstring said the shape check compares against gtdb_id, which it never reads;
and the CLI and justfile described only the evidence-count half of a script that
can now exit 1 for a lineage conflict, including that a single-file run cannot
find a cross-record one.

The review also cleared the risk I was most unsure of: across all 92,711
crosswalk rows there is no GTDB segment with more than one parent path, and
though 1,237 bare names are reused across ranks (UBA1381 is an order, a family
and a genus), keying on the rank-prefixed segment keeps them distinct. The
corpus check has no latent false positive from name reuse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address the #455 review, round 2: wire the shape check where the docs promised

The CLI docstring said passing one file checks its shape. It did not - the
script imported only check_corpus - so `just validate-gtdb-all`, the recipe the
docstring points at, still had the failure mode round 1 was meant to remove: a
rank-skipping lineage surfacing only as a hierarchy conflict that names the
record which is correct. The shape check now runs there too, and prints first,
so a curator sees the malformed record before the conflict it causes. Verified:
a skip-rank file alone exits 1, a clean file exits 0.

Also fixed the #365 comment, which I orphaned a second time - merged into the
new paragraph one call site down, so it documented the wrong loop and claimed
something false of it. Each comment now sits with the loop it describes, and the
#454 one names the contiguity and d__-first rules that are its substance.

Round 2 confirmed the riskiest part of round 1: making the shape rule stricter
rejects nothing real. All 727 KB lineages pass, and replicating the tool's own
lineage builder over all 92,711 crosswalk rows at every truncation level gives
47,996 distinct lineages, none of which skips a rank or fails to start at d__.
The tool cannot write data its own validator rejects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 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

None yet

Development

Successfully merging this pull request may close these issues.

A rank-for-rank GTDB vote is over-broad when GTDB demoted the NCBI clade

1 participant