Stop a plant genus from carrying a bacterial GTDB lineage (#365) - #436
Conversation
`NCBITaxon:169215` is the plant genus Bosea (Amaranthaceae). Two records used it for the alphaproteobacterium of the same name and carried a GTDB block derived from it, so the KB asserted a plant was a bacterium — in the field that looks most independently sourced. Nothing could see it. `ncbi_source_id == term.id`, so #364's freshness test passes; "Bosea" really is that id's label, so id<->label passes; and the id appears once per record, so #292's shared-id gate has nothing to compare. The block came from a name collision, not a typo: gtdb_ground.py's higher-rank path matches the cleaned label string rather than the id. Add `prokaryotic_lineage`, which needs no curation judgement: GTDB classifies only Bacteria and Archaea, so an id outside those domains cannot carry a GTDB lineage at all, and an archaeon under d__Bacteria is the same contradiction. Measured over the KB it flags exactly the two known records and nothing else. Wired into validate_strict.py with a `just validate-gtdb-domain` recipe. Correct both records to NCBITaxon:85413 — the bacterium NCBI renamed *Allobosea*, aliased "Bosea Das et al. 1996"; its type species Allobosea thiooxidans confirms the parentage. The ids in the interaction participants are corrected too, where no lineage exists for the gate to check. The GTDB block itself was always right for the organism, so it is pinned with `curated: true` rather than dropped: the mapping snapshot still carries the pre-rename genus name, so the tool cannot re-derive it from the corrected label. Canaried — `--apply` skips both and leaves the files byte-identical. Also reconcile NEXT_TASKS.md, which was three days and 38 merged PRs stale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Network integrity findingsWarnings 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. The full report is attached to the workflow run as an artifact. |
…i_domain The review refuted a claim in this gate's own docstring. It asserted that NCBI and GTDB never disagree about Bacteria vs Archaea, and flagged the disagreement as a contradiction. They do disagree: 8 rows of this repo's own NCBI2GTDB.tsv.gz do, and the gate fired on every block gtdb_ground.py would build from them (#437). A gate that rejects its own grounding tool's output is worse than no gate, so that arm is gone. What remains is the prokaryote-only rule, which has no counterexamples because GTDB models no other domain: a taxon that is provably a eukaryote or a virus can carry no GTDB block at all, whatever the block says. That also makes the check independent of parsing the lineage, which is what the removed arm needed. Reuse ncbi_domain instead of re-deriving it (#438). This module had shipped its own copy of the adapter, the four domain roots and the ancestor query — the same lookup #393 already added, differing only in return type. Two copies is how two gates come to disagree about one taxon; `outside_gtdb_scope` already said exactly what this needed. Fix a crash of the same class as #429: `gtdb_lineage` has no `range` in the schema, so a YAML list is schema-valid, and `(lineage or "").split` raised AttributeError out of the worker, aborting all 316 files and writing no TSV. Walk interaction participants too (#439). `source_taxon`/`target_taxon` share `taxon_term`'s range, so a GTDB block is schema-valid there — and both defective records named the plant id in exactly that position. Test fixes (#440): a wrong organism name in the file whose subject is wrong organism names (NCBITaxon:2172 is Methanobrevibacter, not Methanosarcina); the once-only warning flag leaked across the session and the warning had no test; and the legitimate cases were conflated with the merely-unjudgeable ones, which now assert their domain really is unresolvable rather than only that the gate stayed quiet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review round 2 — findings addressedThree independent adversarial passes (gate correctness, the taxonomic call, tests/enforcement). Six issues filed: #437–#442. Four fixed here; two deferred. The review refuted a claim in this gate's own docstring — #437It asserted that NCBI and GTDB never disagree about Bacteria vs Archaea, and treated a disagreement as a contradiction. They do disagree: 8 rows of this repo's own What remains is the half with no counterexamples, because GTDB models no other domain: a taxon that is provably a eukaryote or a virus can carry no GTDB block at all, whatever the block says. That also makes the check independent of parsing the lineage — which only the removed arm needed. Also fixed
Plus a wrong PR number in the Deferred
What the review confirmed rather than brokeThe taxonomic call stands:
|
Closes #365.
The defect
NCBITaxon:169215is the plant genus Bosea (Viridiplantae → Amaranthaceae). Two records used it for the alphaproteobacterium of the same name and carried a GTDB block derived from it:kb/communities/MSC1_Dominant_Core.yaml—Bosea sp.kb/communities/EcoFAB_Ring_Trial_SynCom17.yaml—Bosea sp. OAE506both reading
d__Bacteria;p__Pseudomonadota;…;g__Bosea. The KB asserted that a plant genus is a bacterium, in the field that looks most independently sourced.It came from a name collision, not a typo:
gtdb_ground.py's higher-rank path matches on the cleaned label string rather than the id, so "Bosea" resolved to the bacterial genus while the id pointed at the plant.Invisible to every gate.
ncbi_source_id == term.id, so #364's freshness test passes. "Bosea" genuinely is that id's label, so id↔label passes.linkml-validatehas nothing to say. And #292's shared-id gate can't help: the id appears once per record, so there is no second organism to disagree with.The gate
prokaryotic_lineageuses a signal that needs no curation judgement: GTDB classifies only Bacteria and Archaea. An id outside those two domains can never carry a GTDB lineage, which makes this a contradiction rather than a suspicion. Both directions are checked, since both are the same contradiction — a non-prokaryotic id under a prokaryotic lineage (the #365 defect), and an archaeon underd__Bacteriaor the reverse. GTDB and NCBI disagree about phyla and genera, which is whatis_reclassifiedrecords; they do not disagree about domain.Measured over all 316 records: flags exactly the two known records, nothing else. As with the #292 gate, an id whose domain cannot be resolved is never judged, and a missing NCBITaxon warns on stderr rather than passing silently.
Wired into
validate_strict.py(categorygtdb_lineage_contradicts_id_domain) with a single-filejust validate-gtdb-domainrecipe.The fix
Both records move to
NCBITaxon:85413— the bacterium NCBI renamed Allobosea, which still carries the alias"Bosea Das et al. 1996". Confirmed via its type species:NCBITaxon:53254"Allobosea thiooxidans" (which the GTDB mapping table still calls Bosea thiooxidans) has parentNCBITaxon:85413. The two genera are homonyms and the bacterial one was renamed.The interaction participants are corrected too — both records also named the plant id as an interaction target, where there is no lineage for the gate to compare.
The GTDB block itself was always right for the organism, so it is pinned with
curated: true+curation_noterather than dropped. The mapping snapshot still carries the pre-rename genus name, so the tool cannot re-derive it from the corrected label. Canaried:--applyon both records reportsapplied 0 block(s), logsskipping curated NCBITaxon:85413, and leaves the files byte-identical — no drift on the next tool run.is_reclassifiedbecomestrue, which is now the honest value: NCBI says Allobosea, GTDB says Bosea.Also
NEXT_TASKS.mdreconciled — it was three days and 38 merged PRs stale. The new section names the thread these gates belong to (a grounding that is wrong in a way every gate reads as right) and records the method that found all three, since none came from a sweep.Validation
just qcgreen. 26 new tests, including the KB-clean sweep, the malformed-input guards (#429's lesson), an adapter-liveness assertion so the sweep can't pass vacuously (#433's lesson), and an end-to-end check that the gate fires throughvalidate-strictwith a vendored fixture (#428's lesson).🤖 Generated with Claude Code