Repository navigation
Description
Tracking the fixes from a whole-repo audit of main at 32fbbc1. The audit report is below, word for word. Numbers 1-20 are its findings; the 10 smaller items it left out are tracked as 21-30. A new row is a difference found while fixing these, not in the report.
Status
| # | Point of the report | PR | Status |
|---|---|---|---|
| 1 | Reads are lost on genomes with many small contigs | #361 | Ready for review |
| 2 | Half-mapped pairs report one location as unique | #367 | Ready for review, based on a copy of #346's branch (perf/hotpaths) |
| 3 | --alignEndsType EndToEnd is wrong for pairs |
#368 | Ready for review, same base. 0 reads differ from STAR only with #367 merged too |
| 4 | Solo runs silently write empty or all-zero side outputs | #360 | Ready for review. Divergence §1.10 needs sign-off |
| 5 | A failed decompress command still exits 0 | #352 | Ready for review, was already open |
| 6 | The GTF parser fails the wrong way in both directions | #356 | Ready for review. Attributes, parentless exons and MissingGeneID now read as STAR (GTF.cpp). Divergence §1.9 needs sign-off |
| 7 | A transcript ID on two chromosomes invents junctions | #356 | Ready for review |
| 8 | A whitelist of the wrong length gives an empty matrix and exit 0 | #360 | Ready for review |
| 9 | CB_UMI_Complex handles UMIs wrongly |
#360 | Ready for review |
| 10 | Junctions near a genome edge stop the run | #357 | Ready for review |
| 11 | SAM output can lose its last bytes with exit 0 | #355 | Ready for review |
| 12 | SJ.out.tab is missing when no junctions are found |
#355 | Ready for review |
| 13 | Chimeric detection can hang on one read | #361 | Ready for review. STAR crashes on the same pair; divergence §1.9 needs sign-off |
| 14 | The seed lookup table goes wrong when an old junction is dropped | #357 | Ready for review |
| 15 | A damaged index loads without complaint | #357 | Ready for review. Flank past the genome end now reads STAR's reverse strand; §3.5 is down to 2 SAindex bytes, needs sign-off |
| 16 | --readFilesCommand takes only one word |
#353 | Ready for review, was already open. Stacked on #352 |
| 17 | FASTQ edge cases fail badly | #352, #358 | Ready for review: quality length in #352, the rest in #358. Divergence §4.8 needs sign-off |
| 18 | Sort temp files land in the wrong folder | #358 | Ready for review. Stacked on #353 |
| 19 | Four read loops are near-copies | #366 | Ready for review. Output and SE/PE writer stage merged; solo loops keep their own consumer. Stacked on #365 |
| 20 | Dead code on the hot path and elsewhere | #365 | Ready for review. Includes #346's commits until that merges; conflicts with #355 |
| 21 | --soloUMIlen above 32 corrupts UMIs |
#360 | Ready for review |
| 22 | Extra --soloUMIdedup values are dropped |
#360 | Ready for review. Divergence §1.10 needs sign-off |
| 23 | soloCellFiltering writes gzip bytes into features.tsv |
#360 | Ready for review |
| 24 | UMI deduplication is quadratic (200k UMIs in one gene took 104 s) | #362 | Ready for review |
| 25 | bamRemoveDuplicates holds the whole BAM in memory |
#363 | Ready for review. Paired-end memory fixed; single-end gains little |
| 26 | The gene name tag is ignored when the GTF is given at mapping | #360 | Ready for review |
| 27 | CellReads.stats mixes features |
#360 | Ready for review |
| 28 | .1/.2 mate names are rejected |
#358 | Ready for review. Stacked on #353 |
| 29 | --outBAMcompression -1 writes uncompressed BAM |
#358 | Ready for review. Stacked on #353 |
| 30 | The docs are stale: docs-old/, and the README still says v0.1 |
#359 | Ready for review |
| new | Unmapped.out.mate* read names lack STAR's 0:N: comment (FASTQ and FASTA) |
#364 | Ready for review. Stacked on #358 |
| new | bamRemoveDuplicates keeps another pair than STAR in some ties (same AS), already on main |
#370 | Ready for review. Ties already match Linux STAR (the macOS build's qsort is not stable); the real gap was a mateless read's PNEXT −1 compared as signed. Stacked on #363 |
| new | Single-end NH under --outSAMmultNmax: NH:2 where STAR has NH:3 |
#375 | Ready for review. Paired-end had the same bug, also fixed |
| new | --outFilterMismatchNoverReadLmax is refused though STAR accepts it |
#375 | Ready for review |
| new | CellReads.stats row order varies with thread count at 4 and 8 threads (same rows), already on main |
#371 | Ready for review. Rows now in barcode order at any thread count; STAR's hash-table order is not copied. Divergence §3.2 needs sign-off |
| new | --outMultimapperOrder Random differs from STAR, already on #346 |
#373 | Same alignments and a best-score primary as STAR; only order and which tied primary differ. Documented in divergence §1.1, no code change |
| new | --alignMatesGapMax is checked against the whole fragment span, not the gap between mates (src/align/read_align.rs:1366): 972 of 4000 fuzzed pairs differ from STAR, already on #346 |
#373 | Ready for review. 0 differing pairs with #367 and #368. Stacked on #346 |
| new | SAM flag 0x200 missing on reads whose header has 1:Y: (STAR sets it on every record), already on main |
#375 | Ready for review. Shares filter_flag() with #364; whichever merges second keeps one copy |
| new | Lowercase bases in reads are written uppercase; STAR keeps them, already on main |
#376 | Ready for review. IUPAC letters kept too, and paired Unmapped.out.mate* now hold the unclipped mates |
| new | --alignEndsType EndToEnd --peOverlapNbasesMin 10: 2 pairs with mates at a chromosome end stay unmapped where STAR clips them (2S/1S) |
#377 | Ready for review. 0 differing pairs with #367. Stacked on #368 |
| new | ReadsPerGene.out.tab stranded columns use one strand per gene; STAR uses each exon's, so a gene ID on both strands (or MissingGeneID) differs, already on main |
#372 | Ready for review |
| new | A truncated sjdbList.out.tab in the index still loads with exit 0 |
#374 | Ready for review. Also refuses cut exonInfo, geneInfo, exonGeTrInfo and a sjdbInfo.txt that disagrees with the genome. Stacked on #357; divergence §1.9 needs sign-off |
| new | test/parity_suite.sh does not run on macOS: /usr/bin/time -f, wc -l padding and zcat on .gz (rustar and STAR both read 0 reads and exit 0) |
#369 | Ready for review |
| new | CellReads.stats values differ from STAR on the parity solo data: 20 extra cell rows, 238 of 726 shared rows differ (mostly exonic, cbMatch, cbMMmultiple), CBnotInPasslist 30 vs 94260, already on main |
— | To do |
| new | --soloCellReadStats Standard (STAR's value) is refused; only CB is accepted, already on main |
— | To do |
| new | --alignEndsType Extend5pOfReads12 with --peOverlapNbasesMin: about 200 of 4000 fuzzed pairs differ; STAR converts a merged transcript lying in one mate, convert_merged_transcript_to_pe drops it, already on #377 |
— | To do |
| new | --alignEndsType EndToEnd paired-end with mismatches: records differ from STAR even at --outFilterMismatchNoverReadLmax 1.0, already on main |
— | To do |
Open PRs touch the same code: alignment fixes (stitch.rs, read_align.rs) stack on #346, and FASTQ input fixes stack on #353. Each PR states its base.
Audit report
What this repo does: rustar-aligner is a Rust copy of STAR, a tool that finds where RNA sequencing reads come from in a genome. It builds a genome index, aligns reads, and writes alignments, gene counts and single-cell (STARsolo) matrices, aiming to match STAR 2.7.11b exactly.
Load I assumed: one person or pipeline running one process with many threads, on hundreds of millions of reads and genomes of about 3 billion bases.
All 813 tests pass and clippy reports no warnings, but these bugs get past them. "STAR" below means real runs of the Homebrew 2.7.11b binary on small made-up genomes.
Must fix
-
Reads are lost on genomes with many small contigs (
src/params/mod.rs:1732-1757)- What this is: This code sets the size of the genome "bins" used to group a read's seeds (short exact matches).
- Problem: It returns before applying the size cap, and the cap reads the command-line value, not the index's. Contigs sharing a bin overwrite each other. With 400 contigs and
--genomeChrBinNbits 11, STAR maps 3998 unique and 7 multi; this tool maps 3945 and 1, with 59 "too short". - Fix: Always apply the cap, using the value stored in the index.
- If we skip it: Draft genomes silently lose reads, and repeat reads get reported as unique.
-
Half-mapped pairs report one location as unique (
src/align/read_align.rs:1316-1376)- What this is: This is the fallback for a pair where only one mate aligns.
- Problem: It keeps one location and skips the multimapper filters. A mate in a 3-copy repeat gets
NH:3 MAPQ:1on 3 loci from STAR, but one record withNH:1 MAPQ:255here, on a different copy. It also adds a record for the unmapped mate, which STAR left out on the same options. - Fix: Send single-mate results through the same selection and filter steps as pairs.
- If we skip it: Downstream tools treat repeat reads as high-confidence unique hits.
-
--alignEndsType EndToEndis wrong for pairs (src/align/stitch.rs:1231-1304,src/align/read_align.rs:884)- What this is: This mode must align every read to its very end, with no clipping.
- Problem: Inner mate ends are never forced, and outer ends don't charge their penalty to the pair score. With 4 mismatches at each inner end, STAR gives
100M/100M AS:182; this tool gives96M4S/4S96M AS:190. - Fix: Pass the end-to-end flags at the mate-boundary and outer extensions, so the pair score carries the penalty.
- If we skip it: Paired runs in this mode get clipped reads and inflated scores, even though the user asked for no clipping.
-
Solo runs silently write empty or all-zero side outputs (
src/lib.rs:906-913,src/lib.rs:1021-1029,src/lib.rs:2461)- What this is: Solo runs use their own read loops, which don't receive the quant or unmapped writers.
- Problem: With
--quantMode GeneCounts TranscriptomeSAM --outReadsUnmapped Fastx, I got exit 0, all zeros inReadsPerGene.out.tab, an empty transcriptome BAM and an empty unmapped file. BySJout, chimeric and WASP options have no code in those loops either. Only a code comment mentions part of this. - Fix: Reject these combinations at startup now, then add support where needed.
- If we skip it: A user can analyse zeros without knowing it.
-
A failed decompress command still exits 0 (
src/io/fastq.rs:167-181)- What this is:
--readFilesCommandruns a program that decompresses the reads. - Problem: Its exit status is never checked.
--readFilesCommand gzcat --readFilesIn missing.gzgave exit 0, 0 reads and a clean log. Withr1.gz,missing.gzthe second file was dropped, while STAR exits 102. - Fix: Wrap the child's output so that end of file calls
wait()and returns an error on failure. - If we skip it: A pipeline can finish "successfully" with reads missing.
- What this is:
-
The GTF parser fails the wrong way in both directions (
src/junction/gtf.rs:155-159,src/junction/gtf.rs:190-213,src/index/mod.rs:311-319)- What this is: This code reads the gene annotation file.
- Problem: A GTF whose chromosome names don't match the genome (
Xvsc1) exits 0 with no junctions, while STAR exits 104. GFF3Parent=attributes and a single exon with no transcript ID stop the build here, while STAR accepts both. - Fix: Fail if no exon lands on a known chromosome, split attributes on a space or
=, and warn and skip on a missing ID. - If we skip it: The common
chrnaming mistake gives an unannotated index and all reads asN_noFeature.
-
A transcript ID on two chromosomes invents junctions (
src/junction/gtf.rs:187-225)- What this is: Exons are grouped into transcripts to derive junctions.
- Problem: Grouping uses the ID only, then places everything on the first exon's chromosome. My test gave three false
c1junctions and lost the realc1 1201-1999andc2 1701-2499. STAR keeps both real ones, though it also writes one odd junction of its own. - Fix: Group by chromosome and ID. This won't copy STAR's odd junction, so add a
DIVERGENCE.mdentry. - If we skip it: False junctions get annotated scoring and affect alignment.
-
A whitelist of the wrong length gives an empty matrix and exit 0 (
src/solo/whitelist.rs:498-559)- What this is: This code matches cell barcodes against the allowed list.
- Problem: Entry length is never compared with barcode length. A 14-base whitelist with
--soloCBlen 16gave 0 valid barcodes and exit 0, while STAR exits 104. LeadingAs pack to zero, so some reads can falsely match. - Fix: Check the length at startup and in
match_cb. - If we skip it: Empty results look like a failed experiment, and false matches give wrong cells.
-
CB_UMI_Complexhandles UMIs wrongly (src/solo/mod.rs:211-216,src/solo/count.rs:1537,src/solo/count.rs:1878)- What this is: A UMI is a short tag that identifies one original molecule.
- Problem: A missing
--soloUMIpositionbecomes an empty UMI, so 200 reads counted as 1 in my run, with exit 0. A bad CB position is dropped the same way. UMI comparison also uses--soloUMIlen(default 10), not the position's actual length, so 12-base UMIs merge wrongly. - Fix: Make bad positions a fatal error, and take the UMI length from the position.
- If we skip it: Counts are silently far too low.
-
Junctions near a genome edge stop the run (
src/junction/sjdb_insert.rs:530-540)- What this is: Inserting a junction copies the sequence on each side of it.
- Problem: An exon at 1-50 on the first contig with overhang 100 exits 1 with "donor underflows", while STAR succeeds. A reviewer also reproduced this after pass 1 of a two-pass run, and with a junction near the genome's end.
- Fix: Leave the out-of-range part as spacer bytes; the buffer already starts that way.
- If we skip it: A long two-pass run can die after its first pass.
-
SAM output can lose its last bytes with exit 0 (
src/lib.rs:294-301,src/lib.rs:336-343)- What this is: BAM writers have a
finishstep; SAM writers use an empty default. - Problem: The last buffer is written when the writer is dropped, and that drop ignores errors. With a file-size limit, 20 reads gave a file with 14 records, exit 0, and a log saying 20 mapped.
- Fix: Add a
finishthat flushes and returns the error, to both SAM writers. - If we skip it: A full disk at the end of a run gives a cut file that looks complete.
- What this is: BAM writers have a
-
SJ.out.tabis missing when no junctions are found (src/lib.rs:1035,src/lib.rs:1103)- What this is: This file lists splice junctions.
- Problem: It is skipped when empty, while STAR writes an empty file. I confirmed this by running both.
- Fix: Delete
&& !sj_stats.is_empty()in both places. - If we skip it: Pipelines that expect the file fail on small or unspliced samples.
Should fix
-
Chimeric detection can hang on one read (
src/chimeric/detect.rs:665-668,src/chimeric/detect.rs:734-743)- What this is:
place()trims a segment at a chimeric (fusion) junction. - Problem: A negative length is cast to
usize, giving a huge number. With--chimJunctionOverhangMin 1and permissive options, one read hung past a 20 s limit. The default overhang of 20 looks safe by reading the check. - Fix: Return
Nonewhenjr_maxis 0 or the new length would not be positive. - If we skip it: The job stalls with no error until the scheduler kills it.
- What this is:
-
The seed lookup table goes wrong when an old junction is dropped (
src/index/sjdb_otf.rs:354-386)- What this is: The SAindex is a lookup table into the suffix array (the sorted list of genome positions), updated when junctions are added on the fly.
- Problem: Removed entries aren't subtracted. A reloaded index holding the same repeat junction on opposite strands, plus one new junction, panicked. The trigger is rare.
- Fix: Rebuild the SAindex when
dropped_oldis true. - If we skip it: A rare crash, and seeds can be misdirected before it happens.
-
A damaged index loads without complaint (
src/index/io.rs:263-284,src/index/io.rs:364-422,src/index/packed_array.rs:152-163)- What this is: This code loads the index files from disk.
- Problem: The sizes recorded in
genomeFileSizesare ignored. An SA file 40 bytes short gave exit 0 with entries missing, and one cut in half panicked mid-alignment. Garbage inchrLength.txtalso panics. - Fix: Compare the file sizes on load, and return errors instead of unwrapping.
- If we skip it: An interrupted copy gives silently incomplete results.
-
--readFilesCommandtakes only one word (src/params/mod.rs:537-538,src/io/fastq.rs:168)- What this is: STAR accepts a command with arguments here.
- Problem:
gunzip -cgives a usage error, and-tries to run a program called-. STAR acceptsgzcat -cand-. - Fix: Use
Vec<String>withnum_args = 1.., and treat-as no command. - If we skip it: Commands copied from existing STAR pipelines fail, though the error is at least loud.
-
FASTQ edge cases fail badly (
src/io/fastq.rs:199-243,src/io/fastq.rs:472)- What this is: This is the read parser.
- Problem: A blank line after the last record kills the run at the very end, while STAR finishes. A quality line shorter than its sequence panics with no read name. FASTA reads are also rejected, while STAR accepts them.
- Fix: Treat a leading blank line as end of file, check the lengths, and name the read in the error. Then support FASTA or document that it is rejected.
- If we skip it: Hours of work are lost to one trailing newline.
-
Sort temp files land in the wrong folder (
src/io/bam.rs:164-173)- What this is: Coordinate sorting spills to temp files next to the output.
- Problem:
Path::new("out/sub/").parent()isout, so--outFileNamePrefix /data/run1/spills into/data. - Fix: Return the prefix itself when it ends in
/. - If we skip it: Spills fill the wrong disk, and a read-only parent should fail the run at the first spill (reasoned, not run).
-
Four read loops are near-copies (
src/lib.rs:1828-4216)- What this is: Single-end, paired-end, solo and solo paired each have their own loop of about 450 to 800 lines.
- Problem: A rule added to one doesn't reach the others, which is how finding 4 happened. Solo and solo paired share 218 distinct lines; single-end and paired-end share 310.
- Fix: Pull the shared per-read output and writer stage into one function, by job, without adding a framework.
- If we skip it: Every new feature must be added up to four times, and the copies keep drifting apart.
-
Dead code on the hot path and elsewhere (
src/align/stitch.rs:3317-3452,src/io/bam.rs:17-35,src/io/sam.rs:172-231,src/io/bam.rs:646-708)- What this is: This code doesn't affect output, or runs only in tests.
- Problem:
- A 135-line per-seed scoring block ends in
let _ = best_pre_score. Deleting it in a scratch copy kept SAM md5 identical on 800k and 300k reads. - The reverse-strand branches are unreachable from the production path, because
stitch.rs:3266-3273forces the flag to false. BufferedBamRecordshas no users, andSamWriter::write_alignmentis only called by its own test.find_paired_seedsandget_genome_positions(src/align/seed.rs) are test-only.- The stdout writers repeat the file writers.
- A 135-line per-seed scoring block ends in
- Fix: Delete the dead code, and make the writers generic over their output.
- If we skip it: People keep reading and maintaining code that does nothing, and one block runs for every read.
Left out (10 smaller items):
--soloUMIlenabove 32 corrupts UMIs.- Extra
--soloUMIdedupvalues are dropped. soloCellFilteringwrites gzip bytes intofeatures.tsv.- UMI deduplication is quadratic (200k UMIs in one gene took 104 s).
bamRemoveDuplicatesholds the whole BAM in memory.- The gene name tag is ignored when the GTF is given at mapping.
CellReads.statsmixes features..1/.2mate names are rejected.--outBAMcompression -1writes uncompressed BAM.- The docs are stale:
docs-old/, and the README still says v0.1.
Verdict: Not healthy yet for general use: the main flows match STAR, but less-tested options give wrong results with exit 0. Fix 1-3 first, then 4, 5 and 11.
Lean: about -400 lines (my estimate, more with the reverse branches and finding 19), -0 dependencies possible.
Not checked:
- Fixes: none has been run, except deleting the dead block in finding 20.
- Real data: nothing was compared on a real genome. Everything used small made-up genomes, and I did not run
test/parity_suite.sh. - Scale: genomes over 4 billion bases were not run. An open question remains on
GstrandBitnear 2^32 (src/genome/mod.rs:568). - Reproduced by me: findings 1-9 and 12, except 8's false matches and 9's UMI-width bug, plus the GTF part of 10.
- Reproduced only by the reviewers I sent out: 11, 13-18, the two-pass and genome-end parts of 10, and the dead-block test in 20. For 13, the hang was run, but its cause comes from reading the code.
- Not read:
- the
src/io/sam.rsrecord builders src/wasp/mod.rsfrom line 480src/junction/sj_output.rssrc/align/transcript.rsandsrc/chimeric/segment.rssrc/index/sa_build.rsafter line 900src/solo/sgt.rsafter line 40, andsrc/solo/libcxx_rng.rssrc/degnorm/mod.rs- most test modules, and the bz2, zstd and xz input paths at runtime
- the
- Solo counts: a reviewer could not get STAR's solo runs going, so solo counts were never compared with STAR.
🤖 Generated with Claude Code
Activity