Summary
Several commands accept or require a reference FASTA for CRAM input but do not attach it to every reader that decodes reference-dependent CRAM records. Legacy threshold readers cannot receive a reference at all in some command paths, and extract full/calls omits the supplied reference from its main serial and indexed body readers.
Equivalent BAM and CRAM inputs can therefore produce different results. In the most serious extract case, modkit 0.6.4 exits successfully after downgrading the CRAM body decode failure and writes zero rows instead of the expected result.
Severity
Severity: High — silent format-dependent data loss
Rationale: A valid reference-dependent CRAM can yield an empty successful result while the equivalent BAM emits hundreds of rows. Other affected paths fail with a misleading zero-contig diagnostic even though the command already has a reference option or should expose one.
User and scientific impact
- Affected paths include reference-capable legacy automatic-threshold consumers and
extract full/calls entry, indexed mapped, indexed-unmapped, fixed-count schedule, and truly unindexed automatic-threshold readers.
- BAM/CRAM conversion alone can change output from complete to empty.
- Missing or wrong references may be reported late or unclearly, and outputs can be created before the true input problem is visible.
- Reference-independent all-unmapped CRAM must remain usable without forcing a FASTA.
Affected versions and environment
- Released version: modkit 0.6.4.
- Development revision:
5cecc3fb3a9336068d9e3c68d5c08d678153dd2c.
- Reproduced on macOS arm64 with equivalent checked-in BAM/CRAM plus FASTA and BAI/CRAI.
Steps to reproduce
From the repository's modkit package directory:
tmp=$(mktemp -d /tmp/modkit-cram-reference.XXXXXX)
bam=../tests/resources/bc_anchored_10_reads.sorted.bam
cram=../tests/resources/bc_anchored_10_reads.sorted.cram
ref=../tests/resources/CGI_ladder_3.6kb_ref.fa
modkit extract full "$bam" "$tmp/full.bam.tsv" --reference "$ref" \
--threads 1 --io-threads 1 --no-headers --suppress-progress --force
modkit extract full "$cram" "$tmp/full.cram.tsv" --reference "$ref" \
--threads 1 --io-threads 1 --no-headers --suppress-progress --force
modkit extract calls "$bam" "$tmp/calls.bam.tsv" --reference "$ref" \
--filter-threshold 0 --threads 1 --io-threads 1 --no-headers \
--suppress-progress --force
modkit extract calls "$cram" "$tmp/calls.cram.tsv" --reference "$ref" \
--filter-threshold 0 --threads 1 --io-threads 1 --no-headers \
--suppress-progress --force
wc -l "$tmp"/*.tsv
The legacy threshold-reader gap can be seen with call-mods:
modkit call-mods "$cram" "$tmp/call.sam" --output-sam \
--sampling-frac 1 --seed 7 --threads 1 \
--sampling-interval-size 20 --suppress-progress
Control or independent oracle
The BAM and CRAM encode the same ten alignments. After normalizing format-specific headers, extract full should emit 218 identical rows and extract calls --filter-threshold 0 should emit 109 identical rows. The supplied FASTA is the reference used to create the CRAM.
Observed behavior
On installed modkit 0.6.4:
| Command |
BAM status/rows |
CRAM status/rows |
extract full |
0 / 218 |
0 / 0 |
extract calls --filter-threshold 0 |
0 / 109 |
0 / 0 |
The CRAM body logs one failed record and returns success. call-mods does not expose a reference argument in 0.6.4; without one, the same CRAM fails during sampling with should be at least 1 contig, while the BAM succeeds with ten records.
Expected behavior
Every reader that may decode reference-based CRAM records must receive the user-supplied FASTA. Equivalent BAM and CRAM must produce identical scientific row/call maps. Missing or wrong references detected by bounded preflight must fail clearly before publishing output; later decoder failures that pass that preflight remain separate lifecycle work. Reference-free header/index probes and genuinely reference-independent CRAM should retain their current behavior.
Root-cause evidence
The reference path is present in command arguments but is dropped at internal reader-construction boundaries. Some legacy sampling APIs have no reference parameter. In extract, initial serial readers, indexed interval readers, indexed-unmapped readers, and schedule constructors create new readers without calling the CRAM reference setter. Header/index probes do not decode record bodies and should remain reference-free.
Proposed fix scope
- Add reference-aware internal sampling/schedule entry points while retaining compatibility wrappers where needed.
- Pass optional references from
call-mods/adjust-mods and existing required references from duplex/entropy consumers to every threshold body reader.
- Thread an owned optional reference path through
extract full/calls, including serial, mapped indexed, indexed-unmapped, fixed-count schedule, and truly unindexed automatic-threshold readers.
- Detect CRAM by content rather than filename extension.
- Validate a missing/wrong reference before output mutation when bounded preflight is possible.
Non-goals
Acceptance criteria
- BAM/CRAM parity holds for
extract full (218 rows) and extract calls (109 rows) in automatic, explicit, and no-filter modes.
- Indexed, truly unindexed, stdin, mixed mapped/unmapped, and fixed-count reader constructors are covered.
call-mods, sampled adjust-mods, pileup-hemi, and entropy threshold readers use the reference where supported.
- Renaming a CRAM with a
.bam suffix does not bypass reference setup.
- Missing/wrong-reference fixtures detected by bounded preflight return nonzero and preserve an absent output or existing sentinel; later decoder failures remain separate lifecycle work.
- Reference-independent all-unmapped CRAM remains usable without a FASTA.
Reproduction artifacts
| Artifact |
SHA-256 |
Notes |
tests/resources/bc_anchored_10_reads.sorted.bam |
4441acbe1ad59caf6ab5d56bf18097d8999c08351395219c919cb863652d85f0 |
BAM control |
tests/resources/bc_anchored_10_reads.sorted.cram |
ec78f57b6524d66ac9e82e0dd1f4fc24f21a9f5959388ffe2a3d2874bcdf881a |
Equivalent reference-dependent CRAM |
tests/resources/bc_anchored_10_reads.sorted.cram.crai |
783940ec8f285dba850852ba3796fb1ecd27991a2c1b930f78c3fa3ea7b0dad1 |
CRAI |
tests/resources/CGI_ladder_3.6kb_ref.fa |
7c02f1e6dc3a3dae186ae47696c822efafe849b8c8526bc131237bf7c3a64570 |
Correct reference |
Related work
Summary
Several commands accept or require a reference FASTA for CRAM input but do not attach it to every reader that decodes reference-dependent CRAM records. Legacy threshold readers cannot receive a reference at all in some command paths, and
extract full/callsomits the supplied reference from its main serial and indexed body readers.Equivalent BAM and CRAM inputs can therefore produce different results. In the most serious
extractcase, modkit 0.6.4 exits successfully after downgrading the CRAM body decode failure and writes zero rows instead of the expected result.Severity
Severity: High — silent format-dependent data loss
Rationale: A valid reference-dependent CRAM can yield an empty successful result while the equivalent BAM emits hundreds of rows. Other affected paths fail with a misleading zero-contig diagnostic even though the command already has a reference option or should expose one.
User and scientific impact
extract full/callsentry, indexed mapped, indexed-unmapped, fixed-count schedule, and truly unindexed automatic-threshold readers.Affected versions and environment
5cecc3fb3a9336068d9e3c68d5c08d678153dd2c.Steps to reproduce
From the repository's
modkitpackage directory:The legacy threshold-reader gap can be seen with
call-mods:Control or independent oracle
The BAM and CRAM encode the same ten alignments. After normalizing format-specific headers,
extract fullshould emit 218 identical rows andextract calls --filter-threshold 0should emit 109 identical rows. The supplied FASTA is the reference used to create the CRAM.Observed behavior
On installed modkit 0.6.4:
extract fullextract calls --filter-threshold 0The CRAM body logs one failed record and returns success.
call-modsdoes not expose a reference argument in 0.6.4; without one, the same CRAM fails during sampling withshould be at least 1 contig, while the BAM succeeds with ten records.Expected behavior
Every reader that may decode reference-based CRAM records must receive the user-supplied FASTA. Equivalent BAM and CRAM must produce identical scientific row/call maps. Missing or wrong references detected by bounded preflight must fail clearly before publishing output; later decoder failures that pass that preflight remain separate lifecycle work. Reference-free header/index probes and genuinely reference-independent CRAM should retain their current behavior.
Root-cause evidence
The reference path is present in command arguments but is dropped at internal reader-construction boundaries. Some legacy sampling APIs have no reference parameter. In extract, initial serial readers, indexed interval readers, indexed-unmapped readers, and schedule constructors create new readers without calling the CRAM reference setter. Header/index probes do not decode record bodies and should remain reference-free.
Proposed fix scope
call-mods/adjust-modsand existing required references from duplex/entropy consumers to every threshold body reader.extract full/calls, including serial, mapped indexed, indexed-unmapped, fixed-count schedule, and truly unindexed automatic-threshold readers.Non-goals
--ignore-indexrouting, zero-target scheduling, or mapped/unmapped eligibility.Acceptance criteria
extract full(218 rows) andextract calls(109 rows) in automatic, explicit, and no-filter modes.call-mods, sampledadjust-mods, pileup-hemi, and entropy threshold readers use the reference where supported..bamsuffix does not bypass reference setup.Reproduction artifacts
tests/resources/bc_anchored_10_reads.sorted.bam4441acbe1ad59caf6ab5d56bf18097d8999c08351395219c919cb863652d85f0tests/resources/bc_anchored_10_reads.sorted.cramec78f57b6524d66ac9e82e0dd1f4fc24f21a9f5959388ffe2a3d2874bcdf881atests/resources/bc_anchored_10_reads.sorted.cram.crai783940ec8f285dba850852ba3796fb1ecd27991a2c1b930f78c3fa3ea7b0dad1tests/resources/CGI_ladder_3.6kb_ref.fa7c02f1e6dc3a3dae186ae47696c822efafe849b8c8526bc131237bf7c3a64570Related work