SURVIVOR version: 1.0.7
Command: SURVIVOR merge
Affected SV type: INS
Summary
When two insertion records at the same genomic position have INFO fields where one starts with END= (e.g. DRAGEN output), SURVIVOR computes an incorrect internal stop coordinate and fails to merge the pair - even when both records differ in position by 0 bp and in SVLEN by only 1 bp. The failure is silent: no error or warning is printed, both records appear in the output as singletons (SUPP_VEC=10 / SUPP_VEC=01).
Minimal reproduction
Save these two VCF files and the file list, then run SURVIVOR:
v_buggy.vcf - INFO starts with END= (DRAGEN-style):
##fileformat=VCFv4.1
##INFO=<ID=END,Number=1,Type=Integer,Description="End position">
##INFO=<ID=SVTYPE,Number=1,Type=String,Description="SV type">
##INFO=<ID=SVLEN,Number=1,Type=Integer,Description="SV length">
##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT SAMPLE_A
22 1000000 INS_A G GACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTAC 999 PASS END=1000000;SVTYPE=INS;SVLEN=136 GT 1/1
v_query.vcf - INFO starts with SVTYPE= (CuteSV-style):
##fileformat=VCFv4.1
##INFO=<ID=SVTYPE,Number=1,Type=String,Description="SV type">
##INFO=<ID=SVLEN,Number=1,Type=Integer,Description="SV length">
##INFO=<ID=END,Number=1,Type=Integer,Description="End position">
##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT SAMPLE_B
22 1000000 INS_B G GACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTA 999 PASS SVTYPE=INS;SVLEN=135;END=1000000 GT 1/1
list.txt:
SURVIVOR merge list.txt 100 1 1 0 0 50 merged_buggy.vcf
Output - two singletons:
SUPP=1;SUPP_VEC=10;... # only SAMPLE_A
SUPP=1;SUPP_VEC=01;... # only SAMPLE_B
Fixed VCF (workaround)
Reorder the INFO field so END= is not the first field:
v_fixed.vcf - same record, INFO reordered:
##fileformat=VCFv4.1
##INFO=<ID=SVTYPE,Number=1,Type=String,Description="SV type">
##INFO=<ID=SVLEN,Number=1,Type=Integer,Description="SV length">
##INFO=<ID=END,Number=1,Type=Integer,Description="End position">
##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT SAMPLE_A
22 1000000 INS_A G GACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTAC 999 PASS SVTYPE=INS;SVLEN=136;END=1000000 GT 1/1
SURVIVOR merge list_fixed.txt 100 1 1 0 0 50 merged_fixed.vcf
# list_fixed.txt contains v_fixed.vcf and v_query.vcf
Output with fixed VCF:
SUPP=2;SUPP_VEC=11;... # correctly merged
Root cause
Two interacting bugs in src/vcfs/Merge_VCF.cpp and src/merge_vcf/combine_svs.cpp.
Bug 1 - parse_stop(): strncmp with wrong length silently fails for INFO starting with END=
// src/vcfs/Merge_VCF.cpp, parse_stop()
if (strncmp(&buffer[i], ";END=", 5) == 0 ||
(i == 0 && strncmp(&buffer[i], "END=", 5) == 0)) { // <-- BUG HERE
pos.pos = atoi(&buffer[i + 5]);
}
The string literal "END=" occupies 5 bytes in memory: {'E','N','D','=','\0'}. strncmp(..., "END=", 5) therefore compares 5 bytes including the null terminator. When buffer[i] points to END=1000000..., buffer[i+4] is '1' (a digit), not '\0' - so the comparison always returns non-zero and the END= value is never parsed.
The ;END= check (5 meaningful bytes: {';','E','N','D','='}) works correctly, so callers whose INFO field has END= anywhere other than position 0 are unaffected.
Fix: change strncmp(&buffer[i], "END=", 5) to strncmp(&buffer[i], "END=", 4).
Bug 2 - combine_calls_svs(): SVLEN added twice to stop position for INS
// src/merge_vcf/combine_svs.cpp
breakpoint_str stop = convert_position(entries[j].stop);
if (entries[j].type == 4) { // type 4 = INS
stop.position += entries[j].sv_len; // always adds SVLEN
}
When parse_stop() fails (Bug 1), the fallback sets stop.pos = POS + SVLEN (line 492–493). Then combine_calls_svs adds SVLEN again for INS records, giving an effective stop of POS + 2×SVLEN.
For a caller whose parse_stop() succeeds (INFO does not start with END=), stop.pos = POS (the END field value for insertions, which equals POS). The combine step then correctly gives stop = POS + SVLEN.
Combined effect
| Caller |
INFO starts with |
parse_stop result |
After combine_calls_svs |
| DRAGEN |
END=P;SVTYPE=INS;SVLEN=N |
-1 (failed) → fallback: P+N |
P+N + N = P + 2N |
| CuteSV |
SVTYPE=INS;SVLEN=N;END=P |
P (success) |
P + N |
The merge criterion requires both start AND stop breakpoints within max_dist. The stop difference becomes |2N_A - N_B| ≈ N_A, which exceeds max_dist for any insertion longer than max_dist bp. For max_dist=100, all insertions ≥ 101 bp from DRAGEN-style callers silently fail to merge.
Condition for failure
The bug triggers when all of the following hold:
- One input VCF has an INS record with INFO starting with
END= (no leading field or semicolon)
- SVLEN >
max_dist (the insertion is longer than the merge window)
- The two records are at the same or nearby position with similar SVLEN
Impact
Callers that begin their INS INFO field with END= (observed in DRAGEN, possibly others) are silently under-merged. All insertions longer than the merge window (max_dist) from such callers are affected. The failure produces no error message.
SURVIVOR version: 1.0.7
Command:
SURVIVOR mergeAffected SV type: INS
Summary
When two insertion records at the same genomic position have INFO fields where one starts with
END=(e.g. DRAGEN output), SURVIVOR computes an incorrect internal stop coordinate and fails to merge the pair - even when both records differ in position by 0 bp and in SVLEN by only 1 bp. The failure is silent: no error or warning is printed, both records appear in the output as singletons (SUPP_VEC=10/SUPP_VEC=01).Minimal reproduction
Save these two VCF files and the file list, then run SURVIVOR:
v_buggy.vcf- INFO starts withEND=(DRAGEN-style):v_query.vcf- INFO starts withSVTYPE=(CuteSV-style):list.txt:Output - two singletons:
Fixed VCF (workaround)
Reorder the INFO field so
END=is not the first field:v_fixed.vcf- same record, INFO reordered:SURVIVOR merge list_fixed.txt 100 1 1 0 0 50 merged_fixed.vcf # list_fixed.txt contains v_fixed.vcf and v_query.vcfOutput with fixed VCF:
Root cause
Two interacting bugs in
src/vcfs/Merge_VCF.cppandsrc/merge_vcf/combine_svs.cpp.Bug 1 -
parse_stop():strncmpwith wrong length silently fails for INFO starting withEND=The string literal
"END="occupies 5 bytes in memory:{'E','N','D','=','\0'}.strncmp(..., "END=", 5)therefore compares 5 bytes including the null terminator. Whenbuffer[i]points toEND=1000000...,buffer[i+4]is'1'(a digit), not'\0'- so the comparison always returns non-zero and theEND=value is never parsed.The
;END=check (5 meaningful bytes:{';','E','N','D','='}) works correctly, so callers whose INFO field hasEND=anywhere other than position 0 are unaffected.Fix: change
strncmp(&buffer[i], "END=", 5)tostrncmp(&buffer[i], "END=", 4).Bug 2 -
combine_calls_svs(): SVLEN added twice to stop position for INSWhen
parse_stop()fails (Bug 1), the fallback setsstop.pos = POS + SVLEN(line 492–493). Thencombine_calls_svsadds SVLEN again for INS records, giving an effective stop ofPOS + 2×SVLEN.For a caller whose
parse_stop()succeeds (INFO does not start withEND=),stop.pos = POS(the END field value for insertions, which equals POS). The combine step then correctly givesstop = POS + SVLEN.Combined effect
END=P;SVTYPE=INS;SVLEN=NSVTYPE=INS;SVLEN=N;END=PThe merge criterion requires both start AND stop breakpoints within
max_dist. The stop difference becomes|2N_A - N_B| ≈ N_A, which exceedsmax_distfor any insertion longer thanmax_distbp. Formax_dist=100, all insertions ≥ 101 bp from DRAGEN-style callers silently fail to merge.Condition for failure
The bug triggers when all of the following hold:
END=(no leading field or semicolon)max_dist(the insertion is longer than the merge window)Impact
Callers that begin their INS INFO field with
END=(observed in DRAGEN, possibly others) are silently under-merged. All insertions longer than the merge window (max_dist) from such callers are affected. The failure produces no error message.