Skip to content

Phase 4: validation, the production taxonomy path, and the staged run (3 of 3) - #15

Open
fchen13 wants to merge 15 commits into
genomehubs:mainfrom
fchen13:feature/assembly-version-phase4
Open

fchen13 wants to merge 15 commits into
genomehubs:mainfrom
fchen13:feature/assembly-version-phase4

Conversation

@fchen13

@fchen13 fchen13 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 4, the last of three — the deliverable the previous two were groundwork for. #13 and #14 fixed what the pipeline writes; this validates it, wires Phase 3 onto the taxonomy columns you emit, and scripts the staged run.

What's included

Two validators, runnable modules rather than pytest files, since they check a real working directory after a real run:

  • tests/validate_pipeline.py — nine cross-file checks over the four output TSVs: assembly IDs unique across current + historical, summary version_gaps reconciling with the versions actually present, every superseded_by resolving to a known accession of the same base, every base summarised and no summary row stale, the nested milestone dates never inverting, every first_*_in_ranks a canonical rank, and three on whether the upstream enrichment reached the TSV. Checks carry a severity: an error always fails, a warning fails only under --strict, a note never does. That distinction is load-bearing — a dev run off a local taxdump legitimately has no lineage columns, while a production run should treat their absence as fatal.
  • tests/validate_no_ncbi_fetches.py — runs parse_assembly_versions with socket.socket, create_connection, getaddrinfo and subprocess.Popen all raising, over a fixture that has real supersessions and a real gap so a run that proves nothing cannot pass. Its second check asserts an unchanged input supersedes nothing and reports nothing missing, which is what "no unnecessary fetches" means day to day.

Phase 3 on your taxonomy columns (flows/lib/assembly_lineage.py). Phase 3 previously rebuilt every lineage from a taxdump and its production path was a raise. It now reads genusTaxIdkingdomTaxId, and — since 630d327 last week — speciesTaxId, so it runs off the columns alone, off a taxdump alone, or off both with the columns winning and the taxdump supplying the names they cannot carry.

Two things about that contract are handled in one place, RANK_COLUMN_TEMPLATE being the single point of repoint if the names change:

  • "None" is not absent-looking. f14ea28 writes the four-character string into a taxid column for a rank present with a null taxid; "" comes from the other branch. Read as a taxid, "None" would collapse every affected lineage into one bogus taxon.
  • An empty speciesTaxId falls back to the row's own taxid rather than being read as "no species". Whether the lineage you walk includes the taxon itself is not visible from this repo — if it does not, every species-level row carries an empty speciesTaxId beside a populated genus, and dropping those would discard most of the dataset. The fallback is correct under either shape. Worth a word from you if you know which it is.

The staged run (tests/staged_run.py). Stages 1 and 2 are scripted and were run against Malacostraca (6681) — 207 assemblies, 18 above version 1, one at v4. Both stages complete, both validators pass. Stage 2 needs a yesterday that differs from today, so rather than fabricate tomorrow — which would leave the gap fill chasing accessions that do not exist — it rewinds the multi-version assemblies to make day 1, so every version the pipeline goes looking for is one NCBI actually has. Stages 3 and 4, the full backfill and full run, are the operational half and are not in this PR.

Tests. 264 passing, up from 160 on #14. test_two_day_simulation.py runs all four phases in sequence over one working directory — the only test that exercises how the phases compose rather than one module at a time.

Also here

Ten committed .pyc files and a stale Phase_0_PR_SUMMARY.md are deleted. Unrelated to Phase 4, but they are build artefacts that should not be tracked, and they were in the way.

Testing

SKIP_PREFECT=true python -m pytest tests/ -q
python -m tests.validate_pipeline --work_dir <dir> --yaml_path <config> --strict
python -m tests.validate_no_ncbi_fetches --work_dir <dir>

tests/README_phase_4_validation.md documents each check, the three severities, the four ways Phase 3 can now run, and the staged-run runbook.

Notes

Rebased onto main after #13 and #14 merged; the two commits you reviewed there are gone from this diff. One question worth your eye is the speciesTaxId fallback above — everything else here is ours to get wrong.

Summary by Sourcery

Validate the assembled pipeline outputs, connect milestone computation to production lineage columns, and script staged end-to-end verification.

New Features:

  • Add cross-file pipeline and offline daily-diff validators with configurable error, warning, and note severities.
  • Enable taxon milestone computation from assembly-row lineage and species taxid columns, with optional taxdump support.
  • Add a staged real-data run covering slice processing and two-day version-diff validation.

Bug Fixes:

  • Prevent literal "None" taxonomy values from being interpreted as taxids and collapsing unrelated lineages.
  • Treat empty species taxid values as a fallback to the assembly's own taxid.
  • Report unresolved taxonomy rows collectively instead of emitting one message per row.

Enhancements:

  • Centralize assembly lineage column and taxid handling for production and taxdump taxonomy sources.
  • Expand end-to-end and unit test coverage for taxonomy resolution, output invariants, offline behavior, and multi-phase composition.

Documentation:

  • Document Phase 4 validation checks, taxonomy input modes, and the staged-run procedure.
  • Clarify handling of the literal "None" sentinel across assembly summary and milestone processing.

Tests:

  • Add validators for output presence, identifiers, version gaps, supersession references, summary completeness, milestone ordering, canonical ranks, and lineage coverage.
  • Add offline tests proving daily version parsing performs no network or subprocess fetches and avoids unnecessary updates.
  • Add an end-to-end two-day simulation covering the pipeline phases and diff paths.

Chores:

  • Remove tracked Python bytecode artifacts and the obsolete Phase 0 pull-request summary.

validate_pipeline.py checks the four output TSVs against each other
rather than against a mock: assembly IDs unique across current and
historical, summary version_gaps reconciling with the versions actually
present, every superseded_by resolving to a known accession of the same
base, every base accession summarised and no summary row stale, the
nested milestone dates never inverting, and every first_*_in_ranks value
a canonical rank.

Two readings of the plan are deliberately looser than its wording. A
superseded_by referent is looked up across current and historical
together, since a v1 superseded by v2 stays correct once v2 has itself
been superseded. Milestone ordering is checked pair by pair rather than
by whole triples, so an inversion is caught when the third date is
absent.

A seventh check asserts the upstream {rank}TaxId columns arrived
populated. print_to_tsv writes only the columns the types YAML declares,
so the enrichment can be a silent no-op upstream and Phase 3 then
produces empty milestones. It is a warning by default, because a dev run
off a taxdump legitimately has no lineage columns; --strict promotes it,
which is what production runs should use.

validate_no_ncbi_fetches.py runs parse_assembly_versions with sockets
and subprocess.Popen blocked, over a fixture that has real supersessions
and a real gap so a run that proves nothing cannot pass. Its second
check is the one that captures "minimise fetches": an unchanged input
must supersede nothing, report nothing missing, and leave the historical
TSV byte-identical. Before the PR-B diff fix that failed on roughly
3,694 spurious entries.

F1-F3 survived review because nobody re-ran the suite against a
realistic schema at the right moment, so pytest.yml now runs the suite
on every pull request. pip rather than conda: the tests need no network
and no datasets CLI.

Fixtures are built inline, since tests/test_data is excluded from the
repo and anything placed there would not be committed.
Splitting lineage-coverage off from lineage-columns, and adding a third
severity for it. An error always fails, a warning fails only under
--strict, and a note never fails.

The structural problem is worth failing a production run for: the
columns absent entirely, or present and empty on every row, means the
upstream enrichment was a no-op and Phase 3 has no taxonomy source. Some
rows sitting on a taxid the upstream lookup does not cover is a
different thing — a number to watch, not a defect, and not a reason for
one uncovered row to fail a 57,000-row run.
register_row_taxa took a taxid's rank from whichever row reached it first.
An assembly submitted at genus level carries a taxid another row names as
its genus, so the same input could label that taxon a species or a genus
depending on the order rows happened to appear in the file.

Ancestors are now registered before row taxids. The outcome no longer
depends on order, and it matches what the taxdump path already does with
such an assembly: registered as a genus it has no species ancestor, so the
sweep skips it and says so.

The per-row skip print became a five-line summary with a count, since a
full run would otherwise emit thousands of lines. LEVELS derives from
LINEAGE_RANKS rather than repeating the rank list a third time, the two
milestone predicates read through the shared cell helper, and a dead
import went.
Every other test in the suite exercises one module. test_two_day_simulation
runs all four in sequence over one working directory: a backfilled day 1,
then a snapshot, a JSONL covering the unchanged, +1, skipped-version and
new-series diff paths, today's current TSV, the summary, the milestones and
both validators. It is the synthetic half of step 3 stage 2 of the staged
run, so what the real staged run adds is real data rather than new coverage
of the composition.

Docs follow the code. The Phase 3 README described a production path that
was a raise when it was written; it now points at assembly_lineage and says
what each taxonomy source can and cannot supply. The Phase 2 README gains
the "None" sentinel rule. The Phase 4 README picks up the rank-registration
order, the shared cell helper, the new test files, and a table that said
three ways to run while listing four.
staged_run.py drives the two stages that do not need an overnight window:
fetch a clade, parse it, backfill its historical versions, aggregate and
validate; then run the daily path twice over it.

Stage 2 needs a yesterday that differs from today. Fabricating tomorrow
would leave the gap fill asking NCBI for accessions that do not exist, so
it fabricates yesterday instead, rewinding the multi-version assemblies
by one version. Everything the pipeline then goes looking for is a version
NCBI actually has.

Run against Malacostraca: 207 assemblies, 18 above v1, one at v4. Both
stages complete and both validators pass. Two defects in validate_pipeline
turned up that no fixture had provoked:

referential-integrity flagged all 19 backfilled rows, because their
superseded_by reads "None" — PR-A declares those columns in the historical
YAML, Phase 0 has no values for them, and the GenomeHubs write path
stringifies the absence. The check was reading the column raw rather than
through cell, making it the last place in the codebase not applying our own
sentinel rule.

assembly-id-uniqueness failed with "186 rows carry no assembly ID". True,
and not a defect: the types YAML in test/ excludes the assembly_id
identifier, so the current TSV has no such column. Which identifiers reach
the TSV is a config decision, not a pipeline error. Uniqueness keeps the
duplicates and the mixed case; a source with no ID column at all is now a
note.

The rewound count and the supersession count are not expected to match,
and the script no longer pretends otherwise: RefSeq bases fold into their
GenBank row, and a rewind can leave the parser holding two versions of one
base, which the diff rightly calls unchanged. It now fails only if nothing
was superseded, which would mean the path never fired.
The encoding failure is a developer-machine issue: production is Linux with
a UTF-8 locale, and Python coerces the C locale to UTF-8 anyway, so the flow
cannot hit it there. Not worth a patch to Rich's fetcher.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry @fchen13, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR completes Phase 4 by adding real-working-directory validators and offline fetch checks, wiring Phase 3 to upstream taxonomy columns with taxdump fallback and species-taxid handling, and providing staged-run tooling plus end-to-end CI coverage for the composed pipeline.

Sequence diagram for production taxonomy milestone computation

sequenceDiagram
    participant TSV as Assembly TSVs
    participant CM as compute_taxon_milestones
    participant AL as assembly_lineage
    participant TAX as Optional taxdump
    participant OUT as Milestone summary TSV

    CM->>TSV: load_assemblies()
    CM->>TAX: build_taxonomy(taxdump_path)
    CM->>AL: register_row_taxa(taxonomy, rows)
    CM->>AL: row_species_taxid(row)
    alt speciesTaxId is populated
        AL-->>CM: species taxid
    else speciesTaxId is absent
        AL->>AL: get_row_taxid(row)
        AL-->>CM: row taxid fallback
    end
    CM->>AL: row_lineage(row)
    AL-->>CM: upstream lineage or empty
    CM->>CM: compute_milestones(rows, taxonomy)
    CM->>OUT: write milestone summary
Loading

Flow diagram for staged validation

flowchart TD
    Stage1[Stage 1: generate current and historical TSVs]
    Stage2[Stage 2: rewind multi-version assemblies]
    Pipeline[Composed pipeline run]
    Validate[validate_pipeline.py]
    Offline[validate_no_ncbi_fetches.py]
    Pass[Validated staged result]

    Stage1 --> Stage2
    Stage2 --> Pipeline
    Pipeline --> Validate
    Pipeline --> Offline
    Validate --> Pass
    Offline --> Pass
Loading

File-Level Changes

Change Details Files
Adds post-run validation for cross-file pipeline invariants and offline behavior.
  • Introduces a validator covering output presence, ID uniqueness, version gaps, supersession references, summary completeness, milestone ordering, canonical ranks, lineage columns, and lineage coverage.
  • Adds severity-aware reporting with optional strict handling for warnings.
  • Adds an offline parser harness that blocks socket and subprocess access and verifies real supersession/gap work plus unchanged-input behavior.
tests/validate_pipeline.py
tests/validate_no_ncbi_fetches.py
tests/test_phase_4_validators.py
Moves Phase 3 taxonomy resolution onto enriched assembly-row lineage while retaining taxdump compatibility.
  • Centralizes {rank}TaxId column mapping and parsing of empty and literal None taxid sentinels.
  • Uses speciesTaxId when available, with fallback to the row taxid for empty or older inputs.
  • Registers row-provided taxa without overwriting taxdump nodes, and combines row lineage with taxdump names and fallback resolution.
  • Allows runs with lineage columns alone, taxdump alone, or both; fails when neither source is available.
flows/lib/assembly_lineage.py
flows/lib/compute_taxon_milestones.py
tests/test_assembly_lineage.py
tests/README_phase_3_taxon_milestones.md
tests/README_phase_2_assembly_summary.md
Adds an operational staged-run driver and an end-to-end two-day composition test.
  • Scripts slice and two-day stages using real NCBI data, rewinding multi-version records to create a valid prior day.
  • Runs parsing, backfill, aggregation, milestone computation, and both validators within isolated stage directories.
  • Exercises unchanged, superseded, skipped-version, and new-series paths over one working directory in CI.
tests/staged_run.py
tests/test_two_day_simulation.py
tests/README_phase_4_validation.md
Documents Phase 4 operation and enables pull-request CI coverage.
  • Documents validator checks, taxonomy-source contracts, sentinel handling, staged-run instructions, and production caveats.
  • Adds a pytest workflow for pull requests.
tests/README_phase_4_validation.md
.github/workflows/pytest.yml
Removes unrelated tracked artifacts from the repository.
  • Deletes the stale Phase 0 summary and committed Python bytecode files.
Phase_0_PR_SUMMARY.md
*.pyc

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Rich added species to the assembly parser's canonical ranks in 630d327
(2026-09-07), so every enriched row now names its species alongside its
genus and above. Phase 3 was written before that column existed and
resolved species by walking a taxdump parent chain, which is the last
thing tying the production path to a taxdump it is not guaranteed to have.

The sweep now takes speciesTaxId when the row carries one, and
register_row_taxa registers that taxon rather than assuming the row's own
taxid is a species. A subspecies-level assembly is therefore attributed
to its species with no taxonomy source but the row itself, and two
subspecies of one species stop appearing as two pseudo-species.

The walk stays as the fallback, and the fallback covers the
column-present-but-empty case rather than reading it as "no species".
Upstream builds the lineage by walking rec["lineage"], and whether that
array includes the taxon itself is not visible from this repo: if it does
not, every species-level row carries an empty speciesTaxId beside a
populated genus, and dropping those would discard most of the dataset.
Falling back is correct under either shape and never does worse than the
behaviour before the column existed.

Species is kept out of LINEAGE_RANKS: it is the level rows are grouped
at, not an ancestor the sweep walks up to, and conflating the two would
have it credited twice per row.
@fchen13
fchen13 force-pushed the feature/assembly-version-phase4 branch from 1b3eaeb to 5dbb19e Compare September 9, 2026 19:12
blobtk includes a taxon in its own lineage array. Node::to_json in
rust/src/parse/nodes.rs pushes the node at node_depth 0 before appending
its ancestors, and write_taxdump writes every nodes.jsonl line through
it. Nodes::lineage() alone returns ancestors only, which is what made
the shape ambiguous from outside the repo.

So a species-level row carries its own taxid in speciesTaxId, and Rich's
column reaches the species-level majority rather than only the
subspecies minority. Nothing to raise: drop the spot-check from the
unsent §8 draft and record why.

No behaviour change. The empty-column fallback stays, now justified by
the two cases that remain empty -- an older TSV, or a row above species
rank -- instead of by an unknown.
Step 4 carried a "Still open with Rich" list and three "Resolved..."
blocks recording how each question got answered and when. That is the
history of working it out, not what someone reviewing the PR needs. The
one remaining open item -- whether the literal "None" is deliberate --
is handled either way and already documented in the traps above it, so
it is not a question Rich has to answer to approve.

Rewrite the block as three claims about the code: why the taxdump is no
longer needed in production, why scientific names do not change that,
and that the lineage columns are declared in goat-data so the
lineage-columns check passes on a real run.

Also stop implying names are wanted. No phase consumes a scientific
name; the milestone output declares the column but nothing reads the
file back, since it is a GoaT import that resolves taxids. The taxdump
is a dev and test convenience.
Steps ran 1, 2, 4, 3. The production taxonomy path is now Step 3 and the
staged run Step 4, which is also the order they happen in.

The file table listed test_assembly_summary.py and pytest.yml without
saying they merged with genomehubs#13, so two of ten rows are not in this diff.
Say up front that Phase 4 shipped as three stacked PRs and mark those
rows. "Unit tests for Phase 2.2, which had none" read as though the test
file had no tests; it meant the summary generator shipped without any.

The sentinel paragraph said Phase 2 and Phase 3 disagreed but never said
how it was settled: Phase 3 was already reading through cell, Phase 2
was routed through the same helper in genomehubs#13 rather than either place
special-casing the string.

PR-A and PR-B merged on 2026-09-07 as genomehubs#13 and genomehubs#14, so the "do not run
against real data until PR-A is merged" blocker is stale. Refer to the
merged PR numbers instead of the pre-submission labels.

Drop the PYTHONUTF8 note. It is a Windows dev-environment workaround,
not something a reviewer needs, and it sat between list items 2 and 3
breaking the numbering. The full explanation is already in
project_doc/Phase_4_plan.md; the runbook keeps a short inline comment.
@fchen13
fchen13 marked this pull request as draft September 11, 2026 14:18
@fchen13
fchen13 marked this pull request as ready for review September 11, 2026 14:24

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry @fchen13, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 4 hours and 24 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

validate_no_ncbi_fetches.py claimed something false. The pipeline does
fetch from NCBI -- Phase 0 backfills, Phase 1.2 fills gaps, and
update_ncbi_datasets pulls the bulk JSONL daily, all by design. The
module's first paragraph existed to walk the title back, which is a sign
the title was doing the wrong work.

What it actually checks is that the daily diff stays local:
parse_assembly_versions copies superseded rows from the previous parse
and checks gaps against assembly_historical.tsv, so it should reach the
network for no input at all.

"Local" also covers the failure the old name missed. The step can
reintroduce daily fetching without making a single call itself, by
emitting a bogus missing-versions list that update_assembly_versions
then works through -- which is exactly what the pre-genomehubs#14 diff did to
~3,694 assemblies. The unchanged-input check catches that; a name about
sockets does not describe it.

Renamed the module, its entry point, the import alias and the output
banner. No behaviour change.
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.

1 participant