Repository navigation
Release gate — the regression panel against 6.6.0 (2026-11-09)
- The 6.6.0 tier on local slices, 411 runs: no run failed in either build; 48,022 changed cells, every
one attributed to a merge (the reduction is exact). - Concordance with the sign-out (matched level): 98.1% of the 13,323 DNA rows within the margin in
6.6.0, 97.3% in 6.7.0 (136 rows left the margin, 33 entered it). 94 of the 136 are now above the
sign-out: 6.7.0 counts carriers by their bases and finds carriers that a count by alignment missed
(read by read, 809 of the changed reads carry the ALT and 150 do not). - 19 signed-out rows read AD 0, each flagged
ZERO_ALTandPARTIAL_DOMINANT, and each checked on
its own:- 9 rows: the row is a part of a complex event that the input also lists. One molecule gives one
allele (RJ-25), so the complex row keeps the reads; the part reads AD 0 with status
MULTI_ALLELIC(operator, 2026-11-09: kept). - 5 rows: every ALT read carries the listed deletion and an unlisted change beside it that no REF
read carries. The reads carry another allele, named inOBSERVED_ALLELE, so the given deletion
reads AD 0 (D4, RJ-25; operator, 2026-11-09: kept). - 1 row: a 279 bp deletion with a change past its junction (a named limit of the junction judge,
RJ-24). 1 row: the reads carry two or more extra changes (another allele). - 3 rows: see "Known issues".
- 9 rows: the row is a part of a complex event that the input also lists. One molecule gives one
Known issues — three shapes that the default read judgment counts as 0
- A pure deletion with a listed change on the first base past its end, carried in cis (#417).
- The exact carriers of a 17 bp tandem duplication: partial on the rules path (#417).
- A delins with a listed SNV at its first base: neither row gets the reads (#425).
--read-judge sequencecounts all three. The rules path is not changed for them (operator,
2026-11-10): the sequence judge is the path to keep, and it becomes the default in 6.8.0 (#331).
Fixed — harness: the cBioPortal guard reads each output stream (Greptile on #426)
- The guard (
.claude/hooks/protect-cbioportal.py) took the>inside2>for a stdout
redirect, so a command that sent only stderr into a protected folder passed while stdout printed
the rows. Now every stdout redirect (>,>>,1>,&>) and every stderr redirect must end
under a protected root (2>&1counts only after the last stdout redirect), and a root matches
only as a whole folder.tests/test_protect_cbioportal_hook.pypins 18 shapes (made-up names).
Tests — Typer 0.29.0
- Two docs tests (every
gbcmsexample parses; theqc summarizeexamples parse) switch off the
path-existence check where Typer 0.29 keeps it, on the parameter's declaration (0.29 removed a
parameter'stype). What they assert is unchanged; gbcms itself runs on Typer 0.29.
Fixed — RNA: ASJD reads the same sibling verdict as the main counts (#359, RJ-5)
- At an RNA row with a GTF and a co-annotated sibling, the allele-specific junction test counted reads the main counts give to the sibling (a REF read that is ALT for the sibling where this row's REF is read; an ALT match the sibling explains as well or better). It now makes the same two sibling checks, so those reads vote in neither the REF nor the ALT junction partition. Only the
asjd_*columns can move; RD, AD, DP and the per-transcript counts are unchanged. Rules path only. Measured on the synthetic set: rows without a sibling are byte-identical (0 of 1,072); with a sibling added to every row, 12 of 2,144 rows move.
Tests — RNA pins (#344, #345, #346)
- Pinned decided behaviour, no count change: reads carrying another allele are in a transcript's DP and in no transcript AD/RD or ASJD partition; antisense reads carrying another allele appear in no column under enforcement, and count as partial evidence with
--no-strandednessor with no GTF; an edit inside an insertion's bases is decided by the reads, and an editing database cannot reach it. - Decided and pinned (RJ-32): a read with no junction fits every transcript it overlaps, so it counts in the DP and RD of each overlapping spliced transcript, and once in the row's counts. A single-exon transcript gets no per-transcript entry. No count changes.
Changed — a listed allele at the site is always another allele, and a read counts REF where it tells REF from the alleles it reaches (RJ-25, RJ-24; operator 2026-11-02)
- No floor for a listed allele, on both paths. A listed allele whose span meets the ALT's site
is another allele there whether or not any read shows it (the input is taken as correct, as
rule X took it for another unit count at a long deletion). The 1-fragment-and-5% test of
2026-10-14 is gone for listed alleles (the rules'other_alleles, the sequence judge's
seen_apart); it stays for an allele nobody listed (3 fragments and 5%). A listed component of
an MNP that no read shows apart from it is another allele too: a carrier that reads only the
component's base cannot tell the MNP from it and is partial evidence (AD 14 to 10 in that
shape). The rules path takes any listed allele of the ALT's length that meets the site and differs
inside the window, not only one that differs nowhere past two bases beside the event. - A REF read is told only from the alleles it reaches. This reverses the REF clause decided
on 2026-10-14 (a REF read short of a REF-length allele was neither). An allele past the read's
end (or before its start) no longer contests it: a read ending inside a long event is judged by
the bases it has (RJ-9); a read that reaches the allele and cannot tell it (the base unread,
below--min-baseq, or masked) stays neither. The sequence judge already ties by reach; the
rules path now does too, for REF-length alleles listed or found, in the exact-carrier
windows and in the MNP check. - An SNV row beside a listed indel or delins is judged by its bases, also under the rules (#398).
An SNV row where the same input lists an indel or delins whose span meets the row's site (the
sequence judge's own test) is counted by the sequence judge, as--read-judge sequencewould;
every other row, MNV rows included, keeps the rules. Prep decides it from the whole input, so
no bin or call changes it; no column, flag or environment variable is added. The rules still
count a routed-class row in two places, each with one warning per count: RNA mode, and a
count given no reference FASTA. The rules' single-base check weighed no listed allele that
changes the length, so a read that spelled one counted ALT. Measured at these rows (rules
lane against judge lane, 2026-11-03): patient sets, wrong-ALT reads 1,230 to 0 and wrong
definite calls 1,258 to 7; HG002, true ALT credited 454 to 520 and reads of the listed allele
credited ALT 142 to 21. Precedent: a deletion of 50 bp or more already goes to a by-bases
junction judge. A MNV-plus-insertion shape (67 true-ALT reads at two gate-set rows) would go to
partial under the judge and stays on the rules (#331). - RJ-24 reaches a delins. A delins that replaces 50 REF bases or more (the changed REF bases once
the shared prefix and suffix are trimmed) has a core, 4 bases in from both ends: a substitution
wholly inside it is a context of REF's molecules, so a read that reaches and spells it is REF
and a sibling there claims no REF read. A 60>30 delins has a core; 5>60 does not. - Stopgap (#418). a found allele's background bases are left out of its reach only where the allele also differs at an unmasked base inside X's window; where its only differences inside the window are masked, those differences are its reach. Without it HG002 c246 INS 1-9 gets 185 true-ALT reads called REF against develop's 182, because the allele's one unmasked difference lies outside the window. It goes when #418 is fixed.
- Routing can raise AD. At a routed SNV row the judge counts a read that carries a listed allele
holding X's change as ALT (RJ-28), and a read whose clipped bases carry X. In rq2, 349 reads enter AD
at SNV rows, 254 of them off the oracle's anchor; HG002 truth at the routed rows shows no new
wrong-ALT.--alignment-backendhas no effect at routed rows (the judge scores them). The judge's
diagnostics (UNEXPLAINED_READS, MNV_SHARED, INDEL_SHARED) can appear on routed rows.
Named limit (#418, as on develop): the rules path's REF side weighs only listed alleles that differ
inside X's window (RJ-25 (5)(a)). - Two corrections against the text above (rq1, operator 2026-11-03). (a) The candidate test
reads the window the event's own windows hold: one flank base each side of a pure indel, two of
any other event. It read two for a pure indel too, so a listed delins that is X plus a
substitution at the second flank base became a candidate whose window equals X's: no read could
tell them apart and every carrier of a 1-base insertion in a 4-base run went to partial (AD 106
to 0 at one panel row, rules path). (b) The reach of a REF-length allele is measured on its
difference from REF before the guard's masks, in every branch (a found allele's background bases at a
mask do not widen it), so an allele that differs only at masked bases still
contests a read that spans them: reached and not told is neither. It had been taken to contest
nothing, and 91 read calls at six HG002 rows (five insertions, one deletion; 5 of them true ALT at
c246 INS 1-9) went from neither to REF. The MNP check measures it the same way. The rules path
only; the judge lane moves no row and no read call. Where the paths still differ at a delins row
(the rules need the read to hold X's whole window, RJ-1 and RJ-2; the judge counts what the read
covers) is older than this change, and closing it would be a third REF rule (#331); RJ-25 (6). - Counts on HG002 (public, local; 27,589 rows), rules path against develop: 598 arm-rows change (87
without the routing); the routing changes no row that is not a class row; at the 1,302 class rows the rules lane equals the judge
lane in every MAF field and read call. Phased truth at those rows (strict view): true ALT credited
454 to 521, reads of the listed allele credited ALT 142 to 21; scored targets (truth v2dbq20): true
ALT credited 30,455 to 30,457, true REF called REF unchanged, true ALT called REF 1,339 to 1,336.
Judge: 49 arm-rows change; truth classes equal. Speed (rq2, on HPC): patient wall time 1.02 times develop; HG002 CPU 1.28 times; nocl peak memory
411 to 905 MiB. Inputs that can be much slower: a long listed deletion with many SNVs inside it,
and dense cohort lists (#412). - Existing expectations changed by the decision (each with its reason in the test):
test_listed_sibling_floor_contract.py(8 cases, AD 14 to 10),
test_locus_alleles_contract.py(a component never seen alone and one seen on 1 of 100
fragments, AD 14 to 10; an SNV among a delins's deleted bases never spelled, RD 24 to 20).
New contract tests:tests/test_ref_reach_contract.py; a routed row intests/test_binning_invariance.py. - Supersedes the option A of the REF-tie question on the judge (#394) and settles #398 (SNV rows
routed to the judge, above; the rules path's sibling scope for exact-carrier rows).
Deprecated — --rescue-homopolymer (removed in 6.8.0)
--rescue-homopolymeris deprecated in 6.7.0 and removed in 6.8.0.gbcms dna,
gbcms rna, thePipeline,observe_molecules(rescue_homopolymer=...)and the Nextflow
--rescue_homopolymeraccept it, warn once and ignore it, as--gtf-cache-dirwas in 6.6.0.
The twin is never built, soWARN_HOMOPOLYMER_DECOMPis never set. It did nothing useful
the diagnostics do not:OBSERVED_ALLELEandCOEXISTING_ALLELEingbcms_diagnostic
name the allele the reads carry.- Measured on origin/develop
df5da55f(hp1): of 20 runs whose variant list holds a twin
candidate (71 rows scored), the twin won at 1 of 20 candidate rows (5%), moving AD by +2
(17 to 19) while 17 reads moved into RD. At the SOX2-shaped rows (REF length 6, ALT length 1)
it won at none, and the default diagnostic named the 1 bp deletion plus the change in 2 of 3. - The Rust binding's
rescue_homopolymerparameter,decomp.rsand the dual count stay
until 6.8.0 removes them; Python no longer sends the flag.
- Measured on origin/develop
Changed — RNA: a splice never lowers a base's quality (#292, RJ-31)
- Heuristic BAQ (
--apply-baq, on by default ingbcms rna) no longer lowers the five read
bases each side of a splice junction (CIGARN) by 20. It still lowers by 20 the five bases
each side of a read's own insertion or deletion, and still spares the counted variant's own
indel. A base is judged by its own quality against--min-baseq, wherever it lies in the
read. This reverses the junction penalty decided on 2026-05-04 (in effect for RNA since
4.1.0), under which a base near a junction passed the bar only where its quality was 20
above it: Q40 data passed at the default bar 20 and nothing passed at 30. GATK's
SplitNCigarReads changes no quality at a splice and htslib's BAQ skips a spliced read whole,
so no surveyed tool lowered a base for lying near a junction. - Counts change in RNA only, and only for reads spliced within five bases of the variant's
bases, GTF or none (with a GTF, BAQ was already skipped within five bases of an annotated
exon edge). Measured on 58 FORTE STAR slices at bar 20: of 163,882 SNV rows in the first five
bases of exons, 136,257 move, ALT +14,269 and REF +5.70 million reads (+1.9%); clean rows
keep 0 ALT, and the 219 germline hets gain the 984 ALT reads (1.9%) the penalty hid (76% at
bar 30). DNA has no junction: unchanged. - At a delins, ALT can fall at bar 30 (FORTE: 280 to 173 reads in reach, reproduced exactly on
the builds before and after the change). That is right by bases: a base under the bar fits any
allele, so with a junction's ten read bases masked a read could fit the ALT on the few bases
left, whatever the masked bases showed. The 117 reads that lose ALT are all spliced with the
event 0 to 4 bases from the junction; 92 lie on a long piece (20 or more aligned bases), not a
short anchor. Under the change 91 of the 117 are REF and 18 partial evidence. - The exon-edge exception (#106) stays; it now lifts only the penalty of a read's own indels.
- The limit (#409, RJ-3's short pieces): a piece too short to place its own bases, at a spliced
read's end or between two junctions, is no longer hidden by the penalty and can read as an ALT it
does not carry. An end piece is written at an annotated junction where its molecule spliced at a
novel one a few bases on (synthetic set: 2.2% of the reads at that stratum, at hom-REF rows); a
6-base piece between two junctions with a misplaced base counted ALT on 4 of 4 reads (develop:
depth only, two penalties stacked to Q0). Ticketed; two strict xfails pin them. gbcms rna --apply-baq/--no-baqhelp and the--traceBAQ line no longer name splice
junctions. No flag or Nextflow parameter changes.- One test expectation changed, with the decision:
test_baq_still_applies_to_an_mnp_ending_six_bases_from_the_edge, (REF, ALT) from (20, 0) to
(20, 10): its ten carriers hold the MNP in their own bases, and the novel donor's junction
penalty had masked both.
Fixed — RNA: an exon piece holding part of a deletion's bases is depth only (#292)
- A spliced read whose exon piece starts at a junction at a deletion's edge and ends inside
the deleted bases (or, mirrored, starts inside them and ends at a junction) holds part of
the deletion and no flank base of its own. The exact-carrier rule now counts it toward depth
only, never REF (RJ-17, amended 2026-10-30), with BAQ on or off. Before, only BAQ's
junction penalty hid the piece's bases: with--no-baq, such reads counted REF.
The CIGAR checks already required the whole span. DNA is unchanged (its reads have no
junction).
Changed — a long deletion's ALT reads must tell it from another unit count present at the site (#297)
- At a deletion of 50 bp or more beside a tandem run of its unit (its deleted copy differing
from the run's, so the junction windows stop at that base), another unit count reads like
the given deletion until the run's copies end. Where such an allele is present at the site
(listed in the input, always: rule X, operator 2026-10-31, the input is taken as correct;
or, unlisted, spelled in full by its own bases on 3 or more fragments and 5% of those
telling the ALT from it, RJ-25's floor; a fragment spells it only where its bases fit it,
and fit REF worse, over 24 bases past where it and the ALT differ; only reads the aligner
places here, RJ-29's test), an ALT read now counts only where its confident bases where
the two first differ show the given count, or show it with a listed row applied (a phased
neighbour the input names, as RJ-24 takes a listed change); a read ending sooner is either allele: partial
evidence (RJ-25), not ALT. Errors where the two read alike cost both and decide nothing
(RJ-26). A site with no other count keeps its reads; REF reads and the sibling claim keep
their windows. Both paths: the rules (junction) and, under--read-judge sequence, the
found alleles as site states (the judge already held listed and aligner-written ones). - The other counts looked for are the other deletions at the breakpoint (fewer bases, or more
up to twice the given); an expansion at the breakpoint is not (a REF read inside its copy
fits it as well). The count that holds a row's ALT calls is named ingbcms_diagnostic,
OBSERVED_ALLELEorCOEXISTING_ALLELEat the deletion's anchor (BaseCounts.unit_count_allele). - A listed other unit count is a candidate allele always, on both paths, whether or not a read
shows it (rule X, operator 2026-10-31): the given count's reads that end before the first
difference stay partial evidence at a listed count no molecule carries. That is the cost of
taking the input as correct, and the spike-in arm counts it (bar 7). A variant that decided
a listed count absent when the deciding fragments show none of it was built, measured and
rejected (REJ-20261031-001): it gained 141 spike-in reads but credited the other allele's
reads as ALT and made false-absent rows. RJ-24 and RJ-25 amended 2026-10-31. UNIT_COUNT_UNSPANNEDweighs the nearest unit count on each side (one that reads like the
given deletion for its shift and 12 bases more), not any count past reach.- Where no read at the site can spell an unlisted count (the run longer than the longest
read), nothing is held for it, the counts stand, andgbcms_diagnosticnames the row:
UNIT_COUNT_UNSPANNED(change:bases/longest), a newBaseCounts.unit_count_unspanned
(no new column). A listed count there is held (below). Prep holds the reference a read reaches
past twice a long deletion's length (whole_ref), and the input's indel rows inside it
(listed_indels). RJ-24 amended 2026-10-30. - The rule closed on one principle, on both paths (operator, 2026-10-31, option A throughout;
RJ-24 and RJ-25 amended): a read is ALT only where its own confident bases carry the given
ALT and tell it from every candidate (REF, every listed allele, each found allele that
passes the presence test). The final review of rule X found four places where the branch
departed from it, three on the rules path only; all are closed, each red first on the rules
path and under--read-judge sequence:- A read is "the deletion with its listed neighbour" only where its confident bases show that
row's change. One input row no read carries (a 1-bp deletion 270 bases past the junction,
or an SNV) turned 6 tie reads into ALT on the rules path, (RD, AD, partial) (6, 0, 6) to
(6, 6, 0); the sequence judge stayed (6, 0, 6). HG002 4:3610329 is unchanged. - A listed other unit count is held wherever it first differs, past every read's reach too:
the reads that cannot tell are partial evidence, (6, 12, 0) to (6, 0, 12) on the rules
path, and the row is namedUNIT_COUNT_UNSPANNED. The cost is a row withalt_count0
until a read can tell (HG002: two rows on the rules path, ALT 15 to 0 and 5 to 0; the
sequence judge already counted 0 there). - A read whose own bases spell an unlisted other unit count in full is never ALT, whatever the
presence floor (RJ-6): partial evidence, or depth only where the aligner doubts its
placement (RJ-29). The floor decides only the reads that cannot tell. The sequence judge
leaves such reads depth only (an allele under the floor is no state), sopartial_alt
differs between the paths on them,alt_countdoes not. - The presence test reads each read's qualities as the hold does (BAQ-adjusted, inside the
readable range), and "within a read's reach" is the first difference and the 24 bases the
spelling test reads (it was 4).UNIT_COUNT_UNSPANNED'sbasesis larger by 20, and a
count that first differs 4 to 23 bases before the longest read's end is named. - One presence test, RJ-25's (
carrier::is_found: 3 fragments and 5% of the fragments
spelling the allele it is nearest), asked by the exact-carrier guard, the junction guard and
the sequence judge's found alleles; the judge counted reads against 5% of all the site's
reads. This changes presence for every allele the sequence judge finds, at every event (SNVs,
MNVs and short indels as well), not only for #297's unit counts. On HG002 nothing moves on
the rules path; under the sequence judge 101 reads on 25 rows (18 distinct) move. One
tolerance, RJ-27's (carrier::one_error_forgiven), for the carrier rule and the junction
judge's telling; no count changes. - Round 2 of the close (operator, 2026-10-31), two more places closed on the same principle:
a read placed from the right flank (its junction bases in a left soft clip) that spells an
unlisted other unit count is partial evidence like one placed from the left, not depth only
(only the aligner's own doubt, RJ-29, makes it depth only); and the sequence judge's
presence test divides by the fragments spelling the allele the found one is nearest (REF's
or X's, by the bases that differ over the window; the larger when as near), not by every
fragment either given allele beats it on, and a fragment whose mates disagree counts for
neither, as the junction guard'sconflict. On HG002 the right-flank change moves no read;
the judge's presence test moves nothing on the rules path and, under the sequence judge, 37
read calls on 8 arm rows (6 distinct rows), 7 of them (5 distinct rows) in the counts: four
rows lose a found allele because a telling fragment's mates disagree, one (a 106-bp
insertion) gains one and 11 of its 79 ALT reads become partial evidence, and one read moves
from depth to partial evidence. - Named limits until the sequence judge is the default (#331): under the judge a read that
spells a deletion of another length at X's anchor, below the presence floor, is depth only
where the rules path calls it partial; and on the rules path the carriers of the deletion
that also carry an unlisted 1-bp insertion are all held, where the judge places the insertion
as a separate event and counts ALT those that reach the run's end (RJ-24). - The review of the pull request (2026-11-02, operator: fix all three, each red first). A listed
deletion at the breakpoint whose REF runs past the reference held (a 600-bp deletion beside a
60-bp one before twelve copies of its unit) was dropped on the rules path, so the given count's
short carriers stayed ALT; its bases cannot be read, so it is now held as a candidate no read can
tell, as any listed count is, and namedUNIT_COUNT_UNSPANNEDwhere it is the nearest on its side:
(RD, AD, partial) (6, 6, 0) to (6, 0, 6) on the rules path, the sequence judge already (6, 0, 6).
Where the reference past it would tell the counts apart (a flank that does not repeat) the rules
path holds all the same and the judge counts ALT, (6, 0, 6) against (6, 6, 0): a named limit. A
fragment whose short mate (under 24 bases past the first difference) tells the given count while the
other mate spells the other count in full counts for neither, as two full mates do: (6, 3, 15) to
(6, 15, 3) on the rules path and (6, 15, 0) under the sequence judge, three such fragments. And the
alleles at the breakpoint past a read's reach keep only their length change and first difference,
not their bases and rows, so memory no longer grows with the square of a long deletion beside a
long simple repeat: a 24-kb deletion before a 24-kb (AC)n repeat peaked at 805 MB through the
engine and now at 101 MB, a 60-kb one at 4.0 GB (a 100-kb one now 131 MB); counts and flags are
the same. On HG002 (public; c246, tr, sub, nocl; both paths) no row and no read call moves for
any of the three. RJ-24 and RJ-25 amended 2026-11-02.
- A read is "the deletion with its listed neighbour" only where its confident bases show that
Fixed — two follow-ups to the equal windows of an insertion of 50 bp or more (#264)
- Under
--read-judge sequence, a read that ties the ALT with another allele (REF beaten) must
hold the ALT's equal window as well as that allele's (RJ-23). Before, the window check ran on the
haplotype of the allele the call names, and a read inside the insert, or holding only its far
junction, held the other allele's window (a haplotype with no long insert, which looks like
REF's run) and counted partial evidence. It is depth only now; no ALT count can rise.
At HG002 (public; c246, tr, sub, nocl) 367 read calls on 82 rows move from partial evidence to
depth only, all at pure insertions of 50 bp or more; no row's REF count, ALT count or VAF changes
(partial evidence falls on all 82 rows; DP falls by 42 reads on 25 rows: those reads lie off the anchor, and a read off the anchor was in DP only through its partial call, so as depth only they now count in no column). - The guard's masks are laid by offset from the anchor on every candidate haplotype along the
columns of each path (RJ-23, RJ-27), not by the offsets the haplotypes have without the
listed or found changes beside the insertion (Greptile's finding on PR #396): an insertion or
deletion between the anchor and a masked base moves that base's offset on every haplotype a
read carrying it lies on. The graph holds a copy of the span where a mask can lie per set of
such changes that can lie together. A state's graph with seven such changes or more in that span (the cap is checked per graph) keeps
the offsets without them (a named limit).
At HG002 the masks are laid by the columns of up to five such changes at the rows with masks and
no read call moves (nor in a sweep of 12,078 synthetic read pairs); the change is in the score of a
read carrying such a change on a candidate (four points lower on X's haplotype with a third base at
the masked base, now equal). - A read lies on one copy of that span for every candidate (RJ-23). A state's best path took
whichever copy masked its own mismatches, so a read ending in the insert, long before a listed
deletion, took the deletion's copy for the ALT (masking a base where the read has a third base)
and the other copy for a listed sibling of the ALT's length that differs at a masked offset: the
ALT scored 0, the sibling -4, and the read was ALT from a deletion it cannot carry. A read now lies
on the changes it carries and those it spans, the same for every candidate, and carries a change only where its bases span the change's place (a read ending on a deletion's padding base, or inside the repeat it slides over, carries nothing); a change it cannot place is a plain alternative for every candidate, with no mask moved for it, as on develop: the read is scored first on graphs with no copy of the masks' zone and again, on graphs with copies for the changes it spans only, where it spans one; the candidates whose
best path took another copy are scored again. A read carrying the deletion is told the ALT from the
sibling at the base its copy leaves unmasked, ALT. - The graph sets a site lays for the changes its reads span (up to 32 per site) count against the
judge's cache budget (GBCMS_JUDGE_BUDGET_MB), once each when kept. Past the budget no more are
kept and a read's set is laid for it alone. No call changes; only what is cached. - The length the windows read on to (RJ-23, RJ-27) where the masks hide the base that tells them
apart is counted in the columns of the path a read lies on, as the masks are. A deletion listed
between the anchor and that base takes bases out of the haplotype of a read that carries it, so
the windows hold as many fewer columns there; a REF molecule carrying it that reached the base was
two columns short of a window measured without the deletion and was depth only. - The window check at a pure insertion of 50 bp or more is the class rule for every call (RJ-23): a
read is told only on the windows of the alleles it ties, so a call holds its own allele's equal
window and that of every other class within the margin of it. Before, only a tie of another allele
with the ALT held the ALT's window too; a tie of REF with another allele was checked on that
allele's window alone, and a call made by the fit check on its haplotype's alone.
A REF call the reach rule made (REF ties another allele the read never reaches, or one it writes its own
indel across) holds REF's window alone, as on develop; a separate class plan decides it for every shape. - The default rules path is unchanged.
Changed — the sequence judge reads an insertion of 50 bp or more over equal windows from its anchor (#264)
- Under
--read-judge sequence, a REF or ALT call at a pure insertion of 50 bp or more now
holds RJ-23's equal windows from the anchor, as the rules read it: the anchor and as many
bases on as REF's window holds (the run the insert slides over and the base past it). The ALT
is read over the insert's first bases, never the whole insert. A read lying inside the insert,
or holding only its far junction, is depth only, as a REF read starting inside the run is.
Before this the judge counted reads inside a long insert ALT, giving the ALT read starts REF
cannot have. - Where the guard's masks hide the base telling the two windows apart (a change past the run,
which on the ALT's sequence lies past the insert, so it told REF's molecules apart and never
the ALT's), both windows read on to the next base telling them apart, and a read short of it
is depth only, REF and ALT alike (RJ-27's read-on). At HG002's 98 bp duplication at
chr22:39653046 this makes 11 REF reads depth (RD 21 → 10, AD 6). - Both windows are counted in bases of the called allele's sequence, so a listed change inside them
lengthens neither; the window check runs before another allele's call, so a read inside the
insert tying the ALT and another allele is depth only, not partial evidence; and the guard's
masks are laid by offset from the anchor on every candidate haplotype (REF's, the ALT's and
each other allele's), so a masked offset weighs on none of them. Where no base within reach tells the windows apart, every read is depth only and the
row is namedNO_TELLING_BASEingbcms_diagnostic(no column added). - Among the reads that hold the windows, the calls now follow the truth's allele shares on HG002
(per-read symmetry bar); the VAF of long insertions stays below 0.5 because fewer reads over
the anchor come from ALT molecules than from REF ones in the BAM itself. - Two measurement options, temporary and retired when the sequence judge becomes the default
(#331):GBCMS_JUDGE_LONG_INS=windowkeeps the windows without the read-on, andoff
restores the judge before them. Reading a masked change on REF's haplotypes as the
reference's where REF's molecules show they lack it was measured with a third option and is not
adopted (it credits a read on other fragments' evidence, REJ-20261023-001); that option is
removed. The rules path is unchanged; RNA does not take the sequence judge. - Decided 2026-10-31 (operator): the masks laid on both windows by offset are accepted and laid
on every candidate haplotype, so a listed allele of the ALT's length that differs from it at a
masked offset is told from it by no base there (the ALT's carriers and a read with a third base
at that offset are partial evidence of either, never ALT); thecisoption is removed; the
anchor is taken from the insertion's run, wherever the row is written; the rule stays for pure
insertions of 50 bp or more (a delins with a longer side over 50 bp is a separate decision).
On HG002 this moves 4 reads in 2 rows on tr and 4 in 2 rows on nocl and sub, each from
partial evidence to REF (3 of the 4 on nocl and sub are true REF, none a true ALT), and no
other row of the four truth arms; the merge with develop's band rule (#369) moves only reads
on itsjudge farlines; the rules path is unchanged byte for byte.
Added — a macOS wheel for Apple silicon (arm64) (#237)
- The release builds
cp310-abi3-macosx_11_0_arm64onmacos-latestwith the new local action
.github/actions/macos-wheel, and publishes it to PyPI and the release page (with its
checksum and build-provenance attestation) beside the Linux wheel.pip install gbcmson
an Apple silicon Mac with macOS 11 or newer, on any Python from 3.10, now installs a wheel;
before, it built the source distribution (Rust, clang and htslib's C source). - The
Wheelsworkflow builds that wheel on a macOS runner on every change to the build, and
installs it on Python 3.10 and 3.14 withpip install --only-binary=:all:. The suite runs
there, and the counting run's output digests must equal the Linux ones. The same steps run
on a Mac withscripts/check_wheel.sh. - The action then checks that the wheel needs no newer macOS than 11.0: its platform tag, and
the minimum macOS in each binary's load commands (otool -l). The tests run on
macos-latest, so they would accept a wheel that needs a newer macOS; the Linux build checks
its glibc floor the same way. - Intel Macs (x86_64) still build the source distribution: nothing in CI uses an Intel Mac,
and a wheel for one would be a second entry in the same two jobs (or a cross build on the
arm64 runner) if a user asks for it. - Checked locally on macOS arm64, with the wheel of the stable-ABI change: the wheel is
3.2 MB, installs on Python 3.10 to 3.14, and the suite passes on all five.
Added — a native arm64 Docker image, joined with amd64 into one multi-arch tag (#236)
- The release builds the image on an amd64 runner and on GitHub's arm64 runner (no QEMU),
pushes each by digest, and a new jobdocker-mergejoins the two into one tag
(X.Y.Zandlatest). Pulling the tag on Apple silicon gets an arm64 image that runs
natively; before, Docker Desktop ran the amd64 image under emulation. The image lock's
hashes cover every platform's wheels, so both architectures install the same versions. - The
docker-testjob runs on both architectures. In each it builds the image on the
runner, checks that the image is the runner's architecture, runs--versionand
pip check, and runs the counting run on a synthetic data set. The new job
docker-comparefails unless amd64 and arm64 write the same bytes. - The Nextflow
dockerprofile still forces--platform linux/amd64. Dropping it would let
Apple silicon run the native image; that waits fordocker-compareto pass on real CI
runs, and it changes what a pipeline run on a Mac uses. - Publishing: a manual run of the release workflow builds both images and pushes nothing. A tag
push pushes two untagged images and joins them; the first tagged release is the first
time the join runs.
Added — CI runs the image through Apptainer, as RHEL clusters do (#233)
- The
docker-testjob installs Apptainer, builds a SIF from the image it just built
(docker-daemon://gbcms:test), and runs the CLI and a counting run on a synthetic data set
in it:apptainer execthrough bash, on the read-only SIF, with the data folders bound in
(what Nextflow'sautoMountsdoes), as the calling user. It fails unless the output digests
equal those of the same run under Docker. It runs on every change that builds the image. - Found by reading how Singularity and Apptainer differ from Docker: both bind the caller's
$HOMEinto the container, and a Python imports~/.local/lib/python3.x/site-packages
before its own packages. Anyone on a cluster withpip install --userpackages in the same
Python version would have had them replace the image's locked ones. Checked on the base
image with Docker (a package in a bound HOME is imported; withPYTHONNOUSERSITE=1it is
not). The image now setsPYTHONNOUSERSITE=1, and the CI run puts a package that raises on
import in the caller's home first, in the folder the image's own Python reads (it asks that
Python, so a Python upgrade moves the trap with it), and shows that Python reads it once
the setting is undone. A regression, or a trap in the wrong folder, fails loudly. APYTHONPATHin the
caller's environment still reaches the container (Apptainer passes the environment on;
--cleanenvstops it): the installation page says so.
Fixed — the sequence judge no longer misses a read's path through a long listed insertion (#369)
- Under
--read-judge sequence, the band around a read's own placement widened only for the rows
within its slack and two bases of the read. A listed insertion further on but within the read's
length could hold the read's best path (a read copied from inside an expanded repeat or a
duplication), and the band stood on a worse score. A listed insertion longer than 16 bases in a
read's reach but past its band now sends every state of the read to the full programme, and the
linear aligner drops its band for the read. Deletions do not trigger it. Measured on every arm:
no patient read changes; on HG002 622 reads change, every one to the call the full programme
gives (537 to depth only: through the insertion they fit each state alike); truth and both
oracles unchanged. CPU +0 to +4% on HG002; on the patient arms within 1% of the build
without the rule.
docs/proposals/sequence-judge.md§10.
Changed — one wheel for every Python from 3.10: the extension uses the stable ABI (abi3-py310) (#235)
- The Linux wheel is now
cp310-abi3: one file that installs on CPython 3.10, 3.11, 3.12, 3.13
and 3.14, and on later ones. Before, the one wheel wascp311, and every other Python built
the source distribution (a Rust toolchain and a C compiler). A release builds one wheel per
platform, not one per Python. - Why one wheel and not five. PyO3 builds under
abi3-py310only if every Python API the
bindings use is in the stable ABI; it builds, and these bindings (#[pyfunction],
#[pyclass]with#[pyo3(get)]fields,VecandOptionconversions,py.detach,
pyo3-log) use nothing else. The price is small. Measured on macOS arm64, Python 3.11 and
3.14, against a per-version wheel of the same commit: making 300,000Variantobjects and reading or
writing their fields is within 6% (the 3.11 run +4%, the 3.14 run -2%); building the small
tuples of aVariant(30,000 objects, three getters) is 15% slower (12.0 to 13.7 ms on 3.11);
one counting call on 4,000 SNVs takes 496 ms at best with either wheel (the median varies
by ±5% with machine load). The wheel is 4 KB larger (0.1%). - What the stable ABI cannot do: a free-threaded Python (3.13t, 3.14t) takes no
abi3wheel.
gbcms is not built or tested free-threaded (gil_used = truein the module), so nothing is
lost today. If a binding ever needs an API outside the stable ABI, remove the feature and
build one wheel per Python. - Checks. The full suite passes on 3.10, 3.11, 3.12, 3.13 and 3.14 with the one wheel, and a
counting run on a synthetic data set writes byte-identical output on all five and with a
per-version wheel of the same commit. The
Wheelsworkflow installs the wheel in Rocky Linux 8 and 9 with Python 3.10 to 3.14. - For developers: the built file is
src/gbcms/_rs.abi3.sonow. Python tries a
version-specific suffix first, so a leftoversrc/gbcms/_rs.cpython-3xx-*.sofrom an earlier
build wins over it and runs the old engine. Move it to the Trash and runmaturin develop --releaseagain; a new test fails when the loaded extension is not the.abi3.so.
Changed — the Linux wheel installs on RHEL 8 (manylinux_2_28), and the extension links no OpenSSL or libcurl (#234)
- The release builds its Linux wheel in the
manylinux_2_28image (AlmaLinux 8, glibc 2.28),
notmanylinux_2_34. The wheel now installs on RHEL 8 and newer; before,pip installon
RHEL 8 built the source distribution. - Why the build image had to change: the curl that rust-htslib builds in refuses OpenSSL older
than 3.0, and RHEL 8 ships 1.1.1. The fix is not a newer OpenSSL but none: rust-htslib is
built without its defaultcurlfeature, which only serves remote files (http, https, s3,
gcs), and gbcms opens only paths that exist.curl-sys,openssl-sysandopenssl-srcleave
rust/Cargo.lock, and with them the OpenSSL license question of a wheel that carries
OpenSSL. Counts are unchanged: a counting run on a synthetic data set (SNV, MNV, insertion,
deletion; two paired-end BAMs; mFSD on) writes byte-identical output with and without the
feature. On macOS arm64 the wheel falls from 5.77 to 3.20 MB. - New
Wheelsworkflow and local action.github/actions/linux-wheel(the recipe the
release also uses): on a change to the build, it builds the wheel and installs it in Rocky
Linux 8 and 9 containers withpip install --only-binary=:all:, runs the test suite there,
and compares the output digests of a counting run (scripts/wheel_fixture_run.py) between
Rocky 8, Rocky 9 and themanylinux_2_34wheel that earlier releases shipped. The same
steps run on any RHEL machine withscripts/check_wheel.sh.checkpoint-wheels.ymlis
unchanged: it builds older commits, which still link libcurl, so it keepsmanylinux_2_34.
Fixed — the docs say what the original's --filter_non_primary removes, and list the measured differences from 1.2.5
docs/reference/read-filters.mdimplied that the original GetBaseCountsMultiSample's single
--filter_non_primaryflag covers supplementary alignments. It removes secondary alignments
only (flag0x100); a read's supplementary records stay in its depth,REFandALT. The
comparison now says so.docs/reference/architecture.mdhas a new list, "Known differences from
GetBaseCountsMultiSample 1.2.5": one line per difference with its cause, the HG002 reads and
rows it moves, and the rule that explains it. Docs only; no count changes.
Fixed — the Smith-Waterman local re-scoring's partial_alt reads the scores that made the call (#195)
- Where a complex row's semiglobal scores are unreliable, the
swstep re-scores with a local
alignment and calls on those scores, but its nearby-evidence check (a REF call with ALT close)
and its tie check still read the semiglobal ones, sopartial_altcould follow scores that
did not make the call. Both now read the scores that made the call. REF, ALT and depth are
unchanged. Only complex rows the exact-carrier rule cannot judge reach this step; on every
arm (about 4.5 million read calls under each backend) none did, so no measured count changes.
Changed — the sequence judge holds one memory budget per thread (#304)
- Under
--read-judge sequence, a row's caches (the verdicts and programmes of reads alike)
and the judge's work space now share one budget of 128 MB per thread. Past it a row caches
nothing more and its later reads are judged afresh, with the same calls; work space past a
quarter of the budget is freed at a row's end. Counts are unchanged. On HPC the deepest HG002
arm's peak memory falls from 2.8 to 1.7 GB for 1.8% more CPU; patient data types are within
2% of no limit at every budget.docs/proposals/sequence-judge.md§10.
Fixed — the sequence judge keeps a read's path steps only for the states its call can examine (#304)
- Under
--read-judge sequence, a read's best path, base by base, was kept for every state of
the site with the read's programmes, which the row keeps for every read alike, so at sites of
many alleles it grew with reads × states × read length. It is now kept only for each class's
best state and state 0, the only ones a call is examined on. Counts are unchanged. The row
caches are 6 to 7% smaller; with the 128 MB cache limit, HG002 peak memory falls 3 to 9%.
Fixed — the sequence judge's band no longer stands on a worse score in a tandem repeat (#369)
- Under
--read-judge sequence, the band around a read's own placement stood on its edge
alone for any score down to −23, what an indel leaving its 16 columns costs. A read its
aligner placed a repeat unit off slides along the repeat at no cost and leaves the band on a
short gap, so the band could stand on a worse score than the read's best. The floor now
takes the longest tandem-repeat period in the read's reach off the band's slack, and asks
for a perfect score once the period reaches it. Measured on every arm, with path-align: on
the patient arms 2 reads change (gate subset, both agreeing with the oracles); on HG002 666,
truth +4 ALT on the tandem-repeat arm and no wrong calls (without path-align, 90% of them
move to the full programme's call). CPU +2 to +12% on the patient arms, +16 to +38% on
HG002.docs/proposals/sequence-judge.md§10.
Changed — the sequence judge examines a call on the alignment its score came from (#304)
- Under
--read-judge sequence, a read's call (the bases at X, the anchor, the deciding
quality) is read off the path the graph programme traced, not a second, linear alignment.
Tied placements in a run are as before; where the two differed (a band missing the graph's
path, a deletion tied across repeat units) the call now rests on its score's own alignment.
Measured on every arm: both oracles pass everywhere, HG002 truth +1 ALT; the judge takes 2 to
28% less CPU.docs/proposals/sequence-judge.md§10.4.
Added — the sequence judge, opt-in (--read-judge sequence, #304)
- A second read judgment for small-variant rows, behind a flag. With
--read-judge sequence, each admitted read is scored whole, every readable base,
against the row's candidate sequences: REF, the row's ALT and the other alleles at its
site (listed rows over its span, and alleles the reads themselves write recurringly and
tell apart), each with every context of the other listed rows within a read's reach and
of the recurring background the carrier guard finds. Costs are affine (a confident
mismatch 4, a gap 6 plus its length); a base below--min-baseq, orN, costs nothing.
A read counts ALT or REF only where its best candidate with that allele beats every
other by one confident base; another allele winning by as much is partial evidence;
anything closer is depth only. Contexts only explain a read's other differences: the
judge does no linkage and no phasing, so a variant linked to the row elsewhere on the
molecule never credits a read. Design and every measurement round:
docs/proposals/sequence-judge.md; its rules RJ-28 and RJ-29 are in
docs/reference/read-judgment.md. - Opt-in: the default is unchanged. Without the flag (
--read-judge rules) the
size-specific rules count the reads, with output unchanged. The Nextflowread_judge
parameter (DNA only) defaults torules.haplotype, the judge's name during
development, is taken assequencewith a warning. Making the judge the default is
6.8.0's work (#331). - Preparation and counting are told the same read judgment.
prepare_variantstakes
read_judge("rules", the default, or"sequence"). It records each row's rows within
a read's reach only for the sequence judge, the only one that reads them. Counting
under the judge refuses rows prepared for the rules, with an error that names the
first such row, so a caller never silently gets the judge without those rows.
observe_moleculestakesread_judgeas well (default: its alignment config's,
"rules") and passes it to both preparation and counting. - Three diagnostics, under the judge only (no new column; they change no count):
MNV_SHARED(chrom:pos:REF>ALT:n/m)at an SNV whose change a co-listed MNV holds (RJ-28:
the MNV's carriers count at both rows, so one molecule can sit in both rows' counts and
mFSD distributions); its indel twinINDEL_SHARED(chrom:pos:REF>ALT:n/m)at a row a
listed indel or delins holds whole with listed neighbours (a delins that is two listed
deletions together counts its carriers at each); andUNEXPLAINED_READS(n/m), the row's depth reads that fit none of
its candidate sequences within ten confident mismatches (RJ-29: such a read is depth
only where its aligner also doubts its placement, XS at least AS). - The alleles the reads spell enter as the bases they change (RJ-25, amended
2026-10-29). An allele the reads spell at the locus is a candidate written past the carrier
guard's masks and trimmed to its change, so a masked background base is never part of it.
Written with its carriers' bases over the guard's window, such an allele took from the
row every read carrying the masked change: at a 17-base duplication whose carriers all
carry a listed SNV beside it, 293 carriers per run were partial evidence instead of ALT.
An allele that is the row's ALT (or REF) with listed neighbours is the row's ALT (or REF)
however a neighbour is written: the judge reads every neighbour whose placements meet it,
so a molecule the input lists several ways (a delins and its two component deletions)
counts at each of those rows (RJ-28's indel twin, decided 2026-10-30), and
INDEL_SHAREDnames the shared molecule. The rules path is unchanged. - Named limits.
- Spliced reads are refused.
gbcms rna --read-judge sequenceexits with an error,
and the engine refuses RNA mode under the judge; in a DNA run, a read whose CIGAR
splices (anNop) is depth only. RNA under the judge is #333. - The Phase 3 options do not apply to a row's counts under the judge
(--alignment-backendand the PairHMM probabilities): it has its own scorer, and it
reads each row's candidate sequences from the reference FASTA. - It is slower than the rules; the speed work is #335.
GBCMS_JUDGE_*environment variables are measurement switches, not options:
they serve the measurement rounds and are removed as their questions are decided.
Removed as decided:SCORER, together with the enumerated scorer it selected (and
FOUND_PROBEandOTHER's found rules other thantold, which only that scorer
read);TRIAGE,TRIAGE_EXACTand
TRIAGE_UNKNOWN, together with the k-mer triage;DELINS;CONTEXTS; andENDS.
The default counts do not change.
- Spliced reads are refused.
Fixed — an unchecked sample no longer reads as a clean one (#227)
- The QC summary names a sample whose output could not be read. When one sample's
output could not be read, the combined block left it out and the closing line said "No row
needs a look". Now the block counts "k of n samples" and lists the sample under "Not
checked" with the reason, a sample whose counting failed is named too, and the closing line
is aWARNING("1 of 3 samples not checked: ..."). The exit code is unchanged.
gbcms summarizedoes the same for an input it refuses or cannot read: it summarizes the
others, names the input, and still exits 1. A MultiQC report carries the warning. gbcms summarizerefuses a VCF that is not gbcms's. An ordinary VCF, a bare header
and an empty.vcfread as clean outputs (every record a PASS row with no flags). A VCF
now needs gbcms's ownGSINFO field, declared in its header or on its first record; the
rest are named under "Not checked".- A MultiQC file left by an earlier run is removed when this run has nothing for it, the
summary table as well as the warning, so a report never reads old counts. A skipped input
that shares a name with a checked sample gets a row of its own in the warning instead of
replacing the checked sample's. - The MultiQC table is built in one pass over the cohort. It looked for the flags that
fired once for every sample, so its time grew with the square of the cohort; the output is
byte-identical.
Added — gbcms summarize, the merge NA flag and --qc_summary (#227)
gbcms summarize <outputs...>makes the QC summary of finished outputs. It reads
.mafor.vcfoutputs (a run's, or a merge's) with the code the run's own summary uses,
and changes nothing in them.--format text(the default) is the block the run logs, for
one sample or many;jsonwrites one<sample>.qc_summary.jsonper output (every flag,
zeros included, and every row that needs a look);tsvwrites one cohort table;multiqc
writes MultiQC custom content (gbcms_qc_summary_mqc.json, and a
gbcms_qc_warning_mqc.jsonsection only when a row needs a look). The exit code is 0
whether or not rows need a look.gbcms dnaandgbcms rnagain no option.- A merged MAF is summarized too, with a flag for NA combined cells. The summary reads
each input's flags under its prefix, andCOMBINED_NA("check the call") marks a row whose
simplex_duplex_*count, total or VAF isNA: an input count was missing or not a number,
or the duplex and simplex rescues adopted different alleles. It is built from what
gbcms mergewrites; no column is added. - Nextflow:
--qc_summary(defaultfalse). After all samples finish, one task runs
gbcms summarize --format multiqcand--format jsonover every output, merged MAFs
included, and publishes them togbcms/qc_summary/.
Added — an end-of-run QC summary in the run log (#227)
- The run log now ends with a QC summary.
gbcms dnaandgbcms rnalog one block
after the last sample: which QC flags fired and on how many rows, which rows need a look
(a flag is "fix the input", "check the call" or informational; the first two need a look),
what to do about each, the first rows to look at, the output fields to filter on, and the
address of the QC Flags page. With several samples in one run the block combines them.
The run's last line is aWARNINGwhen any row needs a look and anINFOline when none
does; the exit code is unchanged. - It reads the written output and changes nothing. The block comes from the MAF or VCF
the run wrote, read back after writing: no count, column, row or file differs (a test pins
the output files byte-identical with and without it), and there is no new flag or output
file. A failure to read the output is a warning, never a failed run. - One table for the code and the docs.
QC_FLAGSingbcms.qc_summaryholds each
flag's tier and one-line action; the QC Flags page repeats it under "End-of-run summary".
Tests fail when a flag the page defines has no entry, when an entry names no flag on the
page, and when the page's tiers, actions or example block differ from the code's.
Fixed — per-transcript count lists in a machine-independent order (#284)
transcript_read_countsandtranscript_fragment_countslist their transcripts in
ascending ID order, on every machine. They listed them in the order the exon interval
tree visited the overlapping exons, which depends on the CPU's SIMD backend (NEON, AVX
or none) and on how equal intervals fall, so one build on one input wrote the same
entries in another order on another machine (23 of 73,649 cells differed between an
Apple Silicon run and an x86 Linux run of the 6.6.0 release panel). Counts are
unchanged; only the order of entries is. The cells of two machines now compare as
bytes, with no set comparison. The RNA docs state the order.
Changed — a read-rule change starts from its mechanism (#304)
- Fix the class, not the row. The procedure for changing how reads are judged
(read-judgment.md, "How a change to read judgment is made") now asks for the
mechanism first, stated without the row that showed the bug, with the rule derived
from the principles. Its tests cover the other shapes of the same mechanism, any cutoff
comes from a mechanism rather than from where a measurement splits, and a rule narrower
than its mechanism says why. The fix is judged on data the bug was not found in. - Each decided read rule states its mechanism. The register has a Mechanism column,
filled for RJ-1 to RJ-27, and new entries state theirs. Counts are unchanged.
Fixed — a GTF transcript ID that another chromosome or strand reuses is another transcript (#216)
- The GTF parser keyed transcripts by ID alone. An ID used on more than one chromosome
or strand (the same RefSeqNM_on chrX and chrY, alternate loci) merged into one
transcript: its exons from both places were sorted together, so the consecutive pairs
made introns that exist nowhere and missed the real ones, and the whole transcript sat on
the first chromosome seen. A spliced read over a real intron then matched none of them
and was left out of the transcript's counts; the same introns fed the intron boundaries
(RETENTION_DOMINANT, the exon-edge BAQ rule, RNA clip edges) and ASJD's known-junction
check. A transcript is now an ID at one chromosome and strand, as the strand spans
already were (#185): its exons, introns, boundaries andtranscript_read_counts/
transcript_fragment_countsentries are its own. The parser warns once when a GTF
reuses an ID. Exons of one ID at the same chromosome and strand still join (versions that
differ only by suffix share a transcript). Counts are unchanged for a GTF that reuses
no ID: Ensembl GRCh38.111 (FORTE's) has none among its 252,989 transcript IDs.
Fixed — the docs state the unreadable-insert rule as decided (#304)
- An insert of the ALT's length whose bases are all masked is the ALT, where no other
allele of that length is at the locus. The rule (RJ-20, amended 2026-10-15 in #278,
re-confirmed 2026-10-26) is applied by the exact-carrier rule, which judges pure indels by
their bases. Several pages still gave the superseded rule, that such a read is neither,
counted inpartial_alt: the register row (amendment at its end),
allele-classification.md(Dispatch, Insertion, the wrong-length table), the glossary's
has_unreadable_insert, and the Read Filters line added in #329. - Those pages now lead with the current rule. They say that the CIGAR checks, which run
only for an insertion the exact-carrier rule cannot judge (no prepared reference), still
count such a read neither, inpartial_alt. Counts are unchanged.
Fixed — a MAF row at Start 0 is kept: an insertion is counted, any other fails visibly (#219)
- An insertion before a contig's first base is no longer dropped. A MAF row with
Reference_Allele-andStart_Position0 (Start_Positionis the base before an
insertion, so 0 is before base 1) failed the internal model'spos >= 0check in the
reader and vanished with a warning: no output row, noFAILrow. It is now resolved
to the VCF spec's form at position 1, the base after the event (REFthe first base,
ALTthe inserted bases then that base), and counted as the same event given as VCF
is, as a deletion at Start 1 is (#122). MAF output keeps the row's input columns; VCF
output andgbcms convertwrite the position-1 record with the row in
MAF_START/MAF_REF/MAF_ALT. Where the first base isN(human contigs), the
ALTcontainsNand the row is a visibleFAILwithALT_CONTAINS_N. None in the
sign-out data (vcf2maf writes such an event with sequence alleles at Start 1). - Any other row at Start 0 is a visible
FAILrow, not a dropped one. A substitution,
a deletion, a delins, or an insertion written with sequence alleles atStart_Position
0 names a base that does not exist. The reader dropped it with a warning (only the first
five named); it is now kept asFAILwith the new reasonSTART_BEFORE_CONTIG, its
input columns as written, not counted. VCF output andgbcms convertwrite it as the
symbolic<NON_SEQUENCE>record at position 1 (REF the first base, reason inGSR).
A negativeStart_Positionis still skipped with a warning. A VCFPOSof 0 is still
an error.
Fixed — --min-baseq help and docs say what it does (#304)
--min-baseqmasks bases, never reads. The CLI help ("Minimum base quality") and
the docs did not say what happens below the threshold, and Read Filters said an SNP or
MNP read was "rejected". A base below it is read as unknown, likeN: it supports no
allele and counts against none, and the read still counts toward depth, judged on its
other bases. An MNP's differing positions are judged one by one (masked ones skipped),
not by the block's lowest quality. The help text, the DNA, RNA, quickstart and
Nextflow parameter tables, and Read Filters now say so. Counts are unchanged.
Changed — two code-quality rules the code did not meet
__all__in every module.report/mfsd_report.pyandrescue_audit.pyhad none, so
a star import or a doc tool took every name, helpers and imports included. Each now
lists its public API (TOOLTIPSandgenerate_mfsd_report; the rescue audit's two
functions, asio.outputlists its functions and leaves its constants to be imported by
name), and a test holds every module of the package to the rule.- No
unwrap()in production Rust. The four lines that had one are changed. The KS
test of fragment sizes (mfsd.rs) sorted withpartial_cmp(..).unwrap(), which panics
on a NaN size; a NaN now makes the test invalid (D = NaN, p = 1.0, as with too few
fragments) and is warned about, and the sort is a total order. No size is ever NaN
today (they are integers read from alignments). The other two could not panic (a branch
that had just matchedSome; a loop guarded by the allele lengths) and are written
without the unwrap (variant_checks.rs,left_align.rs). Counts and outputs are
unchanged.
Fixed — companion file names, .vcf.bgz in normalize, the amplicon rule, Nextflow RNA defaults (#309)
- Companion files keep every dot of the output name. The mFSD Parquet and report
were named by swapping the main output's extension, which dropped a dotted
--suffix(S1.genotyped.maf→S1.fsd.parquet) and a dotted sample name, so two
samples (S.1,S.2) wrote the same file. They are now
{sample}{suffix}.fsd.parquet/.mfsd_report.html, as documented, and the
observations Parquet honours--suffix({sample}{suffix}.observations.parquet). gbcms normalizereads.vcf.bgz, which the CLI already accepted: one
variant_format()ingbcms.io.inputnow reads a variant file's extension for the
CLI, the pipeline and normalize.- The amplicon rule applies to programmatic callers too.
GbcmsRnaConfigwith
library_type="amplicon"now reads the library unstranded, and an unstranded
protocol disables strand enforcement, as the CLI already did. --mfsd-reportrequires--format maf. The report reads its statistics from
the MAF output; with--format vcfthe run is refused at configuration time.- A read calling no base over a row's window is depth only at a contig end. Where
the exact-carrier rule cannot judge a row (its repeat runs to the contig end), the
CIGAR checks' reference-coverage rules (a deletion's anchor covered with no gap; a
deletion-direction delins' anchor covered by a clean match) counted a read whose
bases there are all N as REF. Such a read is now neither (N class), as the
exact-carrier rule counts it elsewhere. No existing test's counts change. - Nextflow RNA tasks pass the RNA CLI defaults. The RNA module passed DNA's
PairHMM gap penalties (1e-4/0.1 over RNA's 5e-3/0.25) and--no-baq(BAQ is on in
gbcms rna). RNA now reads its own params,rna_gap_*andrna_apply_baq(on), as
it already didrna_min_mapq; a test pins every module's param defaults to its
CLI's. Measured on the 33 local FORTE slices (94 rows), old module arguments against
the CLI defaults: with the GTF no row changes; without it one row's REF count was 5
reads (3 fragments) higher under the old arguments; AD and depth unchanged.
Fixed — user docs that were wrong or failed (#310)
- Install. One page states what the release builds (one cp311
manylinux_2_34
x86_64 wheel and the sdist; every other platform builds from source with a Rust
toolchain and a C compiler); the README links to it. Nextflow 24.04.0 or later, as
the manifest enforces. - Examples. Every
gbcms dna|rna|merge|normalize|convertexample in the docs parses
against the CLI (the synopses and three DNA examples lacked the required
--output-dir). - Unstranded RNA is
--strandedness unstranded;--no-strandednessalone only stops
the filtering. The RNA MAPQ text links to the measured default. - Output reference.
MFSD_QVAL_ALT_REF(13 mFSD INFO fields),GRonly with
--rescue-mnp, ASJD fields in the VCF, the realnorm_*andrna_splice_spanning
names, nogene_strandMAF column (the GTF supplies the strand), version examples. - Nextflow.
--gtf,--publish_dir_modeand--tracedirdocumented;
GBCMS_NORMALIZEis in no workflow. - Release gate. BAM Evidence Caveats lists C6 #143, C24 #195, C33 #214, C34 #216 and
all of C31 #212, with the status their issues give. - Guard tests (
tests/test_docs_contract.py): the fenced examples, the documented
MAF columns and VCF INFO/FORMAT names against the writers, and the Nextflow parameters
table againstnextflow.config.
Changed — a co-annotated sibling contests an exact-carrier ALT read by its bases (#300)
- The AD-claiming sibling guard re-tested every non-anchor ALT call against REF on the
read's aligned slice of the variant's context, whenever a sibling was listed. An
exact-carrier call had already weighed REF against ALT by its bases wherever the read
could lie; where the aligner wrote carriers a unit off (a VNTR) that slice read as REF
and every carrier was demoted. Listing a sibling zeroed the row's AD, even with no
read of the sibling. - Such a read is now placed whole on this row's ALT and each sibling's: the sibling
claims it where it fits with as few confident mismatches or fewer (a tie is neither
row's AD, partial evidence at both); a read placed on no allele keeps the aligned
tests; other calls keep the guard as it was (operator, 2026-10-22). - Measured against #294's final build:
- Synthetic same-site 40-bp VNTR compound hets, BWA-placed: AD 0/0 to each row's
count alone (41/48). - HG002 (public): 6 insertion rows gain 41 AD (a VNTR compound het 0/0 to 6/6). On
250 compound hets (500 rows) the summed distance of AD from a per-read oracle falls
from 1127 to 831; reads counted ALT at both rows of a site from 17 to 5, and
other-allele reads counted ALT from 13 to 0. - Local arms (aggregates): IMPACT panel 11 rows, AD −267; the gate set 41 rows, AD
−232; ACCESS 1 row, AD +1; WES and FORTE unchanged. Against every allele of the
cluster (an independent BWA check of REF, the row's ALT and each sibling's ALT), 556
of the ALT calls removed were a sibling's molecules or ties counted at both rows; 51
of the calls gained are the row's, 17 are not; 126 changed calls stay unresolved.
- Synthetic same-site 40-bp VNTR compound hets, BWA-placed: AD 0/0 to each row's
Changed — reads of repeats whose telling base the guard masks, and reads placed a unit off (#294)
- The problem. In a repeat whose units differ at single bases, the guard can mask
the only base telling a one-unit insertion from REF: a germline SNP that turns the
next unit into the inserted one makes both haplotypes read alike over a short read.
The rule then gave up and the CIGAR checks counted the reads. The fix decided on
2026-10-17, leaving such a mask unapplied, counted the SNP's haplotype as the
insertion (HG002's 28-bp VNTR row: RD/AD 10/0 → 1/8, both phased ALT reads from the
SNP's haplotype); it is rejected (REJ-20261020-001). - The rule (RJ-27, amended; every event the exact-carrier rule judges).
- The windows read on past the masks to the next base telling the alleles apart.
Prep fetches reference until, with every base the guard could mask masked, the
windows reach one. A read short of it is depth only unless its own bases fit one
allele alone and exactly. - Reads placed apart from the event tell a germline change from the ALT's own
base. An ALT read written without its gap shows the ALT's base past REF's tract,
which looks like a change on the REF reads; a germline SNP can give the same base.
The fragments placed apart from the event (whose bases at the telling bases past it
fit the reference more often than the shifted ALT) decide:- carried by three or more of them and 3%: a haplotype's change, masked at any
share of the REF reads (an SNP haplotype at a minority of molecules, after a
tumour's gain or loss, never counts as the insertion), unless its carriers are
too one-sided in pair orientation against the locus's own balance for a
haplotype's (binomial chance under 0.1%): an oxoG or FFPE artifact, not masked
(an amplicon library, read in one orientation, weighs none); - otherwise, read by 15 or more: the ALT's own base, never masked and never another
allele; with fewer, masked where it is background.
- carried by three or more of them and 3%: a haplotype's change, masked at any
- An error both readings share decides nothing. A read placing no length change
whose REF and ALT readings miss exactly the same bases is read on with those bases
unread, not counted for whichever allele tolerates the error at the length the
aligner wrote. - One error forgiven past the windows (operator, 2026-10-21). Read on past the
windows, one confident error the other allele's reading does not share is forgiven
where that reading misses three or more, the read writes that allele's length, and
the error is a one-off (recurring on two fragments and 5% of those fitting the
allele, it is another allele's base). Within the windows only a shared error is
tolerated (2026-10-15). - Reads placed a unit or a copy off are read whole. A read whose alignment is not
clean (another event of its own in the window, or two or more confident changes
from the reference) and that is not spliced is placed ungapped on each allele over
300 bases of reference past the change (Variant.whole_ref, fetched by prep). The
allele whose windows it reads exactly, with at most one mismatch elsewhere, is its
call where every other allele misses by more. A read fitting the longer allele only
from inside its event is depth only. - A call decided past the windows must hold wherever the read could lie (operator,
2026-10-22). Past the windows a read is read where the aligner wrote it; in a repeat
the other allele may hold the same bases a copy over. The call stands only where the
other allele, the whole read placed ungapped anywhere on it, misses more confident
bases than the called allele; otherwise the read is depth only. HG002's chr21 89-bp
duplication, whose cis SNP sits where the inserted copy's last base shows: 2 of its
insertion reads, written through the copy, counted REF. The read is placed on the
alleles as the reference spells them, no mask laid: a base the guard masks beside the
event may be the other allele's own shifted onto the reads written through it (HG002's
16-bp TC insertion where the run meets an AT run lost 6 true carriers on the masked
alleles). A read left depth only so still counts in the census of recurring
substitutions: its bases were read (leaving two reads out moved a substitution on 5% of
a FORTE row's REF fragments across that line, flipping 36 other reads).
- The windows read on past the masks to the next base telling the alleles apart.
- New diagnostic:
POSSIBLE_MINORITY_HAPLOTYPE(chrom:pos:REF>ALT:c/n). A change at
the ALT's shifted base that two or more of the reads placed apart carry (over 1%),
too few or too one-sided for a haplotype's, left unmasked: a minority haplotype
carrying it would count ALT. Counts unchanged (BaseCounts.possible_minority). - Binning. The guard reads fragments within 300 bases of the variant, so every bin's
fetch now holds each member's guard window (window::guard_window). - Cost. One indel with 17k reads: 0.22 s against develop's 0.21 s at 0.1% error,
0.48 s against 0.21 s under a 1% error stress. - Measured against develop (final build), rows changed and RD/AD:
- HG002 (public): insertions 96 of 1440 rows (RD −64, AD +137); small insertions 50 of
1560 (RD −17, AD +47); small deletions 41 of 1560 (RD +2, AD +32); delins 19 of 800
(RD −1, AD +51); deletions of 50 bp or more none of 600. - Local arms (aggregates): IMPACT panel 8 of 1170 rows (RD +2, AD +19); ACCESS and
normals 6 of 1402 (RD +4); WES 9 of 686 (RD −5, AD +13); the gate set 151 of 4606
(RD +21, AD +47); FORTE RNA 1 of 94 rows (RD −1), with the GTF and without. - The past-windows check alone (against the build before it): REF calls only, never
AD. HG002: 27 REF calls left depth only, on 13 reads whose bases favour ALT, 13 that
tie and 1 favouring REF (scored against the truth haplotypes). Local arms: 101, every
one on a read an independent BWA check of REF against ALT calls ambiguous (83) or ALT
(18).
- HG002 (public): insertions 96 of 1440 rows (RD −64, AD +137); small insertions 50 of
- Known limits.
- In an imperfect repeat the windows anchor on the exact repeat's edge (#290), so a
read lying inside it, or read from a flank the repeat runs on past, is still placed
by the aligner where the windows decide it. Pinned as strict xfails. - Insertion reads the aligner soft-clipped over a long duplication, or wrote through it
as REF with mismatches, are depth only though their bases show the insertion (13 of
20 such reads at HG002's chr21 and chr22 rows; #290). - A haplotype showing on fewer than three, or under 3%, of the reads placed apart is
taken for errors and its reads count ALT where they read as the ALT (with 30 to 45
of them, an SNP haplotype on 5–10% of the molecules in about a third of runs; at
depth, any under 3% of the molecules); such rows are named by the diagnostic. - A position-specific error giving that base on 3% of the reads placed apart in both
orientations, or a one-orientation artifact on fewer carriers than tell it from a
haplotype (about ten), masks it: the ALT's reads short of the next telling base are
depth only.
- In an imperfect repeat the windows anchor on the exact repeat's edge (#290), so a
Fixed — PairHMM aligned the haplotype whole and the read's ends free
- The PairHMM fallback gave the library the read as its free-ended sequence: the read's
ends went unaligned at no cost and every haplotype base it did not reach was charged
as a gap, the reverse of the semiglobal alignment theswbackend uses. An insertion
read whose window held only part of the insert (a read ending at the anchor, clipped
a few bases in, or a long insertion written without its gap) scored as REF (LLR -20 to
-59); a read ending inside a repeat run leaned REF. The read is now aligned whole and
the haplotype's ends are free: such reads are neither or ALT as their bases show, and
calls on reads holding every base of both alleles are unchanged (red-first unit tests
on each shape). - Measured: no read reached the PairHMM, or the WFA triage before it, on the IMPACT
panel, ACCESS and normals, WES, the gate set, any HG002 arm (develop and the #294
builds) or the FORTE RNA slices (develop, with and without the GTF), so no count there
changes. - An N at the deleted base of a REF-length read leans REF by its length (operator,
2026-10-22): the regression test that held it near-neutral scored a flankless run with
the old orientation; with flanks the PairHMM gave the same REF lean before this fix.
Changed — a run beside a long deletion is a neighbour where its bases rejoin the flank (#285, #296)
-
The rule (RJ-24, amended). A run of changes past a long deletion's junction used
to be another allele. It is now a phased neighbour, masked like a single change,
when two things hold:- its bases rejoin the allele's continuation after it;
- no other breakpoint gives the reads' bases exactly: neither the deletion at another
start or of another length, nor the allele with one more indel.
This applies to the carriers, and to REF reads inside the deleted bases. It covers a
run inside the window as well as one running past it.- A run that keeps mismatching after it is still another breakpoint.
- So is a run at the junction whose bases are the deleted bases' own: the deletion
shifted right. - On the carriers alone, the diagnostic names the deletion with the run.
-
Single changes are checked the same way (#296, folded in). An allele's recurring
changes in the window are taken together. Where another breakpoint gives the reads'
bases exactly, none is masked and the reads count as another allele. Before this, a
same-length deletion 2–3 bp right of the given one, or another unit count of a VNTR
whose units differ at one base, could count ALT. -
A change another input row lists is that row's allele (operator, 2026-10-19). Prep
records on each deletion of 50 bp or more the input's other substitution rows inside
the reference it holds (Variant.listed_neighbours). A carrier change one of them
lists is masked as a neighbour, never read as another breakpoint, and not named. With
the input taken as correct, HG002's 90-bp deletion reads VAF 0.11 with the deletion
alone in the input and 0.58 with its phased SNV row (truth 0.5). Its carriers there
read the same as another unit count of the repeat. -
How the guard reads.
- Each fragment is read once, its mates merged base by base.
- It is taken as an allele by its bases past the windows, read on as RJ-27 reads (12
bases, a shift test, and on through 24), or else by its four continuation bases. - Prep now holds as many reference bases past a long deletion as it deletes.
-
Measured against develop, on 587 local deletions of 50+ bp and every other arm:
- Panel: three rows change.
- The phased-DNP row: ALT 0 → 48 (sign-out 49).
- A 167-bp deletion: RD 117 → 319 and AD 293 → 284 (sign-out 352/291). Its REF
molecules carry a germline SNP on the first deleted base. - A 67-bp deletion beside its sibling 2 bp right: AD 47 → 46.
- Gate panel's pure indels: the same three rows. Normals and ACCESS, WES, HG002's
300 deletions, and FORTE are unchanged. - Diagnostics: at three 1,023-bp panel rows the observed allele now names the
reads' 1,086–1,118 bp deletions.
- Panel: three rows change.
-
Known limit (#297). A read of another unit count still counts ALT where the
repeat's units are identical as far as the read reaches.
Changed — a read aligned over its reading flank, confirmed from the other (#279)
- The rule (RJ-23, amended). Some reads are aligned straight across a variant's
windows and fit the shorter allele read from their reading flank. Such a read is now
also read whole from the other flank, at the longer allele's length, wherever its own
bases place that flank unambiguously.- Its bases may favour the longer allele there by two bases. That means the
aligner wrote the longer allele's other copy over the reading flank: an insert,
or a deleted sequence, whose end resembles the flank, aligned onto it as
mismatches with the rest clipped. - If the read holds the longer allele's window from there and reaches past it, it
counts as that allele. - If it starts inside the event, it is depth only, never the shorter allele.
- Its bases may favour the longer allele there by two bases. That means the
- Why spanning carriers only (operator, 2026-10-16). Counting carriers that start
inside the insert would give the longer allele read starts the shorter one cannot
have. On HG002's long insertions that pushed inserts of 50 bp or more to a median VAF
of 0.61–0.86. - Measured against the build before it:
- HG002 insertions of 10+ bp in band: 334 → 343. Mean |VAF − 0.5| goes 0.157 →
0.153; 57 rows move nearer 0.5 and 24 farther. - HG002 short insertions in band: 552 → 555. Deletions are unchanged.
- Gate panel's pure indels: 7 insertion rows change (AD +10, RD −29). Against
the bases oracle, no ALT read is lost, and 39 REF reads move from partial
evidence to depth. - WES, normals and ACCESS, and FORTE: unchanged.
- HG002 insertions of 10+ bp in band: 334 → 343. Mean |VAF − 0.5| goes 0.157 →
Fixed — a pure indel the exact-carrier rule cannot judge is warned
- A pure insertion or deletion the exact-carrier rule could not judge fell back to the
CIGAR checks with nothing logged. - Its reads now count in the fallback tally, and the variant is warned once with the
reason, as a complex variant's fallback is. One example is a VNTR whose recurring
masks leave no base telling the alleles apart (#294). - Counts are unchanged.
Changed — pure insertions and deletions judged by their bases (#278)
- The rule (RJ-26, RJ-27). A pure insertion or deletion under 50 bp is judged by
the exact-carrier rule, as complex events are: by the read's bases, not where the
aligner wrote the gap; the CIGAR checks judge only what the rule cannot (no prepared
reference). Its windows are its tract (where REF and ALT differ over every
placement), grown through the repeat its change lengthens or shortens, with one base
each side (D1). A read's own indel is placed by its bases: inside the discrimination
window, of the ALT's length and spelling it, it is the ALT; another length or
sequence there is another allele (partial evidence); placed only outside, a separate
event. One substitution is an error, tolerated where the other allele mismatches the
same base, unless it recurs inside the event (another allele) or sits in the flank,
where it is a phased neighbour and the reads keep their call. Reads whose windows fit
both alleles, or neither alike, are read on past them; a fit resting on too few bases
is confirmed there (a read whose further bases fit the allele shifted by another
length is another allele). - Decisions (operator, 2026-10-15/16). One flank base (D1); a truncated copy of a
long insert is another allele (D2: the CIGAR path's identity band was measured and
not adopted); the ALT written far off with an SNV in its flank is ALT (D3); a
substitution all carriers share inside the event is another allele (D4); a deletion
whose bases between its flanks are all masked is ALT where its confident flanks lie
the ALT's length apart (D5); a change the REF molecules carry inside a deletion's
deleted bases is background on REF (D7); mates that disagree resolve by quality
(D8); an unreadable insert of the ALT's length at its junction is the ALT (RJ-20,
superseding C35's partial); soft-clipped bases are read (RJ-13); a pure indel grows
only through repeats on the allele holding its bases. - Why. Develop's CIGAR checks took the aligner's placement of the gap as the
allele, so reads carrying the ALT written elsewhere, or another allele written where
the ALT goes, were counted by where they were written. On HG002's het indels (truth
VAF 0.5) against develop's CIGAR checks: insertions of 10+ bp in band (0.35-0.65)
110 -> 334 of about 560 rows, median VAF 0.150 -> 0.424; short insertions 465 -> 552
(0.429 -> 0.486); deletions 529 -> 563 (0.455 -> 0.500). - On real data. The full local gate (411 runs) changes 1,062 dna-arm rows of the
flagged strata against develop; within tolerance of a bases-only oracle (any
discriminating base): sign-out 749, develop 838, this release 867 (14 rows farther
from the two-flank oracle, 58 closer). The strata the gate flags moved away from the
sign-out, which the oracle shows is the least concordant of the three. Matched
normals: AD +3 over three rows that already carry the allele, RD -138; the 1,402
normal and ACCESS rows of the local arms AD +3, RD +171. FORTE (RNA): 11 of the 25
pure-indel rows change, all deletions, RD -230, AD +225 (with the GTF; without it
RD -211, AD +226), most of the AD on a 12-bp deletion whose carriers STAR wrote as
soft clips (AD 2 -> 107; 175 reads carry its junction in their bases). - Found and fixed on the way. A tolerated error now costs both alleles alike; a
read's own indel is placed by its bases only up to its next indel; a call whose held
bases are all below--min-baseqtakes the quality of the bases it read, so the
fragment layer counts it too; a read whose bases place no change is read on to
confirm the ALT; a read short of the masks' read-on is decided by its own bases
where they fit one allele exactly. An adversarial review found reads of a third
allele counted ALT or REF where one base past the tract was all the windows read (a
C>CATT row took a +ATTC read as ALT); on its fuzz, other-allele reads counted REF
fall 315 -> 52 and ALT 74 -> 6 with no correct call lost. - RNA. A read spliced at the exon edge that cuts the left flank reads from the left
on its spliced reference; an unspliced read starting at that exon start, or within
the five bases of overhang an aligner writes into the intron, reads the windows up to
the edge, so REF and ALT molecules starting inside a run that crosses the junction
count alike (an ALT-only miscount at an exon-start A run, present on develop). A
read spliced just past the windows reads on along its own mRNA, not the intron:
spliced reads at pure indels two to four bases before an exon's end had lost their
call (REF reads neither, or partial under BAQ) where develop counted them, and with
BAQ masking the bases at a junction (the RNA default, without a GTF) spliced reads
beside an exon edge count as with BAQ off. The exon edges are typed: an exon end
where the event starts leaves its left flank whole (#291). On the FORTE slices one
row changes, with the GTF or without: RD +1, a spliced REF read the intron had left
undecided.
Changed — reads judged beside the other alleles at their locus (#277)
- The rule (RJ-25). A read that fits the ALT counts ALT only when, for every
other allele of the ALT's length at the locus, it reads a base telling the two
apart (the ALT's base, at or above--min-baseq); otherwise it is that allele or
the ALT, neither with partial evidence. The other alleles are the co-annotated
siblings that spell one and those the reads spell in full on 3 or more fragments,
found by the reads' bases placed from the event's left flank, not by where the
aligner wrote the gap. An allele whose every difference from the ALT is a change
from the reference that the REF molecules carry too is a separate event, not
another allele; an allele lacking part of the event (the germline haplotype beside
a somatic change written as one MNP) is one. Where the reads show no other allele
nothing changes. Complex events (the exact-carrier rule) and the MNP check's
partly read reads alike; the opt-in homopolymer twin reads the same siblings. - Why. Develop credited a read that showed only the length of a complex event,
or only the germline base of a merged MNP, as the ALT, even beside another allele
that shows the same: on a synthetic arm, 165 such reads under stress masks, 15
where the given allele is absent at natural quality, a third of AD for a somatic
MNP at 10% on a germline het. The rule credits none and loses no carrier where no
other allele exists; demoting every such read (the first cut) lost carriers there
and at the normals. As GATK and Octopus treat a read that cannot tell two
haplotypes apart. - On real data, against develop: 18 gate-panel rows change (AD -22, RD +197), 9
matched-normal and ACCESS rows (RD +347), 3 WES rows (RD +28) and 3 HG002 rows (RD
+9); the FORTE RNA delins is unchanged. The ALT side moves 10 panel rows, each beside
another allele the reads show (AD -43, of which 22 carriers count again under the
component and seen-share rules below); REF alike moves 185 reads (147 read the
telling base below--min-baseq, 31 sit at two WES rows whose given REF holds a
homozygous germline change); the grown event restores 766 REF reads. On every MNP
and delins row of the 6.6.0 gate panel (742 and 180 rows over 235 runs, sliced
locally): 39 MNP rows (RD -203, AD -49) and 13 delins (RD +230, AD -7) change, each
explained by its reads. - REF alike, and components (operator, 2026-10-14). A read that fits REF counts REF
only when it tells REF from every other allele of REF's length at the locus (the
germline haplotype beside a merged MNP, a germline change inside a delins's deleted
bases): otherwise neither. A co-annotated sibling is another allele only where the
fragments spelling it in full are at least one and 5% of those spelling the allele
it is nearest: an MNP's component SNV that sign-out reports beside it, never seen
apart from the MNP, is part of it (20 panel carriers had been demoted for it), and
at depth one error read spelling it no longer makes it another allele (a Monte
Carlo at 1000x: 9 of 60 loci flipped on one error, losing about 5% of RD and AD;
none now). The annotation lowers RJ-25's 3-fragment floor, not its share. As on
the ALT's side, a REF-length allele whose every difference from REF is a change the
ALT molecules carry too (a germline change at a base the event keeps, in cis) is a
separate event, not another allele; a REF read that is ALT for a co-annotated
sibling stays partial evidence (RJ-5). The older sibling claim still takes reads
that fit a co-annotated sibling no read shows (#282). - A change inside the grown event (RJ-23, amended). A REF read holding the given
REF allele with a germline SNP in the repeat the event was grown through was
neither, silently (RD down by a third to a half at 2 panel rows, a matched normal
and a WES row). Such a change, carried by the REF molecules, is now background,
masked in both windows where it sits on each allele, so REF reads and carriers
carrying it keep their call. One on the ALT molecules alone stays unmasked. The
same at the bases an MNP keeps inside its core (operator, 2026-10-14): a germline
het there in trans made the germline haplotype's REF reads neither (RD halved, VAF
0.20 -> 0.33 in a synthetic case), and in cis carriers written with an indel at the
block were neither while plain ones counted. No real row changes. - From the adversarial review (2026-10-14). An allele the reads spell must also
reach 5% of the fragments reading the same bases in full, so a recurring error at
depth is no other allele; one differing from the ALT only where the guard masks is
no other allele (it took every carrier); the alleles are found from the variant's
own reads only (binning invariance); a molecule may differ from its allele at one
base past the event (a phased SNP hid a germline allele); a molecule past a run
must read a base telling its allele from the other (a shifted carrier was taken for
REF at one HG002 row); every read the MNP check calls ALT follows the rule, and one
set aside keeps its N. A second review: an allele the reads spell must recur on 5% of
the fragments spelling the allele it is nearest (REF or the ALT), so one as common as
a low-VAF ALT is found at any depth; molecules are told by all their bases (a run past
the event, or a large deletion, left none placed) with up to two phased changes
beside the event; a change beside an MNP's own bases is a phased neighbour, not
another allele. A third review: the fragments spelling REF or the ALT are counted
past the masks (a masked change on every carrier left none, so any recurring error
became an allele); for an MNP the run beside it is beside it on both routes, so a
carrier with a phased neighbour there counts ALT however it is aligned. A sixth
review: a mask inside the event's span took any base, so a third allele's carriers
counted ALT once a germline change had masked the position (it now takes the
reference's base and the change's only); a change at a base the event changes, where
a third allele's molecules count among the other allele's, is no separate event; and
a co-annotated allele faces the separate-event test as a spelled one does. - Read census. Told the locus's other alleles, it judges a read fitting the ALT
and another allele as neither (FITS_OTHER). - Tests. Two
--rescue-mnptests' unrescued counts move from (REF 20, ALT 1,
partial 9) to (20, 0, 10), with the operator's OK: their masked stray reads only
the base it shares with the first-change allele nine fragments spell. A new test
keeps a stray ALT read beside the rescue.
Changed — complex events read by one pair of equal-length windows (#253)
-
The rule (RJ-23). A complex event (delins, an anchor-changing indel, an MNP
read with an indel at the block) is read by one pair of windows of equal length,
inward from the POS flank: the shorter allele's whole window (its event, grown
through repeats, with two flank bases each side) against as many of the longer
allele's bases, then on through the rest of the longer allele as far as the read
reaches. A REF and an ALT molecule get the same number of read starts that can
judge them. Before, the shorter window was padded with flank to the longer one's
length, which cost reads, and events past 50 bases were read at both junctions,
which let the longer allele count at either end. When both alleles pass 50 bases
the windows run from the flank nearer the first difference (an event ending a
long run is out of a read's reach from the other); an RNA variant whose POS flank
an exon edge cuts reads from the other flank. A read judged from the right counts
although it starts past POS, as the other allele's molecule starting there would. -
A change beside the event is a separate event. A change recurring within 25
bases of the event (overlapping mates count once) is masked in both windows, as
RJ-8 treats another indel outside the window, so a read carrying the given
allele counts ALT. One on the ALT reads alone (a phased neighbour, or an event
the input under-describes) is named ingbcms_diagnosticunder the
OBSERVED_ALLELE/COEXISTING_ALLELErule. Withholding those reads was measured
and not adopted: on HG002 it took 5 het delins with a phased SNV in cis from VAF
0.42 to 0.09. -
Placement. Windows anchor on the aligned base of the reading flank. A read
whose clip or own deletion holds that flank (an aligner using a microhomology at
the event's far end), or whose aligner wrote it over the event's other copy, is
placed from the other flank as the allele its confident bases favour: each
allele's window placed from the same aligned anchor at its own length, its bases
at or above--min-baseq(not N) must favour one by at least one such base. A
REF molecule behind a poor clip (a NovaSeq Q12 bin) shows no placement, where it
could fit the ALT through its masked bases; a placement whose allele does not fit
is another allele (partial when the ALT's). -
Windows run past bases the alleles share. When the shorter allele's flank
repeats the longer allele's next bases, both windows run through them to two
bases from where the alleles differ: a REF read carrying only a substitution of
the event's first base is neither allele. Unpadded windows that stopped before
the alleles differ credited such reads: 235 ALT at a 17-to-1 delins in one
matched normal (6.6.0, padded: 0). -
Germline changes beside the event are background. A change recurring on the
REF fragments too (on 3 or more of them), a run included (a germline MNP), is
masked like a germline SNP; a run on the ALT fragments alone is another allele,
and no larger allele is named beside it (per-base tallies cannot spell an
indel; the observed allele names it from the reads' alignments). -
Measured against 6.6.0 on every data type: the 6.6.0 gate panel's delins
rows (IMPACT, ABRA2-realigned) and their matched normals, ACCESS duplex and
simplex with their normals, TEMPO WES (BWA only), and HG002 germline het delins
(GIAB v5.0q, truth 0.5; WGS 35x, BWA-MEM):6.6.0 6.7.0 Panel, all delins (161 rows): within 10% of sign-out 88/135 100/136 Panel, span 20+ (14 rows): AD / RD; within 10% of sign-out 1,203 / 4,117; 0/14 1,648 / 5,433; 8/14 Panel, span under 20 (70 rows) 9,557 / 24,053; 52/70 9,827 / 25,124; 60/70 Panel, recurrent neighbour (49 rows) 2,398 / 14,609; 19/49 3,624 / 15,590; 27/49 ACCESS duplex / simplex (8 rows each): within 10% of sign-out 4/6; 4/6 4/6; 4/6 TEMPO WES (54 rows): within 10% of sign-out 12/50 16/52 HG002, span 20+: median VAF; within 0.35–0.65 0.464; 166/237 0.477; 180/236 HG002, span 5–19 0.500; 124/149 0.500; 122/149 Matched normals, somatic delins rows (IMPACT 105, ACCESS 14): rows with ALT 6; 0 8; 0 The two normal rows gaining ALT gain one soft-clipped read each whose bases fit
the ALT through the window padding had cut: one of 641 judged reads, and one of
5 at a row whose other reads carry a germline substitution of the event's first
base (neither allele). Pure indels and SNVs are unchanged (195 germline het
indels: 2 count changes; 139 on WES: 2). -
Spec. Reads ending inside a long insertion that hold the REF window's length
are judged (ALT where their bases contradict REF), a carrier ending on the base
after its run is ALT, and a read one base from each allele is neither, not
partial.tests/test_carrier_window_balance.pychecks that equal molecules give
AD = RD for shrinking, growing, long, both-long and run-ending shapes, in DNA and
in RNA read from the right.
Changed — deletions of 50 bp or more judged at the POS breakpoint (#254)
-
The rule (RJ-24). A prepared pure deletion of 50 bp or more is REF or ALT by
its own bases at the POS breakpoint, however the aligner wrote it (a D op, a soft
clip, a split read). Both alleles are read from the left flank over one window of
equal length: 4 flank bases, the microhomology, then 4 bases of the allele's own
continuation (REF: the deleted bases; ALT: the right flank). Nothing past the
window is read, for either allele. Short-read aligners without a realigner write
most such carriers as clips: on HG002 (BWA-MEM) a CIGAR-only rule read VAF 0.000 at
287 of 300 het deletions. -
Placement. A read whose left flank is not aligned where the windows start (in
its clip, under its own deletion, or written over the deleted bases' end) is
placed from the right flank, which both alleles share, as the ALT's window sits
there; its bases at or above--min-baseqmust favour the left flank over the
deleted bases REF has before the right flank by one such base. A REF molecule
starting inside the deleted span behind a poor or N clip is depth only. A decided
read admitted through its clip, or placed from the right, counts toward depth in
DNA. -
Beside the junction. A change recurring at one window offset is masked as a
separate event (as RJ-23): in the flank and microhomology, a germline SNP or MNP
the REF reads carry too; an isolated change of the ALT reads' own; past the
junction an isolated change on the REF reads. A run of changes is another allele,
and the diagnostic names the deletion with an isolated ALT-only change as the
larger allele. -
Siblings. A co-annotated sibling claims a REF read (RJ-5) only from the anchor
through REF's window's last deleted base. Over the whole deleted span, as for other
variants, it withdrew REF reads the windows never read there and no ALT molecule
can carry: 2,667 REF reads at 10 of the panel's 60 rows (one row's VAF 0.17 against
the reads' 0.17 became 0.30); now 378 at 2 rows, where the sibling sits inside
REF's window. The flank bases before the anchor, which both windows read, stay
outside the guard. -
Retired: the 3-base length band for deletions of 50 bp or more and its Phase 3
route; a deletion of another length is another allele at any size. -
Measured against 6.6.0:
6.6.0 6.7.0 TEMPO WES, BWA only (150 rows): AD; within 10% of sign-out 4; 0/132 6,028; 40/143 HG002 het deletions (280 rows): median VAF; within 0.35–0.65 0.000 at 287 of 300 0.500; 132/188 Panel, ABRA2-realigned (60 rows): median VAF; within 10% of sign-out 0.192; 50/60 0.188; 45/60 Matched normals, somatic rows (IMPACT 60, ACCESS 2): ALT reads 0; 0 0; 1 On the panel ABRA2 writes carriers as D ops, which 6.6.0 and the sign-out both
count from the CIGAR, with REF read anywhere over the deleted span; 6.7.0 reads
REF at the breakpoint, as the bases-only oracle does (40/60 within 10% of
sign-out); against the reads' own VAF 6.7.0 agrees on 50/60 (6.6.0 53/60). On HG002 that oracle reads median VAF 0.514 (105/169 in the band).
Removed
gbcms build-gtf-cache,gbcms rna --gtf-cache-dirand the Nextflow--gtf_cache
(#269). They were deprecated in 6.6.0, when they did nothing and warned; the GTF
loads in a few seconds without a cache.- The command and the option are now usage errors.
- The Nextflow parameter is gone, and the pipeline no longer warns about it.
- Pipelines that still call
build-gtf-cachemust drop that step before moving to
6.7.0.
Fixed — a REF read carrying another indel inside an MNP's windows (#287)
- C39's exact-carrier windows counted REF for reads carrying a 24-bp insertion
written right after an MNP's block: the inserted bases repeat the right flank, so
the block's windows fit REF and nothing past them was read (REF 439 -> 467 at one
gate row; 6.6.0 called them neither, as RJ-7 and RJ-21 require). A read whose own
alignment changes the length inside the windows with an indel that does not recur
as background is now judged by its bases read on to its end; where they show the
indel it is another allele (neither, partial evidence). A read whose bases still
fit REF stays REF, and a germline indel beside the event keeps its reads' call.
Found by the local interim 6.7.0 gate; on the full gate panel 3 rows change (RD
-37 in all), on the plan arms one panel, one WES and one HG002 row (RD -1 to -3),
normals and ACCESS none.
Fixed — mFSD sizes an ALT fragment as the molecule carrying the ALT
- A fragment's size is TLEN less the deletions (plus the insertions) its reads'
CIGARs write. A deletion carrier written as a clip, or judged by its bases, has no
D op, so its fragment read longer than the molecule by the deleted bases (227
instead of 167 for 60 bp), and a fragment of a deletion over about 830 bp fell out
of mFSD. When an ALT fragment covers the variant without a read writing its length
change, the change is applied.tests/test_mfsd_allele_size.py.
Fixed — release tooling
- Attribution leaves out runs whose latest attempt failed (#255).
attribute.py
prepare,checkandattributeread the builds' time records as
compare_panel.pydoes, and name each run they skip. A run that crashed partway
leaves a partial MAF, which otherwise reads as changed rows. A reduced run whose
latest attempt failed failscheck, andattributetreats its output as missing,
whatever it holds. The 6.6.0 gate needed those MAFs set aside by hand.