Problem
classify_from_header (BAM/CRAM) and classify_from_vcf_header both accept a file_name parameter and never read it. Each unconditionally synthesizes a filename from the extension:
# header_classifier.py:65-67 (BAM)
filename = "sample.bam"
if file_format:
filename = f"sample{file_format}"
# header_classifier.py:141-143 (VCF)
filename = "sample.vcf.gz"
if file_format:
filename = f"sample{file_format}"
Every filename-scoped tier-2 rule therefore matches against sample.bam / sample.vcf.gz instead of the real name. Any signal carried by the filename — hifi_reads, rnaseq, CHM13Y, chm1 — is silently discarded for the two most common file types in the corpus.
pipeline.py passes the real file_name into the classifier (line 242), so the data is present and thrown away at the last step.
Reproduction
engine.classify_extended(FileInfo("sample.rnaseq.bam")) -> data_modality = transcriptomic.bulk
engine.classify_extended(FileInfo("sample.bam")) -> data_modality = None
classify_from_header("<hdr>", file_name="sample.rnaseq.bam", file_format=".bam")
-> data_modality = None # file_name dropped
The committed golden fixture already records the symptom — tests/fixtures/golden/expected_output.json has file_name: sample.rnaseq.bam classifying data_modality: not_classified.
Measured impact
Ran the production classifier bodies over the real cached headers in data/evidence/anvil/, twice per file: once with the synthesized sample{ext} name they use today, once with the real filename. Tier-3 header rules and contig-length detection were active in both arms, so this is the net cost after header signals get their chance to recover the information.
| type |
headers |
output changes with real filename |
| BAM |
16,409 |
1,371 (8.4%) |
| VCF |
20,000 (sample) |
10 (0.1%) |
Fields recovered (currently not_classified, would be classified):
BAM data_modality 1,371 e.g. m84046_230719_001055_s4.hifi_reads.bc2061.bam -> genomic
data_type 1,371 -> alignments
assay_type 1,371 -> WGS
platform 110 e.g. HG02055.paternal.hifi.bam -> PACBIO
VCF reference_assembly 10 e.g. 1kgp.chr20...variants.chm1 -> CHM13
No value disagreements. In no case does the real filename produce a different value than a header-derived one — it only fills fields that are currently unclassified. Contig-length detection already recovers reference_assembly for nearly all VCFs, which is why VCF barely moves; BAM's loss is concentrated in data_modality/data_type/assay_type, which contig detection does not supply.
So this is a coverage defect, not a correctness one: ~8% of BAM files are left not_classified on three dimensions that the filename would settle. Consistent with accuracy over coverage — nothing wrong is being emitted — but the coverage is recoverable for free.
Caveats on the measurement
file_size is not stored in the evidence cache, so both arms passed None. Size-dependent assay_type inferences (WGS/WES thresholds) fired in neither arm; with real sizes the assay_type figure could move in either direction.
- VCF was a random 20,000-file sample of 204,692 cached headers (seed 0). BAM was the full cached set (16,409 of 18,663 corpus BAM/CRAM files).
Fix
Prefer the real filename, fall back to the extension, mirroring what classify_from_gfa_segment_tags now does (#151):
if file_name:
filename = file_name
elif file_format:
filename = f"sample{file_format}"
else:
filename = "sample.bam"
Same-class survey
| classifier |
filename derivation |
status |
| BAM |
sample{fmt} — file_name ignored |
this issue |
| VCF |
sample{fmt} — file_name ignored |
this issue |
| GFA |
file_name -> file_format -> default |
fixed in #151 |
| FASTQ |
file_name or 'sample.fastq.gz' — file_format ignored |
benign today: .fastq/.fq/.fastq.gz/.fq.gz never split across rules |
| FASTA |
file_name or 'sample.fa.gz' — file_format ignored |
benign today, same reason |
| BED |
file_name or 'sample.bed' |
takes no file_format param; .bed vs .narrowpeak/.broadpeak do split across rules (unified_rules.yaml:795 vs 843), so an empty file_name would apply bed-only rules to a peak file |
FASTQ/FASTA are benign only because their extension variants always appear together in a rule's extensions: list. Worth fixing for symmetry, not urgency.
Tasks
Refs: #151 (GFA fix + the same-class scan that surfaced this), #148.
Problem
classify_from_header(BAM/CRAM) andclassify_from_vcf_headerboth accept afile_nameparameter and never read it. Each unconditionally synthesizes a filename from the extension:Every filename-scoped tier-2 rule therefore matches against
sample.bam/sample.vcf.gzinstead of the real name. Any signal carried by the filename —hifi_reads,rnaseq,CHM13Y,chm1— is silently discarded for the two most common file types in the corpus.pipeline.pypasses the realfile_nameinto the classifier (line 242), so the data is present and thrown away at the last step.Reproduction
The committed golden fixture already records the symptom —
tests/fixtures/golden/expected_output.jsonhasfile_name: sample.rnaseq.bamclassifyingdata_modality: not_classified.Measured impact
Ran the production classifier bodies over the real cached headers in
data/evidence/anvil/, twice per file: once with the synthesizedsample{ext}name they use today, once with the real filename. Tier-3 header rules and contig-length detection were active in both arms, so this is the net cost after header signals get their chance to recover the information.Fields recovered (currently
not_classified, would be classified):No value disagreements. In no case does the real filename produce a different value than a header-derived one — it only fills fields that are currently unclassified. Contig-length detection already recovers
reference_assemblyfor nearly all VCFs, which is why VCF barely moves; BAM's loss is concentrated indata_modality/data_type/assay_type, which contig detection does not supply.So this is a coverage defect, not a correctness one: ~8% of BAM files are left
not_classifiedon three dimensions that the filename would settle. Consistent with accuracy over coverage — nothing wrong is being emitted — but the coverage is recoverable for free.Caveats on the measurement
file_sizeis not stored in the evidence cache, so both arms passedNone. Size-dependentassay_typeinferences (WGS/WES thresholds) fired in neither arm; with real sizes theassay_typefigure could move in either direction.Fix
Prefer the real filename, fall back to the extension, mirroring what
classify_from_gfa_segment_tagsnow does (#151):Same-class survey
sample{fmt}—file_nameignoredsample{fmt}—file_nameignoredfile_name->file_format-> defaultfile_name or 'sample.fastq.gz'—file_formatignored.fastq/.fq/.fastq.gz/.fq.gznever split across rulesfile_name or 'sample.fa.gz'—file_formatignoredfile_name or 'sample.bed'file_formatparam;.bedvs.narrowpeak/.broadpeakdo split across rules (unified_rules.yaml:795 vs 843), so an emptyfile_namewould apply bed-only rules to a peak fileFASTQ/FASTA are benign only because their extension variants always appear together in a rule's
extensions:list. Worth fixing for symmetry, not urgency.Tasks
file_namewhen present inclassify_from_headerandclassify_from_vcf_header.tests/fixtures/golden/expected_output.json— the BAM golden (sample.rnaseq.bam) will start classifyingtranscriptomic.bulk, which is the point.file_formatfallback whenfile_nameis empty; header/contig signals still win where they should.not_classified, with no value regressions.file_formatparam.Refs: #151 (GFA fix + the same-class scan that surfaced this), #148.