Skip to content

Read the real file_name in BAM/VCF/FASTQ/FASTA/BED classifiers (#152)#240

Open
NoopDog wants to merge 6 commits into
mainfrom
noopdog/152-classifiers-read-file-name
Open

Read the real file_name in BAM/VCF/FASTQ/FASTA/BED classifiers (#152)#240
NoopDog wants to merge 6 commits into
mainfrom
noopdog/152-classifiers-read-file-name

Conversation

@NoopDog

@NoopDog NoopDog commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Closes #152

What changed

classify_from_header (BAM/CRAM) and classify_from_vcf_header accepted a file_name parameter 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. Both now use the real file_name.

Name-only, no file_format signal. An earlier cut routed all classifiers through the filename_for_rules helper, which grafts file_format as a fallback extension. Per #157 (AnVIL file_format is redundant with the name), that path was removed. The graft was measured to never fire anyway: 0 of 254,478 cached records have a file_name lacking 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, because RuleEngine.classify_extended derived the extension from the filename. That's now decoupled: the engine derives the extension from the filename when one is present, and otherwise uses the file_format the caller set. So all five header classifiers (BAM/VCF/FASTQ/FASTA/BED) build ExtendedFileInfo(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_name pattern ^.+$) and divert to validation_failed — so the no-name path is only exercised by header-only callers/tests.

Why

~8% of BAM files were left not_classified on 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

How to verify

Definition of done from #152:

  • Use file_name when present in classify_from_header and classify_from_vcf_header. See src/meta_disco/header_classifier.py.
  • Regenerate the goldenpython -m tests.test_output_shape; sample.rnaseq.bam now classifies transcriptomic.bulk.
  • Teststests/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.
  • Re-run the corpus — over the 16,409 cached BAM headers: 1,371 files (8.4%) move off not_classified for data_modality/data_type/assay_type, 110 recover platform, 0 value regressions.

To reproduce locally:

make test          # 739 pass, incl. the new filename tests and the golden
make lint
make format-check

NoopDog and others added 2 commits July 20, 2026 17:00
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>
Copilot AI review requested due to automatic review settings July 20, 2026 07:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py and import them into file_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.

Comment thread scripts/fetch_bed_headers.py Outdated
Comment thread scripts/fetch_bed_headers.py
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 AI review requested due to automatic review settings July 20, 2026 07:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread src/meta_disco/header_classifier.py Outdated
Comment thread src/meta_disco/header_classifier.py Outdated
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>
Copilot AI review requested due to automatic review settings July 20, 2026 08:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ignores file_format entirely when choosing the synthetic filename. If callers pass file_format but omit/empty file_name (e.g. ClassifyPipeline.classify_single(file_name="", file_format=".cram")), the classifier will always behave like a BAM (sample.bam), and infer_assay_type() can’t apply the CRAM-specific size thresholds because ExtendedFileInfo.file_format is left unset. Consider falling back to sample{file_format} only when file_name is falsy, and set ExtendedFileInfo.file_format from the derived extension so assay inference can distinguish BAM vs CRAM without treating AnVIL file_format as 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 to sample.vcf.gz whenever file_name is missing/empty, even if the caller provided file_format (previously this produced sample{file_format}). That can change which extension-scoped rules run for callers that only know file_format (e.g. .vcf vs .vcf.gz). Suggest keeping the same fallback behavior as before: use file_name when present, else synthesize sample{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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_header now hard-codes file_format=".bam" and ignores the file_format argument. That breaks the documented header-only fallback behavior (e.g., a CRAM header-only call passing file_format=".cram" will still run BAM extension rules) and also makes the preceding comment inaccurate (“known .bam”). Use the provided file_format only as an extension fallback when file_name is 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_header now hard-codes file_format=".vcf.gz" and ignores the file_format argument. 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. Use file_format as the extension fallback when file_name is 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,

Comment thread src/meta_disco/rule_engine.py Outdated
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>
Copilot AI review requested due to automatic review settings July 20, 2026 13:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_header still accepts file_format (".bam"/".cram"), but the new code hard-codes file_format=".bam" on ExtendedFileInfo. This breaks the documented fallback behavior for header-only/extensionless calls (and makes the surrounding comment inaccurate), because a caller-provided .cram fallback 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_header still accepts file_format (".vcf"/".vcf.gz"), but the new code hard-codes file_format=".vcf.gz" on ExtendedFileInfo. That makes header-only/extensionless calls ignore a caller-provided .vcf fallback 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(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

BAM/VCF classifiers ignore file_name — filename-scoped rules never see the real name (~8% of BAM under-classified)

2 participants