All notable changes are documented here. Format follows Keep a Changelog; the project follows Semantic Versioning.
--filter-fileoption onanalyze. Accepts a plain text file with rsIDs and gene names (one entry per line) for custom-panel filtering of the analyze report. Lines matching^rs\d+$(case-insensitive) are treated as rsIDs; everything else is a gene name. Comments (#) and blank lines are ignored. Gene and rsID matches combine with OR — an annotation passes if it matches either set. An empty filter file (or one with only comments) produces an empty report. Not added tomethylationorpharmacogenomics, which already operate on curated panels.
--no-caddflag on analyze, methylation, and pharmacogenomics commands. Per-invocation CADD enrichment exclusion. Required for commercial deployments without a CADD license from UW CoMotion (CADD iscommercial_ok=False). Mirrors the existing--exclude-snpedialicensing-exclusion pattern.
- README quickstart. Lead with
pip install allelixfor end users. Development setup moved to the Development section. - PyPI publish workflow hardened. Pinned
pypa/gh-action-pypi-publishto full commit SHA (v1.14.0) instead of moving branch ref. - README links. Relative markdown links replaced with absolute GitHub URLs so they resolve on PyPI.
- Security policy scope. Narrowed supported versions to current minor only (1.8.x). v1.x feature line is frozen at v1.8.3.
- HTML report link colors. Links in dark mode were browser-default neon blue (#0000EE), unreadable against the dark background. Dark mode now uses #93c5fd; light mode uses #1976d2 (matching the existing accent).
- Favicon SVG rendering. The report's inline SVG favicon used a
linearGradientwith an internal IRI reference (url(#g)) that doesn't resolve insidedata:URIs, rendering the icon invisible. Replaced with a solid fill and URL-encoded angle brackets. - Genotype column header. Abbreviated "Genotype" to "GT" to prevent column header overlap on narrow/mobile screens.
- PyPI metadata. Homepage now points to allelix.io. Added Source and Changelog links.
- Automated PyPI publishing. GitHub Actions workflow publishes to PyPI via Trusted Publishing on every GitHub Release.
- Updated sample reports. Regenerated
examples/sample_reports/with v1.8.0 report format (5-column table, detail sidebar, dark mode).
- Test fixture genotype format. Corrected the
test_html.pyfixture'sgenotype_matchdefault fromA/Gto the concatenatedAGform that production emits for SNVs.
- HTML report redesign. Replaced the 12-column scrollable table with a compact 5-column layout (Magnitude, Gene, Genotype, Repute, Summary). Annotations from multiple sources for the same variant are grouped into a single row. Clicking a row opens a sliding detail sidebar showing all source annotations vertically — genotype, zygosity, significance, review status, condition, description, frequency, AlphaMissense, CADD PHRED, and references.
- Dark / light mode. Follows system preference (
prefers-color-scheme) by default. A toggle button lets the user override. All component CSS uses custom properties — no hardcoded colors outside semantic badges and the accent color. - CADD and AlphaMissense legend. The "Understanding Magnitude Scores" section now includes CADD PHRED tier thresholds (≥30 top 0.1%, ≥20 top 1%, ≥10 top 10%) and AlphaMissense classification bands (≥0.564 likely pathogenic, 0.340–0.564 ambiguous, <0.340 likely benign).
- CADD tier context in sidebar. CADD scores display the PHRED percentile tier inline (e.g. "38.0 (top 0.1% most deleterious)") instead of a bare number.
- Embedded variant JSON uses numeric types.
allele_frequency,am_pathogenicity, andcaddare now floats in the<script id="variant-data">blob, matching the v4 JSON schema. Field names aligned:am_pathogenicity,am_class,allele_frequency(wasamScore,amClass,frequency).
- PLINK split-chromosome error. MHG exports can have straggler
autosomal variants appended after the Y chromosome section, producing
non-contiguous chromosome blocks in the .bim. PLINK1.9 rejects these.
export plinknow sorts variants by chromosome then position before writing. The exporter itself remains a single-pass writer — the sort lives in the CLI layer.
- PLINK export (#29).
allelix export plinkconverts any supported genotype format to PLINK1 binary (.bed/.bim/.fam) for downstream tools (plink2 PCA, ADMIXTURE, PRSice). Single-sample, SNP-major encoding. Uses gnomAD ref/alt for allele coding when available. No-call variants skipped.
- Multi-allelic strand collision in PLINK coord selection. At sites where one alt is the complement of another (e.g. ref=G, alts=A,T), single-pass coord selection picked the complement match over the forward match — same bug class as CADD #45. Fixed with a two-pass loop that prefers forward allele matches.
- ADR-0034: Magnitude scoring scale and ceiling (#23). Formalizes the 0-10 scale with practical ceiling of 9. Documents max-across-sources composite rule and reserves per-source scoring for v2.0.0.
- Zygosity column in all report outputs (#41). Every annotation row
now shows
Heterozygous,Homozygous, orNo Call— derived from the genotype call. Appears in HTML, terminal, and JSON reports. Functional-medicine+/−notation deferred until the risk allele field lands (v2.1+). - CADD PHRED styling in HTML reports (#42). CADD scores are now color-coded by deleteriousness tier: red (≥30, top 0.1%), orange (≥20, top 1%), gray (10–20, top 10%), gray (<10, no tooltip). Tooltip shows the percentile tier on hover for scores ≥10.
- Config file path in
config get/config setoutput (#43). Both commands now print the resolved config file path, so users know which file is being read or written.
- JSON schema version bumped to 4. New
zygosityfield on every annotation. Diff between v1–v4 reports still works. - Methylation panel expanded to 34 genes (#31). Added ACAT1, DHFR, GNMT, MAOA, NOS3, SUOX, VDR. Sorted alphabetically.
- CADD multi-allelic scoring bug (#45). At multi-allelic positions,
_enrich_caddmax-reduced CADD PHRED across all alts, stamping the highest score regardless of which allele the user carries. Now looks up the score for the user's specific allele (direct match preferred over complement). Biallelic sites were unaffected. - Test protocol AM column name.
FULL_TEST_PROTOCOL.mdreferenced "AM Score" but the actual HTML header is "AM".
- CADD v1.7 variant deleteriousness scores (ADR-0032). PHRED-scaled scores ranking how deleterious any single-nucleotide variant is, using 100+ annotation tracks. Enrichment-only annotator following the gnomAD/AlphaMissense pattern. Two modes: cache (pre-built SQLite from HuggingFace, ~5 GB, ~120M variant keys) and full (81 GB tabix file via pysam, GRCh38 only). CADD column appears in HTML, terminal, and JSON reports when scores are present.
- Non-commercial source opt-in pattern. CADD is the first source
with
commercial_ok=False. Disabled by default (sources.cadd = false). Users opt in viaallelix config set sources.cadd trueorallelix db update --cadd. First download shows a license confirmation prompt. - Strand normalization for array data.
resolve_strand()maps array-reported alleles to reference-forward orientation using gnomAD ref/alt as ground truth. Palindromic SNPs (A/T, C/G) return None rather than guessing. - CADD cache build script.
scripts/build_cadd_cache.pyfilters the full CADD SNV and indel files to positions present in gnomAD, AlphaMissense, and ClinVar (GRCh38). Uses int64 packing for SNV keys to fit the ~120M position set (117M SNV + 3M indel). options.cadd_fullconfig key. Enables full CADD mode (tabix queries against the complete CADD file). Requirespip install allelix[cadd]for pysam.
- Added missing GWAS Catalog attribution to HTML and JSON reports. GWAS Catalog was registered as an annotator but absent from the license attribution maps in both renderers.
- SECURITY.md. Vulnerability reporting policy (GitHub private vulnerability reporting), supported versions, and scope definition.
- License metadata centralized on annotator base class via
LicenseDescriptor (ADR-0031). Each annotator now declares its
license as a required
licenseClassVar. Non-commercial gating derived from SPDX identifier instead of a hand-maintained set. Report attribution text generated from the descriptor at render time. TheNON_COMMERCIAL_SOURCESfrozenset inconfig.pyis deleted. - JSON schema version bumped to 3. The
license_attributionsblock now carriessource_url(source website) andlicense_url(license deed) as separate keys.licensefield uses SPDX identifiers. Diff between v2 and v3 reports still works.
- Coverage gate enforced by pytest again. v1.5.1 moved the threshold
to
[tool.coverage.report], but pytest only fails on low coverage when--cov-fail-underis set (CI runspytest, notcoverage report), and the config key is honored version-dependently on unpinned pytest-cov. Restored--cov-fail-under=92toaddopts;precision = 2retained, so the v1.5.1 rounding fix stands. Pinnedpytest-cov>=7,<8to prevent future drift.
- Download integrity verification. ClinVar downloads now verify their md5 checksum against the NCBI sidecar file. HuggingFace downloads (gnomAD, AlphaMissense, SNPedia) pin to specific commit SHAs and verify SHA256 after download. Mismatches delete the corrupt file and raise. GWAS Catalog and PharmGKB are documented gaps — no upstream checksum exists. See ADR-0029.
- Coverage gate rounding. pytest-cov with default
precision=0rounded 91.91% to 92%, silently passing--cov-fail-under=92. Setprecision=2andfail_under=92.00in[tool.coverage.report]. - Circular import (#36).
_versions.pymoved fromallelix/annotators/toallelix/databases/to break thegnomad_loader → annotators → alphamissense → gnomad_loadercycle.
- Extracted shared loader utilities.
install_prebuilt_cache()was duplicated across gnomad_loader, alphamissense_loader, and snpedia_loader. Extracted toinstall_prebuilt_gz_cache()indatabases/loader_utils.py. - Two-tier data source model (ADR-0030). Server-driven sources
(ClinVar, GWAS Catalog, PharmGKB) probe for freshness at runtime.
Code-driven sources (gnomAD, AlphaMissense, SNPedia) use
commit-pinned HuggingFace URLs — no HEAD requests, no signal
stamping, refresh only via
--forceor code bump of the pinned commit SHA. Vestigialprobe_http_signal()and all redundant test monkeypatches removed.
-
Version tag consolidation across all six annotators. Local processing stamps are now stored in a dedicated
local_version_tagcolumn indatabase_versionsinstead of being appended toremote_signalas|iv:N/|pv:Nsuffixes. This eliminates the fragile suffix-parsing pattern that caused the SNPedia signal-loop bug:remote_signalnow holds only the remote ETag/Last-Modified,local_version_tagholds the local processing state. All six annotators use the same dual-version mechanism:- ClinVar:
iv:N(interpreter version) - PharmGKB:
iv:N(interpreter version) - SNPedia:
pv:N(parser version) - GWAS Catalog:
cv:N(categorizer version) - gnomAD:
sv:N(schema version) - AlphaMissense:
sv:N(schema version)
The
sv:tag is new for gnomAD and AlphaMissense — pre-built caches now stamp their schema version so a future schema change forces re-download, matching the cache-invalidation behavior the other four annotators already had. Existing caches self-heal on first run — no re-download required.get_database_info()lazily adds thelocal_version_tagcolumn when reading pre-v1.5.0 caches. - ClinVar:
- Multi-allelic enrichment accuracy (#25). gnomAD and AlphaMissense
enrichment now uses exact alt-allele matching instead of
MAX()aggregation at multi-allelic sites. Addedaltfield to the Annotation model;bulk_lookup_by_alt()on both enrichment annotators; pipeline splits exact-match and MAX-fallback paths. - Disk preflight multiplier (#27). Bumped from 5x to 6x for both gnomAD and AlphaMissense loaders. AlphaMissense compresses at 4.4x, so peak disk (gz + decompressed) is 5.4x gz — the old 5x check would greenlight a disk that ENOSPC'd at ~90% decompression.
- Test suite disk usage. db update tests were downloading real databases (7.8 GB AlphaMissense, 678 MB GWAS TSV) into pytest tmp_path because they lacked monkeypatches for all annotators. All db update tests now stub every annotator. Real-data GWAS tests delete the extracted TSV after SQLite load. Total pytest tmp_path reduced from ~3.4 GB to ~376 MB.
- SQLite variable limit portability (#33).
bulk_lookup_by_alt()batched at 900 keys (1800 bound variables) — over the 999 limit on SQLite < 3.32. Now batches at 450 keys (900 variables). - GWAS enrichment regression. GWAS annotations set
altto the risk allele, but GWAS risk alleles are not VCF-normalized ALT. This caused exact-match lookups to miss, skipping the MAX fallback and losing gnomAD/AM enrichment on GWAS rows. Fixed by not settingalton GWAS annotations (risk alleles are conceptually different from VCF ALT alleles). - SNPedia
db updatecrash and re-download loop. Three related bugs in the SNPedia download flow: (1)install_prebuilt_cachecrashed withno such table: database_versionsbecause the third-party HuggingFace cache doesn't include that table — fixed by creating the table on demand before stamping, consistently across all three pre-built cache loaders and the CLI signal-stamp fallback path. (2)parse_raw_pagesoverwrote the ETag remote signal with only the parser version tag, causing every subsequentdb updateto see a signal mismatch and re-download — fixed by preserving the existing ETag when appending the parser version. (3)cached_remote_signalreturned the raw signal including the parser version suffix, which never matched the remote ETag — root cause eliminated by the version tag consolidation above.
- ADR-0028: Local version tag convention. Documents the
local_version_tagmechanism, the tag prefix vocabulary per annotator, the new-annotator checklist, and the lazy migration strategy. - AlphaMissense gnomAD version stamping (#28). Build script stamps which gnomAD version provided the rsID mapping. Runtime warning on version mismatch: "AlphaMissense cache was built against gnomAD X but installed gnomAD is Y."
- SNPedia HuggingFace download (#30).
db updatenow downloads the SNPedia cache from HuggingFace automatically — same pattern as gnomAD and AlphaMissense. The manual scraper scripts remain as a rebuild-from-source option. allelix/databases/snpedia_loader.py— pre-built cache download and decompression.test_data/FULL_TEST_PROTOCOL.md— external reviewer checklist for full real-data verification.
- AlphaMissense variant pathogenicity enrichment. New
AlphaMissenseAnnotatorenriches annotations with missense variant pathogenicity scores from DeepMind's AlphaMissense (71M variants, CC BY 4.0). Pre-built SQLite cache downloaded from HuggingFace viadb update. AM Score column in terminal, HTML, and JSON reports. PharmGKB rows show AM scores as neutral with caveat (protein structure impact only — tooltip in HTML, dimmed*footnote in terminal,am_caveatfield in JSON).--no-alphamissenseflag to skip. - Config file system.
config.tomlwith per-source on/off toggles andlicense.commercial = truesafety switch that auto-disables non-commercial sources (SNPedia).allelix config show/set/resetCLI commands. CLI flags override config per-invocation. scripts/build_alphamissense_cache.py— AlphaMissense cache build script with Zenodo HTTPS streaming (default) and local TSV modes. Joins against gnomAD cache for coordinate-to-rsID mapping.- AlphaMissense CC BY 4.0 attribution in HTML and JSON reports.
- Magnitude scoring legend in HTML report (collapsible, per-source scoring tables for ClinVar, PharmGKB, GWAS, SNPedia).
- Source floor note in HTML report when per-source magnitude minimums are active.
- Repute row background tints in HTML report (red for pathogenic/risk, green for protective/benign) derived from existing significance field.
- Sortable columns in HTML report (magnitude, gene, source, AM score) via inline JavaScript.
- ADR-0027 documenting the AlphaMissense enrichment cache architecture.
scripts/run-tests.sh— detached background test runner with log rotation.
- HTML report table overflows viewport, columns clipped on left (#20).
Added
overflow-x: autocontainer, sticky rsID column,max-widthon description cells, refs collapsed into<details>toggle, conditional Review Status column (hidden when all empty), stat cardflex-wrap. - AlphaMissense build script has zero unit-test coverage (#24). Added
25 tests covering TSV parsing, gnomAD rsID join, chr prefix
normalization,
--no-gnomadNULL-rsid path, multi-allelic composite PK, batched insert, and end-to-end integration. - Download integrity: Content-Length check after downloads catches truncated files.
- Disk space preflight before decompressing
.sqlite.gzcaches uses 5x gz size (accounts for gz + decompressed tmp on disk simultaneously). _connection()guards on gnomAD and AlphaMissense annotators raiseFileNotFoundErrorwith actionable message when cache is missing.- Dead
cache_exists()removed from gnomAD and AlphaMissense loaders. - Legacy caches stamp remote signal instead of re-downloading on
db update. - README database sizes updated to match actual on-disk measurements.
db updatedisplay includes gnomAD and AlphaMissense in "Analyzing against" annotator list.- Both build scripts (
build_gnomad_cache.py,build_alphamissense_cache.py) runVACUUMfor smaller output files.
- Test suite downloaded real ~6 GB gnomAD cache on every run, filling CI
runner disk. All
db updatetests now use a 792-byte mock fixture viafile://URL -- same pattern as ClinVar, PharmGKB, and GWAS. No production code changes.
- CI: job timeout (20 min), pytest step timeout (15 min),
workflow_dispatchtrigger, verbose output (pytest -v --tb=short) - Ship tooling:
scripts/tag-release.shderives tag from pyproject.toml (single source of truth) - Git hooks: raw
.githooks/pre-pushreplaces pre-commit framework shim, blocks tag pushes where version doesn't match - CONTRIBUTING.md: corrected slow-test documentation (CI skips them, not runs them), added "Run the full suite locally" section emphasizing that developers must run the full suite with real-data fixtures locally before pushing
- Documentation: fixed stale hook instructions, added missing changelog comparison links and ADR index entries
- Removed dead code:
scripts/check_version_tag.sh - Removed
version-tag-matchentry from.pre-commit-config.yaml
- gnomAD population allele frequencies (R-6). New
GnomadAnnotatorenriches report annotations with population frequency context from gnomAD v4.1 exomes (~16M rsIDs). Pre-built cache downloaded from HuggingFace viadb update. Frequency column appears in terminal, HTML, and JSON reports when gnomAD data is available.--no-gnomadflag onanalyze,methylation,pharmacogenomics, anddb updateto skip. - CPIC fallback for PharmGKB (R-5).
db updatesucceeds when the CPIC API is unreachable — PharmGKB downloads complete and the non-finding filter degrades gracefully. Signal carriescpic:unavailableso recovery auto-triggers a refresh. scripts/build_gnomad_cache.py— streaming VCF build script for the gnomAD frequency cache. Downloads ~185GB over HTTPS, never saves VCFs to disk, outputs ~6GB SQLite.scripts/extract_array_manifest.py— extracts rsID superset from genotype files for filtered gnomAD cache builds.- gnomAD ODbL v1.0 attribution in HTML and JSON reports.
- JSON report
schema_versionbumped to"2"(addedallele_frequencyfield on annotations). Diff engine accepts both v1 and v2 baselines. db updatenow handles individual annotator failures gracefully — prints error and continues to remaining annotators instead of aborting.
- Offline claim in README and ADR-0012 corrected: analysis runs offline
by default with an opt-out freshness check (
--no-update), not opt-in network access (#10). .gitignoreupdated for GWAS Catalog test data (#12).scripts/fetch_testdata.shdownloads GWAS Catalog associations from EBI FTP (#12).
- Pre-push hook reduced to version-tag check only; pytest removed (CI runs the full suite on every PR, pre-push pytest caused SSH timeouts).
- CI version-tag guard job added to
.github/workflows/ci.yml(#11).
1.2.0 — 2026-06-07
pyproject.tomlversion corrected to match release (was1.1.0on the v1.1.1 release).- GRCh36 fallback bug. Non-confident GRCh36 detection (e.g., 3/4
probe SNPs matched) was falling back to GRCh37 as the effective build,
silently bypassing the ClinVar safety guard and annotating GRCh36
positions against GRCh37 coordinates. Fixed in both the end-of-stream
flush()path AND the buffer-limit path (large files where probe SNPs appear past the 100K-variant buffer cap). The pipeline now uses GRCh36 as the effective build whenever detection points to GRCh36, even non-confidently or with a single probe SNP match.
- Auto-refresh stale databases.
analyze,methylation, andpharmacogenomicsnow check database file ages before running. If any database is older than 7 days and the remote signal (MD5/ETag) has changed, the database is refreshed automatically. If the network is unreachable, a warning is printed and analysis continues with the stale cache. SNPedia is excluded (no remote download). Use--no-updateto skip the freshness check entirely. - Corpas family exome VCF attribution in
test_data/edge_cases/README.mdwith paper DOI (Corpas et al., BMC Genomics 2015, doi:10.1186/s12864-015-1973-7). Licensing table intest_data/README.mdupdated. Every genotype fixture in the repo now has documented provenance and license. - Version-tag drift guard. Pre-push hook
(
scripts/check_version_tag.sh) asserts any pushedv*tag matches the version inpyproject.toml. Prevents the class of bug where a release ships with a stale version string.
1.1.1 — 2026-06-06
- Relocated real genotype test data (
test_data/real/andtest_data/transcoded/) to GitHub release assets. Fresh clone size reduced from ~650 MB to ~150 MB. Tests skip gracefully when data is absent;scripts/fetch_testdata.shrestores it. - Clarified
.gitignoreandtest_data/README.md: the "never commit" rule applies to private genetic data, not CC0 public-domain openSNP fixtures hosted as release assets.
- Orphaned
[Unreleased]changelog sections assigned proper version numbers ([0.7.2]and[0.8.0]) matching their chronological position in the development history. - Duplicate
[0.7.1]changelog header consolidated into a single entry. - Dead compare links for internal pre-release versions removed (0.x tags were never pushed to the public repository).
1.1.0 — 2026-06-06
allelix comparecommand with strand-aware concordance classification: concordant, strand-flip match, discordant, strand-ambiguous, no-call. Build detection viadetect_build()withget_metadata()fallback. Per-chromosome breakdown. Build rows in Coverage Summary table.- High-value SNP no-call flagging. YAML data file with 12 clinically
important SNPs (APOE, BRCA1, MTHFR, CYP2D6, etc.). No-call warnings surface
in
stats,analyze, and all report formats. Cluster-incomplete detection (e.g., APOE genotype cannot be determined). Loader supports merging user-provided YAML overrides with error handling for malformed input. - ClinVar review status column. CLNREVSTAT surfaced in terminal, HTML, and JSON renderers including all diff tables (new, changed, removed). Users can distinguish expert-panel-reviewed from single-submitter pathogenic calls.
- GRCh36 position-based build detection. All 11 probe SNPs now have GRCh36 positions. 3-way voting across builds. Headerless files (FTDNA, MyHeritage) with GRCh36 positions now detected correctly.
- CONTRIBUTING.md with "How to add a parser" and "How to add an annotator" tutorials, development setup instructions, and coding standards summary.
- 23andMe parser detection tightened. Anchored to canonical first-line
header
# This data file generated by 23andMe. Bare substring matches rejected. Fallback loop for user-prepended comments before the canonical line.
is_must_includeinternal field no longer leaks into public JSON output (filtered fromannotations,diff.new, anddiff.changedpaths).- Build detection docstrings updated for GRCh36 (module docstring and
BuildDetectionResult). _ready_annotatorsreturn type annotation corrected.- Compare command now uses
detect_build()instead of reading the parser's default build fromvariants[0].build. - README status updated from "Pre-release" to "Production".
1.0.0 — 2026-06-05
Six parsers, four annotators, three report formats, report diffing, 794 tests, 94% coverage. All array-based consumer DNA formats supported.
- 23andMe parser (
parsers/twentythreeandme.py). Four-column tab-delimited format with concatenated genotype. Handles I-prefixed probe IDs, haploid MT/Y calls, no-calls (--), CRLF line endings. Detection by "23andMe" in early comment lines. Build from header comments (supports build 36/37/38). - AncestryDNA parser (
parsers/ancestrydna.py). Five-column tab-delimited format with separate allele columns. Chromosome mapping: 23→X, 24→Y, 25→X (PAR), 26→MT. No-calls as0. Detection by#AncestryDNAfirst-line signature. Default build 37. - FTDNA parser (
parsers/ftdna.py). CSV format with double-quoted fields, concatenated genotype in RESULT column. Handles quoted/unquoted headers, haploid MT/Y calls. Detection byRSID,CHROMOSOME,POSITION,RESULTheader pattern. Default build 37. - MyHeritage parser (
parsers/myheritage.py). CSV format, structurally identical to FTDNA. Detected by "MyHeritage" in first comment line. Handles double-double-quoted field variant. Shares_helpers.pywith FTDNA. - Living DNA parser (
parsers/livingdna.py). Tab-delimited despite.csvextension. Handles AX-, AFFX-prefixed probe IDs and CHR:POS positional notation. Build detection from header comments vianormalize_build_label. - Shared parser helpers (
parsers/_helpers.py).split_csv_lineandsplit_genotypeextracted from FTDNA to share across CSV-family parsers. - GRCh36/hg18 build detection.
normalize_build_labelrecognizes "36", "hg18", "build 36", "GRCh36". 23andMe parser detects "build 36"/"hg18" in header comments. CLI emits warning that GRCh36 positions won't match modern references. BUILD_GRCH36constant inbuild_detect.py.- Build mismatch warning in HTML report (R-9). When the file header claims a different build than position data indicates, the HTML report renders a visible warning banner matching the CLI warning.
- "Reading This Report" education section (R-7). Static HTML block after the regulatory notice covering pseudogene cross-hybridization, ClinVar aggregation, carrier vs. affected, and confirmatory testing.
- Signal guard on all annotator
setup()methods. ClinVar, PharmGKB, and GWAS annotators abortsetup()iffetch_remote_signal()returns None, preventing persistence of incomplete cache stamps. - Diff key collision fix. Diff key extended from
(source, rsid, condition)to(source, rsid, condition, description)— prevents silent data loss when PharmGKB has multiple annotations for the same rsid/condition with different drugs. - Terminal diff test coverage. Four tests covering new-only, changed-only,
removed-only, and no-changes branches of
render_terminal_diff. --exclude-snpediaflag onanalyze,methylation, andpharmacogenomicscommands. Suppresses SNPedia annotations at the CLI level — required for commercial use (CC BY-NC-SA 3.0). Wired through existingexclude_sourcesplumbing.- 39 FTDNA tests, 37 23andMe tests, 31 AncestryDNA tests, 34 MyHeritage tests, 34 Living DNA tests, 8 new HTML tests. Full suite: 790.
--diffCLI help reframed as a dev/QA tool for version-to-version validation, not monitoring.- CLI build banner now shows "header (no position confirmation)" when probe SNPs don't match but a header build is present, instead of the misleading "fallback (no known SNPs matched)".
- Parser registry includes all six parsers in detection order: MyHappyGenes, 23andMe, AncestryDNA, Living DNA, MyHeritage, FTDNA.
- Removed all internal phase numbering from user-facing documentation.
- GRCh36 build detection is incomplete on headerless formats. The build
detector's probe table (
KNOWN_SNP_POSITIONS) only has GRCh37/GRCh38 positions. FTDNA files with GRCh36 coordinates are silently labeled as GRCh37 (no warning). PharmGKB, GWAS, and SNPedia continue to fire via rsID-only lookups, but ClinVar would query against the GRCh37 cache with GRCh36 positions and miss any variants whose coordinates shifted between builds. Tracked as R-12. - No ClinVar GRCh36 cache.
CLINVAR_SUPPORTED_BUILDSis GRCh37/GRCh38 only. When a file is correctly detected as GRCh36 (e.g. 23andMe with "build 36" in the header),analyzeproduces zero ClinVar annotations — PharmGKB, GWAS, and SNPedia still fire normally. The CLI prints a warning. Full GRCh36 annotation support requires either an NCBI GRCh36 VCF or liftover. Tracked as R-12.
- SNPedia parser now drops stale unique index before recreating with
COALESCE, fixing silent no-op on caches that already had the old
idx_snpedia_genotype_dedupdefinition. - Backfill dedupe removes pre-existing NULL-summary duplicate rows (2 G6PD-family entries) on parser re-run.
- SNPedia annotator now auto-reparses when parser version changes.
_PARSER_VERSIONstamped intodatabase_versions.remote_signal;is_ready()rejects stale caches and triggers re-parse automatically. Eliminates the recurring "code fix shipped but cache not rebuilt" failure. - SNPedia parser extracts alleles from the page title when the
{{Genotype}}template omitsallele1/allele2. 22 genotype pages (e.g.Rs104894073(A;G)) carried alleles only in the title. These were silently dropped before; now parsed correctly. All 79 originally-dropped pages verified against live SNPedia API — zero content differences between our scrape and source. - SNPedia parser and annotator now handle I-prefixed 23andMe probe IDs
(
I3000043,I5006212, etc.). 1,402 genotype pages (1,401 rows after 1 allele-order dedupe on I4000178) and 2,851 SNP pages use 23andMe internal probe IDs instead of rs-numbers. Gene mapping readsGene_sfrom{{23andMe SNP}}templates. 392 I-probe SNP pages carry gene mappings. Total structured rows: 104,720 (101,328 with gene, 3,392 without). Prepares for 23andMe parser I-probe annotation.
- ClinVar interpreter version stamp (ADR-0023).
CLINVAR_INTERPRETER_VERSIONinannotators/_versions.pyis stamped as|iv:Ninto each per-build cache'sremote_signalduring ingest.is_ready()rejects caches with a stale or missing stamp. One-shot migration self-heals pre-mechanism caches without re-downloading. Eliminates the "annotator logic changed but cache wasn't rebuilt" failure mode for ClinVar. - GWAS auto-reingest from cached TSV (ADR-0024).
setup()now retains the raw GWAS Catalog TSV (~200 MB) after ingest. When_CATEGORIZER_VERSIONbumps and the cached TSV is present,is_ready()auto-reingests from the local file without re-downloading. Users who already have the data get updated classification automatically on first run. TestInterpreterVersionStamp(3 tests) pinning ClinVar interpreter version stamp behavior: matching stamp accepted, missing stamp self-heals, old stamp rejected.TestAutoReingest(2 tests) pinning GWAS auto-reingest: categorizer bump with TSV present triggers reingest, missing TSV returns False.
cached_remote_signal()now strips internal stamps. ClinVar'scached_remote_signal()was returning the raw stored signal including|iv:N, causing freshness comparisons to always show a mismatch and triggering unnecessary re-downloads on everydb update. Internal stamps are now stripped before composing the comparison string.
- ADR-0023 amended with ClinVar interpreter version stamp mechanism.
- ADR-0024 amended with GWAS auto-reingest from cached TSV.
- Added roadmap feature R-11: supplemental genotype file merging (custom panels, Sanger confirmations).
- SNPedia parser dedupes source-level genotype duplicates. Five rsIDs (rs4950928 and four G6PD-family entries) had paired identical rows in snpedia_genotypes from SNPedia source pages whose titles differed only in whitespace.
- GWAS rollup: collapse MTAG twins and PheCode hierarchical sub-classifications before rendering. rs10455872 (LPA) drops from 8 to 5 distinct findings. Must-include rows exempt. See ADR-0024.
- ADR-0008 corrected SNPedia/ClinVar overlap claim (~11% complementary, not 0%).
- GWAS categorizer now classifies against
DISEASE/TRAITin addition toMAPPED_TRAIT. UKB data-field rows with emptyMAPPED_TRAIT(e.g. impedance traits) were leaking into reports at mag-9 in v0.8.1.classify_gwas_trait()now concatenates both fields for keyword matching.
_CATEGORIZER_VERSIONmarker stamped intodatabase_versions.remote_signal.schema_is_current()rejects caches built under a different categorizer version, forcing auto-rebuild ondb update. Eliminates the stale-cache failure mode from v0.8.0/v0.8.1.
- GWAS categorizer keyword leaks. 11 UKB body-composition and
metabolite-ratio rows leaked into the "other" category at mag-9.
Three structural noise patterns addressed:
- UKB bioimpedance traits ("whole body water mass", "impedance of arm",
etc.) — 7 keywords added to
_BODY_MEASUREMENT_KW. - NMR metabolite ratios ("cholesterol-to-phospholipid ratio") — new
_is_metabolite_ratio()helper catches the-to-…ratiopattern. - Uncharacterized analytes ("X-12345 level") — new
_is_uncharacterized_analyte()helper catches thex-prefix pattern. ADR-0024 amended with step 1.5 structural noise detection.
- UKB bioimpedance traits ("whole body water mass", "impedance of arm",
etc.) — 7 keywords added to
- Must-include rsID allowlist (ADR-0024).
_MUST_INCLUDE_RSIDSfrozenset ingwas.pylists clinically significant GWAS associations that bypass--gwas-min-magnitudefloor: rs10737680 (CFH/AMD), rs11209026 (IL23R/IBD), rs9271366 (HLA-DRB1/MS). Global--min-magnitude, trait-category filter, and carrier rule still apply. Annotation.is_must_includefield. Boolean flag set by the GWAS annotator;AnalysisResult.filter()exempts flagged annotations from per-source magnitude floors.TestStructuralNoiseDetection(18 parametrized tests) covering UKB body-composition, metabolite ratio, and uncharacterized analyte classification plus disease non-misrouting assertions.TestMustInclude(5 tests) covering constant shape, carrier flag, source floor bypass, global min-magnitude enforcement, and trait filter enforcement.
ClinVar REF allele is the primary PharmGKB non-finding filter (ADR-0023). Five prior releases (v0.5.x–v0.7.1) iterated on a CPIC-based filter that required CPIC to classify the user's allele as
Normal function. CPIC's vocabulary and coverage are heterogeneous across genes — CFTR uses"ivacaftor responsive", MTHFR/F2/F5 have no entries at all. Real-world v0.7.3 output leaked ~30+ CFTR × ivacaftor reference-homozygote rows.The fix is structural: the inclusion/exclusion question is "does the user carry the variant allele?" — and ClinVar publishes REF universally for every variant it catalogs. The new primary filter is one check: if ClinVar's REF is single-base and matches both of the user's alleles, the row is a non-finding. CPIC is demoted to a secondary tier for the rare rsid ClinVar doesn't know about.
- CFTR-class reference-homozygote leak. Real-world report at
--min-magnitude 5previously surfaced ~30+ CFTR × ivacaftor rows saying "do not have a copy of the variant." All of these are now suppressed by the ClinVar REF check. - Inconsistent genotype display across annotators. ClinVar rows
used to show single-letter
genotype_match(the matched ALT base); PharmGKB rows showed the user's diploid. Both now show the user's sorted diploid ("AG","GG", etc.). Indel passthrough is verbatim ("CTT/C").
--include-benignflag. ClinVar Benign/Likely_benign annotations are now suppressed by default at the annotator level. Pass--include-benignonanalyze,methylation, orpharmacogenomicsto restore them. ADR-0008 amended to document the policy.--gwas-min-magnitudeflag (default 9.0). Per-source magnitude floor for GWAS Catalog annotations. On real data the GWAS Catalog produces ~88,000 associations; the 9.0 floor keeps only hyper-significant + large effect size signals. Floor raised from 7.0 to 9.0 after real-data testing showed 30,000+ mag-7 common-trait rows (Height, BMI, blood counts). ADR-0024 amended.--include-gwasflag onmethylationandpharmacogenomics. Focused reports exclude GWAS Catalog annotations by default — methylation biology is interpreted from ClinVar + PharmGKB, not GWAS trait associations. Pass--include-gwasfor completeness.- GWAS trait-category filtering (ADR-0024). Each GWAS Catalog row is
classified into a trait category (disease, cancer, drug_response, immune,
cardiovascular, metabolic, neurological, body_measurement, lipid_measurement,
hematological_measurement, other_measurement, behavioral, other) using EFO
ontology labels. Default excludes measurement and behavioral categories.
On real data: 605 GWAS rows pass at mag-9 (down from 3,314 pre-filter).
--gwas-alldisables trait-category filtering for the full unfiltered dump. TestBenignSuppressionEndToEndandTestDefaultReportSanityintest_end_to_end.pypinning that benign suppression works and that default filters produce a tractable annotation count (≤ 20).TestMethylationSanityintest_end_to_end.pypinning that methylation output (ClinVar + PharmGKB, no GWAS) stays under 20 rows.TestRealDataGwasSanity(@pytest.mark.slow) intest_end_to_end.py— runs against the real GWAS Catalog (test_data/, gitignored) and pins that default filters keep output bounded. Skips when real data hasn't been downloaded.test_benign_suppressed_by_default,test_include_benign_flag,test_gwas_min_magnitude_default, andtest_gwas_min_magnitude_loweredintest_cli.pycovering the new CLI flags.test_gwas_excluded_by_defaultandtest_include_gwas_flagonTestMethylationCommand;test_gwas_excluded_by_defaultonTestPharmacogenomicsCommand— pin GWAS exclusion from focused reports.- GWAS Catalog annotator. Trait–SNP associations from EBI/NHGRI.
Carrier rule (ADR-0007): only fires when the user carries the risk
allele. P-value magnitude scoring (ADR-0024): six tiers from 2.0
(weak) to 8.0 (hyper-significant) with an OR/beta modifier (+0.5
or +1.0, capped at 9.0). Unknown-risk-allele entries fire on rsID
match alone but are capped at magnitude 3.0 so they don't pass
typical
--min-magnitude 5thresholds. - ADR-0024 documenting GWAS Catalog magnitude scoring from p-value and effect size, unknown-risk-allele cap rationale, and the forward-strand assumption (deferred to R-1).
TestGwasMockInvariants(four tests) pinning the GWAS fixture shape per ADR-0015: single-base risk allele, unknown risk allele, p-value tier coverage, and haplotype-skip row.ClinVarAnnotator.reference_for(rsid, build) -> str | None. Per-build lazy-built(rsid -> single-base REF)lookup. Indel REFs are excluded by the loader's SQL filter (WHERE length(ref) = 1).PharmGKBAnnotator(..., clinvar_ref_provider=callable)accepts the REF provider.get_annotators()wiresclinvar.reference_forinto the PharmGKB instance.- ADR-0023 documenting the architectural shift.
TestClinvarRefPrimaryFilter(six tests) pinning the REF-primary filter behavior: homozygous-reference suppression, heterozygous emission, CFTR-class leak fix, fallback to CPIC for rsIDs ClinVar doesn't know, multi-base REF (indel) fall-through, and consistent diploid genotype display.
- Default
--min-magnituderaised from 0.0 to 5.0. Previous default dumped every annotation regardless of importance. The new default surfaces only clinically meaningful findings (ClinVar Pathogenic/Likely_pathogenic, PharmGKB LoE 1–2, GWAS genome-wide significant). Pass--min-magnitude 0for a full dump. - GWAS Catalog download URL updated to FTP ZIP. EBI deprecated the old
API endpoint (Nov 2025). The loader now downloads the ZIP archive from
ftp.ebi.ac.ukand extracts the TSV. - Focused reports exclude GWAS by default.
methylationandpharmacogenomicscommands no longer run the GWAS annotator. On real data, 98.5% of methylation output was GWAS noise (342/347 rows, 228 from FUT2 human-milk-oligosaccharide studies).--include-gwasopts in. PharmGKBAnnotator.annotate()filter order. Primary tier: ClinVar REF check (suppress hom-ref, emit carrier WITHOUT the cache'sis_nonfindingfilter so a CPIC-driven false-suppression can't hide a real carrier). Secondary tier (no ClinVar REF data for rsid): fall through to the cache's pre-computedis_nonfindingflag.Annotation.genotype_matchsemantics. Now always the user's sorted diploid for SNVs; passthrough for indels. ClinVar previously set this to the matched ALT base — that was lossy and inconsistent with PharmGKB's display.- ADR-0022 scope reduced. Still applies, but only to rsIDs where BOTH ClinVar and CPIC lack data. The CFTR/MTHFR/F2/F5 cases that motivated ADR-0022 are now caught by the ClinVar REF check.
- 347 tests pass (was 341; +6 new for the primary tier). Coverage steady. Lint and format clean.
- The recurring "PharmGKB filter still leaks on a new gene" failure mode that spanned v0.5.x–v0.7.1 is structurally resolved: the filter no longer depends on CPIC's per-gene vocabulary.
Genome build auto-detection (ADR-0021 + ADR-0022) with Round 23 audit follow-up. A real-world MyHappyGenes/Tempus export was confirmed to ship GRCh38 positions while its header claims "build 37.1." Cross-build REF/ALT comparison against ClinVar's GRCh37 VCF produced a false-positive pathogenic call on NIPA1
rs104894490. The carrier check (ADR-0007) was correct — it was matching against the wrong build's REF/ALT. The fix is structural: detect build from position data, hold per-build ClinVar caches, dispatch per variant. ADR-0022 documents the deliberate decision NOT to filter PharmGKB reference-genotype rows on non-CPIC genes.An external audit (Round 23) subsequently found three fixture-layer defects and one
.gitignoreomission in the build-detection work. The new code was right; the test fixtures were lying about what they covered.
allelix/utils/build_detect.pywith a hardcoded ~11-entry(rsid, build) → (chromosome, position)table covering chromosomes 1, 10, 11, 12, 17, 19, and 22.detect_build()streams variants and returns the matching build once any table entry's position confirms.allelix.databases.cpic_loader's API +manager.CLINVAR_URL_BY_BUILD— per-build URL map containing bothCLINVAR_URL_GRCH37and a newCLINVAR_URL_GRCH38(https://ftp.ncbi.nlm.nih.gov/pub/clinvar/vcf_GRCh38/).ClinVarAnnotator(data_dir, builds=("GRCh37", "GRCh38"))holds one SQLite cache per build (clinvar.GRCh37.sqliteandclinvar.GRCh38.sqlite).annotate(variant)dispatches byvariant.build.is_ready()requires every managed build to be cached.fetch_remote_signal()returns a composite signal"GRCh37:md5:…|GRCh38:md5:…"so either side's update triggers a refresh.- CLI flags.
allelix db update --build {grch37, grch38, both}(defaultboth) andallelix analyze --build {auto, grch37, grch38}(defaultauto).methylationandpharmacogenomicsaccept the same--buildflag. - Build mismatch warning. When
analyzedetects a build different from the file header's claim, a yellow warning surfaces explaining the discrepancy and naming both builds. - ADR-0021 documenting the auto-detection feature and the policy of distrusting file headers in favor of position data.
- ADR-0022 documenting the deliberate decision NOT to filter PharmGKB reference-genotype rows on non-CPIC genes. The rows emit; the README documents the limitation.
- Mock data generator
--buildand--header-buildflags. Default emits GRCh38 (matching real MHG behavior). Three test fixtures committed:mock_myhappygenes.txt(clean GRCh38),mock_myhappygenes_grch37.txt(clean GRCh37), andmock_myhappygenes_mislabeled.txt(GRCh38 positions, GRCh37 header — replicates the real-world MyHappyGenes mislabel). tests/utils/test_build_detect.pywith 18 tests pinning the detector's behavior on confident matches, single-match unambiguity, inconsistent files, table-shape invariants, and label normalization.- Three end-to-end CLI tests in
test_cli.py:test_analyze_warns_on_build_header_data_mismatch,test_analyze_no_warning_on_clean_grch37, andtest_analyze_build_override_skips_detection. - README "Known PharmGKB limitation" section documenting non-CPIC reference-genotype rows per ADR-0022.
tests/fixtures/mock_clinvar_grch37.vcfandmock_clinvar_grch38.vcfwith 11 single-allele records + 1 multi-allelic row. rs104894490 NIPA1 included with build-specific REF/ALT (the strand-inverted regression case).tests/test_mock_data_invariants.py::TestClinvarFixturePositionInvariantspinning that every rsID in the build-detect table uses build-authoritative positions in the matching fixture, and that rs104894490 specifically has the strand-inverted REF/ALT pair preserved across the two fixtures. ADR-0015's "mock-as-spec" invariant now applies to the ClinVar fixture too.- rs104894490 NIPA1 entry in the MHG generator's known SNPs (chr15, G/G genotype, both build positions). The mock MHG default fixture grew from 2,015 to 2,016 SNPs to carry the regression case.
- Per-build ClinVar fixtures. The pre-Round-23 mock_clinvar.vcf
had mixed-build positions (some GRCh37, some GRCh38, rs80357906 at
41245466 matching neither). This was the exact failure mode
ADR-0021 was written to detect, hardcoded into the project's own
test fixture. Replaced by two build-correct fixtures:
tests/fixtures/mock_clinvar_grch37.vcfandmock_clinvar_grch38.vcf. Generator rewritten (tests/generate_clinvar_fixture.py) to emit per-build VCFs with positions verified against NCBI dbSNP. The oldmock_clinvar.vcfis removed. - rs80357906 BRCA1 position corrected. Old fixture had 41245466;
authoritative is GRCh37 41209080 / GRCh38 43057063 (verified
against NCBI Variation API and the live ClinVar GRCh37 VCF). The
build_detect.pytable was correct; the fixture was wrong. - Dual-cache dispatch test coverage is no longer fictional. The
conftest now loads
mock_clinvar_grch37.vcfinto the GRCh37 cache andmock_clinvar_grch38.vcfinto the GRCh38 cache, so per-build dispatch produces DIFFERENT results across caches. Two new end-to-end tests pin the contract directly using rs104894490 NIPA1:test_nipa1_strand_inversion_no_emission_on_grch38_data— the smoking-gun case from the user's report. MHG fixture has G/G at the GRCh38 NIPA1 position; auto-detection identifies GRCh38; dispatch queries the GRCh38 cache (REF=G ALT=A); zero A alleles → no annotation. The false positive is gone.test_nipa1_grch37_dispatch_reproduces_legacy_false_positive— forces--build grch37on the same data; dispatch queries the GRCh37 cache (REF=C ALT=G); user's G matches ALT=G → annotation DOES emit. This is the OLD wrong behavior pinned so a future "default to GRCh37" regression visibly flips this assertion.
- Auto-detection is the analyze default. Files without a CLI
--buildoverride get detected from position data; the parser-reported header build is treated as informational. A[dim] Build:banner prints to terminal showing the effective build, the source (detected / override / fallback), and how many known SNPs matched. - README updated to note that real-world MyHappyGenes files ship GRCh38 despite their header claim. Developer documentation explicitly tells future contributors not to trust the header.
- Conftest populates both build caches via
clinvar_data_dirso the rsID-based annotator queries work regardless of detected build. - Database versioning uses per-build record names
(
clinvar.GRCh37/clinvar.GRCh38) indatabase_versions. mock_clinvar.vcffixture removed. The single mixed-build file predated ADR-0021's per-build split. Callers that referenced the generic name (test_manager.py, test_cli.py legacy paths) now use a back-compat alias fixture that points tomock_clinvar_grch37.vcf. New tests should use the build-specific fixtures directly.- Count snapshots updated for the +1 NIPA1 row. MHG 2,015 → 2,016; ClinVar 12 → 13 per build; composite status display 24 → 26.
.gitignoreexcludestest_data/— exploratory scripts kept out of CI lint/format and out of the committed tree.pyproject.toml [tool.ruff]addsextend-exclude = ["test_data"]belt-and-braces: even if ruff is run with--no-respect-gitignore, exploratory scripts undertest_data/are skipped.
- v0.7.x users have a single
clinvar.sqlitecache, which the new annotator ignores. Runallelix db update --forceto populate the new per-build caches (clinvar.GRCh37.sqliteandclinvar.GRCh38.sqlite). The legacyclinvar.sqlitecan be deleted; nothing references it anymore.
- 341 tests pass (was 336; +5: two end-to-end NIPA1 regressions, three fixture invariants). Coverage 95.61%. Lint and format clean.
- The NIPA1 case from the user's real-world report has BOTH directions of the dispatch contract pinned: correct behavior on GRCh38 data, reproducible legacy false positive when GRCh37 is forced.
- The user's NIPA1 false positive was diagnosed in this cycle: header mislabel + cross-build strand inversion + correct carrier rule matching the wrong build's REF/ALT = false pathogenic call. v0.7.1 shipped a carrier check the user briefly thought was broken; this release vindicates that code and addresses the actual root cause.
- Coverage of the seven remaining ClinVar pathogenic hits (NCR3, IL10,
GP9, ADRA2A, TMPRSS6, PKD1, FLG) requires running
db update --forceon real data and re-annotating; some may be the same cross-build artifact as NIPA1. The tool now uses the correct build automatically.
Patch release for v0.7.0's PharmGKB regression. v0.7.0 left three production rows leaking — CACNA1S rs1800559 CC (×7 anesthetic annotations) and NUDT15 rs116855232 CC (×3 thiopurine annotations). Root cause: v0.7.0 populated the per-allele function table from CPIC template sentences embedded in PharmGKB's annotation_text. That's regex on description text — and many CPIC genes (including CACNA1S and NUDT15 in real data) don't publish those sentences at all, leaving the lookup empty for those rsids and the prose-fallback tier failing to catch the carrier-vs-reference distinction.
The filter is a table join, not a text classifier. The per-allele function table now comes from CPIC's structured API (
api.cpicpgx.org/v1), where every CPIC-curated allele carries a discreteclinicalfunctionalstatusvalue. ADR-0020 documents this.
- PharmGKB non-finding filter is now a pure structured-data lookup.
is_nonfinding_by_allele_lookup(rsid, genotype, lookup)is the filter: for the user's two bases, look up each in(rsid, base) → function_class; if both map toNormal function, suppress the row. No regex, no description parsing, no text classification. - v0.7.0 production leakers all classify correctly. Verified
end-to-end against the real PharmGKB July-2025 dump joined against
the live CPIC API:
- rs1800559 CC (CACNA1S, 7 anesthetic annotations) → all
is_nonfinding=1. - rs116855232 CC (NUDT15, 3 thiopurine annotations) → all
is_nonfinding=1. - v0.6.1 DPYD cluster (rs115232898 TT ×2, rs1801266 GG ×1, rs3918290 CC ×5)
→ all
is_nonfinding=1. - Known carriers (rs1801133 AG/AA ×20, rs1799853 CT ×3, rs4149056 CT ×39,
rs4244285 AG ×7) → all
is_nonfinding=0. Zero over-suppression.
- rs1800559 CC (CACNA1S, 7 anesthetic annotations) → all
- C-1: test suite no longer hits the real CPIC API.
test_db_update_with_file_urlmonkey-patchesfetch_cpic_allele_functionson the annotator module to return the sameMOCK_CPIC_LOOKUPthe rest of the suite uses. Run time on that single test dropped from ~4 s (network-bound) to 0.6 s (offline). - C-2: README refreshed to v0.7.1. Status banner, supported- databases table, and architecture-decision summary reflect ADR-0020 (CPIC API as the structured per-allele function source) and drop obsolete references to ADR-0013 / ADR-0014.
- M-3: CPIC conflict-resolution policy flipped. When the same
(rsid, base)appears under multiple allele definitions with a Normal-vs-non-Normal disagreement, the loader now picks the non-Normal classification. Suppressing happens only when EVERY base is Normal — biasing conflicts toward non-Normal ensures we never silently suppress a real variant just because one CPIC row flagged it Normal. Pinned bytest_conflict_prefers_non_normal.
- ADR-0020: CPIC API as the structured per-allele function source.
Documents the three-way client-side join across CPIC's
sequence_location(rsid),allele_location_value(base), andallele(clinicalfunctionalstatus) tables. - New module
allelix.databases.cpic_loaderwithfetch_cpic_allele_functions()— returns(rsid, base) → function_class. load_pharmgkb_tsvacceptsallele_function_lookupdirectly. Production fetches it from CPIC's API in the annotator'ssetup(); tests inject a synthetic dict.mock_cpic_lookuppytest fixture inconftest.pyholds the canonical synthetic CPIC lookup the test suite joins against.- M-1: retry-with-backoff on CPIC fetches.
_http_get_jsonretries up to 3 times with(1, 2, 4) sbackoff onURLError,TimeoutError, andJSONDecodeError. A single transient TCP RST duringdb updateno longer aborts the refresh. - M-2: composite PharmGKB + CPIC freshness signal.
PharmGKBAnnotator.fetch_remote_signal()now returnspgkb:<pgkb-signal>|cpic:<cpic-signal>where the CPIC portion is the latest date from CPIC'schange_logtable. If CPIC publishes new allele functions while PharmGKB's zip is unchanged, the nextdb updatedetects it and refreshes. CPIC probe failure returns None (existing "can't verify, pass --force" UX). - M-4: mutation-gap tests pinning critical policies.
_classify_cpic_statusreturning None for unknown statuses, andfetch_cpic_allele_functionsskipping multi-base / non-ACGT values, both have direct pins. A future refactor can't silently break either without a test failure. - m-2: zip cleanup wrapped in try/finally in
PharmGKBAnnotator.setup(). A failure between download and ingest no longer leaves the stagedclinicalAnnotations.zipon disk. - m-6:
MOCK_CPIC_LOOKUPshape invariants. NewTestCpicLookupMockInvariantschecks every fixture entry is(rsid, single-base)keyed and every value is a recognizedfunction_class. Mirrors what realfetch_cpic_allele_functionsreturns, so the same fixture-as-spec violation that bit v0.6.0 and v0.7.0 can't recur with CPIC data. tests/databases/test_cpic_loader.pywith 16 tests covering the three-way join, multi-base filtering, unknown-status filtering, null-dbsnp filtering, conflict resolution, network-error propagation, retry-then-success, timeout retry, malformed-JSON retry, and the CPIC freshness probe's failure modes.
- All regex-on-prose tiers gone.
_NONFINDING_PROSE_FALLBACK,_CPIC_ALLELE_FN_RE,extract_cpic_allele_function, andbuild_allele_function_lookup(the CPIC-template extractor) deleted. ADR-0017 and ADR-0018 superseded. - Prose-fallback safety-net test deleted. No prose tier to safety- net any more.
- Mock fixture trimmed. PA-010 (somatic-prose synthetic) and PA-011 (CPIC-template synthetic) removed. PA-008 retained but rewritten to exercise the structured-lookup path (reference homozygote suppression).
- v0.7.0 caches have the same SQLite schema as v0.7.1 (no schema
change), but the
pharmgkb_allele_functiontable is empty/sparse on v0.7.0 caches built from PharmGKB's clinical_ann_alleles. Users on v0.7.0 must runallelix db update --forceto re-ingest with the CPIC API as the lookup source.
Architectural fix for PharmGKB SNV classification. v0.6.1's hybrid classifier still leaked DPYD rows because its prose fallback couldn't catch every CPIC phrasing variant. The fix is not more regex — it is recognizing that PharmGKB publishes per-allele function for SNV alleles inside a canonical CPIC template sentence embedded in
annotation_text. Parsing a bounded template to extract fielded data is structurally distinct from regex-on-prose intent classification. v0.7.0 extracts that field at load time and stores it in a new structured table.
- PharmGKB SNV per-allele function now structurally extracted (ADR-0018). The DPYD cluster the v0.6.1 reviewer flagged (rs115232898 TT, rs1801266 GG, rs3918290 CC and ~17 others of the same shape) now classifies correctly. Reference-allele homozygotes do not emit.
- ADR-0018: PharmGKB per-allele function via CPIC template
extraction. Refines ADR-0016 (Data Classification Principle) by
drawing a line: bounded canonical templates with enumerated fields →
structured extraction (allowed); arbitrary prose intent inference →
forbidden. The CPIC sentence
"The {A|C|G|T} allele of {rsid} is assigned [a] {normal|decreased|no|increased} function [allele] by CPIC."is parsed at load time. - New SQLite table
pharmgkb_allele_function. Stores(rsid, allele) -> function_classwith asourcediscriminator so future per-allele function sources (e.g. var_fa_ann aggregation or a new PharmGKB schema column) populate the same table. - Pre-pass / main-pass loader.
load_pharmgkb_tsvnow does a pre-pass overclinical_ann_alleles.tsvto build the global(rsid, allele) -> function_classlookup, populates the new table atomically, then re-iterates to classify each row using the lookup. - Hybrid classifier priority (
is_nonfinding_for_row): (1) structuredAllele Functioncolumn → (2) per-allele CPIC lookup → (3) prose fallback (ADR-0017 residual). Tier 2 is the primary path for SNV rows where CPIC has published guidance. Tier 3 becomes inert wherever tier 2 has entries. - Mock fixture row PA-011 / rs900000111. Three allele rows (CC, CT,
TT) each carrying
"The C allele of rs900000111 is assigned decreased function by CPIC."Exercises the per-allele lookup path end-to-end. Fixture now ships 10 annotations + 21 allele rows = 17 stored records. - Tests:
TestExtractCpicAlleleFunction(direct regex unit tests),TestBuildAlleleFunctionLookup(fixture-integration), andTestIsNonfindingByAlleleLookup(per-allele classifier including the reviewer's exact leaker shapes).
- v0.6.x caches lack
pharmgkb_allele_function, soschema_is_current()returns False anddb updaterefreshes automatically. Runallelix db update --forceto re-ingest with the new classifier.
Recovers v0.5.2's filtering. v0.6.0 was a regression on real data — the structured classifier keyed off
Allele Function, which PharmGKB populates only on haplotype rows (the loader rejects those). 100% of in-scope SNV rows had emptyAllele Function→ ~13,500 rows emitted in production. v0.6.1 restores filtering via a documented hybrid: structured signal when present, prose pattern set as the row-level fallback per ADR-0017.
- PharmGKB classifier is now hybrid (ADR-0017).
is_nonfinding_for_row(allele_function, annotation_text)checks the structured field first; if empty, falls back to a bounded named prose pattern set. Structured-first invariant preserved wherever PharmGKB publishes the structured signal. If PharmGKB addsAllele Functionon SNV rows in a future revision, the fallback becomes inert automatically.
- Mock fixture now models real PharmGKB shape (ADR-0015 + ADR-0017).
SNV genotype rows have
Allele Function = ""(matches real PharmGKB). Haplotype rows retain populatedAllele Function(matches real PharmGKB and exercises the structured path through unit tests). The earlier inverted-shape fixture was the proximate cause of the v0.6.0 regression; the new fixture-shape invariant test (test_snv_rows_have_empty_allele_function) fails loudly if anyone inverts the shape again.
- ADR-0017: PharmGKB SNV row-level prose fallback. Documents the narrow ADR-0016 exception: when a database publishes a structured classification field but leaves it empty on every row the consumer code path processes, a bounded prose pattern set may serve as the row-level fallback. Four guardrails enforced (structured-first; named bounded patterns; real-shape invariant test; ADR-scoped boundary).
- Data Classification Principle amendment. New subsection defining the narrow row-level exception. The general rule is unchanged: regex on prose is forbidden for classification. The exception is precisely scoped.
- Test:
test_snv_rows_have_empty_allele_function. Pins the real-data shape on the mock fixture. If a future fixture revision re-populates Allele Function on SNV rows, the test fails with a message naming the v0.6.0 regression and pointing at ADR-0015 + ADR-0017. - Test:
TestIsNonfindingForRow. Exercises the hybrid classifier across structured-wins, prose-fallback, and edge cases.
- v0.6.0 caches (built between this morning and now) lack the new
hybrid classification — every row has
is_nonfinding=0. The schema is the same as v0.6.1, soschema_is_current()returns True and the freshness check would skip the rebuild. Users on v0.6.0 must runallelix db update --forceto re-ingest with the hybrid classifier. - v0.5.x caches → automatic refresh (schema check fails on missing
function_class).
- A redacted slice of real PharmGKB committed to the repo and run through the pipeline in CI nightly. Mock-shape invariant is necessary but not sufficient; real-source-data smoke is the next layer. Tracked in ADR-0017; not blocking v0.6.1.
- Audit of
variantAnnotations.zip/genes.zipfor a structured per-SNV classification field. If one exists, replace the prose fallback with it and ADR-0017 becomes inert.
Functional regression. ADR-0016's structured classifier consumed PharmGKB's
Allele Functionfield, which is empty on every SNV row. The annotator (per ADR-0009) processes only SNV rows. Net effect: every PharmGKB row emitted in production. Use v0.6.1 or later. The ADR-0016 principle is correct and remains in force; ADR-0017 documents the row-level fallback that v0.6.0 lacked.
Architectural correction. Classification by regex against prose was the wrong mechanism. v0.6.0 enforces the Data Classification Principle (ADR-0016): structured database fields are the only classification input. Regex against description text is permitted only in test safety nets.
- PharmGKB non-finding suppression now uses the structured
Allele Functioncolumn. The previous regex-on-prose mechanism (eight patterns in v0.5.0, expanded to ten in v0.5.2) is deleted from production code.pharmgkb_loader.classify_function()maps the authoritative field to a normalized enum (normal/decreased/no_function/increased/unknown). A row is a non-finding ifffunction_class == "normal". Stable against PharmGKB editorial drift. is_nonfindingis now a derived structured signal, not a regex hit count.- Annotator SELECT filters by
is_nonfinding = 0and no longer referencesis_somatic.
_NONFINDING_PATTERNS,_is_nonfinding(annotation_text),_SOMATIC_PATTERN,_is_somatic(annotation_text)— all four regex-on-prose classifiers excised from production code per ADR-0016.is_somaticcolumn frompharmgkb_annotations. PharmGKB has no structured germline/somatic flag, so per ADR-0016 the decision cannot be automated. The common case (somatic-context rows describing reference genotypes) is correctly caught by the non-finding filter viaAllele Function = Normal function; rare residuals surface for manual review.
- ADR-0016: Data Classification Principle — structured fields only, regex forbidden in production, regex permitted only as a test safety net. Codifies the project's non-negotiable architectural directive.
function_class TEXT NOT NULLcolumn onpharmgkb_annotations, storing the normalized enum fromclassify_function(). Required byschema_is_current(), so v0.5.x caches refresh automatically on nextdb update.tests/test_pharmgkb_safety_net.py— the one place the regex now lives. Runs as a canary: regex match must agree with the structuredis_nonfindingcolumn. Disagreement = loader bug, not regex bug.
- v0.5.x caches lack
function_class→PharmGKBAnnotator.is_ready()returns False → nextdb updaterefreshes. No user action beyond runningallelix db updateafter upgrading.
- ADR-0013 amended: mechanism revised to structured-field-based; user-facing contract unchanged.
- ADR-0014 superseded: somatic suppression removed; structured signal does not exist in PharmGKB; gap documented.
- Full repo audit (recorded in commit message): two regex violations
identified, both in
pharmgkb_loader.py, both removed. Remaining regexes (_RSID_RE,_TWO_LETTER_GENOTYPE_RE, VCF header parsers, test fixture validators) are structural-format checks, not prose classification.
- PharmGKB non-finding suppression: two more patterns. Real-data
review on the v0.5.1 report surfaced 30+ rows that still leaked
through ADR-0013's filter. The DPYD cluster was the most visible
(15+ entries at magnitude 9.0 saying "Both variants of rsX are
assigned normal function by CPIC"). Pattern set extended to include
assigned normal functionandmay not have altered risk. The expanded patterns are pinned by new tests underTestNonFindingClassifier. Mock PharmGKB fixture gains two PA-008 allele rows demonstrating the new patterns (ADR-0015 contract). - New regression tests pin that the classifier does NOT over-filter: protective findings ("decreased risk of neutropenia") and clinical dosing guidance ("require a decreased dose of warfarin") are preserved as findings.
allelix extract --snps rs1,rs2,...: spot-check diploid genotypes at specific rsids without running a full analyze. Implements the CLI command that had been on the deferred list. Useful for verifying ClinVar/PharmGKB hits against the actual file before trusting them — particularly for the residual high-magnitude ClinVar hits flagged in the v0.5.1 review (NIPA1, NCR3, IL10, GP9, ADRA2A, TMPRSS6).
- No schema change. v0.5.1 PharmGKB cache is compatible with v0.5.2,
but the new patterns only apply to rows ingested after upgrade. Run
allelix db update --forceto re-ingest with the expanded classifier and drop the leaked non-finding rows.
Process correction. Closes the underlying failure mode that allowed the v0.4.2 and v0.5.0 clinical-safety bugs to ship. See ADR-0015.
- MHG mock generator wrote
CTT/Cfor rs113993960 (CFTR ΔF508). Real MyHappyGenes arrays cannot call indels — every genotype is one ofA/T/G/Cor the no-call marker-. The buggy entry violated the MHG format spec and is the proximate reason the ClinVar indel-anchor incident (ADR-0011) wasn't caught by tests before shipping in v0.4.0. Corrected to-/-.
- ADR-0015: Mock data generators are the contract. The mock generators are the canonical model of real source data. Code that doesn't work against generator output is buggy; generators that don't model real data are buggy. Hand-authored ad-hoc fixtures are forbidden.
tests/test_mock_data_invariants.py: structural assertions on every generator's output (MHG must produce single-base or no-call alleles only; ClinVar must include both SNVs and indels and multi-allelic rows; PharmGKB must include carrier-findings, non-findings, and somatic rows). Drift in a generator now fails the suite explicitly.tests/test_end_to_end.py: full pipeline against the canonical mock generators with snapshot assertions. Pins the absence of every categorical bug closed across v0.4.2/v0.5.0 (CFTR indel-anchor leak, TP53 wild-type leak, PharmGKB non-finding leak, PharmGKB somatic leak) plus a numeric snapshot. Any drift in the snapshot demands code review, not a blind update.
test_indel_variantrenamed totest_cftr_indel_position_is_no_calland rewritten to assert the corrected fixture behavior. The old test assertedCTT/Cwas a valid MHG genotype — exactly the assumption that hid the bug.- Stats count assertions updated for the corrected fixture: 102 no-call → 103, 564 het → 563. Hom unchanged.
- End-to-end snapshot: 7 ClinVar + 4 PharmGKB annotations against the corrected fixture. CFTR no longer fires (no-call short-circuits before the indel-anchor check).
Clinical-safety release. v0.5.0 closes three categorical false-positive bugs in the annotator layer. v0.4.2 already shipped the ClinVar indel fix (#1 below); v0.5.0 adds the two PharmGKB fixes (#2 and #3) and is the first version where reports against array-based parsers can be read with confidence. Treat all v0.4.x and earlier reports as untrusted; regenerate against v0.5.0.
-
ClinVar: indel-anchor false positives (originally shipped in v0.4.2; restated for users skipping straight from v0.4.1). ClinVar encodes indels with an anchor base (e.g.
REF=AT ALT=Afor a single-T deletion). Array-based parsers report single-base genotypes. The carrier rule treated the anchor character as equivalent to the array readout and emitted hundreds of false-positive "Pathogenic" calls in cancer-predisposition genes (MSH6, APC, PTEN, MLH1, MSH2, RB1, BRCA1, BRCA2, TP53, …). Indel rows are now suppressed when the user's genotype is single-base only. See ADR-0011. -
PharmGKB: non-findings emitted as findings. PharmGKB stores one row per genotype at each variant position, including the reference (wild-type) genotype, with text like "do not have a copy of the variant" or "decreased but not absent risk". These rows were emitted at the same magnitude as real variant calls. Real-data measurement: ≥12% of PharmGKB matches in a production array report were explicit non-findings. Rows whose annotation text matches the non-finding pattern set are now classified at load time and suppressed at query time. See ADR-0013.
-
PharmGKB: somatic-tumor annotations on germline data. A subset of PharmGKB rows (EGFR T790M / L858R, BRAF/KRAS/PIK3CA tumor markers) describe somatic variants only present in tumor tissue. Consumer DNA tests sample germline DNA. These rows are now suppressed on germline parsers. See ADR-0014.
pharmgkb_annotationsgains three columns:allele_function TEXT,is_nonfinding INTEGER NOT NULL,is_somatic INTEGER NOT NULL.PharmGKBAnnotator.is_ready()now consultsschema_is_current(db_path)(PRAGMA-based check). v0.4.x caches return False →db updatesees "not ready" and refreshes into the v0.5.0 schema. No silent degradation.
- Treat all v0.4.x reports as untrusted. Regenerate against v0.5.0.
- Run
allelix db updateto rebuild the PharmGKB cache (no--forceneeded; the schema check forces a refresh automatically).
- ClinVar annotator emitted hundreds of false-positive "Pathogenic" calls
in cancer-predisposition genes (APC, MSH6, MLH1, MSH2, PTEN, RB1, BRCA1,
BRCA2, TP53, …) when run against array-based genotype files
(MyHappyGenes, 23andMe, AncestryDNA). Root cause: ClinVar encodes indels
with an anchor base (e.g.
REF=AT ALT=Afor a deletion of T). Array parsers report single-base genotypes at probe positions. The carrier rule's string equality treated ClinVar's anchor character as equivalent to the array's readout, producing pathogenic calls for users who carried only the wild-type sequence. Indel rows are now skipped when the user's genotype is single-base only. Multi-base genotypes (future VCF parsers that actually call indels) are unaffected. See ADR-0011. - Affects every report generated by v0.4.0 and v0.4.1 against array data. Treat all v0.4.x outputs as untrusted until regenerated against v0.4.2.
- Freshness detection (ADR-0012).
allelix db updatenow detects when the remote source has changed and refreshes only when needed, without requiring--force. Each annotator implementsfetch_remote_signal()that fetches a small published signal (NCBI'sclinvar.vcf.gz.md5for ClinVar; HEAD-requestETag/Last-Modifiedfor PharmGKB). Stored signals are type-prefixed (md5:…,etag:…,lm:…) so a server switching signal types triggers a refresh rather than a silent miss. Network helpersfetch_remote_textandhead_request_headersswallow allOSError/ValueErrorand returnNone, so a flaky network never crashesdb update— at worst the user sees "freshness can't be verified" and can--forceto refresh. - Schema migration:
database_versionsgains a nullableremote_signalcolumn.get_database_infofalls back to a 4-column SELECT for pre-v0.4.2 caches and reportsremote_signal=None. The decision tree treatsNone ≠ remoteas "refresh" so v0.4.1 caches auto-upgrade on first v0.4.2db update. - ADR-0011 (indel-anchor protection) and ADR-0012 (freshness detection).
- Three regression tests under
TestIndelAnchorProtectionpin the negative cases the carrier rule used to get wrong; existingtest_indel_carrier_triggerscovers the multi-base positive path. - Tests for
fetch_remote_text/head_request_headersagainst a local HTTPServer fixture; CLI tests for every freshness decision-tree branch (match, differ, unverifiable, legacy-cache, --force).
- PharmGKB loader looked for
clinical_ann.tsv, but the real PharmGKBclinicalAnnotations.zipshipsclinical_annotations.tsv(plural). Loader updated; mock fixture renamed; new schema-pinning test snapshots the real zip file listing so a future PharmGKB rename trips a test rather than adb updatefailure. allelix db updatenow skips annotators whereis_ready()is True; pass--forceto refresh anyway. Previously every invocation re-downloaded every annotator.
- Round-11 minors (originally listed under Unreleased; folded into 0.4.1):
- m-1 Removed dead
render_annotationsfromreports/terminal.py;render_terminalis the single entry point. Test file rewritten to assert againstrender_terminal+AnalysisResultfixtures. - m-2 Dropped the unreachable
elsebranch in_run_analysis_command; renderer dispatch is now a ternary. - m-4
analyze --categoryhelp text no longer lists "methylation" (no annotator emits that category — useallelix methylationinstead). - m-5 HTML and JSON outputs are now atomic via a shared
atomic_write_texthelper inallelix.reports(.tmpthenos.replace). A killed process mid-write leaves either the previous file or no file, never a half-written one. - m-6
REGULATORY_NOTICEmoved fromjson_report.pytoallelix.reports. Bothjson_reportandhtmlimport from there.
- m-1 Removed dead
- m-3 coverage: PA-007 fixture row in the synthetic PharmGKB dump
exercises the inner
_normalize_genotypeskip path (single-rsid annotation with only non-SNV per-allele rows). tests/reports/test_init.pycoversREGULATORY_NOTICEpresence andatomic_write_text(success path + simulated-rename-failure cleanup).
- Reports (Phase 6). New
allelix/reports/_pipeline.pybuilds anAnalysisResultonce and hands it to format-specific renderers, so the CLI streams the file and queries each annotator exactly once peranalyzeinvocation regardless of output format. - JSON report (
reports/json_report.py): versioned schema (v1), embedded regulatory notice (ADR-0003), per-annotation source attribution preserved, applied filters echoed in the payload. - HTML report (
reports/html.py): single self-contained file with inline CSS, no external resources, attribution column, informational-only banner. XSS-safe viahtml.escapeon every user-supplied string. allelix analyze --output report.{html,json}dispatches by extension;--report-format html|jsonoverrides explicitly.allelix methylationsubcommand: focused report filtered to the curated methylation gene panel (MTHFR, MTR, MTRR, COMT, CBS, BHMT, …).allelix pharmacogenomicssubcommand: focused report filtered tocategory=pharmaannotations (PharmGKB-style drug response).- Strand-flip helpers (Phase 7 polish, partial):
allelix.utils.alleleshipscomplement,flip_genotype,is_strand_ambiguousfor parser / annotator authors. Not wired into matching logic yet — opt-in. - ADR-0010: documents the strand-flip decision and explicitly defers liftover to v1.0.0 with the rationale (chain-file weight, no real GRCh38 consumer yet).
- Annotators are entered into a
contextlib.ExitStackinside the pipeline module, replacing the inline ExitStack in the CLI'sanalyzecommand. Same deterministic-cleanup contract (C-1) applies across all report commands now. allelix/cli.pyfactored:_resolve_parser,_ready_annotators,_run_analysis_commandare shared byanalyze,methylation, andpharmacogenomics.
- R-1:
test_version_flagno longer hardcodes a version string; it asserts the rendered output contains__version__and a newtest_pyproject_version_matches_metadatatest intests/test_version.pypins thatpyproject.toml's version equals the installed package metadata. Catches the regression class where someone bumps pyproject.toml without reinstalling the editable package.
- R-2: PharmGKB loader gets the W-1-style batch-flush spy test
(
test_batched_insert_flushes) —INSERT_BATCH_SIZE = 3against the 9-record fixture asserts[3, 3, 3]. A mutation flipping the flush condition toif False:now fails the suite. - Asymmetric no-call test (
Variant("A", "-")and("-", "A")) for both ClinVar and PharmGKB annotators (r-2). Verified that the ClinVar test catchesif variant.is_no_call:→if variant.allele1 == "-":. _safe_floatdirect unit tests cover the empty-string and ValueError branches inpharmgkb_loader(r-1).test_leftover_tmp_file_is_clearedcovers the stale-.tmpcleanup branch inload_pharmgkb_tsv(r-5).
pharmgkb_loader._open_directorynow importsPathat module top instead of inline-aliasing inside the function (r-3).extractallnow has an explicit code comment about Python 3.11+'s built-in../absolute-path sanitization (r-4).
- PharmGKB annotator (Phase 3, first cut).
allelix db updatenow also downloads PharmGKB'sclinicalAnnotations.zipintopharmgkb.sqlite;allelix analyzelayers in pharmacogenomic annotations alongside ClinVar's clinical-significance calls. allelix.databases.pharmgkb_loaderparses the joinedclinical_ann.tsv+clinical_ann_alleles.tsvview, normalizes 2-letter SNV genotypes (sorted, uppercased), and skips star alleles, multi-rsid composites, and indel genotypes (Phase 7+).PharmGKBAnnotatormatches the user's exact normalized diploid call (per ADR-0009), with magnitude derived from PharmGKB Level of Evidence (1A → 9.0, 1B → 8.0, …, 4 → 2.0; per ADR-0008).- ADR-0009 documents the per-genotype matching rule that diverges from the ClinVar carrier rule (ADR-0007).
- Synthetic PharmGKB fixture (
tests/fixtures/mock_pharmgkb/) with two TSVs whose genotypes line up with the MyHappyGenes mock fixture so end-to-endanalyzetests fire on known carriers. - 37 new tests (162 total): genotype normalization, single-rsid filtering,
ZIP + directory ingestion, atomic SQLite load, end-to-end
db update/db status/analyzeagainst both annotators.
db statustable now shows record counts for any annotator that exposesrecord_count()(no longer a ClinVar-specific switch in the CLI).allelix/databases/schema.pyfactors the shareddatabase_versionstable into a constant; bothCLINVAR_SCHEMAandPHARMGKB_SCHEMAembed it soget_database_infoworks uniformly across annotators.
.pre-commit-config.yamlandpre-commitin dev deps. Two hook stages installed viapre-commit install --hook-type pre-commit --hook-type pre-push:- pre-commit runs
ruff check+ruff format --checkso a commit that doesn't lint or format clean is blocked. - pre-push runs the version-tag check only (fast, no test suite). The full test suite runs in CI on every pull request.
- pre-commit runs
- Annotator test fixture now closes connections on teardown (round-4 N-1).
pytest.PytestUnraisableExceptionWarningis escalated to error infilterwarnings, so future fixtures without teardown fail CI loudly.- Tests covering the INSERT batch flush path, the download timeout path,
the
parse_clinvar_versionend-of-file fall-through, and friendlydb updatefailure output. - Round 5 (W-1, W-2): the batch-flush test now spies on
executemanyvia a delegating connection proxy so a mutation that disables mid-iteration flushing fails the suite. Thedb updatefriendly-error test now asserts Click'sError: clinvar:prefix and thatresult.exceptionis not aURLError, so removing theClickExceptionwrap now fails the suite. - More cheap coverage: VCF rows with <8 columns,
_chrom_sort_keyunknown-chromosome fallback,_percent(0, 0), MyHappyGenes junk-before-header warning, and metadata blank-line skip.
db updatenow wrapsannotator.setup()in aClickException, so a network/disk/parse failure shows a one-line "clinvar: …" message instead of a Python traceback.
- First annotator: ClinVar. Downloads the GRCh37 ClinVar VCF (
allelix db update), parses it into a local SQLite cache, and annotates carriers —allelix analyzeflags variants where the user carries the ALT allele (ADR-0007). - Multi-allelic VCF rows are split into one record per ALT during parse, with parallel-indexed CLNSIG/CLNDN/ALLELEID.
- Indel matching for variants like CFTR ΔF508 (REF=CTT ALT=C).
allelix db updateandallelix db statuscommands. Status table shows version (from VCF##fileDate) and record count.allelix analyzewith--min-magnitudeand--categoryfilters; reports versions used and an annotation count.- Data-directory resolution:
--data-dir>$ALLELIX_DATA_DIR>$XDG_DATA_HOME/allelix>~/.local/share/allelix(ADR-0006). - Annotators implement the context-manager protocol; the CLI uses
contextlib.ExitStackfor deterministic SQLite-connection cleanup. allelix/py.typedmarker (still present from v0.1.0; restated for completeness).- ADRs 0006 (data-dir), 0007 (carrier rule), 0008 (Allelix-derived magnitude scoring from CLNSIG).
pytest-covwith a 92% coverage floor;filterwarnings = ["error::ResourceWarning"]in pytest config so future leaks fail CI.- README adds "Supported Databases" table and a regulatory posture summary.
download()is now atomic (.partthenos.replace), times out after 60s, sends anallelix/<version>User-Agent, andfsyncs before rename.load_clinvar_vcf()is now atomic (writes to a.tmpSQLite thenos.replaces); a failed mid-parse leaves the previous cache intact.GenotypeMetadatano longer carriessnp_count; the only authoritative source isparse()(ADR-0005, restated).- Annotation
categoryfield is documented as a non-diagnostic filter bucket; never bare medical terms.
- The old non-atomic
db_path.unlink()step inload_clinvar_vcfthat destroyed the previous cache before writing the new one.
- Project scaffolding:
pyproject.toml, package layout, MITLICENSE, comprehensive.gitignore. - Core models:
Variant(withis_heterozygous,is_no_call,genotypeproperties) andAnnotation(with requiredattributionfield). - Plugin parser architecture:
GenotypeParserABC, registry with auto-detection,MyHappyGenesParser(streaming, malformed-line tolerance, logged warnings). allelix statsCLI command with parser-warning surfacing and a clean logger lifecycle.- Synthetic MyHappyGenes test fixture and deterministic generator script.
- README with regulatory posture, data sources & licensing table.
- ADRs 0001–0005 documenting the meta process, plugin architecture, source-attributed annotations, offline-first data model, and the parse-derived SNP count contract.
- GitHub Actions CI matrix on Python 3.11 and 3.12.