feat(cli): add -st/--stratification manifest loading and validation (#234) - #244
Merged
Conversation
…234) Part of #47. The flag and the loader only: region sets are parsed, merged, and resident in Globals, and nothing reads them yet, so no output changes but --help. -st takes one hap.py-compatible manifest TSV whose two tab-separated columns name a stratum and its BED file, with further columns ignored. Relative BED paths resolve against the manifest's own directory rather than the working directory, so the GIAB manifests work unmodified. Blank and '#'-prefixed lines are skipped, and load order is manifest order, which is also the output row order the strata will take. Each region set is normalized at load, since a third-party set we neither author nor control would otherwise have to be sorted and non-overlapping for contains() to answer correctly; normalizing does not exempt it from validation, so a malformation sorting and merging cannot repair is still fatal. Five conditions are fatal: an unreadable manifest, a line naming fewer than two fields, a repeated stratum name (which would make an output row ambiguous), a stratum named '*' (reserved for the all-regions row), and an unreadable stratum BED, which names both the manifest-relative and the resolved absolute path so that a path resolved somewhere unintended is distinguishable from a missing file. A manifest that parses to zero strata warns instead, and the run proceeds as though -st were absent. The realistic failure mode is an assembly or contig-naming mismatch, which would otherwise emit rows of zeroes that read as genuine results, so each region set's contigs are compared against the reference FASTA's once it is loaded. A set sharing no contig is named; every set failing at once is diagnostic rather than incidental, so that case names the likely cause instead.
TimD1-bot
force-pushed
the
234_td_stratify-manifest
branch
from
August 11, 2026 17:41
77a65be to
11a5083
Compare
TimD1
requested changes
Aug 11, 2026
| /**************************************************************************************************/ | ||
| } else if (std::string(argv[i]) == "-p" || | ||
| } else if (std::string(argv[i]) == "-st" || | ||
| std::string(argv[i]) == "--stratification") { |
Owner
There was a problem hiding this comment.
we added a new CLI parameter. Shouldn't this be added to parameters.tsv? Are there other parameters you forgot to add/delete?
…n parameters.tsv The new -st/--stratification manifest path was not written to parameters.tsv. Auditing every CLI-settable parameter against the writer surfaced two pre-existing omissions with the same shape: -sc/--max-supercluster-size (added in 41cc0a0, never wired in) and -v/--verbosity. No stale rows: all 22 existing keys still map to live Globals members.
… nstrata The writer emitted all 26 rows from a single fprintf, whose format string was split across seven fragments and whose arguments ran across five lines, so a key and its value were never adjacent and a row could be added to one without the other. Each row is now its own call, with its key, conversion, and value on one line. Two changes to the rows themselves. The stratification manifest key becomes 'stratification', matching the --stratification flag that sets it. And nstrata is now recorded: the manifest path alone does not say how many region sets were loaded from it, which is precisely what a reader needs to tell a fully-loaded run from one whose manifest parsed to nothing and warned. WriteParams.WritesEveryKeyOnceInOrder pins the whole key list in order, since one call per row makes a dropped or duplicated row a one-line edit away.
TimD1
approved these changes
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.
Resolves #234. Part of #47.
Change
-st, --stratification <STRING>takes one hap.py-compatible manifest TSV:directory, so the GIAB manifests work unmodified.
#are skipped.Region sets are parsed, merged, and resident, and nothing evaluates against them yet — the
only output they reach is the
parameters.tsvprovenance rows below. NewGlobalsmembers(
src/globals.h:33):Each region set is normalized at load (
src/bed.cpp:407), so it is sorted and merged before beingvalidated. These are third-party files we neither author nor control, and
contains()locates avariant with two binary searches, so an unsorted or overlapping set would return wrong answers
rather than merely being untidy. Normalizing does not exempt a stratum BED from validation: a
malformation sorting and merging cannot repair — a flipped or zero-length interval — is as fatal
here as in an evaluation BED.
parameters.tsvThe manifest path and the number of strata loaded from it are recorded as
stratificationandnstrata, so a run's provenance says whether the region sets actually arrived — a manifest thatparsed to nothing warns at load, but only
nstrata 0preserves that after the fact.Auditing every CLI-settable parameter against the writer turned up two pre-existing omissions of
the same shape, now also recorded:
-sc/--max-supercluster-size(added but never wired in) and-v/--verbosity. All 22 keys that were already there still map to liveGlobalsmembers.write_params()now emits one row perfprintfinstead of building all 26 from a single callwhose format string was split across seven fragments and whose arguments ran across five lines —
which is how a key could exist with no value passed for it, and how the two omissions above went
unnoticed.
WriteParams.WritesEveryKeyOnceInOrderpins the full key list in order, since one callper row makes a dropped or duplicated row a one-line edit away.
Validation
All
ERROR, inload_strata()(src/bed.cpp:357):*Line numbers count skipped comment and blank lines, so a reported line is the line in the file.
An empty second field (a trailing tab) counts as a missing BED path rather than parsing as one.
A manifest that parses to zero strata is a
WARN, not an error;nstratastays 0, which is thestate every future consumer gates on, so the run proceeds as though
-stwere absent.The zero-overlap warning
The realistic failure mode is an assembly or contig-naming mismatch —
chr20-prefixed regionsets against
20-named reference contigs, or GRCh37 sets against a GRCh38 run — producing rows ofzeroes that read as genuine results.
check_strata_contigs()(src/bed.cpp:438) runs frommain()right after the reference FASTA is read, before any evaluation, andWARNs by name foreach region set whose contigs do not intersect the reference's at all.
If every set has zero overlap, that pattern is diagnostic rather than incidental, so it becomes
one message naming the likely cause instead of one message per set.
A region set that shares a contig but contains no evaluated variant is not warned about — that
is a legitimate result, and it will emit a full zero row rather than being omitted, so a consumer
joining on stratum name never sees a missing key.
Testing
Unit (
tests/unit/src/test_bed.cpp, 19 new cases): relative-path resolution against the manifestdirectory (the path is unresolvable from the working directory, so only manifest-relative
resolution can find it); absolute paths; comment and blank lines; extra columns ignored; manifest
order preserved; normalize-at-load, including the sort warning and the flipped interval that is
still rejected; each of the five
ERRORs; the zero-stratumWARN; the zero-overlapWARNnamingthe offending set; and the all-zero case naming the cause instead.
Unit (
tests/unit/src/test_print.cpp, 4 new cases): the manifest path and stratum count arerecorded; both are empty/zero when
-stis unused;-scand-vare recorded; and the full keylist is pinned in order.
Unit (
tests/unit/src/test_globals.cpp, 6 new cases): both flag forms populatestrat_tsv_fn,strat_names,strataandnstrata; the flag's absence leaves all four unset; a missing valueand an unreadable manifest each exit 1; an empty manifest warns and leaves
nstrata0.PrintUsage.ListsDocumentedFlagsnow requires-st, --stratificationto be advertised.Integration:
test_stratification_manifest-loads-without-bedruns theswallowed_snpsfixtureswith
-stand no-b, pinning counts identical to the default run and requiring no contig-mismatchwarning; since
pytest-workflowruns each case in its own temporary directory and the newstrata_synthetic.tsvfixture names its BEDs relatively, this also exercises manifest-relativeresolution end to end.
test_stratification_missing-manifest-exits-nonzerorequires exit 1 withFailed to open stratification manifest.write_tmp_bed()'s uncompressed branch was extracted aswrite_tmp_text()so a manifest fixturecan be written without a BED-specific writer.
Rebased onto
devnow that the rest of the chain (#241, #242, #243) has merged, which brought inthe
bedData::merge()→normalize()rename and the always-runcheck()from #241; the loaderand its tests were updated to that contract.
Three commits, intended to be squashed: the loader and flag, the
parameters.tsvomissions, andthe per-row writer plus
nstrata.Verified locally on a clean rebuild of both
src/andtests/unit/build/: 810/810 unit tests pass(781 on
dev, plus 29 new), and 132/132 pytest cases pass (124 before, plus the eight assertionnodes of the two new integration cases). A real run's
parameters.tsvwas inspected directly toconfirm both new rows are written with the values the flag was given.
-Wall -Wextraproduces nowarnings, and
doxygen src/Doxyfilereports none.