Reworks the scheduled drift bot. Fewer PRs, each carrying a signal that means something, and each visible enough to get reviewed.
In its first month the bot opened 34 PRs from 7 of 26 datasets, all looking alike. On 2026-08-28 five were found sitting unreviewed for three days and were then merged without CI having ever run on them.
- A rebuild ledger.
hvantk/resources/drift_ledger.jsonrecords each accepted upstream change per dataset.hvantk drift --ledgerlists datasets whose upstream moved since their last rebuild;hvantk drift --mark-rebuilt <dataset>clears one. A fingerprint bump is not only a test-baseline update — it is also the signal that a built artifact may be stale (ClinVar gained ~408 KB of variants across 2026-08), and that fact previously survived only in git history. informational, a fingerprint block excluded from drift comparison. Probes can record context for human readers — upstream publish dates and similar — without it becoming a drift trigger. Sits alongsidefetched_atandprobe_versioninPROBE_FINGERPRINT_IGNORED_KEYS.- Risk classification and batching. Drifted datasets are split into routine (schema signal unchanged) and schema (column list or header hash moved). All routine ones share one branch and become one PR; each schema change keeps its own. Classification defaults to schema for anything it cannot read — burying a builder-breaking change inside a batch is worse than one extra PR.
- A weekly probe-health workflow (
drift-health.yml) that opens no PRs and only files an issue when a probe reportsprobe_failed. Exists because moving regeneration to fortnightly doubles the worst-case delay on a broken probe, and thecptacprobe had failed silently for months before anyone noticed. - A fast path-filtered CI job (
drift-validate.yml) gating fingerprint-only PRs in ~1m40s instead of the ~25-minute three-version matrix. - A monthly promotion workflow (
drift-promote.yml) that opens onedev→mainPR when the delta is fingerprints-only. It never merges. - Stale-PR escalation. A drift PR open past a full cycle gets one comment on itself — never a second PR, which is the failure #262 fixed for force-pushes.
hgncandgenccprobes recordContent-Lengthas their content signal, andLast-Modifiedmoves toinformational. Both previously carriedLast-Modifiedinsource_version, which drift compares, while hashing only the column-header line — a schema signal. So they recorded no content signal at all, and a byte-identical re-publish was indistinguishable from a real update. Across all 8 committed hgnc fingerprints from 2026-05-16 to 2026-08-27 the checksum never moved whileLast-Modifiedmoved every time. Both probes now fail closed if the server omitsContent-Length, rather than recording a fingerprint with no content signal — the scheduled bot regenerates drifted baselines automatically, so one transient omission would otherwise be baked in permanently.PROBE_VERSION→ 2 for both; baselines regenerated. Anything parsing those files by hand needs updating.- The drift bot authenticates as a GitHub App rather than
GITHUB_TOKEN. A PR authored byGITHUB_TOKENhas its workflow runs parked ataction_requireduntil a human approves them, so no drift PR had ever been CI-tested unattended. - Drift PRs open ready for review, never as drafts, labelled
drift:routineordrift:schema, assigned from the plugin'smaintainers:or theDRIFT_DEFAULT_ASSIGNEEenvironment variable, with a classification table leading the body ahead of the raw JSON diffs. - Regeneration runs fortnightly (1st and 15th) rather than daily. Nothing upstream moves faster than weekly in a way that matters. Note GitHub schedules cron best-effort under load — the day is reliable, the hour is not.
hvantk drift --ledgerrejects being combined with--all, a dataset argument,--regenerateor--json, rather than silently ignoring them.
- A failed mid-batch regeneration no longer contaminates an unrelated PR. Earlier
datasets' regenerated files were left in the working tree; the next handler's
git checkout -Bcarried them onto its branch andgit add hvantk/skillscommitted them — invisible in that PR's diff, body and ledger, while stderr claimed no PR had been opened for them. - The promotion gate can fire at all. It excluded only
drift_fingerprint.json, but every drift commit also writesdrift_ledger.json, so from the first merged drift PR onward it would have skipped permanently, green and silent. - Stale-PR escalation is idempotent. It was a pure function of the PR's creation time with no memory, so a PR open six months would have collected ~11 identical comments.
load_ledgerno longer raises on truthy non-dict JSON, honouring its documented contract; and ledger staleness compares timestamps as instants rather than strings, so a-05:00offset no longer reads as older than a+00:00one.
--n-partitionsnow controls the VDS → MatrixTable partitioning on bothhvantk hgc vds2mtandhvantk hgc pipeline, coalescing the dense MatrixTable before the write. A VDS's on-disk layout is derived from its reference-block count, which is a property of the genome and saturates (~229 M on chr1 by N≈500 samples) while the dense matrix keeps growing with N×M(N). Past that point the partition count stops tracking the size of the data it partitions and work-per-task collapses — measured at 0.69 MiB/partition on a 1,005-sample chr1 cohort, where densify and QC plateaued at 1.29× and 1.64× going from 16 to 128 cores while well-sized stages scaled 7.8× (#207). Implemented withnaive_coalesce, which merges adjacent partitions without a shuffle so the densify for a merged group runs inside one task. Reduces only; the default is unchanged. Not implemented at the read:hl.vds.read_vds(n_partitions=…)looks tidier but derives intervals from the reference data via_calculate_new_partitions, whose count saturates independently of the request — measured on the 2,586-partition test VDS it returned 2 intervals for every request from 2 to 100, andto_dense_mtthen failed a Scalarequireon the reference/variant mismatch for requests of 2, 4 and 16, where coalescing returned exactly 2, 4 and 16.
- Datasets that share a
drift_fingerprintbaseline are now treated as sharing one drift signal, rather than as N independent ones.hvantk driftprobes such a group once and fans the result out — every dataset still gets its own report entry, and each now carriesfingerprint_path— and the drift workflow opens a single PR per signal.ucsc-cellbrowseris the case that forced it:default,adult-ctxanddev-ctxare genuinely distinct schema variants (their obs cell-type column iscelltype,ClassandType_v2, which is why each earns its own snapshot), butfetch_fingerprint()takes no arguments and fingerprints the provider-wide catalog atcells.ucsc.edu/dataset.json. One upstream event therefore produced three identical PRs whose branches all wrote the same file, so merging any one made the other two conflict — #241 merged, #242 and #243 were closed as superseded. Grouping is keyed on (baseline path, probe callable), not the path alone: two datasets sharing a baseline while declaring different probes would each overwrite the other's, so they deliberately do not group, andhvantk plugins validatenow rejects that declaration outright. A lone dataset keeps its historicaldrift/<provider>-<dataset>branch name exactly, so existing open PRs are still matched; a group usesdrift/<provider>.
-
PipelineConfig.n_partitionsreached no pipeline stage. It was accepted from the CLI and echoed back in the run plan while being read by nothing, so the run plan affirmatively told the user a setting had taken effect when it had not — the worst failure mode for a dead flag, and the first knob a user reaches for when they hit #207. It is now forwarded toconvert_vds_to_mt; the run plan line names the stage it governs (#208). -
Every open drift PR was force-pushed and its body re-edited once a day, forever. Nine PRs churned daily for a week — roughly 63 notification events, none carrying new information.
hvantk drift --regeneraterewritesfetched_aton every run, and the existing emptiness check compared against the base branch, so for a dataset that was still drifted it could never fire: the timestamp alone guaranteed a non-empty diff. Nothing compared the freshly regenerated fingerprint against what the branch already proposed.drift_to_pr.pynow skips the push and the PR edit when the branch already carries a materially identical fingerprint — "materially" meaning equal oncefetched_atandprobe_versionare dropped, the same keys the drift detector ignores. The skip additionally requires an open PR to still exist: a branch outlives its PR when one is closed, and a run whose push succeeded whilegh pr createfailed leaves a branch with no PR at all — in both cases the branch content matches, so a content-only check would suppress that dataset's drift forever. Every other outcome (no branch yet, a file the branch lacks, an unreadable blob, a git failure) still pushes: suppressing a real drift PR is far worse than one redundant force-push. A skipped dataset also restores the index and working tree before returning —git checkout -Bdoes not clear the index, so a leftover staged fingerprint would be committed onto the next dataset's branch — and still reports itself in the job's step summary. Note this does not reduce how often drift is detected — a content revision, such as ClinGen'scontent_lengthmoving while the checksum holds, is still a genuine change and still opens a PR. -
The
cptac:expressionandcptac:phosphodrift probes had never once succeeded. The drift workflow installed with a barepip install -e ., but the cptac probe fingerprints the installedcptacversion (viaimportlib.metadata) against PayneLab's latest GitHub release — andcptacis declared in theptmextra. Every scheduled run reportedThe 'cptac' Python package is not installed; cannot fingerprint, so upstream CPTAC drift has never been detectable. The workflow now installs.[ptm]. Of the 22 drift probes these two are the only ones needing an extra; the other 20 userequestsor the stdlib alone. -
A single rate-limited response could fail the whole scheduled drift run. On 2026-08-04 GenCC answered the regeneration request with HTTP 429; the probe had no retry, so it raised, no PR could be opened for the drifted dataset, and the run exited non-zero with nothing actually wrong. New
hvantk/core/utils/http.pyprovidesrequest_with_retry, which retries transient statuses (429 and the 5xx family) and connection errors with exponential backoff. It honoursRetry-Afterbut clamps it:urllib3.util.Retrysleeps for the header's full value with no upper bound (backoff_maxcaps only the exponential path), so a host answeringRetry-After: 3600would park CI for an hour. The helper deliberately does not callraise_for_status, so callers keep their existing error handling and only the transient case changes. Wired into the GenCC probe; the other 12 HTTP probes can adopt it as needed.
First tagged release. Everything below had accumulated under Unreleased since 0.1.0,
which sat on main unchanged from 2025-05-04 across 61 merges.
- Declarative feature selection for
hvantk rerank(Python API:Config.selection). Filters run within each axis — univariate AUC with within-axis BH-FDR, then Spearman redundancy — re-fitted inside every cross-validation fold on the training slice only, so the reported ΔAUC is not inflated by selection that has seen the held-out labels. A third RFECV step is available but off by default (SelectionPolicy(wrapper="rfecv")): across four real cohorts it eliminated columns almost exclusively in the one with the fewest positives, and pruned the ablation baseline axis, so it needs an out-of-fold outcome comparison before it can be trusted by default.Config.selection = None(the default) reproduces the previous code path exactly, and the CLI is unchanged. rerank_arms(config)runs each analysis as two arms,cleanandall, over identical folds.clean(columns with no provenance conflict against the label source) is the headline;alladds conflicted and undeclared columns so the circularity channel is a measured number rather than an assumption.RerankResult.selectioncarries the per-fold selection frequency, the global-pass feature list, and both nested and global AUCs.- Plugin manifests may declare per-predictor training provenance: an optional
scores: {<column>: {trained_on: [...]}}block per dataset.hvantk/skills/dbnsfp/plugin.yamldeclares it for 55 of its 57 rankscore predictors. An omitted score means unknown and is never treated as clean. - Plugin system for data-provider adapters. Each provider now lives in a single folder under
hvantk/skills/<provider>/with aplugin.yamlmanifest, builder code, drift probe, downloader CLI, and tests. The loader auto-discovers plugins from the in-tree filesystem and Python entry points. hvantk plugins {list,describe,errors,validate}commands for inspecting the registry.hvantk drift <provider:dataset>for upstream-drift detection against committed expected fingerprints.hvantk reprocess <provider:dataset>for chaining download -> parse -> build -> drift-check from a single command.- 13 migrated provider plugins: clingen (gene-disease), clinvar, cptac (expression + phospho), expression-atlas, gencc (submissions), gtex-eqtl, gwas-catalog, hgnc, insider, msigdb, peptideatlas (phospho), ucsc-cellbrowser (default / adult-ctx / dev-ctx), uniprot-ptm (sites).
- Scheduled CI workflow (
.github/workflows/drift.yml) that runshvantk drift --all --jsondaily and opens a draft PR per drifted plugin with the regenerated fingerprint pre-committed.
- Version bumped to
0.2.0, andpyproject.tomlmigrated to PEP 621[project]. The version had been0.1.0since 2025-05-04, across 61 merges intomain— so no release in fifteen months was distinguishable from any other by version. Separately,name,version,description,authors,license,readme,keywords,urls,plugins,extrasandscriptsall used the deprecated[tool.poetry.*]spelling — 11 warnings on everypoetry check. They now live under[project],[project.optional-dependencies],[project.entry-points],[project.scripts]and[project.urls]; only genuinely Poetry-specific keys (include/exclude, dependency groups) remain under[tool.poetry]. The migration is resolution-neutral: the lock resolves to the same 187 packages, name-for-name and version-for-version, before and after. Poetry's caret shorthand is spelled out as the PEP 508 equivalent it always meant (^8.1.3→>=8.1.3,<9.0.0), not re-pinned. With nothing deprecated left, the defensivepoetry>=2.0,<3.0pin in thepoetry.lock in syncCI job is unpinned again. One consequence is new: PEP 621 puts the full specifier in each extra, soscipy>=1.8is written six times andscikit-learn>=1.4,<2.0three times, and one could be re-pinned with the others left behind — resolving differently depending on which extra a user installs.test_pyproject_extras.pynow asserts every extra spells a shared package identically, alongside a check that no extra re-declares a base dependency. gnomadis no longer a dependency. hvantk used exactly one function from it,annotate_adj, which is ~15 lines of Hail expression with no gnomAD data behind it. It is now ported intohvantk/algorithms/hgc/adj.py(gnomad_methods is MIT; the port keeps the logic and thresholds verbatim and carries the attribution), soadjmeans exactly what it means in a gnomAD callset. Dropping the dependency removes 35 packages from the lock —hgvs,ga4gh-vrs,onnx,onnxruntime,skl2onnx,psycopg2,protobuf,sympy,slackclientand more — and, critically, removes the transitivejsonschema<4pin that conflicted with hvantk's own declaredjsonschema>=4.0. That conflict is what had madepoetry.lockimpossible to regenerate in place.adjust_genotypes=Trueno longer requires an optional install, so thehgcextra is now just["matplotlib", "seaborn"].poetry.lockregenerated and now consistent withpyproject.toml. It had drifted across ~28 commits — pinningjsonschema3.2.0 against a declared>=4.0, and missing theancestry/ml/constraint/expressionextras entirely — sopoetry installfailed on a clean checkout. 208 → 181 packages; the only version change besides the removals isjsonschema3.2.0 → 4.26.0.scanpy's move behind theexpressionextra is now actually in effect rather than merely declared.- Breaking (rerank).
ArmAssignment.unknownis renamedundeclared, and an undeclared predictor is now treated as conflicted rather than getting a bucket of its own. Arm membership is otherwise unchanged. Code readingArmAssignment.unknownmust be updated. - Rebuild your dbNSFP artifact.
dbnsfp:variantsnow parses the ~57*_rankscorecolumns tofloat64with proper missingness, instead of leaving them as raw strings ("."for missing).schema_idstaysdbnsfp-v1— the column set is unchanged and string rankscores were always a parsing bug rather than an intended schema — so nothing will warn you: an artifact built before this release carries strings where a fresh build carries floats. Re-runhvantk reprocess dbnsfp:variantsbefore relying on those columns. - Specificity features in the annotation matrix now emit a per-group vector by default
(one column per surviving atlas group, named
{atlas}_{sanitized_group}) instead of a single rolled-up scalar. A named roll-up is additive whenspecificity.targetsis given;specificity.emit: rolluprestores the previous single-column output. Two matrix axes may no longer share anatlaslabel, since their vector columns would collide. hvantk driftcomparators can now actually detect an upstream change. Previously the comparison could pass regardless of source content, so drift went unreported.scikit-learnfloor raised to>=1.4(NaN-tolerant tree estimators, needed by rerank's optional RFECV wrapper).scipyis now declared explicitly, as an optional dependency in theml/ancestry/psrocextras.- Every command that imports
scipyat module scope now has an extra that installs it. Three modules do, and they sit on three different commands — a mapping the previous known-gaps note got wrong:algorithms/ptm/constraint.py→hvantk ptm constraint(constraintextra);algorithms/enrichex/overlap.py→hvantk enrichex overlap(newenrichexextra);algorithms/burden/fet.py→hvantk cohort burden(newcohortextra).fet.pyis not reached byhvantk enrichex burden: its only importer isalgorithms/burden/pipeline.py, imported bytools/cohort/cohort_cli.pyalone. Previouslyconstraintomittedscipy, sopip install hvantk[constraint]yielded a documented extra whose own command still raisedModuleNotFoundError, and neitherhvantk enrichex overlapnorhvantk cohort burdenhad any extra to install.enrichexalso carries matplotlib/seaborn, becauseenrichex/__init__importsplot.py/report.pyunconditionally and a scipy-only extra would break on import.scipywas added toexpressiontoo — a resolution no-op, since scanpy already depends on it, butvisualization/expression/anndata.pyimportsscipy.sparsedirectly and this project declares what it imports rather than inheriting it from a transitive edge that can move. A base install still raises a bareModuleNotFoundErrorrather than a message naming the extra; arequire_scipy()guard (cf.require_scanpy) would fix the text, and is tracked separately because it changes no extra's contents. hvantkwas unusable from apip install. The console script importedhvantk.tools.enrichexat module scope, which ranalgorithms/enrichex/__init__.py, which eagerly importedenrichex/plot.py,enrichex/report.pyandvisualization/base.py— all three importmatplotlibat module scope. matplotlib is optional, so on a base install every command includinghvantk --helpraisedModuleNotFoundError. Those three imports are now resolved on attribute access (PEP 562__getattr__), so the package imports without matplotlib and the CLI runs. The eight plotting/reporting names stay in__all__and stay importable; touching one without matplotlib now raises anImportErrornaming theenrichexextra, matchingrequire_scanpyand_require_matplotlib. No plotting behaviour changed — the enrichex CLIs already importedgenerate_reportinside the functions that use it.- The wheel shipped 43.3 MB of test data.
hvantk/tests/**was absent fromexclude(190 files, including a 14.7 MB VDS zip and an 11.4 MB expression-atlas fixture); the skills excludes were overridden byinclude = "hvantk/skills/**/*.py", since a path named byincludewins; and the excludes namedtests/data/**where the skills actually usetests/testdata/**. Fixed all three: the wheel goes from 46.2 MB to 2.86 MB uncompressed (28 MB to 897 KB on disk) with every manifest, skills module, catalog and drift fingerprint intact. - CI now installs the package. New
packaging-smokejob builds the wheel, checks its contents with.github/scripts/check_wheel.py, installs it into a clean environment with no extras, and runshvantk --help/hvantk plugins listfrom a directory where the checkout is not importable — so the console script, the entry-point registrations and the packaging globs are exercised against the installed copy. It also asserts the provider count matches the tree, since a dropped manifest would otherwise still exit 0. Both bugs above were found by writing this job. hvantk/tests/hgc/now runs in CI as a newhgc-hailjob — separate fromPlugin contract (hail)rather than appended to it, so the contract signal is not delayed behind ~6 min of unrelated HGC work. This is the first automatic run oftest_convert_vds_to_mt, the end-to-end exercise of theadjcode ported in #252.- The Python version matrix now runs on
devPRs, not onlymain, so an incompatibility is caught on one commit instead of at the release gate with a whole release to bisect.actions/setup-pythonmoved v3 → v5 and both workflows now declarepermissions: contents: read(both raised in review on #249). - The extras table is now guarded by a test. It is duplicated in three places — the
[tool.poetry.extras]block,README.mdanddocs_site/getting-started/installation.md— and only the first is executable, so the prose copies had drifted eight cells (psroc/ancestry/mlmissingscipy,ptmmissingsorted-nearest) across two releases.hvantk/tests/test_pyproject_extras.pynow parses both markdown tables and fails if either disagrees withpyproject.toml, and also fails if an extra names a package that is not declaredoptional = true. Non-Hail, so it runs in the default suite. scanpymoved out of the base install into a newexpressionextra. It is required byhvantk expression summarize,hvantk expression markers, andhvantk ptm constraint --expression-metric mean; those now fail with an actionable message naming the extra rather than a bareModuleNotFoundError. The extra cannot be installed on Intel macOS (scanpy → numba → llvmlite ships no x86_64 macOS wheel from 0.47).- Package restructured into 4 purpose-driven roofs:
core/(platform models, utilities, plugin/tool runtime, streamers, transient builders),algorithms/(analytical computation: ptm, psroc, qtlcascade, enrichex, hgc, ancestry, annotation, visualization, expression, statistics, training_sets),skills/(data ingestion plugins),tools/(CLI surface). Insidecore/there are now sub-packagesmodels/,utils/,streamers/,plugin/,tool/,builders/so adding a new format helper has one obvious home. One-way dependency rule (skills/,tools/→algorithms/→core/) is enforced byhvantk/tests/test_dependency_directions.py.hvantk/data/,hvantk/utils/,hvantk/tables/, and 8 top-level algorithm dirs (hvantk/{ptm,psroc,qtlcascade,enrichex,hgc,ancestry,annotation,visualization}/) are gone.ClinVarStreamerno longer imports fromhvantk.skills.clinvar.builder— it accepts a pre-built Hail Table via its constructor. - Registry keys for migrated providers use compound
provider:datasetform. Recipe JSONs and any custom callers should update from bare names (e.g.,clinvar) to compound (clinvar:variants). The legacyhvantk mktable/hvantk mkmatrixCLI surfaces have been retired; data builds now go throughhvantk reprocess <provider>:<dataset>with--plugin-arg key=valuefor builder kwargs. - Plugin manifests gain an optional
catalog: <path>field pointing at a per-plugincatalog/datasets.json.unified_registry.HvantkRegistrynow aggregates per-plugin catalogs from the plugin loader in addition to the legacyresources/registry/genomics/datasets.json. - Per-domain catalogs
resources/registry/{transcriptomics,proteomics,epigenomics}/datasets.jsonare removed; their entries now live inside each owning plugin'scatalog/datasets.json(expression-atlas, ucsc-cellbrowser).registry/genomics/datasets.jsonis intentionally retained until orphan entries (dbNSFP, gnomad-metrics, ensembl-gene, gevir, cosmic-cgc) gain owning plugins. hvantk catalogCLI rewritten to read per-plugin catalogs viaHvantkRegistry. New subcommands:list(with--omics-type/--data-source/--organismfilters),show,stats,search. The legacycatalog buildsubcommand is removed; usehvantk reprocess <provider:dataset>instead.
- Per-provider downloader modules under
hvantk/commands/*_downloader.pyfor migrated providers (moved into their plugin folder'scli.py). - Per-provider dataset classes under
hvantk/datasets/*_datasets.pyfor migrated providers (moved intohvantk/skills/<provider>/shared/). - Per-provider builder functions in
hvantk/tables/table_builders.pyandmatrix_builders.pyfor migrated providers (moved intohvantk/skills/<provider>/[<dataset>/]builder.py). hvantk/resources/generate_catalog.py(regenerated the now-removed per-domaindatasets.jsonfiles). Catalog regeneration is now a per-plugin concern; if a maintainer needs a packaged regenerator in the future it should live alongside each plugin'scatalog/datasets.json.hvantk/resources/catalog.yaml(auto-generated summary file pointing at deleted per-domain JSON files). Equivalent information is available on demand viahvantk catalog stats.openai,anthropic,google-genaiandRestrictedPythondropped fromrequirements.txtandenvironment.yml. None is imported anywhere in the tree, and none was ever declared inpyproject.toml— CI had been installing four packages the library does not use.
- The following plugins reference snapshot files (
schema.json,sample_rows.json) in theirplugin.yamlmanifests that have not yet been seeded on disk:clingen,gencc,hgnc,uniprot-ptm,expression-atlas,peptideatlas:phospho,cptac:expression, andcptac:phospho. The first hail-enabled CI run with--regenerate-snapshotswill bootstrap them. Allucsc-cellbrowservariants (default,adult-ctx,dev-ctx) already have populated snapshot dirs. - (Resolved: version bumped to 0.2.0 — see Changed.) Releases are still not git-tagged, so
a release is identifiable by version but not by a tag.
(The three CI gaps previously listed here — no install job, the version matrix running
only on
main, andhvantk/tests/hgc/running in no job — are resolved; see the packaging and CI entries under Changed.)