Skip to content

Phase 3: taxon milestone computation#12

Draft
fchen13 wants to merge 1 commit into
genomehubs:mainfrom
fchen13:feature/assembly-version-phase3
Draft

Phase 3: taxon milestone computation#12
fchen13 wants to merge 1 commit into
genomehubs:mainfrom
fchen13:feature/assembly-version-phase3

Conversation

@fchen13

@fchen13 fchen13 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 3 of the assembly version tracking system. Computes per-taxon assembly milestone firsts from the version-tracked TSVs produced by Phases 0–2, writing taxon_milestone_summary.tsv.

GoaT stores only the current assembly version and has no concept of "first" — a genome that first reached a milestone but was later superseded leaves no trace in it. Phase 3 supplies that missing time axis: when a taxon (at species and every higher rank) first reached each milestone, and which assembly/species got there first.

What's included

Two modules + tests + docs (4 files):

  • flows/lib/load_taxonomy.py — dev/test NCBI taxdump parser building the taxonomy contract (taxid -> scientific_name, rank, parent, canonical-rank lineage). Pure parse/build, no download or cache. Cycle-guarded parent-chain walks. In production the lineage is attached upstream; this module is dev/test only.
  • flows/lib/compute_taxon_milestones.py — a single chronological sweep producing species and higher-rank milestones together; first-in-ranks recorded as a by-product of the higher-rank step; per-species latest_assembly / current_assemblies / total_assemblies. Runnable with SKIP_PREFECT=true; emits compute.taxon_milestones.completed.
  • tests/test_taxon_milestones.py — 38 tests (parser, resolve_to_species, the sweep, output schema, validation invariants).
  • tests/README_phase_3_taxon_milestones.md — run/test guide and output schema.

The four milestones

Milestone Predicate
first_assembly any assembly
first_ebp_assembly PRJNA533106 in bioProjectAccession
first_metric ebpStandardDate non-empty
first_ebp_metric metric AND affiliated

first_assembly_in_ranks / first_metric_in_ranks record the canonical ranks at which a species was the earliest in its clade to reach the milestone.

Wiring

Phase 3 triggers off update.assembly_versions.completed (parallel with Phase 2), same as documented in the README. The Prefect deployment wiring is intentionally not included here — left to the workflow owner.

Testing

```
SKIP_PREFECT=true python3 -m pytest tests/test_taxon_milestones.py -v
```

38 passing. Validation invariants (nested milestone-date ordering; in_ranks ⊆ CANONICAL_RANKS) asserted in the suite.

Summary by Sourcery

Introduce Phase 3 taxon milestone computation that derives per-taxon assembly milestone firsts from version-tracked assembly TSVs and writes a consolidated milestone summary TSV.

New Features:

  • Add a taxonomy loader that parses NCBI taxdump files into a canonical taxonomy contract with species-to-lineage mapping for dev/test use.
  • Implement a single-sweep taxon milestone computation flow that aggregates species and higher-rank milestone dates, first-in-ranks flags, and per-species assembly counts into taxon_milestone_summary.tsv.

Enhancements:

  • Provide a command-line entry point and Prefect-integrated flow for computing taxon milestones, emitting a completion event for downstream workflows.
  • Add a dedicated test suite and README documenting the Phase 3 milestone computation, taxonomy contract, invariants, and end-to-end usage.

Documentation:

  • Add Phase 3 README explaining taxon milestone semantics, taxonomy contract, pipeline wiring, output schema, and testing instructions.

Tests:

  • Introduce comprehensive tests for taxonomy parsing, species resolution, milestone sweep logic, output schema, validation invariants, and an end-to-end flow run.

Add per-taxon assembly milestone tracking, computing when each taxon (at
species and every higher rank) first reached four milestones — any assembly,
EBP-affiliated assembly, meets-metric, and affiliated-and-meets-metric — from
the version-tracked assembly TSVs that GoaT itself does not retain.

- flows/lib/load_taxonomy.py: dev/test NCBI taxdump parser building the
  taxonomy contract (taxid -> scientific_name, rank, parent, canonical-rank
  lineage). Cycle-guarded parent-chain walks. Production uses upstream lineage.
- flows/lib/compute_taxon_milestones.py: single chronological sweep producing
  species and higher-rank milestones; first-in-ranks recorded as a by-product;
  per-species latest_assembly / current / total counts. Emits
  compute.taxon_milestones.completed. Writes taxon_milestone_summary.tsv.
- tests/test_taxon_milestones.py: 38 tests covering the parser, resolve_to_species,
  the sweep (each milestone earliest, tie-break, empty/None dates, in-ranks,
  latest-assembly), output schema, and validation invariants.
- tests/README_phase_3_taxon_milestones.md: run/test guide and schema.
@sourcery-ai

sourcery-ai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements Phase 3 of the assembly version tracking system: a taxonomy loader and a single-pass taxon milestone computation flow that reads version-tracked assembly TSVs, resolves taxids to species + canonical lineage, computes four milestone “firsts” across all ranks in one chronological sweep, and writes taxon_milestone_summary.tsv, with a focused test suite and documentation describing usage and invariants.

Sequence diagram for the compute_taxon_milestones flow

sequenceDiagram
    participant Driver as compute_taxon_milestones
    participant Taxdump as taxdump_dir
    participant Taxonomy as build_taxonomy
    participant Assemblies as assembly_TSVs
    participant Milestones as compute_milestones
    participant Output as build_output_rows
    participant Storage as taxon_milestone_summary.tsv
    participant Events as emit_event

    Driver->>Taxonomy: build_taxonomy(taxdump_path)
    Taxonomy-->>Driver: taxonomy

    Driver->>Assemblies: load_assemblies(assembly_current.tsv, assembly_historical.tsv)
    Assemblies-->>Driver: rows

    alt rows is empty
        Driver->>Events: emit_event(event="compute.taxon_milestones.completed")
        Driver-->>Driver: return
    else rows is non_empty
        Driver->>Milestones: compute_milestones(rows, taxonomy)
        Milestones-->>Driver: taxa, species_extra

        Driver->>Output: build_output_rows(taxa, species_extra)
        Output-->>Driver: output_rows

        Driver->>Storage: csv.DictWriter(writerows(output_rows))

        Driver->>Events: emit_event(event="compute.taxon_milestones.completed")
    end
Loading

File-Level Changes

Change Details Files
Introduce dev/test NCBI taxdump parser that builds the taxonomy contract used by milestone computation.
  • Parse nodes.dmp into a taxid→{parent, rank} mapping with robust .dmp line splitting.
  • Parse names.dmp into taxid→scientific_name, retaining only scientific-name rows.
  • Build canonical-rank lineage maps by walking parent chains with cycle protection and filtering to genus..kingdom.
  • Compose the full taxonomy contract (scientific_name, rank, parent, lineage) and expose CANONICAL_RANKS plus a CLI entry point via shared_args.
flows/lib/load_taxonomy.py
Add taxon milestone computation flow that performs a single chronological sweep over current+historical assemblies to compute per-taxon milestone “firsts”, species-level first-in-ranks, and per-species counts/latest, then writes a summary TSV and emits a completion event.
  • Define milestone configuration (four keys, output column prefixes, in_ranks applicability) and output schema fieldnames.
  • Normalize assembly rows across phases (accession, releaseDate, versionStatus) and implement EBP affiliation and metric predicates, including handling of literal 'None'.
  • Load and combine current and historical assembly TSVs, resolving each taxId to its species ancestor via resolve_to_species, attaching canonical lineage, and skipping/logging unresolvable taxids.
  • Compute per-species totals, current_assemblies, and latest assembly with a preference for current over superseded, including fallback when all rows are superseded.
  • Run a global chronological sort by (date, accession), exclude empty-date rows from milestone dates while still counting them, and for each milestone walk species+lineage ranks, recording first-touch dates/accessions and populating species in_ranks for any-submitter milestones.
  • Ensure all resolved species get taxa entries even if they have only empty-date rows, then build deterministic output rows for all taxa, populating species-specific counts/in-ranks/latest and blanking those fields for higher-rank rows, sorted by (rank, taxid).
  • Wrap the computation in a Prefect flow that loads taxonomy from a dev/test taxdump (failing loudly if absent), reads inputs from work_dir, writes taxon_milestone_summary.tsv, logs a summary, and emits compute.taxon_milestones.completed with payload for both success and no-op cases, plus a CLI entry point via shared_args.
flows/lib/compute_taxon_milestones.py
Add focused tests and documentation for Phase 3, covering taxonomy parsing, species resolution, milestone sweep behavior, output schema, invariants, and end-to-end flow.
  • Create a comprehensive pytest module that exercises load_taxonomy parsing (nodes, names, lineage, taxonomy shape), resolve_to_species (species passthrough, subspecies→species, unresolvable/unknown, cycle/self-parent guards), and the milestone sweep (earliest-row selection, accession tiebreaks, EBP affiliation and metric predicates, handling of empty/"None" dates, species with only empty-date assemblies, current vs superseded logic for counts/latest, higher-rank clade-first behavior, and in_ranks correctness including subset-of-CANONICAL_RANKS).
  • Add tests for build_output_rows ensuring species vs higher-rank rows have appropriate counts/in-ranks/latest fields, that all rows match OUTPUT_FIELDNAMES exactly, and that nested milestone-date ordering invariants and in_ranks⊆CANONICAL_RANKS hold across all rows.
  • Include end-to-end tests that write minimal assembly_current.tsv and assembly_historical.tsv fixtures, run compute_taxon_milestones against the test taxonomy, validate presence and rank of species rows, check clade-earliest first_assembly at a higher rank, assert header equality to OUTPUT_FIELDNAMES, and confirm no-op behavior when no TSVs exist.
  • Document Phase 3 in a README describing modules, pipeline wiring, taxonomy contract, milestone definitions, chronological sweep design (including first-in-ranks derivation), output schema, invariants, CLI usage, and a minimal example run with expected behaviors.
tests/test_taxon_milestones.py
tests/README_phase_3_taxon_milestones.md

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

@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.

Hey - I've found 4 issues, and left some high level feedback:

  • The console print usage in compute_taxon_milestones (including per-row skip messages) may get very noisy at scale; consider routing these through the existing logging/Prefect facilities or gating verbose output behind a flag.
  • In compute_milestones, the empty_date counter is initialized and then immediately reassigned after filtering; you can drop the initial assignment and only compute it once from len(enriched) - len(dated) to avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The console `print` usage in `compute_taxon_milestones` (including per-row skip messages) may get very noisy at scale; consider routing these through the existing logging/Prefect facilities or gating verbose output behind a flag.
- In `compute_milestones`, the `empty_date` counter is initialized and then immediately reassigned after filtering; you can drop the initial assignment and only compute it once from `len(enriched) - len(dated)` to avoid confusion.

## Individual Comments

### Comment 1
<location path="tests/test_taxon_milestones.py" line_range="255" />
<code_context>
+        taxa, _ = compute_milestones(rows, taxonomy)
+        # 'None' string must not count as metric; first metric is GCA_002.1.
+        assert taxa[ASELLUS_AQUATICUS]["firsts"]["metric"][1] == "GCA_002.1"
+        assert "metric" not in {k for k in taxa[ASELLUS_AQUATICUS]["firsts"] if False}
+
+    def test_ebp_metric_requires_both(self, taxonomy):
</code_context>
<issue_to_address>
**issue (testing):** The assertion in `test_metric_predicate_and_none_string` is effectively a no-op and does not test anything.

Because the comprehension `{k for k in taxa[ASELLUS_AQUATICUS]["firsts"] if False}` is always empty, this assertion will always succeed and never fail based on the data. If you want to verify the `firsts` content, consider asserting directly on the keys, e.g. `assert set(taxa[ASELLUS_AQUATICUS]["firsts"].keys()) == {"metric"}` or whatever set of milestones you expect.
</issue_to_address>

### Comment 2
<location path="tests/test_taxon_milestones.py" line_range="219" />
<code_context>
+# The chronological sweep
+# ---------------------------------------------------------------------------
+
+class TestSweep:
+    def test_first_assembly_is_earliest_row(self, taxonomy):
+        rows = [
</code_context>
<issue_to_address>
**suggestion (testing):** Missing test coverage for rows with non-integer or malformed `taxId` values being safely skipped.

`compute_milestones` treats rows with non‑integer `taxId` values as `skipped`, but this behavior is currently untested. Please add a test (e.g., with `taxId="abc"` or empty) that confirms these rows are excluded from the output and do not raise errors.

Suggested implementation:

```python
class TestSweep:
    def test_first_assembly_is_earliest_row(self, taxonomy):
        rows = [
            make_row("GCA_002.1", ASELLUS_AQUATICUS, "2020-01-01"),
            make_row("GCA_001.1", ASELLUS_AQUATICUS, "2018-05-05"),
        ]
        taxa, _ = compute_milestones(rows, taxonomy)
        assert taxa[ASELLUS_AQUATICUS]["firsts"]["assembly"] == ("2018-05-05", "GCA_001.1")

    def test_non_integer_taxid_rows_are_skipped(self, taxonomy):
        rows = [
            make_row("GCA_BAD.1", "abc", "2019-01-02"),
            make_row("GCA_001.1", ASELLUS_AQUATICUS, "2018-05-05"),
        ]
        taxa, skipped = compute_milestones(rows, taxonomy)

        # Rows with non-integer taxId should be safely skipped and not appear in the taxa output
        assert ASELLUS_AQUATICUS in taxa
        assert "abc" not in taxa
        assert any(row["accession"] == "GCA_BAD.1" for row in skipped)

```

Depending on the exact structure of elements in `skipped`, you may need to adjust the last assertion:
- If `skipped` is a list of dicts, `row["accession"]` is appropriate.
- If `skipped` contains row objects or tuples, update the accessor accordingly (e.g., `row.accession` or `row["assembly_accession"]`).
You may also want to add a variant using an empty string taxId (e.g., `taxId=""`) following the same pattern if `compute_milestones` distinguishes between different malformed values.
</issue_to_address>

### Comment 3
<location path="tests/test_taxon_milestones.py" line_range="312-321" />
<code_context>
+        assert species_extra[ASELLUS_AQUATICUS]["total_assemblies"] == 2
+        assert species_extra[ASELLUS_AQUATICUS]["current_assemblies"] == 1
+
+    def test_latest_is_current_version(self, taxonomy):
+        # Latest = the current (non-superseded) version, even if an out-of-order
+        # superseded row has a newer date it must not win over a current one.
+        rows = [
+            make_row("GCA_001.1", ASELLUS_AQUATICUS, "2018-01-01", version_status="superseded"),
+            make_row("GCA_001.2", ASELLUS_AQUATICUS, "2021-01-01", version_status="current"),
+        ]
+        _, species_extra = compute_milestones(rows, taxonomy)
+        latest = species_extra[ASELLUS_AQUATICUS]["latest"]
+        assert (latest["date"], latest["accession"]) == ("2021-01-01", "GCA_001.2")
+
+    def test_latest_prefers_current_over_newer_superseded(self, taxonomy):
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for the interplay between undated current assemblies and dated superseded ones in latest-selection logic.

Current tests cover dated current vs dated superseded rows and superseded-only species, but not the case where a current row has an empty `releaseDate` and a superseded row has a non-empty date. Please add a test that creates this scenario (current with empty date, superseded with a later date) and asserts that the current assembly is still selected as `latest`, to guard the `(is_current, date, accession)` tie-breaking logic against regressions.
</issue_to_address>

### Comment 4
<location path="flows/lib/compute_taxon_milestones.py" line_range="169" />
<code_context>
+    return None
+
+
+def compute_milestones(rows: list[dict], taxonomy: dict) -> tuple[dict, dict]:
+    """Run the single chronological sweep over all assembly rows.
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring latest-assembly tracking and the milestone sweep into small helpers to simplify `compute_milestones` and `build_output_rows` without changing behavior.

You can reduce complexity in `compute_milestones` and `build_output_rows` with a couple of small, local refactors that keep behavior identical:

### 1. Simplify latest-assembly tracking

The nested `latest` dict with a `key` tuple adds indirection. You can track the candidate tuple directly and derive `date`/`accession` only when needed:

```python
def _better_latest(current: tuple | None, candidate: tuple) -> bool:
    """Return True if candidate is a better 'latest' than current."""
    if current is None:
        return True
    return candidate > current  # (is_current, date, accession)
```

Use it in `compute_milestones`:

```python
# inside species_extra.setdefault(...)
"latest": None,  # (is_current, date, accession)

# ...

candidate = (is_current, item["date"], item["accession"])
if _better_latest(extra["latest"], candidate):
    extra["latest"] = candidate
```

And in `build_output_rows`:

```python
if is_species:
    extra = species_extra.get(taxid, {})
    latest = extra.get("latest")
    if latest:
        _, latest_date, latest_accession = latest
        row["latest_assembly_date"] = latest_date
        row["latest_assembly_accession"] = latest_accession
    else:
        row["latest_assembly_date"] = ""
        row["latest_assembly_accession"] = ""
```

This removes the `latest["key"]` indirection and makes the comparison logic more obvious without changing semantics.

### 2. Extract the milestone sweep into a helper

The nested loops over `MILESTONES` and `LEVELS` make the core sweep dense. A small helper reduces nesting and centralizes the “apply milestone for a row” logic:

```python
def _apply_milestones_for_row(
    item: dict,
    taxonomy: dict,
    taxa: dict,
    species_extra: dict,
    seen: dict[str, set[int]],
) -> None:
    sp = item["species_taxid"]
    lineage = item["lineage"]

    def ensure_taxon(taxid: int):
        if taxid not in taxa:
            node = taxonomy.get(taxid, {})
            taxa[taxid] = {
                "rank": node.get("rank", ""),
                "scientific_name": node.get("scientific_name", ""),
                "firsts": {},
            }

    for milestone in MILESTONES:
        key = milestone["key"]
        if not milestone_predicate(item["row"], key):
            continue
        for level in LEVELS:
            taxid = sp if level == "species" else lineage.get(level)
            if not taxid or taxid in seen[key]:
                continue
            seen[key].add(taxid)
            ensure_taxon(taxid)
            taxa[taxid]["firsts"][key] = (item["date"], item["accession"])
            if milestone["in_ranks"] and level != "species":
                species_extra[sp]["in_ranks"][key].append(level)
```

Then `compute_milestones` becomes:

```python
taxa: dict[int, dict] = {}
seen = {m["key"]: set() for m in MILESTONES}

for item in dated:
    _apply_milestones_for_row(item, taxonomy, taxa, species_extra, seen)

for sp in species_extra:
    # ensure species rows exist
    if sp not in taxa:
        node = taxonomy.get(sp, {})
        taxa[sp] = {
            "rank": node.get("rank", ""),
            "scientific_name": node.get("scientific_name", ""),
            "firsts": {},
        }
```

This keeps the chronological sweep and all milestone behavior intact, but flattens the nested control flow and makes the milestone logic easier to follow and test in isolation.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

taxa, _ = compute_milestones(rows, taxonomy)
# 'None' string must not count as metric; first metric is GCA_002.1.
assert taxa[ASELLUS_AQUATICUS]["firsts"]["metric"][1] == "GCA_002.1"
assert "metric" not in {k for k in taxa[ASELLUS_AQUATICUS]["firsts"] if False}

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.

issue (testing): The assertion in test_metric_predicate_and_none_string is effectively a no-op and does not test anything.

Because the comprehension {k for k in taxa[ASELLUS_AQUATICUS]["firsts"] if False} is always empty, this assertion will always succeed and never fail based on the data. If you want to verify the firsts content, consider asserting directly on the keys, e.g. assert set(taxa[ASELLUS_AQUATICUS]["firsts"].keys()) == {"metric"} or whatever set of milestones you expect.

# The chronological sweep
# ---------------------------------------------------------------------------

class TestSweep:

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.

suggestion (testing): Missing test coverage for rows with non-integer or malformed taxId values being safely skipped.

compute_milestones treats rows with non‑integer taxId values as skipped, but this behavior is currently untested. Please add a test (e.g., with taxId="abc" or empty) that confirms these rows are excluded from the output and do not raise errors.

Suggested implementation:

class TestSweep:
    def test_first_assembly_is_earliest_row(self, taxonomy):
        rows = [
            make_row("GCA_002.1", ASELLUS_AQUATICUS, "2020-01-01"),
            make_row("GCA_001.1", ASELLUS_AQUATICUS, "2018-05-05"),
        ]
        taxa, _ = compute_milestones(rows, taxonomy)
        assert taxa[ASELLUS_AQUATICUS]["firsts"]["assembly"] == ("2018-05-05", "GCA_001.1")

    def test_non_integer_taxid_rows_are_skipped(self, taxonomy):
        rows = [
            make_row("GCA_BAD.1", "abc", "2019-01-02"),
            make_row("GCA_001.1", ASELLUS_AQUATICUS, "2018-05-05"),
        ]
        taxa, skipped = compute_milestones(rows, taxonomy)

        # Rows with non-integer taxId should be safely skipped and not appear in the taxa output
        assert ASELLUS_AQUATICUS in taxa
        assert "abc" not in taxa
        assert any(row["accession"] == "GCA_BAD.1" for row in skipped)

Depending on the exact structure of elements in skipped, you may need to adjust the last assertion:

  • If skipped is a list of dicts, row["accession"] is appropriate.
  • If skipped contains row objects or tuples, update the accessor accordingly (e.g., row.accession or row["assembly_accession"]).
    You may also want to add a variant using an empty string taxId (e.g., taxId="") following the same pattern if compute_milestones distinguishes between different malformed values.

Comment on lines +312 to +321
def test_latest_is_current_version(self, taxonomy):
# Latest = the current (non-superseded) version, even if an out-of-order
# superseded row has a newer date it must not win over a current one.
rows = [
make_row("GCA_001.1", ASELLUS_AQUATICUS, "2018-01-01", version_status="superseded"),
make_row("GCA_001.2", ASELLUS_AQUATICUS, "2021-01-01", version_status="current"),
]
_, species_extra = compute_milestones(rows, taxonomy)
latest = species_extra[ASELLUS_AQUATICUS]["latest"]
assert (latest["date"], latest["accession"]) == ("2021-01-01", "GCA_001.2")

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.

suggestion (testing): Consider adding a test for the interplay between undated current assemblies and dated superseded ones in latest-selection logic.

Current tests cover dated current vs dated superseded rows and superseded-only species, but not the case where a current row has an empty releaseDate and a superseded row has a non-empty date. Please add a test that creates this scenario (current with empty date, superseded with a later date) and asserts that the current assembly is still selected as latest, to guard the (is_current, date, accession) tie-breaking logic against regressions.

return None


def compute_milestones(rows: list[dict], taxonomy: dict) -> tuple[dict, dict]:

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.

issue (complexity): Consider refactoring latest-assembly tracking and the milestone sweep into small helpers to simplify compute_milestones and build_output_rows without changing behavior.

You can reduce complexity in compute_milestones and build_output_rows with a couple of small, local refactors that keep behavior identical:

1. Simplify latest-assembly tracking

The nested latest dict with a key tuple adds indirection. You can track the candidate tuple directly and derive date/accession only when needed:

def _better_latest(current: tuple | None, candidate: tuple) -> bool:
    """Return True if candidate is a better 'latest' than current."""
    if current is None:
        return True
    return candidate > current  # (is_current, date, accession)

Use it in compute_milestones:

# inside species_extra.setdefault(...)
"latest": None,  # (is_current, date, accession)

# ...

candidate = (is_current, item["date"], item["accession"])
if _better_latest(extra["latest"], candidate):
    extra["latest"] = candidate

And in build_output_rows:

if is_species:
    extra = species_extra.get(taxid, {})
    latest = extra.get("latest")
    if latest:
        _, latest_date, latest_accession = latest
        row["latest_assembly_date"] = latest_date
        row["latest_assembly_accession"] = latest_accession
    else:
        row["latest_assembly_date"] = ""
        row["latest_assembly_accession"] = ""

This removes the latest["key"] indirection and makes the comparison logic more obvious without changing semantics.

2. Extract the milestone sweep into a helper

The nested loops over MILESTONES and LEVELS make the core sweep dense. A small helper reduces nesting and centralizes the “apply milestone for a row” logic:

def _apply_milestones_for_row(
    item: dict,
    taxonomy: dict,
    taxa: dict,
    species_extra: dict,
    seen: dict[str, set[int]],
) -> None:
    sp = item["species_taxid"]
    lineage = item["lineage"]

    def ensure_taxon(taxid: int):
        if taxid not in taxa:
            node = taxonomy.get(taxid, {})
            taxa[taxid] = {
                "rank": node.get("rank", ""),
                "scientific_name": node.get("scientific_name", ""),
                "firsts": {},
            }

    for milestone in MILESTONES:
        key = milestone["key"]
        if not milestone_predicate(item["row"], key):
            continue
        for level in LEVELS:
            taxid = sp if level == "species" else lineage.get(level)
            if not taxid or taxid in seen[key]:
                continue
            seen[key].add(taxid)
            ensure_taxon(taxid)
            taxa[taxid]["firsts"][key] = (item["date"], item["accession"])
            if milestone["in_ranks"] and level != "species":
                species_extra[sp]["in_ranks"][key].append(level)

Then compute_milestones becomes:

taxa: dict[int, dict] = {}
seen = {m["key"]: set() for m in MILESTONES}

for item in dated:
    _apply_milestones_for_row(item, taxonomy, taxa, species_extra, seen)

for sp in species_extra:
    # ensure species rows exist
    if sp not in taxa:
        node = taxonomy.get(sp, {})
        taxa[sp] = {
            "rank": node.get("rank", ""),
            "scientific_name": node.get("scientific_name", ""),
            "firsts": {},
        }

This keeps the chronological sweep and all milestone behavior intact, but flattens the nested control flow and makes the milestone logic easier to follow and test in isolation.

@fchen13
fchen13 marked this pull request as draft July 16, 2026 15:17
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