Read the real file_name in BAM/VCF/FASTQ/FASTA/BED classifiers (#152)#240
Read the real file_name in BAM/VCF/FASTQ/FASTA/BED classifiers (#152)#240NoopDog wants to merge 6 commits into
Conversation
classify_from_header (BAM) and classify_from_vcf_header accepted file_name but
never read it — each synthesized sample{ext} from file_format alone, so every
tier-2 filename rule matched the synthetic name and any signal the real name
carried (hifi_reads, rnaseq, chm13) was silently dropped for the two most
common file types in the corpus.
Route all five hand-deriving classifiers through the filename_for_rules helper
(#151), which prefers the real name and grafts file_format only when the name
yields no usable extension — the same fix GFA already uses. FASTQ/FASTA were
benign but are included for symmetry (the helper's "usable extension" test,
not "ends with file_format", is what keeps a *.fastq.gz name declaring
file_format ".fastq" from becoming *.fastq.gz.fastq). classify_from_bed_signals
gains a file_format parameter, threaded from classify_bed_files.py and
fetch_bed_headers.py.
Each file type's extension tuple now lives in header_classifier.py next to the
classifier that trusts it and is imported by file_types.py, so a record's
selection set and the classifier's trusted set cannot disagree (the pattern
GRAPH_TEXT_EXTENSIONS already followed).
Corpus over the 16,409 cached BAM headers: 1,371 files (8.4%) recover
data_modality / data_type / assay_type, 110 recover platform, with zero value
regressions — matching the measurement in the issue. The sample.rnaseq.bam
golden fixture flips from not_classified to transcriptomic.bulk, which is the
point.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…omments - classify_bed_files.py / fetch_bed_headers.py: the BED record-selection filters now reference the new BED_EXTENSIONS constant instead of hardcoded ".bed"/".bed.gz" literals, so the selection set and the classifier's trusted set share one definition (the drift the constant exists to prevent). - header_classifier.py: trim the five filename_for_rules call-site comments to concise pointers; the helper's docstring carries the full rationale. Behavior unchanged (str.endswith(tuple) / tuple membership match the literals). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a coverage gap in meta-disco’s header-based classifiers by ensuring the real file_name (when provided) is used to drive tier-2 filename rules across BAM/VCF/FASTQ/FASTA/BED, with a safe fallback that grafts file_format only when the name lacks a usable extension. It also centralizes per-file-type extension tuples alongside the classifiers that rely on them to prevent drift between pipeline selection and classifier expectations.
Changes:
- Route BAM/VCF/FASTQ/FASTA/BED header classifiers through
filename_for_rules(file_name, file_format, ...)so real filenames influence tier-2 rules. - Define file-type extension tuples in
header_classifier.pyand import them intofile_types.py(keeping selection and “trusted extension” logic aligned). - Add/adjust tests and golden fixture output to reflect recovered classifications (notably
sample.rnaseq.bam).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/meta_disco/header_classifier.py |
Uses filename_for_rules across classifiers; introduces shared extension tuples; threads file_format into BED classifier. |
src/meta_disco/file_types.py |
Imports extension tuples from header_classifier.py to keep pipeline selection aligned with classifier expectations. |
scripts/fetch_bed_headers.py |
Threads file_format into BED classification and reuses BED_EXTENSIONS for filtering (but currently misses file_format-based selection). |
scripts/classify_bed_files.py |
Reuses BED_EXTENSIONS for BED selection and passes file_format into BED classification. |
tests/test_header_classifier.py |
Adds tests asserting real filenames drive tier-2 rules (BAM rnaseq/hifi; VCF chm13) and that grafting works. |
tests/test_bed_classification.py |
Adds a test ensuring file_format rescues names lacking a .bed extension. |
tests/fixtures/golden/expected_output.json |
Updates golden output to reflect newly classified fields driven by the real filename. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Per the #157 finding that AnVIL file_format is redundant with the file name, this reverts the file_format machinery added in the first cut and keeps only the actual fix. The real defect was narrow: only classify_from_header (BAM) and classify_from_vcf_header ignored file_name — each synthesized sample{ext} from file_format. Both now use `file_name or default`. FASTQ/FASTA/BED already read file_name and were never broken, so their changes, the filename_for_rules routing, the file_format threading through the BED scripts, and the extension- constant refactor are all reverted. Measured: the file_format graft never fired — 0 of 254,478 cached records (BAM/VCF/BED/FASTQ/FASTA) have a file_name lacking a usable extension — so dropping it changes no classification. The 1,371-BAM recovery comes entirely from reading file_name and is unaffected. Net change vs main: BAM + VCF plus their tests and the sample.rnaseq.bam golden flip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot: the BAM/VCF comments stated absolutely that file_format "adds nothing the name lacks" and attributed it to #152. Reword to attribute the redundancy decision to #157 and scope the "usable extension" claim to the measured corpus, rather than an absolute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/meta_disco/header_classifier.py:112
classify_from_header()now ignoresfile_formatentirely when choosing the synthetic filename. If callers passfile_formatbut omit/emptyfile_name(e.g.ClassifyPipeline.classify_single(file_name="", file_format=".cram")), the classifier will always behave like a BAM (sample.bam), andinfer_assay_type()can’t apply the CRAM-specific size thresholds becauseExtendedFileInfo.file_formatis left unset. Consider falling back tosample{file_format}only whenfile_nameis falsy, and setExtendedFileInfo.file_formatfrom the derived extension so assay inference can distinguish BAM vs CRAM without treating AnVILfile_formatas an independent signal.
# Use the real filename so its tokens reach the tier-2 filename rules. The
# AnVIL file_format is not consulted: it is redundant with the name for
# classification (#157), and in the corpus every selected BAM record's
# file_name already carries a usable extension (#152).
filename = file_name or "sample.bam"
src/meta_disco/header_classifier.py:199
classify_from_vcf_header()now defaults tosample.vcf.gzwheneverfile_nameis missing/empty, even if the caller providedfile_format(previously this producedsample{file_format}). That can change which extension-scoped rules run for callers that only knowfile_format(e.g..vcfvs.vcf.gz). Suggest keeping the same fallback behavior as before: usefile_namewhen present, else synthesizesample{file_format}when it looks like an extension, else use the hard-coded default.
# Use the real filename so its tokens reach the tier-2 filename rules. The
# AnVIL file_format is not consulted: it is redundant with the name for
# classification (#157), and in the corpus every selected VCF record's
# file_name already carries a usable extension (#152).
filename = file_name or "sample.vcf.gz"
…name
Per review: when a classifier is called with only a header and no file_name,
it should hand the engine the file-type extension it already knows — not invent
a whole filename like "sample.bam" to smuggle the extension through.
Engine: classify_extended now derives file_format from the filename only when a
filename is present; otherwise it uses the file_format the caller set. So a
caller can supply the known extension directly with no name.
Classifiers: BAM/VCF/FASTQ/FASTA/BED now build ExtendedFileInfo with
filename=(file_name or "") and file_format set to the type's extension
(".bam"/".vcf.gz"/".fastq.gz"/".fa.gz"/".bed"). The real filename still drives
the tier-2 filename rules when present; when absent, the extension rules run off
the explicit file_format instead of a fabricated name.
Behavior-preserving: 739 tests pass (incl. the golden and the 40 header-only
calls that now pass filename="" plus an explicit extension). Note nameless files
never reach a classifier in production — they fail the input contract
(file_name pattern ^.+$) and divert to validation_failed — so this only tidies
the header-only path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
src/meta_disco/header_classifier.py:115
classify_from_headernow hard-codesfile_format=".bam"and ignores thefile_formatargument. That breaks the documented header-only fallback behavior (e.g., a CRAM header-only call passingfile_format=".cram"will still run BAM extension rules) and also makes the preceding comment inaccurate (“known .bam”). Use the providedfile_formatonly as an extension fallback whenfile_nameis empty, defaulting to ".bam".
# the file_format we set — the known ".bam" — instead of a fabricated name.
file_info = ExtendedFileInfo(
filename=file_name or "",
file_format=".bam",
file_size=file_size,
src/meta_disco/header_classifier.py:200
classify_from_vcf_headernow hard-codesfile_format=".vcf.gz"and ignores thefile_formatargument. For header-only calls this prevents callers from selecting ".vcf" vs ".vcf.gz" extension rules, and it makes the comment about the “known .vcf.gz” fallback too strong. Usefile_formatas the extension fallback whenfile_nameis empty, defaulting to ".vcf.gz".
# the file_format we set — the known ".vcf.gz" — instead of a fabricated name.
file_info = ExtendedFileInfo(
filename=file_name or "",
file_format=".vcf.gz",
file_size=file_size,
Copilot: with `if ext_info.filename:`, a truthy-but-extensionless filename made extract_extension return "" and clobbered the caller-provided file_format fallback, so extension rules would not run. Override file_format only when the filename actually yields a non-empty extension; otherwise keep the fallback. Behavior-preserving on the corpus (every file_name carries a usable extension); 739 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/meta_disco/header_classifier.py:112
classify_from_headerstill acceptsfile_format(".bam"/".cram"), but the new code hard-codesfile_format=".bam"onExtendedFileInfo. This breaks the documented fallback behavior for header-only/extensionless calls (and makes the surrounding comment inaccurate), because a caller-provided.cramfallback will be ignored and extension-scoped rules will run as.bam.
# Use the real filename so its tokens reach the tier-2 filename rules. The
# AnVIL file_format is redundant with the name and not consulted (#157). When
# there is no name (a header-only call), the engine reads the extension from
# the file_format we set — the known ".bam" — instead of a fabricated name.
file_info = ExtendedFileInfo(
src/meta_disco/header_classifier.py:197
classify_from_vcf_headerstill acceptsfile_format(".vcf"/".vcf.gz"), but the new code hard-codesfile_format=".vcf.gz"onExtendedFileInfo. That makes header-only/extensionless calls ignore a caller-provided.vcffallback and contradicts the intent described in the comment (use the caller’s known extension when no filename is present).
# Use the real filename so its tokens reach the tier-2 filename rules. The
# AnVIL file_format is redundant with the name and not consulted (#157). When
# there is no name (a header-only call), the engine reads the extension from
# the file_format we set — the known ".vcf.gz" — instead of a fabricated name.
file_info = ExtendedFileInfo(
Closes #152
What changed
classify_from_header(BAM/CRAM) andclassify_from_vcf_headeraccepted afile_nameparameter but never read it — each synthesizedsample{ext}fromfile_formatalone, so every tier-2 filename rule matched the synthetic name and any signal the real name carried (hifi_reads,rnaseq,chm13) was silently dropped for the two most common file types in the corpus. Both now use the realfile_name.Name-only, no
file_formatsignal. An earlier cut routed all classifiers through thefilename_for_ruleshelper, which graftsfile_formatas a fallback extension. Per #157 (AnVILfile_formatis redundant with the name), that path was removed. The graft was measured to never fire anyway: 0 of 254,478 cached records have afile_namelacking a usable extension.Pass the known extension explicitly, don't fabricate a filename. The classifiers used to invent a placeholder name (
"sample.bam") purely to carry the file-type extension into the engine, becauseRuleEngine.classify_extendedderived the extension from the filename. That's now decoupled: the engine derives the extension from the filename when one is present, and otherwise uses thefile_formatthe caller set. So all five header classifiers (BAM/VCF/FASTQ/FASTA/BED) buildExtendedFileInfo(filename=file_name or "", file_format="<type ext>")— the real filename drives the tier-2 rules when present, and when it is absent the extension rules run off the explicit, known extension instead of a made-up name.Nameless files never reach a classifier in production regardless — they fail the input contract (
file_namepattern^.+$) and divert tovalidation_failed— so the no-name path is only exercised by header-only callers/tests.Why
~8% of BAM files were left
not_classifiedon three dimensions the filename would settle. This is a coverage defect, not a correctness one — nothing wrong was emitted, but the coverage was recoverable for free. Consistent with accuracy over coverage: the filename never contradicts a header-derived value, it only fills gaps.Assumptions I made
file_formatcarries no classification signal the name lacks (Classify Salmon quant.sf as transcriptomic quantification (from the name; do not route through AnVIL file_format) #157); the fallback graft never fires on the corpus, so name-only loses nothing.sample.rnaseq.bamgolden fixture flip is intended (issue task Determine/validate the approach for representing "NA" (not_available) vs. unspecified or not_applicable in TDR #2): it now classifiestranscriptomic.bulk/alignments/RNA-seqinstead ofnot_classified.FileInfo-based callers); only the header-only path (no filename) now reads the explicitfile_format.How to verify
Definition of done from #152:
file_namewhen present inclassify_from_headerandclassify_from_vcf_header. Seesrc/meta_disco/header_classifier.py.python -m tests.test_output_shape;sample.rnaseq.bamnow classifiestranscriptomic.bulk.tests/test_header_classifier.py: real filename drives tier-2 rules (BAM rnaseq → transcriptomic, hifi → PACBIO; VCF chm13 → CHM13), and a header aligner signal still wins where the filename is silent.not_classifiedfor data_modality/data_type/assay_type, 110 recover platform, 0 value regressions.To reproduce locally: