Skip to content

AlignAIR v3.0.0 — PyTorch rewrite, CLI, self-contained models, docs site, release automation - #10

Merged
MuteJester merged 469 commits into
mainfrom
feature/alignair-pytorch
Jul 18, 2026
Merged

AlignAIR v3.0.0 — PyTorch rewrite, CLI, self-contained models, docs site, release automation#10
MuteJester merged 469 commits into
mainfrom
feature/alignair-pytorch

Conversation

@MuteJester

Copy link
Copy Markdown
Owner

Release candidate for v3.0.0 — a complete PyTorch rewrite of AlignAIR (the alignair package), replacing the legacy TensorFlow lineage.

Highlights

  • End-to-end neural model (orientation + V/D/J segmentation + allele calls in one forward pass; deterministic post-processing for coordinates, junction, and the AIRR record).
  • First-class alignair CLI (predict / train / demo / models / reference / convert / validate-airr / compare / genotype / doctor / benchmark).
  • Self-contained, pickle-free .alignair model format with an embedded, fingerprinted germline reference; pretrained human IGH, IGK+IGL, TRB.
  • Donor-genotype constraint at inference; standard AIRR output.
  • New React/TypeScript documentation site (reference, guides, interactive lessons) with an IgBLAST migration guide and a real end-to-end worked example.
  • Consolidated release automation: one authoritative artifact set → TestPyPI (with provenance) → GHCR → protected PyPI → GitHub Release, via OIDC Trusted Publishing.

Status: draft — not merge-ready yet

Opened to start remote CI. Outstanding before merge/tag:

  • CI fully green (ci.yml, cross-platform wheels gate, website build check).
  • Browser accessibility gates (axe/Lighthouse, keyboard, screen-reader).
  • PyPI/TestPyPI Trusted Publishers + pypi/testpypi environments configured.
  • Pages source switched to GitHub Actions.
  • Hugging Face token rotated.
  • CHANGELOG / CITATION release date set at tag time.

…rd, LCB promotion); training-side gates -> 5b
…essons

Replaces the MkDocs theme with a Vite + React + TypeScript site that is both
the reference and a teaching surface, deployed to GitHub Pages from
website.yml (base /AlignAIR/, refresh-safe routing).

Reference: all 15 pages authored as typed TSX rather than markdown, carrying
the corrected content -- the AIRR field contract (orientation, derived
productive, reserved is_contaminant), the model contract, benchmarks with the
24 metric definitions, and measured per-device throughput stated with its
hardware, backend and timing boundary.

Lessons: a typed step model with immediate-feedback checks and progress kept
in the browser, across five tracks -- foundations, predicting, training,
benchmarking, integrating -- with interactive widgets for V(D)J assembly,
reverse-complement, the CLI, AIRR fields, record filtering, donor genotypes,
the benchmark sandbox, and the training curriculum. The curriculum widget's
ramps mirror Curriculum().params(), including SHM dropping to zero for a TCR
locus.
….0.0

The image installed from source without setup.py, so the optional Cython
germline-CIGAR kernel was never built and the container silently ran the
pure-Python fallback -- slower than a local install, while the published
throughput figures assumed the compiled path. The Dockerfile is now
multi-stage: a builder with a C toolchain compiles the wheel, the slim
runtime installs it, and `alignair doctor` in the image reports
derive_backend=cython. The image does not grow; the toolchain stays behind.

doctor also reports parasail and derive_backend, so the active backend is
visible rather than assumed -- the docs claimed doctor reported parasail
before it did.

Models cache under a declared volume; the docs show mounting it so a
`--model <id>` run does not re-download on every invocation.

Version is 3.0.0 across pyproject, the conda recipe and the Galaxy wrapper.
The MkDocs CI job and the [docs] extra are removed with the MkDocs source;
package metadata and the conda summary drop the "dynamic reference" wording
that contradicted the fixed-reference contract.
An external review of the site found claims that were wrong against the
code. Each was verified against the implementation before editing.

- *_call_set was documented as an exhaustive equivalence class. It is
  not: threshold.py applies an absolute p>=0.5 cut, caps at 3, and falls
  back to top-1 when nothing clears. So it is the model's candidate set,
  it can be truncated, a one-member set is not necessarily confident, and
  its members need not share a gene or family. Reworded site-wide and
  documented the real rule; the benchmark truth-side "equivalence set"
  wording is correct and kept.
- --columns core was labelled "no gapped alignment". needs_assembly()
  returns True for core (it emits the junction, an assembly product), so
  core buys a smaller file, not a skipped stage. minimal is the only
  preset that skips assembly, and therefore the only one without
  coordinates. Fixed in nine places.
- The junction field showed an amino-acid string; junction is
  nucleotides. Split into junction / junction_aa.
- alignair models info / reference export do not resolve catalog ids
  (unlike predict). Examples now resolve via `alignair models path`.
- validate-airr was described as the official AIRR-C schema validator. It
  imports only csv; it is a structural check. Documented as such, with
  airr.validate_rearrangement() named for formal validation.
- The 10x example kept a column name that does not exist in the raw
  input (umis, not umi_count), and the Change-O hand-off did not filter
  to complete records.
- The reserved *_resolved_call / *_call_level / *_set_confidence fields
  read as conditionally usable. They are always blank; say so.

Also corrects a latent error in the calibration section: temperature
scaling is monotonic, so it cannot change the top call, but pipeline.py
applies it to the same probabilities the absolute-threshold selector
consumes, so it *can* change call_set membership.

Website builds clean; 740 tests pass.
The two interactive widgets presented numbers that were never measured.
Both are now backed by artifacts, and one of them was not merely
imprecise but directionally wrong.

BenchSandboxWidget interpolated linearly between two hand-set endpoints
across a continuous 40-312nt slider, so almost every number it displayed
was invented. Worse, it showed AlignAIR ahead of IgBLAST for every gene
at every slider position, captioned "drag to see AlignAIR's edge grow on
short fragments". The frozen benchmark says the opposite for V: IgBLAST
wins V on the full-length strata (high_shm .990 vs .895, hard_full .990
vs .980, clean_full a 1.000 tie) and wins it decisively on 5'-anchored
fragments (amplicon_fr2 .995 vs .820). Its "full-length" V figures
(.776/.745) matched no stratum at all - clean_full is 1.000/1.000 - and
appear to be aggregates from an older run.

Rebuilt on six discrete measured strata (200 cases each) from the
2,600-case human-IGH benchmark, showing both tools honestly, including
where AlignAIR loses. Each gene now carries the benchmark's own verdict
from the Bonferroni-corrected paired bootstrap, so the two panels whose
visible gap is not significant say so - which is what the preceding
lesson step already told the reader to look for. The real story is more
specific than the old one: the gains are in D, J, junction-anchored
reads and orientation, not everywhere.

CliWidget invented its entire output: a device banner, a fingerprint
line, a "2.4s" timing and a "982 complete / 14 partial / 4 failed"
tally, none of which the CLI prints. Replaced with output captured
verbatim from a real run against alignair-igh-human on 400 simulated
full-length IGH reads, matching the format string in cli/predict.py. The
captured run's 68 partial assemblies are now a teaching point rather
than something to hide.

Hero: the network runs once, but coordinates, CIGARs and the junction
come from deterministic post-processing after it, so "in a single
forward pass" overstated the claim; it now says what is true. Added an
above-the-fold evidence strip with the NAR paper DOI, the benchmark page
and the failure-modes page.

TRB card: alpha/gamma/delta are absent from the pretrained registry, not
unsupported by the architecture - you can train them.

Website builds clean; 740 tests pass.
Semantic controls, keyboard/focus handling, contrast, motion and
responsive fixes across the site. Treated as engineering, not a palette
tidy-up.

Semantic controls
- Every toggle group (model chooser, all seven widget toggles, MCQ
  options) now carries role="group" with a label and aria-pressed per
  option, so state is announced instead of conveyed by fill colour alone.
- The curriculum range gained a real <label>, aria-valuetext (position +
  stage) and aria-describedby to its note.
- Lesson and track progress bars are role="progressbar" with
  value/valuetext; the model-finder panel and every value readout that
  changes on interaction are aria-live regions.
- MCQ options are no longer hard-disabled after answering: they stay
  focusable and reviewable by assistive tech, with the correct/your-answer
  status in sr-only text and the result panel as role="status". Full
  reference fingerprints are exposed via sr-only, not title-only.

Keyboard and focus
- Skip-to-content link; a single global :focus-visible ring (many
  controls were inline-styled with none).
- After Continue/Previous/Try again/completion, focus moves to the new
  step heading; the smooth scroll respects reduced motion.

Contrast (all verified numerically against WCAG AA)
- Replaced every muted grey carrying readable text with a 5:1 floor
  (#6f6d85 on light, #9d9bb0 on dark). #8b899d (3.4:1), #b9b7c7 (2.0:1),
  #9b99ac (2.8:1), #a09eb2 and slate-400 headers all failed. Selected/
  dropped/correct states no longer rely on colour, opacity or
  strikethrough alone.

Motion and responsive
- prefers-reduced-motion neutralizes transitions, animation and smooth
  scroll site-wide. The aa-rise keyframe was referenced everywhere but
  never defined (a no-op); it is now defined AND reduced-motion-gated.
- Grids use minmax(min(px,100%),1fr) and the V/D/J builder reflows, so
  nothing overflows at 320px. Decorative graphics (hero strand and
  ruler, segment bars, window chrome, icon glyphs) are aria-hidden; the
  hero illustration is a single labelled role="img".

Website builds clean (tsc + vite).
Migration guide
- examples/igblast-migration/README.md: a runnable, CI-validated guide
  (its alignair commands parse against the real CLI via the docs-cli-sync
  test). Maps invocation, reference selection and output fields; explains
  the semantic differences (call sets, coordinates, junction assembly,
  confidence, partial predictions); gives CPU/GPU guidance with no
  unsupported performance claims; ends with a migration checklist.
- The field-by-field side-by-side is genuine output from BOTH tools on
  the same 348 nt IGH read: IgBLAST -outfmt 19 and a real
  `alignair predict --model alignair-igh-human@1.0.0` run (1-based AIRR
  TSV, not the benchmark's internal 0-based artifact). It shows honest
  agreement (V, J, junction nt, productivity), a real D disagreement, the
  call_set / assembly-status extensions, and the documented 1-2 nt J
  boundary jitter (CAKGVILAVT. vs CAKGVILAVTG).
- website: pages-migrate.tsx renders the same content as a docs page.

IA restructure (primary journey unmistakable, everyday use separated
from training and benchmarking)
- New section order: Get started, Using AlignAIR, Reference, Training,
  Evaluation, Design, Maintainer. "Guides" is renamed "Using AlignAIR"
  and training is pulled into its own Training section; benchmarking
  already lived in Evaluation.
- DOCS is recomposed in section-grouped order so the sidebar and the
  Previous/Next flow agree.
- Get started now reads getting-started -> migrating-from-igblast ->
  concepts. Getting started states the five-step journey up front
  (install -> demo -> model -> align -> read output) and notes you do
  not need Learn or Training to reach a first prediction; it links the
  migration guide at the top and a Next block at the end.
- Contextual link added from Known failure modes' "when to prefer
  IgBLAST" section.

Website builds clean; docs-cli-sync passes.
- website: new Community section with a Citation & support page - the
  NAR citation and DOI, a note about the CITATION.cff "Cite this
  repository" button, copyable BibTeX, a routing table (bug / feature /
  model request / usage question / scientific question / security), and
  a checklist of what a useful bug report contains (alignair --version,
  pinned model, alignair doctor, exact command, minimal reproducer).
  Linked from the footer.
- bug_report.md was still the default GitHub template (asking for
  Browser and iPhone model). Replaced with an AlignAIR-specific one that
  asks for version, model, `alignair doctor`, the command, and a minimal
  reproducer - matching the checklist on the page.
- SECURITY.md's scope described `torch.load` on "bundles", which
  predates the .alignair format. Corrected: inference sections (config,
  safetensors weights, reference_json) load with no pickle execution and
  are fingerprint-verified; only resumable-checkpoint sections
  (dataconfig, train_state) use pickle, so trust still matters for those.
- Consistency: retired the last "equivalence set" wording in the
  examples READMEs (candidate set).

Website builds clean; docs-cli-sync passes.
A complete run on a real, public human IGH repertoire, from raw reads to
a validated AIRR table. Every number is captured from an actual run;
nothing is illustrative.

Dataset: Briney et al. 2019 (Nature), ENA/SRA run SRR8283604, BioProject
PRJNA406949 - a baseline healthy-donor human antibody heavy-chain
repertoire, Illumina 2x250, INSDC free reuse. Pinned to
alignair-igh-human@1.0.0.

Reproducible and self-contained: the first 2000 read pairs are streamed
(no 1.2 GB download), with published md5s for both subsets; a bundled
zero-dependency Python merger (merge_pairs.py) overlaps the mates into
full VDJ amplicons deterministically (stable md5), so the example runs
with nothing but curl and Python.

The captured result is the centerpiece: 1945 reads, 0 dropped, 0 failed,
97.8% complete assemblies, validate-airr clean, and 99.6% auto-reverse-
complemented by the orientation head (this library's reads are antisense)
- with real CDR3s out the other end (CAKNRVAAAGTMFATW, ...). It is
honest about the rough edges too: 43 partial (nonproductive_indel), and
only 687/1902 complete records productive, attributed correctly to raw
reads without UMI consensus plus a demo-grade merge, not to the model -
which is the point of the honesty flags.

- examples/end-to-end/README.md: validated (alignair commands parse in
  CI), structured download -> merge -> predict -> inspect -> validate ->
  summarise.
- examples/end-to-end/merge_pairs.py: the bundled merger.
- website: pages-tutorial.tsx renders it as the "Worked example" page in
  Get started (getting-started -> migrating-from-igblast -> worked-example
  -> concepts); linked from Getting started's Next block.

Website builds clean; docs-cli-sync passes (18).
… example

Three precision fixes, no new content:
- Checksum semantics: state explicitly that the R1/R2 md5s are of the
  extracted 2000-read subset computed locally, not ENA/SRA's published
  hashes, which validate the complete ~1.2 GB source files.
- Bundled merger: describe merge_pairs.py as deterministic demonstration
  preprocessing, not a production pipeline - record its parameters
  (largest overlap >=20 nt, <=10% mismatch, concatenate on overlap),
  that it drops unmerged pairs (55 of 2000), and that it does no
  quality-aware consensus or UMI correction.
- Call INSDC availability a data-access policy (free reuse), not a
  software-style license.
- Also drop the "398-444 nt" merged-length claim; that was only the
  upper tail of the histogram, so state the longest reach (~444 nt).

Website builds clean; docs-cli-sync passes (18).
Root cause of the unreadable docs: index.html force-added class="dark"
to <html> whenever the OS preferred dark mode, but the site is only
half dark-mode-aware. The docs pages and shared components carry
Tailwind dark: variants (dark:text-slate-300, light text), while the
body background is hardcoded light (#fbfbfd) and never flips, and there
is no theme toggle to turn it off. Under OS dark mode that produced
light-grey text on a light background - the "almost impossible to read"
docs.

The light palette is the real, contrast-verified design, so the fix is
to render light-only: removed the dark-activation script from
index.html. The class is now never added, all 68 dark: variants stay
dormant, and every page renders in the intended light palette
regardless of OS theme. (color-scheme: light already pins UA rendering.)

Also fixed the homepage signature-card overlap: the field labels were
width:68px, so "junction_aa" overflowed its column into the value.
Widened to 92px with flex-shrink:0.
Removed the BrandMark component and its use in the navbar and footer;
the brand is now just the "AlignAIR" wordmark.
Add a docs badge and a centered call-to-action under the header linking
to the documentation, guides, and interactive lessons at
mutejester.github.io/AlignAIR, so new users land on the site to learn.
Align the README with the terminology corrected across the site and
examples: *_call_set is a capped, threshold-based candidate set, not an
exhaustive equivalence class.
These files become the public PyPI description and citation/package
metadata, so they are corrected to match the reviewed website:

README:
- "in one pass, with no multi-stage heuristic pipeline" overstated the
  pipeline; the network runs once, then deterministic post-processing
  produces coordinates, the junction, and the AIRR record.
- "largest improvements on short fragments" contradicted the measured
  results: gains concentrate in D/J and orientation, and V accuracy is
  comparable to IgBLAST (not better) on short fragments and heavy-SHM
  full-length reads. Now stated honestly, linked to known failure modes.
- alignair validate-airr described/shown as an AIRR-C schema validator;
  it is a structural check (columns, coordinate/CIGAR bounds,
  productivity), not the official airr validator.

CHANGELOG:
- 3.0.0 dated (2026-07-17; update to the actual tag date if it slips).
- equivalence set -> candidate set; retired the stale "Material for
  MkDocs" line (docs are the React/TypeScript site).

CITATION.cff: add version 3.0.0 and date-released.
conda/meta.yaml: Python floor >=3.10 to match pyproject requires-python
(was >=3.9).
…istry

Rewrite release.yml so a tag builds one authoritative artifact set, tests
it, and publishes those exact artifacts without partial-release risk:

- build-wheels (cross-platform matrix) + build-sdist run once; publishing
  jobs only download those artifacts, never rebuild.
- smoke tests one built wheel before any publication.
- TestPyPI publish (OIDC, environment testpypi, skip-existing) -> install
  smoke that pulls AlignAIR from TestPyPI and deps from PyPI.
- Draft GitHub Release (contents: write) with all wheels, the sdist, and
  SHA256SUMS, created before any irreversible publish.
- PyPI publish (OIDC, protected environment pypi) and GHCR push run only
  after the draft exists; the Release is flipped to published only after
  both succeed. No PYPI_API_TOKEN anywhere.
- Container: GHCR, hardcoded lowercase ghcr.io/mutejester/alignair, tag
  strips the leading v (v3.0.0 -> :3.0.0), built once, smoke-tested in the
  container, then pushed as :<version> and :latest (packages: write).
- wheels.yml stays as the PR/main build gate.

website.yml: add a build-only PR check; deployment stays restricted to
main. (Requires Pages source = GitHub Actions.)

Docs: migrate every Docker reference (README badge + commands, examples,
website pages) from Docker Hub thomask90/alignair to
ghcr.io/mutejester/alignair.
…/main gate

release.yml owns the authoritative release build + publish, so wheels.yml
no longer needs to run on release:published (it would rebuild the matrix
redundantly). It remains the continuously-green cross-platform build gate
for PRs and main.
Release workflow (release.yml):
- guard job: refuse to publish unless the tag matches pyproject's version
  AND the tagged commit is contained in origin/main; a test job reruns the
  Linux suite on the tagged commit. Nothing builds until both pass.
- TestPyPI provenance: instead of installing with a competing
  --extra-index-url (dependency-confusion surface), download the wheel
  TestPyPI serves and require it to be byte-identical (SHA-256 + filename)
  to the authoritative build, then install it with deps from normal PyPI.
- Ordering: draft-release -> GHCR (reversible) -> protected PyPI
  (immutable, last) -> flip the Release to published. The irreversible
  step runs last.
- draft-release is rerun-safe: create if absent, re-upload with --clobber
  if a draft exists, refuse to touch an already-published release.

website.yml: deployment gated to refs/heads/main (a manual dispatch on a
feature branch can no longer publish it); PRs still get the build check.

Benchmark reconciliation: the eval page, the design-page summary, the
training lesson, and the README all cited the older run's aggregate
(V 0.745/0.776, "23 of 24", 4,400/22-strata) that the widget correction
had already retired. Reconciled to the frozen 2,600-case / 13-stratum
dataset used by the widget, verified from the artifact: AlignAIR better
on 18 of 24 metrics, gains in D/J and orientation, and V an honest
aggregate loss (IgBLAST 0.856 vs 0.828), driven by short fragments and
heavy-SHM full-length. Short-fragment V weakness is now visible everywhere.

Dates: revert CHANGELOG to "unreleased" and drop CITATION date-released
until the tag date is known at finalization (version 3.0.0 kept).
…ding

Three evidence corrections + a supply-chain pin.

1. Evidence bundle (benchmarks/bench_v2/): the numbers were embedded only
   in TSX; now there is an auditable bundle - the full model-comparison
   report (24 metrics, per-stratum + overall, CIs, verdicts), the case
   manifest (13 strata, 2600 cases, seed 123, germline reference SHAs,
   software versions), the generated cases, and SHA256SUMS. A new CI job
   (benchmark-artifact.yml) validates the report and manifest against the
   alignair_benchmark artifact contracts and checks the checksums. The
   bundle README documents the reproduction flow and flags the provenance
   still missing (evaluated model id/fingerprint/commit; IgBLAST version;
   cases generated with alignair 2.0.2) - to be supplied or regenerated
   before it is a complete v3.0.0 record.

2. Point estimates vs statistical decisions: the eval page said "better
   on 18" while its callout called the counts point-estimate tallies.
   Reconciled from the artifact: AlignAIR is point-estimate-higher on 18
   (IgBLAST on 6); under the Bonferroni-corrected paired bootstrap that is
   18 statistically better / 4 / 2 inconclusive, and all 18 AlignAIR-higher
   metrics are significant. "better" now means a supported verdict, in the
   eval page, its callout, the README, and the design summary.

3. Reproduction command: the widget said reproduce with `alignair
   benchmark` (the AlignAIR-only 4-stratum self-check). It now points to
   the alignair_benchmark harness (build -> evaluate each tool -> compare)
   and the frozen bundle, and keeps the self-check explicitly separate.

Supply chain: pin pypa/gh-action-pypi-publish to commit 76f52bc (v1.12.4)
instead of the mutable release/v1 tag.
The source igh_airr_input.tsv used CRLF, which git normalized to LF on
commit, so the recorded SHA256SUMS (computed on CRLF) no longer matched
the stored blob and the artifact CI checksum check would have failed.
Normalize the case files to LF, pin LF via .gitattributes, and recompute
SHA256SUMS on the LF bytes. Records are unchanged; results reproduce.
The bench_v2 comparison does not pin which AlignAIR model produced the
predictions or the IgBLAST version, so its results cannot be attributed
to the v3.0.0 product. Per the decision to re-run before launch, remove
all quantitative head-to-head claims and the comparison widget from
launch-facing surfaces until the verified rerun exists; keep only
qualitative / model-validation statements.

- README, Benchmarks page, and the design summary: the "18 of 24",
  aggregate V/D/J values, and tool-comparison verdicts are replaced with
  a qualitative "validated on a frozen ground-truth benchmark" statement
  and a note that a verified v3 head-to-head with full provenance is in
  preparation.
- Benchmarking lesson: the per-stratum comparison widget is removed and
  the step reworked to teach how to read a benchmark (per-stratum,
  point-estimate + interval) without publishing comparative scores. The
  self-check example JSON is labelled illustrative.
- BenchSandboxWidget is dropped from the widget barrel and carries a
  do-not-republish header; the file is retained for the verified rerun.
- benchmarks/bench_v2/ is relabelled ARCHIVAL / pre-v3.0.0 with a status
  banner; its cases remain canonical, its comparison is not
  v3-attributable. Added comparisons/RERUN.md: the target layout, exact
  AlignAIR + IgBLAST commands, the provenance to capture, and the rule
  that published numbers are regenerated from comparison_report.json.

Website builds clean; docs-command gate passes; bundle checksums verify.
1. Python 3.10: test_conda_recipe imported tomllib (3.11+). Fall back to
   tomli, and add `tomli; python_version < "3.11"` as a dev/test dep.

2. WFA fallback: get_aligner() returned WFAAligner without checking that
   pywfa is importable (the native import is lazy, so it failed only at
   align() time). It now probes wfa_available() / parasail_available()
   before selecting each backend.

3. Apple Silicon: parasail ships no arm64-macOS wheel, so `AlignAIR[cli]`
   tried to compile it and failed. Add a pure-Biopython fallback aligner
   (biopython is already a core dep) as the final link in the chain
   WFA -> parasail -> Biopython, and exclude parasail from [cli]/[reader]
   on Darwin/arm64 via an environment marker so the default Apple Silicon
   install succeeds and uses the fallback. parasail stays wherever a
   binary wheel exists (Linux, Windows amd64, macOS x86_64).

4. Windows 32-bit: cibuildwheel was producing win32, which PyTorch does
   not support. Add *-win32 to the skip list; win_amd64 stays.

5. Conda: `conda render` was removed from modern conda. Use
   `conda-build conda/ --output` (renders without building).

Tests: new test_bio_fallback.py covers the clean-environment path (no
pywfa, no parasail -> Biopython) and the AlignResult contract. Full
suite 746 passed; env markers verified (parasail excluded only on
Darwin/arm64).
…ass)

The dual-mode recipe used a top-level Jinja set-preamble plus environ to
switch between local (path) and PyPI (url+sha) source. Modern
conda-build's second rendering pass drops those preamble variables, so
`local_build` went undefined -> the PyPI branch ran -> `version` was
undefined -> `conda-build --output` failed.

Simplify conda/meta.yaml to a literal, local-source recipe: no top-level
Jinja variables, no environ, inline version, `source: path: ..`. It now
renders identically on both passes (there is nothing context-dependent
to lose). The eventual Bioconda recipe (published sdist url+sha, and
GenAIRR on a conda channel) is separate future work, noted in the recipe.

Also removed a `{% set %}` sequence that was sitting inside a YAML
comment - jinja2/conda-build parse the whole file, so that literal
delimiter was itself a render error.

Dropped the now-vestigial ALIGNAIR_LOCAL_BUILD env from the CI step;
updated test_conda_recipe to assert the literal/stable render.
registry/sources.py imported tomllib directly to parse config.toml. The
import is lazy (only when a config file exists), so smoke tests never hit
it, but on Python 3.10 (no stdlib tomllib) reading a registry config
raised ModuleNotFoundError at runtime.

Move a module-scope `try: import tomllib except ModuleNotFoundError:
import tomli as tomllib` to the top of sources.py, and add
`tomli; python_version < "3.11"` to the CORE dependencies (not just dev)
- reading the registry config is runtime functionality. This was the
only remaining direct tomllib import under src/. The redundant [dev]
entry is dropped (core is inherited there).
_excl_lock() only caught FileExistsError. On Windows, opening the
lockfile with O_EXCL while another holder has it open raises
PermissionError (a sharing violation: WinError 32/5 -> errno 13), not
FileExistsError, so a waiting thread crashed instead of polling. This
surfaced under real contention (test_excl_lock_serializes_concurrent_holders).

Catch PermissionError too and route it through the existing
poll/stale-reclaim/timeout path, but ONLY when the lockfile actually
exists - a genuine permission failure (the path cannot be created) still
propagates immediately rather than spinning to timeout.

Regression tests simulate both: a Windows-style PermissionError while the
lockfile exists (must be treated as contention, not a crash) and a
PermissionError with no lockfile (must surface as PermissionError, not
TimeoutError).
@MuteJester
MuteJester marked this pull request as ready for review July 18, 2026 12:29
@MuteJester
MuteJester merged commit d9eea5b into main Jul 18, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant