This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Meta-disco extracts and validates metadata from biological data files (BAM, CRAM, FASTQ, etc.) for the AnVIL Explorer and Terra Data Repository. It infers five dimensions — data_modality, data_type, reference_assembly, assay_type, platform — from filenames, extensions, and file headers (BAM/SAM @SQ/@RG, VCF ##contig, FASTQ read names, FASTA/GFA content), using a deterministic tiered rule engine.
The project has two main components:
-
Classification (
src/meta_disco/,src/meta_disco/rules/unified_rules.yaml): the tiered rule engine that classifies files. Rules are declared in YAML and executed byrule_engine.py; content-based classifiers inheader_classifier.pyinspect fetched headers.ClassifyPipeline(pipeline.py) fetches, classifies, and writes output for each file type infile_types.py. This is what every classification runs through. -
Schema (
schema/directory): LinkML-based schema and validation of the classification output.src/meta_disco/schema/classification.yaml: LinkML schema defining theClassificationRecord(the five metadata dimensions nested underclassifications, each a{value, status, evidence}entry) and the controlled vocabularyscripts/validate_outputs.py: Validates YAML instances against the schema- Uses uv for dependency management (Python 3.10+); its own env, separate from the runtime
The classification
data_modality/reference_assemblyinference was originally LLM-based (Ollama); that path has been removed in favor of the rule engine.
# Setup (its own uv env — keeps linkml out of the runtime)
uv sync
# Validate a metadata file
make validate INSTANCE=path/to/metadata.yaml
# or
uv run python scripts/validate_outputs.py path/to/metadata.yaml
# Run tests
make test
# or
uv run pytest tests/# Validate a freshly downloaded metadata file against the input contract before
# a long run (issue #161). Non-zero exit + grouped summary on any shape violation.
make validate-metadata
# Full pipeline over all file types, in parallel
make classify
# One file type (network required for header fetches)
make classify-bam # or classify-vcf / classify-fastq / classify-fasta / classify-gfa
# Tests and lint
make test
make lintThe LinkML schema (classification.yaml) defines the ClassificationRecord — the
five metadata dimensions nested under classifications, each a {value, status, evidence} entry — plus the controlled vocabulary:
- reference_assembly_enum: GRCh37, GRCh38, CHM13
- data_modality_enum: genomic, transcriptomic., epigenomic., imaging.histology
- classification_status_enum: classified, not_applicable, not_classified, conflict
- also data_type_enum, assay_type_enum, platform_enum
status is required on every dimension; value is null unless status is classified.
- Accuracy over efficiency: Always prefer reading actual file content (headers, indices, range requests) over guessing from filenames. If there is an exact method to determine a classification — even if it requires downloading headers or running compute — use it.
- Accuracy over coverage: It is better to leave a file as
not_classifiedthan to guess wrong. Only classify when evidence supports it. - No speculation as fact: Never confidently assert something unless you actually know it. If inferring or guessing, say "I think" or "it could be". This applies to root cause analysis, data interpretation, and codebase history.
- Claim tiers: A classification field's value is resolved from competing
claims by tier (
evaluate_claims, highest unique tier wins). Tiers 1–3 are the rule tiers declared inunified_rules.yaml(extension / filename / header).CONTENT_TIER(4, inrule_engine.py) is reserved for claims derived from reading file bytes (contig lengths, VCF##contiglengths, FASTA content, GFA segment tags) — the definitive signal from the accuracy principle above, which must out-rank even a disagreeing tier-3 header rule. Give any content-read claimtier=CONTENT_TIER, never a hard-coded number (issue #226).
You may encounter an environment, tool, dependency, or constraint that the user never mentioned and that changes your approach. Examples: an unexpected conda environment, a missing credential, a second uv project. When that happens, STOP and ask before proceeding. Do not work around the surprise silently.
- Never amend commits. Use a separate commit for each fix round. Amending rewrites commits a reviewer already read, which loses the review context. This is the behavior to avoid; adding commits is how review history stays intact.
- Never force push
main. - On a feature branch, prefer adding commits over force pushing, so
review history is preserved. Force pushing is allowed only for the
structural rebase a stacked pull request needs: when the branch it was
based on merges, rebase onto the new
mainand push with--force-with-lease. A rebase does rewrite commit SHAs, so re-request review afterward if the branch was already reviewed. The reason it is allowed and amending is not: it replays the same reviewed changes onto a new base, rather than altering the content of a commit that is under review.
- After any rename/move: grep the entire codebase for all references to the old name — imports, comments, docs, Makefile targets, YAML, tests. Do not assume you found them all by hand.
- After changing a function signature or rule ID: grep for all callers/references before committing.
- After changing function behavior: verify the docstring still matches — especially guard conditions, side effects, return values, and mutation behavior.
- After changing output format: check all consumers — summary printers, tests, downstream scripts.
Docstrings and comments are claims about behavior and MUST be literally true and
precisely scoped — verify each against the code before committing. This is a
review gate: /simplify and /code-review must check docstrings/comments for
these, and flag any that overclaim.
- No overclaiming scope/coverage: do not say "every"/"all"/"any" when the code covers a subset (e.g. a check that only iterates
CLASSIFICATION_FIELDSis not "every emitted field"). State the actual scope. - No false absolutes: do not say "byte-identical" for a semantic dict compare, or "deterministic / no network" unless the code guarantees it. Describe the real mechanism.
- No speculation as fact (see Design Principles): if a comment asserts what a code path does, confirm it actually does that on the inputs in question.
- Prefer precise over tidy: a longer accurate sentence beats a clean wrong one. Re-read every docstring/comment you touched against the final code.
- Classification (root): Python 3.10+,
pyproject.toml; runtime depspyyaml,requests; devpytest,ruff - Schema (
schema/): a separate uv project (Python 3.10+) with linkml/linkml-validator; kept out of the runtime env