fix(variant): error on a coordinate-unsorted input VCF (#232) - #242
Merged
Conversation
TimD1-bot
force-pushed
the
231_td_bed-helper-fns
branch
from
August 11, 2026 16:34
63f53ff to
65e32d0
Compare
TimD1-bot
force-pushed
the
232_td_error-unsorted-vcf
branch
from
August 11, 2026 16:54
7357247 to
c9814f3
Compare
parse_variants() rejected a VCF that returned to a contig it had already left, but nothing checked the order of positions within a contig. A record that moved backwards was dropped one at a time by the overlap filter, which warns only above the default verbosity, so the run finished successfully, wrote a full set of outputs, and every denominator silently omitted it. Track the previous record's position alongside prev_end and prev_type, resetting it on a contig change, and ERROR when a record's position decreases. The check runs before FILTER and quality filtering, since being sorted is a property of the file rather than of the records that survive. The ordering that held before was incidental: the overlap filter skips a record unless pos >= prev_end[hap], which is per-haplotype, applies only to accepted records, and runs after the region-membership lookup that #47's monotonic per-region cursor will depend on. Enforcing the precondition here means nothing downstream has to assume it. Equal positions are still accepted -- a site split across rows is ordinary in a sorted VCF -- and so is a contig that starts before the previous one ended, since positions are compared only within a contig. The integration case is worth its own fixture rather than only a unit test precisely because the failure mode it replaces was a successful run.
TimD1-bot
force-pushed
the
232_td_error-unsorted-vcf
branch
from
August 11, 2026 16:58
c9814f3 to
42041e3
Compare
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Authorship: the content below was drafted by Claude Opus 5 (an AI coding agent) and
filed via
ghunder @TimD1-bot, a bot account operated by @TimD1. It reflects theagent's analysis, not a statement authored by @TimD1.
Summary
A coordinate-unsorted input VCF becomes a hard failure instead of a quietly wrong result.
Problem
Half the guard already existed. #149 restored the contig-re-entry check, so a record returning to a
contig already left errors out and contigs never interleave. Nothing checked the order of positions
within a contig.
A record that moved backwards was dropped one at a time by the overlap filter, which
WARNs onlyabove the default verbosity. The run therefore succeeded, wrote a full set of output files, and
every denominator silently omitted the dropped records. Nothing about the failure mode looked like
an error.
The ordering that did hold was an accident: accepted records are non-decreasing per haplotype
because the overlap filter skips a record unless
pos >= prev_end[hap]. That is per-haplotype, itcovers only accepted records, and it is a side effect of a filter written for an unrelated
purpose.
Change
src/variant.cpp, in the record loop:prev_posis tracked alongsideprev_end/prev_typeand reset in the same place they are, on acontig change. One integer comparison per record; the message reuses the existing
"Unsorted %s VCF"phrasing from the contig check.Two deliberate placement choices:
records that survive filtering.
split across rows -- and a contig that begins before the previous one ended is fine, since
positions are compared only within a contig.
Why it lands before the code that needs it
#47's per-variant region-membership lookup uses a monotonic cursor per region set, which requires
the query stream to be non-decreasing within a contig. Resting that on the overlap filter's
incidental ordering would make stratified counts silently depend on an unrelated filter's semantics
-- and the membership query runs before that filter in the loop, so it would not even be covered
by it. Enforcing the precondition here means nothing downstream has to assume it.
Testing
Unit (
tests/unit/src/test_variant.cpp), 3 added:ParseVariants.UnsortedPositionErrorsParseVariants.RepeatedPositionParsesParseVariants.PositionResetsOnNewContigThe existing
ParseVariants.UnsortedContigErrorsandContigsOutOfHeaderOrderParseare unchangedand still pass, so the contig-level guard and the sorted-but-reordered-contigs case are unaffected.
Integration:
test_unsorted-query-vcf-exits-nonzero, on a newtests/integration/data/unsorted_position_query.vcf-- theswallowed_snpsquery's three SNPs withthe last two swapped, so
sc1146 followssc1256. It asserts exit code 1 and the error text.This one earns an integration case rather than only a unit test precisely because the failure mode
it replaces was a successful run.
Verification
pytestfromtests/: 118 passed (115 before; the new case contributes its exit-code checkand two
stderrassertions as three separatepytest-workflowitems).make cleanunder-Wall -Wextra -Werror=missing-field-initializers;doxygen src/Doxyfileemits no warnings.Unsorted QUERY VCF '...', record 3 at sc1:145 precedes position 255.Release note
Behavior change. Runs that previously produced misleading numbers on an unsorted input now fail
loudly. The README's Usage section now states the requirement.
Part of #47. Resolves #232.
Targets
devdirectly: the change touches onlyparse_variants()and its tests, and depends onnothing in the rest of the #47 chain.