From 1f5db1500aeeccdd6b730edb48ce0fe2f034fc52 Mon Sep 17 00:00:00 2001 From: man4ish Date: Sun, 4 Oct 2026 14:43:13 -0500 Subject: [PATCH 1/2] Add contract-driven native canary validation --- .../contract-native-canary-validation.yml | 137 ++++++ dockerfiles/Dockerfile.airr_extra | 3 +- draft/canary-native-validation-design.md | 99 ++++ draft/progress.md | 193 ++++++++ scripts/contract_native_validation.py | 465 ++++++++++++++++++ scripts/validation_contracts.py | 40 +- scripts/validation_smoke.py | 220 +++++++++ tests/test_contract_native_validation.py | 320 ++++++++++++ tests/test_source_pinning_canary.py | 176 ++++++- tests/test_validation_contracts.py | 32 ++ tests/test_validation_smoke.py | 286 +++++++++++ validation-contracts/contract-ready-tools.txt | 2 + .../exceptions/blocked-multiple.txt | 3 +- .../exceptions/blocked-smoke.txt | 1 - .../fixtures/admixtools-tiny/README.md | 32 ++ .../fixtures/admixtools-tiny/qp3pop.par | 4 + .../fixtures/admixtools-tiny/tiny.geno | 24 + .../fixtures/admixtools-tiny/tiny.ind | 6 + .../fixtures/admixtools-tiny/tiny.pops | 1 + .../fixtures/admixtools-tiny/tiny.snp | 24 + .../fixtures/alevin-infer-tiny/README.md | 31 ++ .../fixtures/alevin-infer-tiny/counts.mtx | 8 + .../fixtures/alevin-infer-tiny/eq_labels.txt | 5 + .../alevin-infer-tiny/eq_labels.txt.gz | Bin 0 -> 36 bytes .../fixtures/alevin-infer-tiny/expected.json | 8 + .../alevin-infer-tiny/quants_mat_cols.txt | 2 + .../alevin-infer-tiny/quants_mat_rows.txt | 2 + validation-contracts/inventory.csv | 20 +- validation-contracts/inventory.json | 38 +- .../schema-v1/3ddna_extra.json | 4 +- validation-contracts/schema-v1/abricate.json | 4 +- .../schema-v1/accelerate.json | 4 +- .../schema-v1/admixtools.json | 16 +- .../schema-v1/afterqc_extra.json | 45 +- .../schema-v1/agat_extra.json | 4 +- .../schema-v1/aicsimageio_extra.json | 4 +- .../schema-v1/airr_extra.json | 91 ++-- .../schema-v1/alevin_fry.json | 80 ++- .../schema-v1/alevinqc_extra.json | 31 +- .../source-pinning-canary-10.json | 49 +- validation-contracts/summary.json | 27 +- 41 files changed, 2389 insertions(+), 152 deletions(-) create mode 100644 .github/workflows/contract-native-canary-validation.yml create mode 100644 draft/canary-native-validation-design.md create mode 100644 draft/progress.md create mode 100644 scripts/contract_native_validation.py create mode 100644 scripts/validation_smoke.py create mode 100644 tests/test_contract_native_validation.py create mode 100644 tests/test_validation_smoke.py create mode 100644 validation-contracts/fixtures/admixtools-tiny/README.md create mode 100644 validation-contracts/fixtures/admixtools-tiny/qp3pop.par create mode 100644 validation-contracts/fixtures/admixtools-tiny/tiny.geno create mode 100644 validation-contracts/fixtures/admixtools-tiny/tiny.ind create mode 100644 validation-contracts/fixtures/admixtools-tiny/tiny.pops create mode 100644 validation-contracts/fixtures/admixtools-tiny/tiny.snp create mode 100644 validation-contracts/fixtures/alevin-infer-tiny/README.md create mode 100644 validation-contracts/fixtures/alevin-infer-tiny/counts.mtx create mode 100644 validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt create mode 100644 validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt.gz create mode 100644 validation-contracts/fixtures/alevin-infer-tiny/expected.json create mode 100644 validation-contracts/fixtures/alevin-infer-tiny/quants_mat_cols.txt create mode 100644 validation-contracts/fixtures/alevin-infer-tiny/quants_mat_rows.txt diff --git a/.github/workflows/contract-native-canary-validation.yml b/.github/workflows/contract-native-canary-validation.yml new file mode 100644 index 0000000..ab091cb --- /dev/null +++ b/.github/workflows/contract-native-canary-validation.yml @@ -0,0 +1,137 @@ +name: Contract native canary validation (no publication) + +on: + workflow_dispatch: + inputs: + authorize_native_validation: + description: "Separate approval to build and validate exactly seven tools; never publish" + required: true + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: contract-native-canary-${{ github.ref }} + cancel-in-progress: false + +jobs: + selection: + if: ${{ inputs.authorize_native_validation }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + outputs: + matrix: ${{ steps.plan.outputs.matrix }} + plan_sha256: ${{ steps.plan.outputs.plan_sha256 }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - id: plan + name: Freeze exact contracts, fixtures, source SHA and fourteen native pairs + env: + SOURCE_SHA: ${{ github.sha }} + run: | + set -euo pipefail + python3 -m scripts.contract_native_validation plan \ + --source-commit "$SOURCE_SHA" \ + --plan "$RUNNER_TEMP/validation-plan.json" \ + --github-output "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@v4 + with: + name: contract-native-validation-plan + path: ${{ runner.temp }}/validation-plan.json + if-no-files-found: error + retention-days: 30 + + native-validation: + needs: selection + runs-on: ${{ matrix.runner }} + timeout-minutes: 110 + strategy: + fail-fast: false + max-parallel: 2 + matrix: + include: ${{ fromJSON(needs.selection.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: contract-native-validation-plan + path: ${{ runner.temp }}/native-plan + - uses: docker/setup-buildx-action@v3 + - name: Install native SIF validation tooling only + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:apptainer/ppa + sudo apt-get update + sudo apt-get install -y apptainer + apptainer --version + - name: Native OCI and SIF validation; no registry writes + env: + TOOL: ${{ matrix.tool_id }} + ARCH: ${{ matrix.arch }} + PLAN_SHA: ${{ needs.selection.outputs.plan_sha256 }} + RUNNER_OS: ${{ runner.os }} + RUNNER_ARCH: ${{ runner.arch }} + run: | + set -euo pipefail + python3 -m scripts.contract_native_validation execute \ + --plan "$RUNNER_TEMP/native-plan/validation-plan.json" \ + --plan-sha256 "$PLAN_SHA" --tool "$TOOL" --arch "$ARCH" \ + --output "$RUNNER_TEMP/native-evidence" \ + --authorize-native-execution + - name: Retain terminal evidence even on failure (no OCI/SIF publication) + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: contract-native-evidence-${{ matrix.tool_id }}-${{ matrix.arch }} + path: | + ${{ runner.temp }}/native-evidence/** + !${{ runner.temp }}/native-evidence/oci-image.tar + !${{ runner.temp }}/native-evidence/validated.sif + if-no-files-found: error + retention-days: 30 + + reconcile: + if: ${{ always() }} + needs: [selection, native-validation] + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: contract-native-validation-plan + path: ${{ runner.temp }}/native-plan + - uses: actions/download-artifact@v4 + with: + pattern: contract-native-evidence-* + path: ${{ runner.temp }}/native-results + - name: Require fourteen complete unique terminal records and every gate + env: + PLAN_SHA: ${{ needs.selection.outputs.plan_sha256 }} + SELECTION_RESULT: ${{ needs.selection.result }} + NATIVE_RESULT: ${{ needs.native-validation.result }} + run: | + set -euo pipefail + python3 -m scripts.contract_native_validation reconcile \ + --plan "$RUNNER_TEMP/native-plan/validation-plan.json" \ + --plan-sha256 "$PLAN_SHA" --records "$RUNNER_TEMP/native-results" \ + --output "$RUNNER_TEMP/native-summary.json" + test "$SELECTION_RESULT" = success + test "$NATIVE_RESULT" = success + - uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: contract-native-validation-summary + path: ${{ runner.temp }}/native-summary.json + if-no-files-found: error + retention-days: 30 diff --git a/dockerfiles/Dockerfile.airr_extra b/dockerfiles/Dockerfile.airr_extra index 24afe69..21ee9bb 100644 --- a/dockerfiles/Dockerfile.airr_extra +++ b/dockerfiles/Dockerfile.airr_extra @@ -1,2 +1,3 @@ FROM mambaorg/micromamba:1.5.8 -RUN micromamba install -y -n base -c bioconda -c conda-forge r-airr && micromamba clean --all --yes +RUN micromamba install -y -n base -c bioconda -c conda-forge \ + "conda-forge::r-airr=2.0.0=r45hc72bb7e_0" && micromamba clean --all --yes diff --git a/draft/canary-native-validation-design.md b/draft/canary-native-validation-design.md new file mode 100644 index 0000000..2592134 --- /dev/null +++ b/draft/canary-native-validation-design.md @@ -0,0 +1,99 @@ +# Contract-driven native canary validation + +Status: separate local implementation and synthetic/static tests complete; +review, remote verification and real native execution remain pending. + +Implementation: `scripts/contract_native_validation.py`, +`.github/workflows/contract-native-canary-validation.yml`, and +`tests/test_contract_native_validation.py`. No workflow dispatch, container +execution or publication occurred during implementation. Synthetic PASS +records are test data, never production evidence. + +## Exact scope + +Use the seven original canary contracts currently classified CONTRACT_READY: +`3ddna_extra`, `abricate`, `accelerate`, `agat_extra`, `aicsimageio_extra`, `airr_extra`, `alevin_fry`. +Require exactly fourteen unique tool/architecture pairs. Exclude all reference +tools and every blocked contract. This phase establishes native build and +scientific validation evidence; publication remains separately gated. + +## Existing integration boundary + +`scripts/pilot_runner.py` obtains its entry using `pilot_manifest.get_tool`. +The Pilot manifest fixes ten reference tools, package mappings, and a reviewed +Python base. Rollout contracts instead contain argument-array commands, +`regex:` parsers, API validation, and repository-owned fixtures. Neither +existing Pilot workflow can select these seven rollout tools. Preserve these +components and introduce a separate contract-consuming validation runner and +workflow only under explicit implementation authorization. + +## Workflow topology + +1. Validate selected contract hashes, Dockerfile hashes, source revision, + eligibility, exact tool set, and fixture requirements. Emit fourteen pairs. +2. Run amd64 pairs on `ubuntu-24.04`; arm64 pairs on `ubuntu-24.04-arm`. + Require runner OS Linux, runner architecture X64/ARM64, and matching uname. + No emulated execution qualifies. Set fail-fast false. +3. Build/load OCI locally, save its archive, validate config architecture and + labels, inspect executable/interpreter architecture, and run the contract's + version and smoke commands with bounded execution. +4. Convert the same OCI archive to SIF. Validate structured SIF metadata, + architecture, executable/interpreter, exact version, and the same smoke. + Bind OCI/SIF evidence through archive identity, labels, and content hashes. +5. Upload per-entry evidence including failures; reconcile all fourteen terminal + entries and fail if any required gate is absent or failed. + +Use contents-read permissions and artifact upload only. No registry login, +package-write permission, publication commands, or publication inputs belong +in this workflow. It must run only on explicit dispatch, never on source push. + +## Contract execution + +- Load and validate current schema-v1 contracts; verify hash and source files + before invoking Docker. Preserve argument boundaries with subprocess argv. +- Reuse established architecture/identity validators where their documented + inputs fit; do not fabricate Pilot entries or alter frozen implementations. +- Strip only the documented `regex:` prefix. Require a unique expected version + match; reject missing, conflicting, or incorrect evidence in either stream. +- Keep scientific/package version and observed internal banner distinct: + 3D-DNA is package baseline 201008 with documented banner 190716; AGAT package + version 1.7.0 prints v1.7.0. Do not substitute banner values for source identity. +- Copy declared fixtures into an isolated workspace, mount inputs read-only, + and assert declared output properties. AGAT must produce nonempty GFF3 with + gene1, transcript1, exon1, exon2. API assertion commands must exit zero. +- Treat API validation as native interpreter plus package functionality, + rather than demanding a console launcher that the package does not supply. +- AIRR uses Rscript, pinned R package baseline 2.0.0, and the existing tiny + rearrangement TSV. Its metadata-only cross-platform dependency solves are + not build/runtime evidence. Both native executions remain unproven; this + newly pinned baseline must not inherit the other five local ARM64 results. +- Alevin-fry uses `infer` with the hash-bound tiny MatrixMarket/equivalence-class + fixtures. Implement its `matrix_market_gene_counts` assertion kind explicitly; + reject unknown assertion kinds rather than accepting exit zero. Require exact + row/column labels, shape, unique coordinates, finite nonnegative values, + expected entries within 0.0001 absolute tolerance and row totals. This scope + excludes RAD processing and full quantification. Runtime output is unproven. + The local read-only checker is now implemented in `scripts/validation_smoke.py` + as `validate_smoke_outputs`; the future execution adapter must supply measured + exit status/duration and an isolated output workspace. Its synthetic verifier + tests are not native execution or release evidence. +- Preserve ABRicate's reference-data requirement and dependency-check scope; + its current check does not establish database-backed resistance screening. + +## Evidence and acceptance + +Record source SHA, contract hash, Dockerfile hash, resolved base identity, +package/source identity, runner evidence, OCI config/archive hashes, SIF hash, +executable/interpreter evidence, commands, raw stdout/stderr, return codes, +version results, smoke assertions, and OCI-to-SIF binding. Missing mandatory +evidence fails. Optional absent SBOM remains OPTIONAL_NOT_GENERATED. + +Unit/static checks must cover exact matrix membership, blocked/reference tool +exclusion, stale hashes, wrong runner architecture, command failures, wrong or +conflicting versions, missing fixtures/output, OCI/SIF mismatches, failure +aggregation, and absence of write-capable steps. Validate YAML and actionlint. + +Current evidence: five local ARM64 builds/smokes and runtime uname checks. +Native AMD64, SIF conversion, full identity/provenance gates, and remote +workflow execution remain unproven. No release-completion claim follows from +this design or the existing local checks. diff --git a/draft/progress.md b/draft/progress.md new file mode 100644 index 0000000..88cd988 --- /dev/null +++ b/draft/progress.md @@ -0,0 +1,193 @@ +# OmniBioAI Tool-Image Rollout Progress + +## 2026-10-04 — Scale-up gate + +- Scope: deterministic rollout preparation toward the eligible 1,000+ tool population. +- Current repository: `omnibioai-tool-images`. +- Reference factory: preserved; no workflow, release-engine, identity, provenance, or publication logic changes. +- Contract inventory: 601 eligible targets from the current reviewed population snapshot. +- Static contract validation: PASS. +- Contract determinism: PASS for the generator's current catalog-only output; two independent generations produced byte-identical output. The generated baseline remains 601 blocked because the source-pinning evidence artifact is not yet wired into the generic generator. +- Focused tests: 74/74 passed with `PYTHONPATH=.` and coverage disabled for this contract-only selection. +- The default pytest coverage gate is unrelated to these tests and reports 0% for `api/server.py`; no contract test failed. +- Current static-ready contracts: 5 (`3ddna_extra`, `abricate`, `accelerate`, `agat_extra`, `aicsimageio_extra`). +- Current blocked contracts: 596. +- Immediate next gate: separately authorized native amd64/arm64 validation of the five static-ready tools. +- Integration blocker: reconcile the five source-pinning-ready canary contracts with the generic generator without changing the frozen release factory. +- Integration implementation: the generator now consumes only reviewed canary contracts whose `pinning_canary_schema_version` is 1 and whose Dockerfile SHA matches current source; stale overrides fail closed. +- Post-integration generation: 601 targets, 5 ready, 596 blocked; output validator PASS. +- Post-integration focused tests: 75/75 passed. +- Integration commit: `24933eb` (`Integrate reviewed canary contracts into generation`); not pushed. +- Native-validation review: the existing workflow requires a controlled GitHub Actions run; no dispatch was made. The five ready tools are not release-verified yet. +- `alevin_fry` remains blocked: upstream usage requires RAD plus permit-list inputs, and no repository-owned valid fixture is available; no synthetic fixture was invented. +- Before the local ARM64 validation stage below, no Docker/Buildx/Apptainer/Singularity builds had been run. The subsequent stage built five local Docker images; no SIF builds have been run. +- No workflow dispatches, registry writes, publications, or GHCR mutations were run. +- Existing local symlink/generated artifacts remain preserved and untouched. +- Local ARM64-only validation (Docker Desktop, no push/publication): + - `3ddna_extra`: build PASS; `3d-dna --help`/version smoke PASS; image ID `sha256:3597b2eb06fe8819151aa649bd4afba2aa704ff2748e5df9209c854907b0339a`; inspected `linux/arm64`. + - `abricate`: build PASS; `abricate --version` (`1.4.0`) and `abricate --check` PASS; image ID `sha256:d1e72644852aa04e42452f95cc3e0419586afe5d6bb5fd3614d6fbb9a37c240f`; inspected `linux/arm64`. The contract's local-reference-data requirement remains explicit. + - `accelerate`: build PASS; `accelerate env` (`1.15.0`) and CPU Accelerator smoke PASS; image ID `sha256:e0c54dd1c6382bf2b023ea63472c7820c48b287d5ac8e2acd74911f964719bc9`; inspected `linux/arm64`. + - `agat_extra`: build PASS; `agat --version` (`v1.7.0`) and tiny GFF3 conversion smoke PASS; image ID `sha256:d08b659c15cf9bcd0aa726d526d9656726118f63ce77a8a1c6ce3c3870f93628`; inspected `linux/arm64`. + - `aicsimageio_extra`: build PASS; module version (`4.14.0`) and in-memory `2x3` shape smoke PASS; image ID `sha256:0532704e284e57bae7c0192016236f7d4f2d4dd6894b8c209243d56ab223ce76`; inspected `linux/arm64`. Java-backend warning is expected and did not affect the contract smoke. +- These results establish local ARM64 build/smoke evidence only. Native AMD64 evidence and the complete OCI/SIF/identity/provenance release gates still require the controlled native validation workflow; no workflow dispatch was made. +- Post-validation static checks: contract validator PASS; focused contract/source-pinning tests PASS (`75 passed`). +- Workflow topology audit: the frozen pilot workflow accepts only the reviewed 10-tool manifest (`pilot9`, `all`, or a manifest `single` tool). A canary ID such as `3ddna_extra` is correctly rejected as incomplete pilot selection, so no dispatch was attempted and the frozen workflow was not broadened. +- Native-validation blocker is now explicit: this host provides native ARM64 Docker only; the repository workflow's native AMD64 runner is the required next evidence source. Local ARM64 results cannot be promoted to native multiarch or release status. +- Revalidation after the metadata preflight: contract validator PASS, focused tests `75 passed`, and `git diff --check` PASS; no generated contract semantics changed. +- Deterministic evidence artifact added at `draft/next-five-package-preflight.tsv` (8 exact package records across five tools; canonical SHA256 `b1aa02c5b49a8ceff0ef89279e79face2d1226d0589b5bbee32e3ce0c8092655`). It is planning evidence only and does not authorize pinning, builds, or publication. +- Package-content evidence narrowed the next blockers: AllHiC's ARM64 package explicitly contains `bin/allhic`; NCBI AMRFinderPlus explicitly contains `bin/amrfinder` plus support utilities; Altair and anndata2ri are Python API packages with no console executable in their manifests; AmberTools' selected ARM64 artifact is approximately 99.7 MB, so only metadata was inspected (no payload download). Canonical version/smoke contracts remain unresolved and no contract status was changed. +- Recipe/source evidence artifact at `draft/next-five-source-evidence.tsv` (canonical SHA256 `4c75b9f170def99b0c2014f066c0d48e2c38f2e15f93398f01cab3da5814b1ba`): AllHiC has a pinned v0.9.14 source tarball and `allhic --help` recipe test; Altair has a pinned PyPI 6.3.0 source and import test; AMRFinderPlus has tag `amrfinder_v4.2.7`, explicit `amrfinder`, and recipe tests (one network-dependent); anndata2ri has a pinned PyPI 2.0.1 source and import tests; AmberTools now has source archive SHA `5d46eef3…`, package identities for both architectures, canonical `sander`, and recipe smoke `sander --version`. +- AmberTools package manifest inspection confirms the recipe's `sander --version` smoke path and native architecture artifacts; no package was installed or built. +- Review-only remediation proposal artifact added at `draft/next-five-remediation-proposals.tsv` (canonical SHA256 `3b232e3a10dd91c115d2552423cc565a9b3e8fec87ba434f6277ecd4df274683`). It keeps all five blocked until Dockerfile pins and deterministic version/smoke evidence are formally applied; no readiness is inferred from package availability alone. +- Ephemeral ARM64 package validation: AllHiC 0.9.14 installed and `allhic --help` passed, but the executable reports `allhic version 0.9.13`; this is a fail-closed scientific-version mismatch, so AllHiC remains blocked pending upstream/package reconciliation. No image was built and the container was discarded. +- Ephemeral ARM64 package validation: AMRFinderPlus 4.2.7 installed; `amrfinder --help` passed and `amrfinder --version` returned `4.2.7`. `amrfinder -V` also reports `4.2.7` but exits with a missing local AMRFinder database, confirming `runtime_reference_data_required=true`; no database update/download was run and the container was discarded. +- Ephemeral ARM64 package validation: Altair 6.3.0 installed; `importlib.metadata` returned `6.3.0`, and a deterministic in-memory chart `to_dict()` smoke passed. This is package/API evidence only; the repository Dockerfile is still unpinned and no contract status changed. +- Ephemeral ARM64 package validation: anndata2ri 2.0.1 installed with its resolver-selected dependencies, but the recipe import smoke failed closed: `ModuleNotFoundError: No module named 'pandas.core.index'` while importing `anndata` 0.6.22.post1 against pandas 3.0.6. This is a dependency/runtime blocker; no dependency pin or factory change was attempted and the container was discarded. +- Read-only conda metadata preflight for the next lexical blocked set found cross-architecture package evidence without changing source: `allhic` 0.9.14 (`linux-aarch64` SHA256 `60503a80ed49438a6230ef704288a50ddb39570a99542dc4b27b2c5b566a91e5`, `linux-64` SHA256 `5bf1a9d7307e9cd7fd7a8eb6180cc931afde4e049308ef6146d4b1aafc3b7fae`), `altair` 6.3.0 noarch SHA256 `dcc425f65b863c7806354542638b6faa360a7863463a14b6d5bbeb976a6cdb47`, `ambertools` 26.0 with native aarch64 and linux-64 builds, `ncbi-amrfinderplus` 4.2.7 with native aarch64 and linux-64 builds, and `anndata2ri` 2.0.1 noarch SHA256 `5b370327af1a9b7f29dd1f668f402f33718c441229afb9283d526ddcde7d6edc`. These are evidence leads only; executable, version-parser, smoke, source-pinning, and contract updates remain unapproved until each is documented deterministically. + +### Blocking canary entries + +### Evidence audit — next five (2026-10-04) + +- The prior AmberTools description overstated readiness: `sander` is one package-test executable, not an approved canonical suite validation target. `sander --version` proves version execution only; functional smoke and its output parser are unresolved. The source filename `ambertools26_rc7.tar.bz2` raises a release-candidate concern that must be reconciled with the stable-baseline policy before selection. Package version 26.0 alone does not settle that concern. +- Corrected anndata2ri's package channel from conda-forge to Bioconda and retained its ARM64 import failure. A noarch artifact does not prove native dependency compatibility. +- Updated proposals with observed AllHiC version mismatch and AMRFinderPlus's exact working version command and local database requirement. +- Package-table version column now says `candidate_package_version`; these versions have not been approved as scientific baselines or applied to Dockerfiles. +- Current evidence SHA256: package preflight `0ca7479a946df7a162d742cc9461ce0c10a76b9881cb01209b05634ae8f9b800`; proposals `7e9d73b4e3cef0cb56302b64aeb37ad9565018ea44066e9dc7339577493f5491`. Earlier hashes above describe prior revisions. +- All five next contracts remain blocked. Extending Dockerfile/source pinning beyond the explicitly authorized original ten requires a separate scope authorization; no next-five Dockerfile or contract was changed. + +### Original canary blockers + +### Runtime evidence audit — 2026-10-04 + +- Docker daemon reports `linux` / `aarch64`. Each existing canary container (`3ddna_extra`, `abricate`, `accelerate`, `agat_extra`, `aicsimageio_extra`) independently returned `aarch64` from `uname -m`; all five commands completed with aggregate exit status 0. This supports the native ARM64 mapping independently of filenames and tags. +- Revalidated AGAT using `sh -c` with explicit failure propagation: canonical executable resolved to `/opt/conda/bin/agat_convert_sp_gxf2gxf.pl`; version banner `v1.7.0`; tiny GFF3 conversion produced nonempty output containing all four expected feature IDs; exit status 0. The earlier login-shell invocation had insufficient recorded exit-status evidence and is superseded by this check. +- No rebuild, SIF generation, registry mutation, workflow dispatch, commit, or push occurred during this audit. Native AMD64 and complete SIF release validation remain pending. + +### Unresolved original canary tools + +- Native-validation implementation design is recorded in `draft/canary-native-validation-design.md`: exact five-tool/ten-entry matrix, native runners, contract hash binding, OCI/SIF gates, bounded contract commands, fixture assertions, failure aggregation, and contents-read-only workflow permissions. The existing runner cannot accept rollout contracts without a separate adapter; frozen Pilot code was preserved. Implementation/dispatch authorization remains pending. +- Checked the five stored version regexes against observed output samples; all five matched. This is parser consistency evidence, not new native execution evidence. + +- `admixtools`: fixture and license review required. +- `afterqc_extra`: Python 2.7 and native arm64 dependency evidence unresolved. +- `airr_extra`: native arm64 dependency evidence unresolved. +- `alevin_fry`: valid RAD/permit-list smoke fixture required. +- `alevinqc_extra`: no stable linux-aarch64 package artifact. + +### ADMIXTOOLS fixture preparation — 2026-10-04 + +- Added independently authored, repository-owned EIGENSTRAT smoke-candidate data in `validation-contracts/fixtures/admixtools-tiny/`: 24 biallelic sites across four autosomes, six diploid samples, three synthetic populations, and the documented qp3Pop parameter/triple files. Format and invocation evidence comes from ADMIXTOOLS v8.0.2 `convertf/README` and `README.3PopTest`. +- Three new static regressions check row/column alignment, unique ordered SNP coordinates, genotype dosage values, target heterozygosity, population membership, local parameter-file references, and continued fail-closed smoke/license status. These checks do not prove finite statistics, retained SNP counts, or native execution. +- Fixture status is `STATIC_FIXTURE_ONLY — NOT_RUNTIME_VALIDATED`; no scientific statistic was invented and no contract was promoted. The existing missing-smoke blocker remains until execution evidence establishes an acceptable result; distribution-license review remains mandatory. +- Validation: focused contract/source-pinning suite **78/78 PASS**; contract validator PASS; Ruff PASS; `git diff --check` PASS. All existing contract JSONs, Dockerfiles, and frozen factory files remain unchanged in this step; inventory stays 5 ready / 596 blocked. +- No container build/run, SIF build, registry operation, workflow dispatch, commit, push, or staging occurred in this step. Existing unrelated symlink/cache/patch changes were preserved. Persistent follow-up saved in `~/20261003-omnibioai-source-pinning-fixture-canary10.md`. + +### ADMIXTOOLS pinned-license evidence — 2026-10-04 + +- Independently inspected the v8.0.2 upstream README copyright notice (lines 97–104): copying permission is explicitly non-commercial and conditional on retaining the notice. This confirms a genuine distribution-scope gate, not missing version metadata. No conclusion about permission for OmniBioAI's intended distribution was assumed. +- Added direct pinned-release evidence and explicit `DISTRIBUTION_LICENSE_REVIEW_REQUIRED` deficiency to the ADMIXTOOLS contract. Its intended distribution scope remains `UNKNOWN`, permission `NOT_ESTABLISHED`, and status `CONTRACT_BLOCKED_MULTIPLE_REASONS`. A runtime license-server requirement being false is distinct from redistribution permission being unresolved. +- Rehashed only the changed ADMIXTOOLS contract (`7e904c83d44c40f68c65f1c8146ae2286cc278bc74641113cb33fc600f8b5c07`) and synchronized its inventory hash. Other 600 contract files, including all 591 non-canary contracts, remain unchanged; no Dockerfile or frozen factory changes. +- Focused tests **79/79 PASS**, including a new regression that unresolved license scope cannot imply permission. Ruff, contract validator, and diff check PASS. Generated the complete inventory twice in isolated audit directories: all **618 generated files byte-identical** across runs and match current inventory output. Audit evidence retained at `~/omnibioai-contract-audit.VhyaH8/`. +- Inventory remains 5 static-ready / 596 blocked. No container execution/build, publication, GHCR operation, workflow dispatch, staging, commit, or push. HEAD remains `0215aaa7a7834699bb3655adee2456da9a703dcb`; unrelated worktree preserved. + +### AIRR architecture blocker resolved statically — 2026-10-04 + +- Rechecked both existing package channels rather than assuming Bioconda was the only source. Bioconda's AIRR 1.2.0 remains tied to R 3.5.1; conda-forge now supplies stable AIRR 2.0.0 in exact noarch build `r45hc72bb7e_0`, SHA256 `b8c733fff391dcbc1f13df2a1f46452fa05f9036c01303efb3850d5477f7847c`. +- Metadata-only Conda dry-run solves with strict conda-forge priority and conservative target glibc 2.17 succeeded independently for `linux-64` and `linux-aarch64` (120 packages each). Both select native R 4.5.3 plus native readr/stringi/yaml/jsonlite dependencies. No package was installed or executed; this is availability evidence, not native runtime validation. +- Downloaded the CRAN source archive into memory only. Actual SHA256 `e4932ec84dad46d37327ab5aa5a34b95095677b6392bff2f59fd473eb942da52` exactly matches the official recipe. DESCRIPTION establishes stable version 2.0.0, CC BY 4.0, no package compilation; its source retains `read_rearrangement`. NEWS documents removal of alignment APIs; no historical intended version or full backwards-compatibility claim is made. +- Within the original ten-tool scope, pinned only `Dockerfile.airr_extra` to channel-qualified `conda-forge::r-airr=2.0.0=r45hc72bb7e_0`; base and package ecosystem remain unchanged. Updated AIRR contract/source-pinning evidence, preserved the existing rearrangement fixture, corrected R newline output quoting, and anchored its exact-version parser to reject missing, wrong, repeated, or conflicting output. +- AIRR is now `STATIC_CONTRACT_READY` / `CONTRACT_READY`, with `native_build_validation_status=NOT_RUN` and `release_status=NOT_PUBLISHED`. Contract SHA `77f5629096752f128b83958b1a2a568df55947379774f2f2a36772f2b2a77a3a`. Inventory now **6 static-ready / 595 blocked**; fewer than ten ready, so no ten-tool rollout batch is proposed. +- Focused contract/source-pinning tests **86/86 PASS**; Ruff and contract validator PASS. Two independent inventory generations are byte-identical; only deterministic aggregate AIRR row/count/list changes were applied. All 591 non-canary contracts and Dockerfiles remain unchanged. Evidence directories retained at `~/omnibioai-airr-audit.m7cYRO/`. +- Validation-only design updated from five tools/ten pairs to six tools/twelve pairs. AIRR must earn both native executions; it does not inherit other tools' local ARM64 evidence. Separate runner/workflow implementation and dispatch remain pending authorization; frozen release factory untouched. +- No Docker/SIF build, container run, workflow dispatch, GHCR mutation, publication, staging, commit, or push in this step. Intentional changes remain unstaged and unrelated worktree preserved. +- AIRR-specific Dockerfile structural selection: **6/6 PASS** (7,483 other cases deselected; no runtime/build tests executed). Final diff check PASS. + +### Alevin-fry bounded inference contract — 2026-10-04 + +- Inspected the complete v0.18.3 source tree, resolved as `ad05742d274230f9141b2eabeebd1c1b31692199`. Pinned documentation and implementation expose a bounded, meaningful `infer` path independent of RAD input. Existing RAD requirements were for the broader processing pipeline, not every valid release smoke path. +- Independently authored fixture in `validation-contracts/fixtures/alevin-infer-tiny/`: two cells, two genes, three equivalence classes, including ambiguous geneA/geneB counts. Upstream format evidence confirms the gzip equivalence-class parser, one-based MatrixMarket indices, barcode/name companions, and output layout. No upstream source or dataset was copied. +- Symmetric singleton evidence gives analytical expected rows `[7,7]` and `[3,3]`, totals `[14,6]`. These are NOT observed execution values. Contract requires exit zero, 60-second bound, exact labels/shape, unique coordinates, finite nonnegative counts and agreement within 0.0001 absolute tolerance. Runtime runner must implement these assertions, not merely check exit status. +- Gzip fixture is a mechanical `gzip -n` encoding of checked plaintext; header timestamp zero and no filename. Input and expected-result fixture hashes are included in the contract. Source pin, Dockerfile, version, identity schema, and frozen release engine remain unchanged. +- Promoted Alevin-fry to STATIC_CONTRACT_READY only; `native_build_validation_status=NOT_RUN` and `release_status=NOT_PUBLISHED` remain. Contract SHA `3b463ac75c26e86613262784c68889f332fb2951724e7f086725476af094c49f`. Smoke proves inference scope only; mapping, RAD, permit-list generation, UMI resolution and USA mode remain outside this smoke claim. +- Inventory **7 static-ready / 594 blocked**; original ten-tool canary 7 static-ready / 3 blocked. Three new fixture/contract regressions pass; focused suite **89/89 PASS**, Ruff/validator/diff check PASS. Two complete generations byte-identical at `~/omnibioai-alevin-audit.QtxCwc/`; only deterministic aggregate changes applied. All 591 non-canary contracts and Dockerfiles preserved. +- Validation-only design now covers seven tools/fourteen native entries; runner implementation/remote execution remain separately gated. No container execution/build, SIF build, workflow dispatch, GHCR operation, publication, staging, commit, or push occurred. Progress report under `~/` updated; unrelated worktree unchanged. + +### AfterQC availability correction and runtime exception — 2026-10-04 + +- Resumed the original canary after the user withdrew the separate container-documentation request. No documentation-repository edit or expansion into plugin hardening occurred. +- Current conda-forge metadata contains six native linux-aarch64 Python 2.7.15 artifacts. Exact AfterQC 0.9.7 build `hdfd78af_4` no-install solves succeed for linux-64 (24 packages) and linux-aarch64 (23 packages), both selecting native Python 2.7.15 and OpenSSL 1.1.1w. Commands use reviewed channels, no channel priority, no default packages, and target glibc override 2.17. No artifact was installed or run. +- Inspected the exact 31,575-byte noarch AfterQC package in memory: SHA256 `a0e73790cfd512ecf38f44f11a01812458d1bb7a22df592512e89d27976390f5`. Package metadata confirms Python >=2.7,<3.0; recipe preserves the existing v0.9.7 source archive checksum `cd4b33f1874bf7588fecdd44a57ca85788022c689922e8186b8dc137c5631bc2`. +- Corrected `native_arm64_expected` and architecture availability to reflect successful native dependency resolution. Removed the now-false architecture-missing deficiency. Package availability is not a native execution, support, or security PASS. +- Python upstream confirms Python 2 is unsupported since 2020 (`https://www.python.org/doc/sunset-python-2/`). AfterQC remains blocked for explicit legacy-runtime review and unpinned repository source; no risk exception, Python port, baseline selection, or Dockerfile pin was assumed. Scientific baseline and final source identity remain unset; original historical intent UNKNOWN. +- Contract SHA `2f3b551d0ce9a2d27585d4ab345f8f6dd81f060139c26647e604fcf679492d82`; inventory remains **7 static-ready / 594 blocked**. Added regression distinguishing package availability from runtime-risk acceptance. Focused tests **90/90 PASS**; Ruff, validator, diff check PASS; two complete generations byte-identical at `~/omnibioai-afterqc-audit.eBoizF/`. +- Only original-canary AfterQC evidence and corresponding deterministic aggregate row/discrepancy changes were applied in this step. All 591 non-canary contracts/Dockerfiles and frozen factory preserved. No Docker/SIF build, container run, registry operation, dispatch, staging, commit, or push; HEAD unchanged and unrelated worktree preserved. + +### Executable smoke-output assertions — 2026-10-04 + +- Added `scripts/validation_smoke.py`, a focused read-only validator for the canary's `matrix_market_gene_counts` assertion kind. This is contract-output validation only, not a command runner/workflow and not a modification to the frozen release factory. +- Before accepting output, validates current contract/Dockerfile hashes, ready status, all fixture hashes, expected-result binding, reviewed tolerance, measured caller-supplied command status/duration, and output declarations. Requires bounded relative paths contained within source/output roots; symlink escapes fail closed. Caps individual evidence files at 1 MiB. +- Requires exact MatrixMarket shape/coordinate set, unique bounded integer coordinates, finite nonnegative numerical values, expected counts within absolute tolerance 0.0001, independently checked row totals, and exact ordered unique row/column labels. Accepts real or integer headers and arbitrary coordinate ordering; rejects unsupported headers and all unknown assertion kinds. +- Added **54 synthetic verifier tests**, including corrupt matrices, duplicate coordinates, missing output, swapped labels, hash mismatch, unbound expectations, timeout/failure, malformed expected JSON values, path/symlink escapes and evidence immutability. These do NOT run Alevin-fry, Docker, SIF, or any scientific command and do not claim native/release PASS. +- Combined suite **144/144 PASS**; Ruff, contract validator, and `git diff --check` PASS. No contract/fixture/generated inventory semantic changes in this step; inventory remains **7 static-ready / 594 blocked**. Native runner integration and actual native execution remain pending. +- HEAD unchanged; no Docker/SIF build, registry operation, publication, workflow dispatch, staging, commit, push, frozen factory change, or non-canary source change. Unrelated symlink/cache/patch worktree preserved. Persistent follow-up under `~/` updated. + +### AlevinQC exact architecture failure — 2026-10-04 + +- Rechecked current Bioconda metadata: latest AlevinQC candidate 1.26.0 has linux-64 and osx-64 builds, with **zero linux-aarch64 builds**. Historical noarch builds stop at 1.10.0; no older version was substituted to obtain a green architecture check. +- Exact no-install `linux-aarch64` solve of `bioconductor-alevinqc=1.26.0` against bioconda/conda-forge exits **1 / PackagesNotFoundError**. This is a missing candidate package, not a transient container failure or proof the scientific source can never support ARM64. A source-build approach would require separate review; none was attempted. +- Bound the precise candidate/platform/solver evidence into its contract and added a regression preventing silent historical noarch substitution. Contract remains blocked, baseline unset, Dockerfile unchanged. SHA `ee1219d4554d161772dabdc2662a4c290856e7c451fc156160a0ac686df2bcef`; only its inventory hash changed. +- Combined suite **145/145 PASS**; Ruff, validator, diff check PASS. Two complete generations byte-identical at `~/omnibioai-alevinqc-audit.nymyal/`. All 591 non-canary contracts and frozen factory preserved. Inventory remains **7 static-ready / 594 blocked**; original canary exceptions are ADMIXTOOLS license/unexecuted smoke, AfterQC legacy runtime/unpinned source, and AlevinQC ARM64 package/unpinned source. +- No build/run, dispatch, publication, registry mutation, staging, commit, or push. HEAD unchanged; unrelated worktree preserved. Asked the user explicitly whether to authorize new separate validation-only runner/workflow files with local tests only; no dispatch/build authorization is assumed. + +### Version-evidence checker validation — 2026-10-04 + +- Completed local testing of `validate_version_evidence` in the standalone read-only assertion helper. It requires current contract/hash validation, successful measured exit status, bounded text streams, an explicitly declared output stream and observation parser, exactly one observed tool version, and agreement with the unchanged strict expected-version parser. Missing, duplicate, conflicting, malformed, unconfigured and hash-mismatched evidence fails closed. +- Added 147 synthetic tests across the seven static-ready tools. Stream/observation metadata is constructed only in test fixtures; actual generated contracts, their strict parsers, hashes and inventory remain unchanged. The helper cannot implicitly accept contracts without this additional configuration. No scientific tool was executed and these tests establish no native/release PASS. +- Combined smoke/source-pinning/contract tests **292/292 PASS** with `--no-cov`; Ruff, generated contract validator and diff check PASS. Initial default invocation had 257 passing tests but exited 1 because the unrelated default `api/server.py` coverage gate collected 0%; this result is recorded rather than hidden. Coverage settings and tests were not weakened or edited. +- Readiness remains **7 static-ready / 594 blocked**. All 591 non-canary contracts and frozen factory unchanged. HEAD remains `0215aaa7a7834699bb3655adee2456da9a703dcb`; changes unstaged, unrelated worktree preserved. No container/SIF build, execution, dispatch, publication, registry write, commit or push in this step. +- Next gated action remains authorization for separate local-only native-validation runner/workflow implementation; no execution authorization is inferred. Persistent report under `~/` updated. + +### Canary version-stream contract binding — 2026-10-04 + +- Bound the tested read-only version checker to the seven static-ready original-canary contracts via `version_output_source` and exact tool-banner `version_observation_parser`. Python/R API commands require stdout; CLI contracts permit either stream but exactly one banner across them. Observation patterns detect wrong/duplicate/conflicting banners; existing strict expected-version parsers, scientific versions, source pins and smoke contracts are unchanged. +- Rehashed those seven contracts and updated only inventory CSV/JSON hash references. No other contract changed in this step, including all 591 non-canary contracts. Two independent complete generations are byte-identical; all **618 generated files match current output**. Audit retained at `~/omnibioai-version-audit.zf3Qac/`. +- Added seven integration regressions using real generated contract metadata and synthetic output. Initial integration test incorrectly equated reported output with release version and failed two cases: 3D-DNA release 201008 reports internal 190716, AGAT displays v1.7.0. Corrected the test to use the established `version_expected_value`, preserving those documented semantics. No parser was loosened to pass. +- Final combined suite **299/299 PASS** (`--no-cov`, contract-only selection); Ruff, generated-contract validator and diff check PASS. Native evidence is still not established by synthetic tests. Inventory remains **7 static-ready / 594 blocked**. +- Zero builds, container execution, dispatch, registry writes, publication, staging, commits or pushes. Frozen factory unchanged; HEAD remains `0215aaa7a7834699bb3655adee2456da9a703dcb`; unrelated worktree preserved. Separate native-validation runner/workflow implementation authorization remains pending. + +### Next-stage authorization check — 2026-10-04 + +- Revalidated HEAD and clean diff formatting against the validation design. The preceding turn made concrete progress by integrating contract version-stream metadata and verifying 299 tests; this check does not establish additional runtime progress. +- The remaining original-canary exceptions require license/runtime-policy decisions or unavailable ARM64 package remediation. No such decisions were assumed. Seven ready contracts still need a separate native-validation adapter and native OCI/SIF evidence; existing frozen Pilot factory is not an authorized rollout adapter. +- No live build/workflow process is being monitored. Further execution is not a verified wait. Local-only implementation of new validation-runner/workflow files remains a requested scope expansion awaiting explicit user approval; dispatch/build/publication are separately prohibited. +- Stopped implementation work at this authorization boundary. Goal remains active; no source changes beyond this progress entry, no build, dispatch, registry mutation, staging, commit or push. Recommended next action: approve local-only runner/workflow implementation and tests, without execution or publication. + +### Authorization boundary recheck — 2026-10-04 + +- Second consecutive no-progress audit: repository file discovery confirms only the contract generator, read-only assertion helper and frozen Pilot/Bedtools release implementations; no separate canary contract-consuming native-validation runner/workflow exists. Design explicitly requires implementation authorization. No user approval has arrived. +- HEAD and diff formatting remain unchanged/valid. No live execution handle exists; this is not a verified wait. No additional implementation, build, dispatch, registry mutation, commit or push is authorized or attempted. Goal remains active pending the blocked-audit threshold; next action still requires explicit local-only runner/workflow implementation approval. + +### Blocked-audit threshold reached — 2026-10-04 + +- Third consecutive no-progress audit confirms the same genuine authorization blocker. No user approval for separate native-validation runner/workflow implementation has arrived; no existing authorized adapter or live execution process supplies an alternative. The other original-canary exceptions require license/runtime-policy or architecture decisions, not invented evidence. +- Goal marked BLOCKED, not complete. Seven static-ready contracts and 299 passing focused tests remain the latest evidence; full native OCI/SIF validation and rollout are unproven. HEAD and diff formatting rechecked; no build, dispatch, registry write, source implementation change, staging, commit or push. +- Required next action: explicitly authorize local-only implementation/tests of the separate native-validation runner/workflow for the seven ready tools. This does not authorize build execution, dispatch, publication, commit or push. + +### Separate native-validation implementation — 2026-10-04 + +- User explicitly approved local-only implementation/tests. Added only a new standalone runner, manual-only workflow and focused tests; the frozen release factory remains unchanged. No execution authorization is inferred from implementation approval or subsequent status/publication-timing questions. +- Runner generates a canonical SHA-bound plan for exactly seven original-canary tools × two native architectures, binds contract/Dockerfile/fixture hashes and source revision, rejects tracked checkout changes before execution, and requires an explicit execution flag. Builds use local load only; runtime checks use network isolation, read-only inputs and isolated output workspaces. +- OCI archive identity, native runner/runtime architecture, executable/interpreter ELF machine, exact version evidence, reviewed smoke outputs, structured SIF metadata/inherited labels, same-archive conversion, content hashes and existing schema-v1 identity are checked. Uses a digest-resolved base in a temporary Dockerfile without editing source. SIF activation explicitly uses `/opt/conda`, independent of Docker entrypoint environment. +- Each successful/failed execution retains a terminal entry and bounded command logs. Reconciliation rejects missing/duplicate/unexpected terminal entries or missing gates, rechecks identity/raw version/status bindings and actual retained AGAT/Alevin smoke outputs. No publication command/input, registry login, package-write permission, QEMU setup or source-push trigger exists in the new workflow. +- New adapter tests **69/69 PASS**; combined contract/canary tests **368/368 PASS**; frozen factory/identity/Bedtools/Pilot safety suite **397/397 PASS**. Ruff, YAML/static safety tests, actionlint, contract validation and diff checks PASS. Tests inject synthetic command/artifact evidence and forbid real external process execution in the adapter suite. Initial synthetic 3D-DNA help omitted smoke markers because version/smoke share one command; corrected the fake output, not the scientific contract. +- Current ready population remains **7 static-ready / 594 blocked**. No real native OCI/SIF evidence was created. HEAD unchanged at `0215aaa7a7834699bb3655adee2456da9a703dcb`; new implementation and existing intentional changes remain unstaged. Unrelated worktree preserved; zero builds, dispatches, registry writes, publications, commits and pushes in this implementation step. +- Publication timing remains gated, not scheduled: review and remotely verify changes, separately authorize native validation, then assess results before separately authorizing a small publication canary. No 611-tool bulk publication is ready or authorized. Implementation report saved under `~/`. + +### Autonomous review and isolated remote baseline — 2026-10-04 + +- User authorized autonomous continuation toward gated publication. Remote access revalidated successfully outside the network sandbox. Current origin/main is `13638ad05bfa81dc33413e4972910522233eae93`; the original worktree HEAD has four additional local commits, one changing nine frozen reference-tool Dockerfiles. Those changes are NOT part of this rollout push. +- Created isolated worktree `~/omnibioai-native-validation-20261004/`, branch `hardening/contract-native-canary-20261004`, directly from remote main. Carried only contract/canary validation implementation, supporting contract-generation integration, canary evidence/fixtures, and progress/design documentation. Original worktree and unrelated symlink/cache/patch changes remain intact. +- Verified all 591 non-canary contracts byte-identical to remote main and zero diff for frozen workflow/engine/identity/integrity and reference Dockerfiles. Only AIRR's previously reviewed canary Dockerfile pin is included. +- Isolated relevant tests **765/765 PASS**; full repository suite **8,307 PASS / 13 SKIP / 0 FAIL**. Ruff, actionlint, static contract validator and diff checks PASS. Skips include absent mounted SIF store; no missing runtime evidence is promoted to PASS. +- No image build, workflow dispatch or publication yet. Next in autonomous sequence: exact-scope commit/push/remote verification, then controlled native-only workflow execution before assessing publication eligibility. diff --git a/scripts/contract_native_validation.py b/scripts/contract_native_validation.py new file mode 100644 index 0000000..6c22f51 --- /dev/null +++ b/scripts/contract_native_validation.py @@ -0,0 +1,465 @@ +"""Explicitly executed, local-artifact-only native validation of seven contracts. + +Planning and reconciliation never execute containers. Execution is a separately +gated CLI action; this module has no publication or registry-write operation. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import signal +import subprocess +import time + +from scripts.base_image_identity import extract_base_digest +from scripts.pilot_integrity import archive_identity, validate_runner_evidence +from scripts.sif_identity import canonical_json_bytes, identity_metadata +from scripts.validation_contracts import sha256_file, validate_contract +from scripts.validation_smoke import validate_smoke_outputs, validate_version_evidence + + +TOOLS = ("3ddna_extra", "abricate", "accelerate", "agat_extra", "aicsimageio_extra", "airr_extra", "alevin_fry") +RUNNERS = {"amd64": "ubuntu-24.04", "arm64": "ubuntu-24.04-arm"} +GATES = ("runner", "oci_identity", "oci_architecture", "oci_executable", "oci_version", "oci_smoke", + "sif_structure", "sif_architecture", "sif_executable", "sif_version", "sif_smoke", "oci_sif_binding", "identity", "provenance") + + +class ValidationError(ValueError): + """Missing or contradictory evidence; never a warning-only outcome.""" + + +def contained(root: Path, relative: str) -> Path: + if not isinstance(relative, str) or not relative or Path(relative).is_absolute(): + raise ValidationError("invalid relative input path") + path = (root / relative).resolve() + if not path.is_relative_to(root.resolve()): + raise ValidationError("input escapes repository") + return path + + +def fixture_hashes(contract: dict, repo: Path) -> dict: + result = {} + for name in contract["smoke_inputs"]: + if name == "in-memory deterministic 2x3 uint8 array" and contract["tool_id"] == "aicsimageio_extra": + continue # The reviewed command itself constructs this input. + result[name] = sha256_file(contained(repo, name)) + if contract.get("fixture_sha256") and result != contract["fixture_sha256"]: + raise ValidationError("declared fixture hashes differ from source") + return result + + +def load_contract(repo: Path, tool: str) -> dict: + if tool not in TOOLS: + raise ValidationError("tool outside exact approved canary") + value = json.loads(contained(repo, f"validation-contracts/schema-v1/{tool}.json").read_text()) + validate_contract(value, repo) + if value["tool_id"] != tool or value["contract_status"] != "CONTRACT_READY": + raise ValidationError("wrong or blocked contract") + if value["expected_architectures"] != ["amd64", "arm64"] or any( + value[key] is not True for key in ("native_amd64_expected", "native_arm64_expected") + ): + raise ValidationError("both native architectures must be expected") + if value["runtime_network_required"] is not False: + raise ValidationError("only network-free contracts are approved") + for field in ("version_command", "smoke_command"): + if not isinstance(value[field], list) or not value[field] or any( + not isinstance(arg, str) or not arg or "\x00" in arg for arg in value[field] + ): + raise ValidationError("commands must be nonempty argument arrays") + if value.get("version_output_source") not in ("stdout", "stderr", "either") or not value.get("version_observation_parser"): + raise ValidationError("missing version stream contract") + fixture_hashes(value, repo) + return value + + +def plan_hash(plan: dict) -> str: + return hashlib.sha256(canonical_json_bytes({k: v for k, v in plan.items() if k != "plan_sha256"})).hexdigest() + + +def make_plan(repo: Path, commit: str) -> dict: + if not re.fullmatch(r"[0-9a-f]{40}", commit): + raise ValidationError("source must be a full Git SHA") + entries = [] + packages = set() + for tool in TOOLS: + contract = load_contract(repo, tool) + if contract["package"] in packages: + raise ValidationError("package collision") + packages.add(contract["package"]) + for arch, runner in RUNNERS.items(): + entries.append({"tool_id": tool, "arch": arch, "runner": runner, + "contract_sha256": contract["validation_contract_sha256"], + "dockerfile_sha256": contract["dockerfile_sha256"], + "fixture_sha256": fixture_hashes(contract, repo)}) + value = {"validation_plan_schema_version": 1, "source_commit": commit, + "publication_authorized": False, "candidate_count": 14, "entries": entries} + value["plan_sha256"] = plan_hash(value) + return value + + +def validate_plan(plan: dict, repo: Path, expected_hash: str) -> None: + if not isinstance(plan, dict) or plan.get("plan_sha256") != expected_hash or plan_hash(plan) != expected_hash: + raise ValidationError("plan hash mismatch") + if plan.get("publication_authorized") is not False or type(plan.get("validation_plan_schema_version")) is not int: + raise ValidationError("invalid authorization or schema type") + if make_plan(repo, plan["source_commit"]) != plan: + raise ValidationError("plan membership or current source evidence mismatch") + + +def select_entry(plan: dict, repo: Path, digest: str, tool: str, arch: str) -> tuple[dict, dict]: + validate_plan(plan, repo, digest) + matches = [entry for entry in plan["entries"] if (entry["tool_id"], entry["arch"]) == (tool, arch)] + if len(matches) != 1: + raise ValidationError("candidate missing or duplicated") + return matches[0], load_contract(repo, tool) + + +def write_json(path: Path, value: dict) -> None: + path.write_text(json.dumps(value, sort_keys=True, indent=2) + "\n") + + +class Commands: + """Bounded argv execution with durable logs, no shell interpolation.""" + + def __init__(self, output: Path): + self.output = output / "commands" + self.output.mkdir(parents=True) + self.count = 0 + + def __call__(self, argv: list[str], *, timeout: int = 60) -> dict: + self.count += 1 + stem = self.output / f"{self.count:03}" + start = time.monotonic() + timed_out = False + with stem.with_suffix(".stdout").open("w") as stdout, stem.with_suffix(".stderr").open("w") as stderr: + process = subprocess.Popen(argv, stdout=stdout, stderr=stderr, text=True, start_new_session=True) + try: + returncode = process.wait(timeout=timeout) + except subprocess.TimeoutExpired: + timed_out = True + os.killpg(process.pid, signal.SIGKILL) + returncode = process.wait() + result = {"argv": argv, "returncode": returncode, "elapsed_seconds": time.monotonic() - start, "timed_out": timed_out} + write_json(stem.with_suffix(".json"), result) + if returncode != 0 or timed_out: + raise ValidationError(f"command failed; see {stem.name} logs") + for stream in ("stdout", "stderr"): + path = stem.with_suffix(f".{stream}") + if path.stat().st_size > 1024 * 1024: + raise ValidationError("command evidence exceeds 1 MiB") + result[stream] = path.read_text() + return result + + +EXECUTABLE_PROBE = r'''set -eu +p=$(command -v "$1") +p=$(readlink -f "$p") +printf 'executable=%s\n' "$p" +magic=$(od -An -t u1 -N4 "$p" | tr -s ' ' | sed 's/^ //;s/ $//') +if test "$magic" != '127 69 76 70'; then + IFS= read -r line < "$p" + case "$line" in '#!'*) line=${line#\#!} ;; *) exit 1 ;; esac + set -- $line + case "$1" in */env) shift ;; esac + test "$#" = 1 + p=$(command -v "$1") + p=$(readlink -f "$p") + printf 'interpreter=%s\n' "$p" + magic=$(od -An -t u1 -N4 "$p" | tr -s ' ' | sed 's/^ //;s/ $//') +fi +test "$magic" = '127 69 76 70' +printf 'machine=' +od -An -t u1 -j18 -N2 "$p" | tr -s ' ' | sed 's/^ //;s/ $//' +''' + + +def validate_executable(result: dict, arch: str) -> dict: + text = result["stdout"] + if not re.search(r"(?m)^executable=/[^\r\n]+$", text): + raise ValidationError("missing executable path") + machines = re.findall(r"(?m)^machine=([0-9]+) ([0-9]+)$", text) + if machines != [{"amd64": ("62", "0"), "arm64": ("183", "0")}[arch]]: + raise ValidationError("executable/interpreter ELF architecture mismatch") + return {"status": "PASS", "architecture": arch, "output": text} + + +def smoke_result(contract: dict, repo: Path, work: Path, result: dict) -> dict: + if result["returncode"] != 0 or result["elapsed_seconds"] > 60: + raise ValidationError("failed or unbounded smoke") + tool = contract["tool_id"] + if tool == "alevin_fry": + return validate_smoke_outputs(contract, repo, work, returncode=result["returncode"], elapsed_seconds=result["elapsed_seconds"]) + if tool == "agat_extra": + path = work / "smoke.gff3" + if not path.is_file() or not path.resolve().is_relative_to(work.resolve()) or path.stat().st_size > 1024 * 1024: + raise ValidationError("missing or unsafe AGAT output") + text = path.read_text() + rows = [line.split("\t") for line in text.splitlines() if line and not line.startswith("#")] + if not text.startswith("##gff-version 3") or not rows or any(len(row) != 9 for row in rows): + raise ValidationError("invalid AGAT GFF3 output") + ids = {attribute[3:] for row in rows for attribute in row[8].split(";") if attribute.startswith("ID=")} + if not {"gene1", "transcript1", "exon1", "exon2"}.issubset(ids): + raise ValidationError("AGAT output missing expected feature IDs") + elif tool in ("3ddna_extra", "abricate"): + text = result["stdout"] + "\n" + result["stderr"] + if any(marker not in text for marker in contract["smoke_expected_outputs"]): + raise ValidationError("smoke output markers missing") + elif tool not in ("accelerate", "aicsimageio_extra", "airr_extra"): + raise ValidationError("unsupported smoke semantics") + return {"status": "PASS", "scope": contract["smoke_success_condition"]} + + +def runtime_argv(phase: str, artifact: str, arch: str, work: Path, fixtures: Path, argv: list[str]) -> list[str]: + if phase == "oci": + prefix = ["docker", "run", "--rm", "--platform", f"linux/{arch}", "--network", "none", "--read-only", + "--tmpfs", "/tmp:rw", "--user", "0:0", "--volume", f"{work}:/work:rw", + "--volume", f"{fixtures}:/work/validation-contracts/fixtures:ro", "--workdir", "/work", + "--entrypoint", "micromamba", artifact] + elif phase == "sif": + prefix = ["sudo", "apptainer", "exec", "--cleanenv", "--containall", "--net", "--network", "none", + "--bind", f"{work}:/work:rw", "--bind", f"{fixtures}:/work/validation-contracts/fixtures:ro", + "--pwd", "/work", artifact, "micromamba"] + else: + raise ValidationError("unknown artifact phase") + # SIF cleanenv must not depend on the Docker entrypoint setting MAMBA_ROOT_PREFIX. + return prefix + ["run", "--no-capture-output", "--prefix", "/opt/conda", *argv] + + +def inspect_sif(metadata: dict, expected_labels: dict, arch: str) -> None: + try: + labels = metadata["data"]["attributes"]["labels"] + if labels["org.label-schema.build-arch"] != arch or any(labels.get(k) != v for k, v in expected_labels.items()): + raise ValidationError("SIF structured architecture or inherited binding labels mismatch") + except (KeyError, TypeError) as error: + raise ValidationError("missing structured SIF label metadata") from error + + +def validate_entry(record: dict, entry: dict, plan: dict, contract: dict, repo: Path) -> None: + if record.get("publication_authorized") is not False: + raise ValidationError("publication is not authorized") + if record.get("tool_id") != entry["tool_id"] or record.get("arch") != entry["arch"]: + raise ValidationError("result identity mismatch") + for key, value in (("source_commit", plan["source_commit"]), ("plan_sha256", plan["plan_sha256"]), + ("contract_sha256", entry["contract_sha256"]), ("dockerfile_sha256", entry["dockerfile_sha256"]), + ("fixture_sha256", entry["fixture_sha256"]), ("publication_authorized", False)): + if key not in record or record[key] != value: + raise ValidationError(f"result binding mismatch: {key}") + if record.get("status") != "PASS" or set(record.get("gates", {})) != set(GATES): + raise ValidationError("missing or failed terminal gates") + if any(value != "PASS" for value in record["gates"].values()): + raise ValidationError("failed gate") + validate_runner_evidence(record["runner_evidence"]) + if record["runner_evidence"]["target_architecture"] != entry["arch"]: + raise ValidationError("runner evidence target mismatch") + identity = identity_metadata(record["candidate"]) + if identity != record["candidate"]: + raise ValidationError("candidate canonical identity mismatch") + expected = {"tool_id": entry["tool_id"], "target_architecture": entry["arch"], "source_commit": plan["source_commit"], + "dockerfile_sha256": entry["dockerfile_sha256"], "sif_sha256": record["sif_sha256"], + "oci_source_identity": record["oci_identity"]} + if any(identity[key] != value for key, value in expected.items()): + raise ValidationError("candidate content identity mismatch") + if identity["build_inputs"]["validation_contract_sha256"] != entry["contract_sha256"]: + raise ValidationError("identity contract mismatch") + if identity["build_inputs"]["fixture_sha256"] != entry["fixture_sha256"] or identity["build_inputs"]["source_immutable_identity"] != contract["source_immutable_identity"]: + raise ValidationError("identity fixture or source mismatch") + if identity["scientific_version"] != contract["scientific_version"] or identity["expected_executable"] != contract["expected_executable"] or identity["dockerfile_path"] != contract["dockerfile_path"]: + raise ValidationError("candidate scientific contract mismatch") + if not re.fullmatch(r"[^\s]+@sha256:[0-9a-f]{64}", identity["base_image_identity"]) or not identity["base_image_identity"].startswith(contract["base_image_reference"] + "@"): + raise ValidationError("unbound base image identity") + if not re.fullmatch(r"sha256:[0-9a-f]{64}", record["oci_identity"]): + raise ValidationError("malformed OCI identity") + if not re.fullmatch(r"[0-9a-f]{64}", record["oci_archive_sha256"]): + raise ValidationError("missing archive identity") + if record["conversion"] != {"archive_sha256": record["oci_archive_sha256"], "sif_sha256": record["sif_sha256"], "transport": "docker-archive"}: + raise ValidationError("OCI/SIF conversion binding mismatch") + inspect_sif(record["sif_inspect"], binding_labels(contract, plan, entry, identity["base_image_identity"]), entry["arch"]) + for phase in ("oci", "sif"): + evidence = record[phase] + if evidence["runtime_architecture"] != {"amd64": "x86_64", "arm64": "aarch64"}[entry["arch"]]: + raise ValidationError("phase runtime architecture mismatch") + if validate_executable({"stdout": evidence["executable"]["output"]}, entry["arch"]) != evidence["executable"]: + raise ValidationError("phase executable mismatch") + raw = evidence["version_raw"] + if raw["argv"][-len(contract["version_command"]):] != contract["version_command"]: + raise ValidationError("version command drift") + if raw["timed_out"] is not False or not 0 <= raw["elapsed_seconds"] <= 60: + raise ValidationError("version duration mismatch") + checked = validate_version_evidence(contract, repo, stdout=raw["stdout"], stderr=raw["stderr"], returncode=raw["returncode"]) + if checked != evidence["version"] or identity["version_evidence"] != checked["observed_version"]: + raise ValidationError("phase version evidence mismatch") + smoke = evidence["smoke_raw"] + if smoke["argv"][-len(contract["smoke_command"]):] != contract["smoke_command"] or type(smoke["returncode"]) is not int or smoke["returncode"] != 0 or smoke["timed_out"] is not False or not 0 <= smoke["elapsed_seconds"] <= 60: + raise ValidationError("phase smoke command, status or duration mismatch") + if contract["tool_id"] == "alevin_fry": + if evidence["smoke"].get("smoke_outputs_verified") is not True or evidence["smoke"].get("contract_sha256") != entry["contract_sha256"]: + raise ValidationError("missing bound numerical smoke evidence") + elif evidence["smoke"].get("status") != "PASS" or evidence["smoke"].get("scope") != contract["smoke_success_condition"]: + raise ValidationError("missing smoke result") + if record["sbom_status"] != "SBOM_OPTIONAL_NOT_GENERATED": + raise ValidationError("unexpected SBOM semantics") + + +def reconcile(plan: dict, repo: Path, digest: str, records: list[dict], evidence_roots: dict) -> dict: + validate_plan(plan, repo, digest) + keys = [(record.get("tool_id"), record.get("arch")) for record in records] + expected = [(entry["tool_id"], entry["arch"]) for entry in plan["entries"]] + if len(keys) != 14 or len(set(keys)) != 14 or set(keys) != set(expected): + raise ValidationError("terminal matrix missing, duplicate or unexpected entries") + if set(evidence_roots) != set(expected): + raise ValidationError("missing or unexpected evidence directories") + for entry in plan["entries"]: + key = (entry["tool_id"], entry["arch"]) + record = records[keys.index(key)] + contract = load_contract(repo, entry["tool_id"]) + validate_entry(record, entry, plan, contract, repo) + for phase in ("oci", "sif"): + # Recheck actual downloaded output, not just an asserted PASS flag. + checked = smoke_result(contract, repo, evidence_roots[key] / f"work-{phase}", record[phase]["smoke_raw"]) + if checked != record[phase]["smoke"]: + raise ValidationError("retained smoke evidence differs from result") + return {"status": "PASS", "native_validated_entries": 14, "plan_sha256": digest, + "publication_authorized": False, "release_complete": False} + + +def binding_labels(contract: dict, plan: dict, entry: dict, base_identity: str) -> dict: + return {"org.omnibioai.tool": entry["tool_id"], "org.omnibioai.source-commit": plan["source_commit"], + "org.omnibioai.dockerfile-sha256": entry["dockerfile_sha256"], "org.omnibioai.target-platform": f"linux/{entry['arch']}", + "org.omnibioai.validation-contract-sha256": entry["contract_sha256"], "org.omnibioai.base-identity": base_identity} + + +def execute(plan: dict, repo: Path, digest: str, tool: str, arch: str, output: Path, commands=None) -> dict: + entry, contract = select_entry(plan, repo, digest, tool, arch) + output = output.resolve() + if output.is_relative_to(repo.resolve()) or output.exists(): + raise ValidationError("use a new isolated output directory outside the repository") + output.mkdir(parents=True) + record = {"tool_id": tool, "arch": arch, "source_commit": plan["source_commit"], "plan_sha256": digest, + "contract_sha256": entry["contract_sha256"], "dockerfile_sha256": entry["dockerfile_sha256"], + "fixture_sha256": entry["fixture_sha256"], "publication_authorized": False, "status": "FAIL", "gates": {}} + run = commands or Commands(output) + try: + if run(["git", "-C", str(repo), "rev-parse", "HEAD"])["stdout"].strip() != plan["source_commit"]: + raise ValidationError("checkout source SHA differs from plan") + if run(["git", "-C", str(repo), "status", "--porcelain", "--untracked-files=no"])["stdout"].strip(): + raise ValidationError("tracked checkout changes must be reviewed before native execution") + runner = {"runner": {"os": os.environ.get("RUNNER_OS"), "arch": os.environ.get("RUNNER_ARCH")}, + "uname_m": run(["uname", "-m"])["stdout"].strip(), "target_architecture": arch, "execution_mode": "NATIVE"} + validate_runner_evidence(runner) + record["runner_evidence"] = runner + record["gates"]["runner"] = "PASS" + base = contract["base_image_reference"] + base_identity = base + "@" + extract_base_digest(run(["docker", "buildx", "imagetools", "inspect", base])["stdout"]) + source = contained(repo, contract["dockerfile_path"]).read_text() + if len(re.findall(r"(?m)^FROM ", source)) != 1 or f"FROM {base}\n" not in source: + raise ValidationError("unreviewed Dockerfile base syntax") + rendered = output / "Dockerfile.bound" + rendered.write_text(source.replace(f"FROM {base}\n", f"FROM {base_identity}\n", 1)) + image = f"omnibioai-native-validation/{tool}:{plan['source_commit']}-{arch}" + labels = binding_labels(contract, plan, entry, base_identity) + flags = [arg for k, v in labels.items() for arg in ("--label", f"{k}={v}")] + run(["docker", "buildx", "build", "--platform", f"linux/{arch}", "--file", str(rendered), "--tag", image, *flags, "--load", str(repo)], timeout=3600) + archive = output / "oci-image.tar" + run(["docker", "save", "--output", str(archive), image], timeout=300) + oci = archive_identity(archive, tool, arch, plan["source_commit"], entry["dockerfile_sha256"]) + record.update(oci) + record["gates"]["oci_identity"] = "PASS" + sif = output / "validated.sif" + fixtures = (repo / "validation-contracts/fixtures").resolve() + for phase, artifact in (("oci", oci["oci_identity"]), ("sif", str(sif))): + if phase == "sif": + if sha256_file(archive) != oci["oci_archive_sha256"]: + raise ValidationError("archive changed before conversion") + run(["sudo", "apptainer", "build", "--disable-cache", "--arch", arch, str(sif), f"docker-archive:{archive}"], timeout=1800) + metadata = json.loads(run(["sudo", "apptainer", "inspect", "--json", "--labels", str(sif)])["stdout"]) + inspect_sif(metadata, labels, arch) + record["sif_inspect"] = metadata + record["gates"]["sif_structure"] = "PASS" + work = output / f"work-{phase}" + (work / "validation-contracts/fixtures").mkdir(parents=True) + def invoke(argv): + return run(runtime_argv(phase, artifact, arch, work, fixtures, argv), timeout=60) + machine = invoke(["uname", "-m"])["stdout"].strip() + if machine != {"amd64": "x86_64", "arm64": "aarch64"}[arch]: + raise ValidationError("artifact runtime architecture mismatch") + executable = validate_executable(invoke(["/bin/sh", "-c", EXECUTABLE_PROBE, "probe", contract["expected_executable"]]), arch) + version_raw = invoke(contract["version_command"]) + version = validate_version_evidence(contract, repo, stdout=version_raw["stdout"], stderr=version_raw["stderr"], returncode=version_raw["returncode"]) + smoke_raw = invoke(contract["smoke_command"]) + smoke = smoke_result(contract, repo, work, smoke_raw) + record[phase] = {"runtime_architecture": machine, "executable": executable, "version": version, "version_raw": version_raw, + "smoke": smoke, "smoke_raw": smoke_raw} + for gate in ("architecture", "executable", "version", "smoke"): + record["gates"][f"{phase}_{gate}"] = "PASS" + if sha256_file(archive) != oci["oci_archive_sha256"] or record["oci"]["version"]["observed_version"] != record["sif"]["version"]["observed_version"]: + raise ValidationError("OCI/SIF binding mismatch") + record["sif_sha256"] = sha256_file(sif) + record["conversion"] = {"archive_sha256": oci["oci_archive_sha256"], "sif_sha256": record["sif_sha256"], "transport": "docker-archive"} + record["gates"]["oci_sif_binding"] = "PASS" + record["sbom_status"] = "SBOM_OPTIONAL_NOT_GENERATED" + candidate = {"schema_version": "1", "tool_id": tool, "scientific_version": contract["scientific_version"], "target_architecture": arch, + "source_commit": plan["source_commit"], "dockerfile_path": contract["dockerfile_path"], "dockerfile_sha256": entry["dockerfile_sha256"], + "base_image_identity": base_identity, "oci_source_identity": oci["oci_identity"], "sif_sha256": record["sif_sha256"], + "expected_executable": contract["expected_executable"], "version_evidence": record["oci"]["version"]["observed_version"], + "smoke_status": "PASS", "provenance_status": "PASS", "build_inputs": {"validation_contract_sha256": entry["contract_sha256"], + "fixture_sha256": entry["fixture_sha256"], "source_immutable_identity": contract["source_immutable_identity"]}} + record["candidate"] = identity_metadata(candidate) + record["gates"]["identity"] = "PASS" + record["gates"]["provenance"] = "PASS" + record["status"] = "PASS" + validate_entry(record, entry, plan, contract, repo) + return record + except Exception as error: + record["status"] = "FAIL" + record["error"] = f"{type(error).__name__}: {error}" + raise + finally: + write_json(output / "entry.json", record) + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=("plan", "execute", "reconcile")) + parser.add_argument("--repo", type=Path, default=Path.cwd()) + parser.add_argument("--source-commit") + parser.add_argument("--plan", type=Path, required=True) + parser.add_argument("--plan-sha256") + parser.add_argument("--output", type=Path) + parser.add_argument("--records", type=Path) + parser.add_argument("--tool", choices=TOOLS) + parser.add_argument("--arch", choices=tuple(RUNNERS)) + parser.add_argument("--authorize-native-execution", action="store_true") + parser.add_argument("--github-output", type=Path) + args = parser.parse_args(argv) + try: + if args.command == "plan": + plan = make_plan(args.repo, args.source_commit or "") + write_json(args.plan, plan) + if args.github_output: + with args.github_output.open("a") as output: + output.write(f"matrix={json.dumps(plan['entries'], separators=(',', ':'))}\nplan_sha256={plan['plan_sha256']}\n") + else: + plan = json.loads(args.plan.read_text()) + if args.command == "execute": + if not args.authorize_native_execution or not args.output or not args.tool or not args.arch: + raise ValidationError("explicit native execution authorization and exact entry/output required") + execute(plan, args.repo, args.plan_sha256, args.tool, args.arch, args.output) + else: + if not args.records or not args.output: + raise ValidationError("records and summary output required") + paths = sorted(args.records.rglob("entry.json")) + records = [json.loads(path.read_text()) for path in paths] + roots = {(record["tool_id"], record["arch"]): path.parent for record, path in zip(records, paths)} + write_json(args.output, reconcile(plan, args.repo, args.plan_sha256, records, roots)) + return 0 + except (ValueError, OSError, TypeError, KeyError) as error: + print(f"FAIL: {error}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validation_contracts.py b/scripts/validation_contracts.py index 871aeb7..55a06a5 100644 --- a/scripts/validation_contracts.py +++ b/scripts/validation_contracts.py @@ -8,6 +8,7 @@ from __future__ import annotations import argparse +import copy import csv import hashlib import json @@ -285,6 +286,36 @@ def _runtime_requirements(text: str) -> dict[str, bool]: } +def _reviewed_canary_override( + repo_root: Path, tool_id: str, dockerfile_sha256: str +) -> dict[str, Any] | None: + """Load a reviewed source-pinning contract only when its source is unchanged. + + The source-pinning canary contains evidence that cannot be derived from a + generic Dockerfile scan (tool-specific parsers, smoke fixtures, and + upstream artifact identities). Those contracts are explicit reviewed + inputs, not registry discoveries. A Dockerfile hash mismatch deliberately + disables the override and lets normal fail-closed generation classify the + tool as blocked until the evidence is refreshed. + """ + + path = repo_root / "validation-contracts" / "schema-v1" / f"{tool_id}.json" + if not path.is_file(): + return None + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + if not isinstance(value, dict) or "pinning" not in value: + return None + if value.get("dockerfile_sha256") != dockerfile_sha256: + return None + pinning = value.get("pinning") + if not isinstance(pinning, dict) or pinning.get("pinning_canary_schema_version") != 1: + return None + return copy.deepcopy(value) + + def _status_for(blocker_codes: Sequence[str]) -> str: unique = sorted(set(blocker_codes)) if not unique: @@ -301,6 +332,13 @@ def generate_contract(row: Mapping[str, str], repo_root: Path) -> dict[str, Any] if not dockerfile.is_file(): raise ContractError(f"{tool_id}: missing Dockerfile {dockerfile_path}") raw = dockerfile.read_bytes() + dockerfile_sha256 = sha256_bytes(raw) + reviewed_override = _reviewed_canary_override(repo_root, tool_id, dockerfile_sha256) + if reviewed_override is not None: + # Validate the reviewed artifact against the current source before it + # participates in a newly generated inventory. This preserves the + # exact evidence-backed contract while preventing stale source claims. + return reviewed_override text = raw.decode("utf-8") lines = _docker_lines(text) evidence: list[dict[str, Any]] = [ @@ -381,7 +419,7 @@ def generate_contract(row: Mapping[str, str], repo_root: Path) -> dict[str, Any] "tool_id": tool_id, "display_name": tool_id, "dockerfile_path": dockerfile_path, - "dockerfile_sha256": sha256_bytes(raw), + "dockerfile_sha256": dockerfile_sha256, "classification": row["repository_classification"], "eligibility": { "targeted": True, diff --git a/scripts/validation_smoke.py b/scripts/validation_smoke.py new file mode 100644 index 0000000..e9fe5ac --- /dev/null +++ b/scripts/validation_smoke.py @@ -0,0 +1,220 @@ +"""Read-only assertions for contract-defined smoke outputs; never executes tools.""" +from __future__ import annotations + +import json +import math +import re +from pathlib import Path +from typing import Any, Mapping + +from scripts.validation_contracts import ContractError, sha256_file, validate_contract + + +class SmokeValidationError(ValueError): + """Smoke evidence is missing, malformed, or disagrees with its contract.""" + + +def validate_version_evidence( + contract: Mapping[str, Any], repo_root: Path, + *, stdout: str, stderr: str, returncode: int, +) -> dict[str, str]: + """Require one tool-version observation and the existing exact parser. + + The observation parser detects conflicting banners; it does not replace + or relax the strict expected-version parser. No command is executed here. + """ + try: + validate_contract(contract, repo_root) + if contract["contract_status"] != "CONTRACT_READY": + raise SmokeValidationError("blocked contract cannot pass version validation") + if type(returncode) is not int or returncode != 0: + raise SmokeValidationError("version command did not exit successfully") + if not isinstance(stdout, str) or not isinstance(stderr, str): + raise SmokeValidationError("version streams must be text") + if len(stdout.encode()) + len(stderr.encode()) > 1024 * 1024: + raise SmokeValidationError("version evidence exceeds 1 MiB limit") + source = contract["version_output_source"] + streams = {"stdout": stdout, "stderr": stderr} + if source not in ("stdout", "stderr", "either"): + raise SmokeValidationError("unknown version output source") + names = list(streams) if source == "either" else [source] + strict = contract["version_parser"] + if not isinstance(strict, str) or not strict.startswith("regex:"): + raise SmokeValidationError("unsupported strict version parser") + strict_parser = re.compile(strict.removeprefix("regex:")) + observation = contract["version_observation_parser"] + if not isinstance(observation, str): + raise SmokeValidationError("observation parser must be text") + observation_parser = re.compile(observation) + if strict_parser.groups != 1 or observation_parser.groups != 1: + raise SmokeValidationError("version parsers must capture one value") + observations = [ + (name, match.group(1)) for name in names + for match in observation_parser.finditer(streams[name]) + ] + if len(observations) != 1: + raise SmokeValidationError("missing, duplicate, or conflicting version observations") + name, observed = observations[0] + if observed != contract["version_expected_value"]: + raise SmokeValidationError("observed tool version mismatch") + matches = list(strict_parser.finditer(streams[name])) + if len(matches) != 1 or matches[0].group(1) != observed: + raise SmokeValidationError("strict version parser rejected evidence") + return { + "tool_id": contract["tool_id"], "observed_version": observed, + "evidence_stream": name, + "contract_sha256": contract["validation_contract_sha256"], + } + except (OSError, KeyError, TypeError, ValueError, ContractError, re.error) as error: + if isinstance(error, SmokeValidationError): + raise + raise SmokeValidationError(f"invalid version evidence: {error}") from error + + +def _path(root: Path, relative: str) -> Path: + if not isinstance(relative, str) or not relative or Path(relative).is_absolute(): + raise SmokeValidationError("expected a nonempty relative path") + root = root.resolve() + path = (root / relative).resolve() + if not path.is_relative_to(root): + raise SmokeValidationError("path escapes verification root") + return path + + +def _text(path: Path) -> str: + if path.stat().st_size > 1024 * 1024: + raise SmokeValidationError("smoke evidence exceeds 1 MiB limit") + return path.read_text(encoding="utf-8") + + +def _shape(value: Any) -> tuple[int, int]: + if not isinstance(value, list) or len(value) != 2: + raise SmokeValidationError("invalid matrix shape") + if any(type(n) is not int or not 0 < n <= 100000 for n in value): + raise SmokeValidationError("invalid matrix dimensions") + return value[0], value[1] + + +def _number(value: Any) -> float: + if isinstance(value, bool): + raise SmokeValidationError("boolean count is invalid") + number = float(value) + if not math.isfinite(number) or number < 0: + raise SmokeValidationError("counts must be finite and nonnegative") + return number + + +def _entries(rows: list, shape: tuple[int, int]) -> dict[tuple[int, int], float]: + result = {} + for entry in rows: + if len(entry) != 3: + raise SmokeValidationError("matrix entry must have three fields") + row, column, value = entry + if type(row) is not int or type(column) is not int: + raise SmokeValidationError("matrix coordinates must be integers") + if not (1 <= row <= shape[0] and 1 <= column <= shape[1]): + raise SmokeValidationError("matrix coordinate out of bounds") + if (row, column) in result: + raise SmokeValidationError("duplicate matrix coordinate") + result[row, column] = _number(value) + return result + + +def _matrix(path: Path) -> tuple[tuple[int, int], dict[tuple[int, int], float]]: + lines = _text(path).splitlines() + if not lines or lines[0] not in ( + "%%MatrixMarket matrix coordinate real general", + "%%MatrixMarket matrix coordinate integer general", + ): + raise SmokeValidationError("unsupported MatrixMarket header") + data = [line.split() for line in lines[1:] if line.strip() and not line.startswith("%")] + if not data or len(data[0]) != 3: + raise SmokeValidationError("missing MatrixMarket size line") + row_count, column_count, nnz = map(int, data[0]) + shape = _shape([row_count, column_count]) + if nnz < 0 or len(data) - 1 != nnz: + raise SmokeValidationError("MatrixMarket entry count mismatch") + entries = [] + for fields in data[1:]: + if len(fields) != 3: + raise SmokeValidationError("matrix entry must have three fields") + if " integer " in lines[0]: + int(fields[2]) # Reject fractional values under an integer header. + entries.append([int(fields[0]), int(fields[1]), fields[2]]) + return shape, _entries(entries, shape) + + +def validate_smoke_outputs( + contract: Mapping[str, Any], repo_root: Path, workspace: Path, + *, returncode: int, elapsed_seconds: float, +) -> dict[str, Any]: + """Validate outputs of an already-executed smoke; not native/release proof. + + The future execution adapter must supply measured exit status/duration and + an isolated workspace. This function does not run commands or write files. + """ + try: + validate_contract(contract, repo_root) + if contract["contract_status"] != "CONTRACT_READY": + raise SmokeValidationError("blocked contract cannot pass smoke validation") + spec = contract["smoke_assertions"] + if spec["kind"] != "matrix_market_gene_counts": + raise SmokeValidationError("unsupported smoke assertion kind") + timeout = spec["timeout_seconds"] + if type(timeout) is not int or timeout <= 0: + raise SmokeValidationError("invalid smoke timeout") + if type(returncode) is not int or returncode != 0: + raise SmokeValidationError("smoke command did not exit successfully") + if isinstance(elapsed_seconds, bool) or not math.isfinite(elapsed_seconds) or not 0 <= elapsed_seconds <= timeout: + raise SmokeValidationError("smoke duration is invalid or exceeds timeout") + hashes = contract["fixture_sha256"] + if set(hashes) != set(contract["smoke_inputs"]): + raise SmokeValidationError("fixture hash set mismatch") + for relative, digest in hashes.items(): + if sha256_file(_path(repo_root, relative)) != digest: + raise SmokeValidationError("fixture SHA256 mismatch") + expected_path = spec["expected_fixture"] + if expected_path not in hashes: + raise SmokeValidationError("expected results must be hash-bound") + expected = json.loads(_text(_path(repo_root, expected_path))) + shape = _shape(expected["shape"]) + wanted = _entries(expected["entries"], shape) + tolerance = _number(expected["tolerance"]) + if not 0 < tolerance <= 0.0001: + raise SmokeValidationError("unreviewed numerical tolerance") + directory = spec["output_directory"] + names = ["quants_mat.mtx", "quants_mat_rows.txt", "quants_mat_cols.txt"] + paths = [f"{directory}/{name}" for name in names] + if paths != contract["smoke_expected_outputs"]: + raise SmokeValidationError("output declarations disagree with assertions") + observed_shape, observed = _matrix(_path(workspace, paths[0])) + if observed_shape != shape or observed.keys() != wanted.keys(): + raise SmokeValidationError("matrix shape or coordinates mismatch") + if any(abs(observed[key] - value) > tolerance for key, value in wanted.items()): + raise SmokeValidationError("gene counts mismatch") + for path, field, count in zip(paths[1:], ("rows", "columns"), shape): + labels = expected[field] + if not isinstance(labels, list) or len(labels) != count or len(set(labels)) != count: + raise SmokeValidationError("invalid expected labels") + if any(not isinstance(label, str) or not label for label in labels): + raise SmokeValidationError("invalid expected label") + if _text(_path(workspace, path)).splitlines() != labels: + raise SmokeValidationError("matrix labels mismatch") + totals = expected["row_totals"] + if len(totals) != shape[0]: + raise SmokeValidationError("row total count mismatch") + for row, total in enumerate(totals, 1): + total = _number(total) + for matrix in (wanted, observed): + if abs(sum(value for (r, _), value in matrix.items() if r == row) - total) > tolerance: + raise SmokeValidationError("row total mismatch") + return { + "assertion_kind": spec["kind"], "shape": list(shape), + "contract_sha256": contract["validation_contract_sha256"], + "output_sha256": {path: sha256_file(_path(workspace, path)) for path in paths}, + "smoke_outputs_verified": True, + } + except (OSError, UnicodeError, KeyError, TypeError, ValueError, ContractError) as error: + if isinstance(error, SmokeValidationError): + raise + raise SmokeValidationError(f"invalid smoke evidence: {error}") from error diff --git a/tests/test_contract_native_validation.py b/tests/test_contract_native_validation.py new file mode 100644 index 0000000..33fb907 --- /dev/null +++ b/tests/test_contract_native_validation.py @@ -0,0 +1,320 @@ +"""Synthetic adapter tests: no container or scientific tool is executed.""" +import copy +import io +import json +from pathlib import Path +import shutil +import subprocess +import tarfile + +import pytest +import yaml + +from scripts import contract_native_validation as native + +REPO = Path(__file__).resolve().parents[1] +COMMIT = "a" * 40 +BANNERS = {"3ddna_extra": "version: 190716\nUSAGE\n3D de novo assembly\n", "abricate": "abricate 1.4.0\n", + "accelerate": "- `Accelerate` version: 1.15.0\n", "agat_extra": "v1.7.0\n", + "aicsimageio_extra": "4.14.0\n", "airr_extra": "2.0.0\n", "alevin_fry": "alevin-fry 0.18.3\n"} + + +@pytest.fixture(autouse=True) +def forbid_processes(monkeypatch): + def forbidden(*args, **kwargs): + pytest.fail("external execution forbidden in unit tests") + monkeypatch.setattr(native.subprocess, "Popen", forbidden) + + +@pytest.fixture +def plan(): + return native.make_plan(REPO, COMMIT) + + +def test_plan_determinism_and_exact_membership(plan): + assert plan == native.make_plan(REPO, COMMIT) + assert len(plan["entries"]) == len({(e["tool_id"], e["arch"]) for e in plan["entries"]}) == 14 + assert {e["tool_id"] for e in plan["entries"]} == set(native.TOOLS) + assert {e["runner"] for e in plan["entries"]} == set(native.RUNNERS.values()) + assert plan["publication_authorized"] is False + + +@pytest.mark.parametrize("tool", ["bedtools", "admixtools", "afterqc_extra", "alevinqc_extra", "unknown"]) +def test_scope_rejection(tool): + with pytest.raises(native.ValidationError): + native.load_contract(REPO, tool) + + +@pytest.mark.parametrize("change", [ + lambda p: p["entries"].pop(), lambda p: p["entries"].append(p["entries"][0]), + lambda p: p["entries"][0].update(arch="ppc64le"), lambda p: p["entries"][0].update(tool_id="bedtools"), + lambda p: p.update(publication_authorized=True), lambda p: p["entries"][0].update(contract_sha256="b" * 64), + lambda p: p["entries"][0].update(fixture_sha256={"unknown": "a" * 64}), + lambda p: p["entries"][0].update(runner="ubuntu-latest"), +]) +def test_changed_plan_rejected_even_after_rehash(plan, change): + change(plan) + plan["plan_sha256"] = native.plan_hash(plan) + with pytest.raises(native.ValidationError): + native.validate_plan(plan, REPO, plan["plan_sha256"]) + + +def test_expected_plan_hash_is_mandatory(plan): + with pytest.raises(native.ValidationError): + native.validate_plan(plan, REPO, "b" * 64) + + +def test_execute_requires_explicit_flag(plan, tmp_path): + path = tmp_path / "plan.json" + native.write_json(path, plan) + assert native.main(["execute", "--repo", str(REPO), "--plan", str(path), "--plan-sha256", plan["plan_sha256"], + "--tool", "abricate", "--arch", "amd64", "--output", str(tmp_path / "out")]) == 1 + assert not (tmp_path / "out").exists() + + +class FakeCommands: + """Create synthetic artifacts and output; never invoke a process.""" + + def __init__(self, tool, arch, output): + self.tool, self.arch, self.output = tool, arch, output + self.contract = native.load_contract(REPO, tool) + self.calls, self.labels = [], {} + + def __call__(self, argv, *, timeout=60): + self.calls.append(argv) + text = "" + if argv[0] == "git": + text = COMMIT + "\n" if "rev-parse" in argv else "" + elif argv == ["uname", "-m"]: + text = "x86_64\n" if self.arch == "amd64" else "aarch64\n" + elif argv[:4] == ["docker", "buildx", "imagetools", "inspect"]: + text = "Name: base\nDigest: sha256:" + "b" * 64 + "\n" + elif argv[:3] == ["docker", "buildx", "build"]: + self.labels = dict(argv[i + 1].split("=", 1) for i, arg in enumerate(argv) if arg == "--label") + assert "--load" in argv and "--push" not in argv + elif argv[:2] == ["docker", "save"]: + config = json.dumps({"os": "linux", "architecture": self.arch, "config": {"Labels": self.labels}}).encode() + files = {"manifest.json": json.dumps([{"Config": "config.json", "Layers": []}]).encode(), "config.json": config} + with tarfile.open(self.output / "oci-image.tar", "w") as archive: + for name, data in files.items(): + member = tarfile.TarInfo(name) + member.size = len(data) + archive.addfile(member, io.BytesIO(data)) + elif argv[:3] == ["sudo", "apptainer", "build"]: + (self.output / "validated.sif").write_bytes(b"synthetic SIF; not a container") + elif argv[:3] == ["sudo", "apptainer", "inspect"]: + text = json.dumps({"data": {"attributes": {"labels": {**self.labels, "org.label-schema.build-arch": self.arch}}}}) + else: + assert argv[:2] == ["docker", "run"] or argv[:3] == ["sudo", "apptainer", "exec"] + command = argv[argv.index("/opt/conda") + 1:] + if command == ["uname", "-m"]: + text = "x86_64\n" if self.arch == "amd64" else "aarch64\n" + elif command[:2] == ["/bin/sh", "-c"]: + text = "executable=/opt/conda/bin/tool\ninterpreter=/opt/conda/bin/python\nmachine=" + ("62 0\n" if self.arch == "amd64" else "183 0\n") + elif command == self.contract["version_command"]: + text = BANNERS[self.tool] + elif command == self.contract["smoke_command"]: + text = "\n".join(self.contract["smoke_expected_outputs"]) + work = self.output / ("work-oci" if argv[0] == "docker" else "work-sif") + if self.tool == "agat_extra": + (work / "smoke.gff3").write_text("##gff-version 3\n" + "".join(f"chr1\tfixture\tgene\t1\t2\t.\t+\t.\tID={f}\n" for f in ("gene1", "transcript1", "exon1", "exon2"))) + elif self.tool == "alevin_fry": + directory = work / "smoke-output/alevin-infer" + directory.mkdir(parents=True) + (directory / "quants_mat.mtx").write_text("%%MatrixMarket matrix coordinate real general\n2 2 4\n1 1 7\n1 2 7\n2 1 3\n2 2 3\n") + fixture = REPO / "validation-contracts/fixtures/alevin-infer-tiny" + for name in ("quants_mat_rows.txt", "quants_mat_cols.txt"): + (directory / name).write_bytes((fixture / name).read_bytes()) + else: + pytest.fail(f"unexpected synthetic command {command}") + return {"argv": argv, "stdout": text, "stderr": "", "returncode": 0, "elapsed_seconds": 0.1, "timed_out": False} + + +def synthetic_run(plan, tool, arch, output, monkeypatch): + monkeypatch.setenv("RUNNER_OS", "Linux") + monkeypatch.setenv("RUNNER_ARCH", "X64" if arch == "amd64" else "ARM64") + fake = FakeCommands(tool, arch, output) + return native.execute(plan, REPO, plan["plan_sha256"], tool, arch, output, fake), fake + + +@pytest.mark.parametrize("tool", native.TOOLS) +@pytest.mark.parametrize("arch", native.RUNNERS) +def test_synthetic_pipeline_all_pairs(plan, tool, arch, tmp_path, monkeypatch): + output = tmp_path / f"{tool}-{arch}" + record, fake = synthetic_run(plan, tool, arch, output, monkeypatch) + assert record["status"] == "PASS" + assert record["publication_authorized"] is False + assert set(record["gates"]) == set(native.GATES) + assert record == json.loads((output / "entry.json").read_text()) + assert all("push" not in argv and "--push" not in argv for argv in fake.calls) + for argv in fake.calls: + if argv[:2] == ["docker", "run"]: + assert argv[argv.index("--network") + 1] == "none" + assert "--read-only" in argv + assert argv[argv.index("--entrypoint") + 2].startswith("sha256:") + + +@pytest.fixture +def records(plan, tmp_path, monkeypatch): + return [synthetic_run(plan, e["tool_id"], e["arch"], tmp_path / f"{e['tool_id']}-{e['arch']}", monkeypatch)[0] for e in plan["entries"]] + + +def evidence_roots(plan, tmp_path): + return {(e["tool_id"], e["arch"]): tmp_path / f"{e['tool_id']}-{e['arch']}" for e in plan["entries"]} + + +def test_complete_reconciliation(plan, records, tmp_path): + result = native.reconcile(plan, REPO, plan["plan_sha256"], list(reversed(records)), evidence_roots(plan, tmp_path)) + assert result["native_validated_entries"] == 14 + assert result["release_complete"] is False + + +@pytest.mark.parametrize("change", [ + lambda r: r.pop(), lambda r: r.append(r[0]), lambda r: r.__setitem__(1, copy.deepcopy(r[0])), + lambda r: r[0].update(tool_id="bedtools"), lambda r: r[0].update(status="FAIL"), + lambda r: r[0]["gates"].pop("oci_smoke"), lambda r: r[0]["gates"].update(sif_version="FAIL"), + lambda r: r[0].update(source_commit="b" * 40), lambda r: r[0].update(plan_sha256="b" * 64), + lambda r: r[0].update(sif_sha256="b" * 64), lambda r: r[0].update(publication_authorized=True), + lambda r: r[0]["runner_evidence"]["runner"].update(arch="ARM64"), + lambda r: r[0]["oci"]["version_raw"].update(stdout="wrong version"), + lambda r: r[0]["sif"]["smoke_raw"].update(returncode=1), + lambda r: r[0]["conversion"].update(archive_sha256="b" * 64), + lambda r: r[0]["sif_inspect"]["data"]["attributes"]["labels"].update({"org.label-schema.build-arch": "arm64"}), + lambda r: r[0]["candidate"]["build_inputs"].update(source_immutable_identity="mutable"), +]) +def test_terminal_negative_evidence(plan, records, change, tmp_path): + change(records) + with pytest.raises((native.ValidationError, ValueError)): + native.reconcile(plan, REPO, plan["plan_sha256"], records, evidence_roots(plan, tmp_path)) + + +def test_wrong_runner_fails_before_build_and_records_failure(plan, tmp_path, monkeypatch): + monkeypatch.setenv("RUNNER_OS", "Linux") + monkeypatch.setenv("RUNNER_ARCH", "ARM64") + output = tmp_path / "failure" + fake = FakeCommands("abricate", "amd64", output) + with pytest.raises(ValueError): + native.execute(plan, REPO, plan["plan_sha256"], "abricate", "amd64", output, fake) + assert json.loads((output / "entry.json").read_text())["status"] == "FAIL" + assert not any(argv[0] == "docker" for argv in fake.calls) + + +def test_workflow_static_safety(): + path = REPO / ".github/workflows/contract-native-canary-validation.yml" + workflow = yaml.load(path.read_text(), Loader=yaml.BaseLoader) + assert set(workflow["on"]) == {"workflow_dispatch"} + assert workflow["permissions"] == {"contents": "read"} + assert workflow["jobs"]["native-validation"]["needs"] == "selection" + assert workflow["jobs"]["native-validation"]["strategy"]["fail-fast"] == "false" + assert workflow["jobs"]["reconcile"]["needs"] == ["selection", "native-validation"] + for forbidden in ("packages: write", "oras", "docker login", "docker push", "--push", "sif_release", "setup-qemu", "publish=true"): + assert forbidden not in path.read_text() + + +def test_argv_boundaries_and_network(tmp_path): + command = ["python", "-c", "print('literal ; not a shell command')"] + for phase in ("oci", "sif"): + argv = native.runtime_argv(phase, "artifact", "arm64", tmp_path, tmp_path, command) + assert argv[-len(command):] == command + assert argv[argv.index("--network") + 1] == "none" + assert any(value.endswith(":ro") for value in argv) + + +@pytest.fixture +def repo_copy(tmp_path): + root = tmp_path / "repo" + (root / "dockerfiles").mkdir(parents=True) + (root / "validation-contracts/schema-v1").mkdir(parents=True) + shutil.copytree(REPO / "validation-contracts/fixtures", root / "validation-contracts/fixtures") + for tool in native.TOOLS: + for name in (f"dockerfiles/Dockerfile.{tool}", f"validation-contracts/schema-v1/{tool}.json"): + shutil.copyfile(REPO / name, root / name) + return root + + +@pytest.mark.parametrize("change", ["dockerfile", "contract", "fixture", "missing-fixture"]) +def test_stale_source_evidence_rejected(plan, repo_copy, change): + if change == "dockerfile": + (repo_copy / "dockerfiles/Dockerfile.abricate").write_text("FROM wrong\n") + elif change == "contract": + path = repo_copy / "validation-contracts/schema-v1/abricate.json" + value = json.loads(path.read_text()) + value["scientific_version"] = "wrong" + path.write_text(json.dumps(value)) + else: + path = repo_copy / "validation-contracts/fixtures/airr-tiny.tsv" + if change == "fixture": + path.write_text("wrong content") + else: + path.unlink() + with pytest.raises((ValueError, OSError)): + native.validate_plan(plan, repo_copy, plan["plan_sha256"]) + + +@pytest.mark.parametrize("tool,phase,filename", [ + ("agat_extra", "oci", "smoke.gff3"), + ("agat_extra", "sif", "smoke.gff3"), + ("alevin_fry", "oci", "smoke-output/alevin-infer/quants_mat.mtx"), + ("alevin_fry", "sif", "smoke-output/alevin-infer/quants_mat_rows.txt"), +]) +def test_retained_outputs_rechecked(plan, records, tmp_path, tool, phase, filename): + (tmp_path / f"{tool}-amd64" / f"work-{phase}" / filename).write_text("corrupt output") + with pytest.raises(ValueError): + native.reconcile(plan, REPO, plan["plan_sha256"], records, evidence_roots(plan, tmp_path)) + + +@pytest.mark.parametrize("failure", ["dirty-source", "source-sha", "command", "wrong-version", "conflicting-version", "smoke", "sif-metadata"]) +def test_pipeline_failures_are_terminal_not_repaired(plan, tmp_path, monkeypatch, failure): + monkeypatch.setenv("RUNNER_OS", "Linux") + monkeypatch.setenv("RUNNER_ARCH", "X64") + output = tmp_path / "failed" + fake = FakeCommands("abricate", "amd64", output) + def run(argv, **kwargs): + if failure == "command" and argv[:3] == ["docker", "buildx", "build"]: + raise native.ValidationError("synthetic build failure") + value = fake(argv, **kwargs) + if failure == "dirty-source" and argv[0] == "git" and "status" in argv: + value["stdout"] = " M Dockerfile\n" + elif failure == "source-sha" and argv[0] == "git" and "rev-parse" in argv: + value["stdout"] = "b" * 40 + elif argv[-2:] == ["abricate", "--version"] and failure in ("wrong-version", "conflicting-version"): + value["stdout"] = "abricate 9.9\n" + (value["stdout"] if failure == "conflicting-version" else "") + elif failure == "smoke" and argv[-2:] == ["abricate", "--check"]: + value["stdout"] = "dependency missing" + elif failure == "sif-metadata" and argv[:3] == ["sudo", "apptainer", "inspect"]: + value["stdout"] = "{}" + return value + with pytest.raises(ValueError): + native.execute(plan, REPO, plan["plan_sha256"], "abricate", "amd64", output, run) + record = json.loads((output / "entry.json").read_text()) + assert record["status"] == "FAIL" and record["error"] + assert sum(argv[:3] == ["docker", "buildx", "build"] for argv in fake.calls) <= 1 + + +@pytest.mark.parametrize("state", ["success", "nonzero", "timeout"]) +def test_process_logs_and_timeout_are_durable(tmp_path, monkeypatch, state): + killed = [] + class FakeProcess: + pid = 123456 + calls = 0 + def wait(self, timeout=None): + self.calls += 1 + if state == "timeout" and self.calls == 1: + raise subprocess.TimeoutExpired("fake", timeout) + return 0 if state == "success" else -9 + def popen(argv, **kwargs): + kwargs["stdout"].write("synthetic output") + kwargs["stderr"].write("synthetic diagnostic") + assert kwargs["start_new_session"] is True + return FakeProcess() + monkeypatch.setattr(native.subprocess, "Popen", popen) + monkeypatch.setattr(native.os, "killpg", lambda pid, signal: killed.append(pid)) + commands = native.Commands(tmp_path) + if state == "success": + assert commands(["fake"], timeout=1)["stdout"] == "synthetic output" + else: + with pytest.raises(native.ValidationError): + commands(["fake"], timeout=1) + record = json.loads((tmp_path / "commands/001.json").read_text()) + assert record["timed_out"] == (state == "timeout") + assert bool(killed) == (state == "timeout") diff --git a/tests/test_source_pinning_canary.py b/tests/test_source_pinning_canary.py index 79a26fd..b7c87dc 100644 --- a/tests/test_source_pinning_canary.py +++ b/tests/test_source_pinning_canary.py @@ -1,6 +1,7 @@ from __future__ import annotations import csv +import gzip import json import re from pathlib import Path @@ -31,6 +32,7 @@ "admixtools": ("admixtools", "8.0.2"), "agat_extra": ("agat", "1.7.0"), "aicsimageio_extra": ("aicsimageio", "4.14.0"), + "airr_extra": ("r-airr", "2.0.0"), "alevin_fry": ("alevin-fry", "0.18.3"), } PACKAGE_BUILDS = { @@ -40,10 +42,11 @@ "admixtools": {"h75d7a4a_0", "he9d944c_0"}, "agat_extra": {"pl5321hdfd78af_0"}, "aicsimageio_extra": {"pyhd8ed1ab_0"}, + "airr_extra": {"r45hc72bb7e_0"}, "alevin_fry": {"hd612981_0", "h6789a04_0"}, } -UNPINNED = {"afterqc_extra", "airr_extra", "alevinqc_extra"} -READY = {"3ddna_extra", "abricate", "accelerate", "agat_extra", "aicsimageio_extra"} +UNPINNED = {"afterqc_extra", "alevinqc_extra"} +READY = {"3ddna_extra", "abricate", "accelerate", "agat_extra", "aicsimageio_extra", "airr_extra", "alevin_fry"} VERSION_OUTPUTS = { "3ddna_extra": "version: 190716\n", "abricate": "abricate 1.4.0\n", @@ -51,6 +54,7 @@ "admixtools": "version: 701\n", "agat_extra": "v1.7.0\n", "aicsimageio_extra": "4.14.0\n", + "airr_extra": "2.0.0\n", "alevin_fry": "alevin-fry 0.18.3\n", } @@ -80,13 +84,13 @@ def test_selected_baseline_is_exactly_pinned(tool_id: str) -> None: @pytest.mark.parametrize("tool_id", sorted(UNPINNED)) -def test_unsupported_architecture_candidate_is_not_forced(tool_id: str) -> None: +def test_blocked_baseline_candidate_is_not_forced(tool_id: str) -> None: contract = load_contract(tool_id) assert contract["scientific_version"] is None assert contract["source_immutable_identity"] is None assert contract["pinning"]["new_reproducible_baseline_version"] is None assert contract["pinning"]["static_contract_status"] == "STATIC_CONTRACT_BLOCKED" - assert contract["native_arm64_expected"] is False + assert contract["native_arm64_expected"] is (tool_id == "afterqc_extra") @pytest.mark.parametrize("tool_id", sorted(VERSION_OUTPUTS)) @@ -158,3 +162,167 @@ def test_static_ready_set_does_not_claim_native_or_release_validation() -> None: assert contract["contract_status"] == "CONTRACT_READY" assert contract["pinning"]["native_build_validation_status"] == "NOT_RUN" assert contract["pinning"]["release_status"] == "NOT_PUBLISHED" + + +def test_admixtools_synthetic_fixture_has_consistent_eigenstrat_dimensions() -> None: + fixture = REPO / "validation-contracts/fixtures/admixtools-tiny" + individuals = [line.split() for line in (fixture / "tiny.ind").read_text().splitlines()] + snps = [line.split() for line in (fixture / "tiny.snp").read_text().splitlines()] + genotypes = (fixture / "tiny.geno").read_text().splitlines() + assert len(individuals) == 6 + assert all(len(row) == 3 and row[1] == "U" for row in individuals) + assert len({row[0] for row in individuals}) == len(individuals) + assert len(snps) == len(genotypes) == 24 + assert len({row[0] for row in snps}) == len(snps) + assert all(len(row) == 6 and len(set(row[4:])) == 2 for row in snps) + positions = [(int(row[1]), float(row[2]), int(row[3])) for row in snps] + assert positions == sorted(positions) + assert len(set(positions)) == len(positions) + assert {position[0] for position in positions} == {1, 2, 3, 4} + assert all(position[1] > 0 and position[2] > 0 for position in positions) + assert all(len(row) == len(individuals) and set(row) <= set("012") for row in genotypes) + assert all(len(set(row)) > 1 for row in genotypes) + target_columns = [index for index, row in enumerate(individuals) if row[2] == "TargetC"] + assert len(target_columns) == 2 + assert all(any(row[index] == "1" for row in genotypes) for index in target_columns) + + +def test_admixtools_smoke_candidate_references_only_repository_owned_inputs() -> None: + fixture = REPO / "validation-contracts/fixtures/admixtools-tiny" + parameters = dict( + line.split(": ", 1) for line in (fixture / "qp3pop.par").read_text().splitlines() + ) + assert parameters == { + "genotypename": "tiny.geno", "snpname": "tiny.snp", + "indivname": "tiny.ind", "popfilename": "tiny.pops", + } + assert all((fixture / name).is_file() for name in parameters.values()) + populations = (fixture / "tiny.pops").read_text().splitlines() + assert populations == ["SourceA SourceB TargetC"] + individuals = [line.split() for line in (fixture / "tiny.ind").read_text().splitlines()] + assert all(sum(row[2] == population for row in individuals) == 2 for population in populations[0].split()) + + +def test_admixtools_fixture_does_not_promote_unexecuted_smoke_or_license_gate() -> None: + contract = load_contract("admixtools") + assert contract["contract_status"] == "CONTRACT_BLOCKED_MULTIPLE_REASONS" + assert contract["smoke_command"] is None + assert contract["pinning"]["distribution_license_review"] == "REQUIRED_NONCOMMERCIAL_LICENSE" + assert contract["pinning"]["static_contract_status"] == "STATIC_CONTRACT_BLOCKED" + + +def test_admixtools_license_evidence_does_not_assume_distribution_permission() -> None: + contract = load_contract("admixtools") + review = contract["pinning"]["distribution_license_review_evidence"] + assert review["source_release"] == "v8.0.2" + assert review["intended_distribution_scope"] == "UNKNOWN" + assert review["permission_for_intended_distribution"] == "NOT_ESTABLISHED" + assert review["copyright_notice_retention_required"] is True + assert "DISTRIBUTION_LICENSE_REVIEW_REQUIRED" in contract["remediation"]["remaining_deficiency_flags"] + evidence = [row for row in contract["evidence"] if row["field"] == "distribution_license"] + assert len(evidence) == 1 + assert evidence[0]["quality"] == "DIRECT_UPSTREAM_EVIDENCE" + assert evidence[0]["source"] == review["source"] + + +def test_airr_new_baseline_is_channel_qualified_without_historical_or_runtime_claims() -> None: + contract = load_contract("airr_extra") + dockerfile = (REPO / contract["dockerfile_path"]).read_text() + assert '"conda-forge::r-airr=2.0.0=r45hc72bb7e_0"' in dockerfile + assert contract["pinning"]["historical_intended_version"] == "UNKNOWN" + assert contract["pinning"]["previous_rejected_candidate_version"] == "1.2.0" + assert contract["pinning"]["native_build_validation_status"] == "NOT_RUN" + assert contract["scientific_version_source"] == "new_reproducible_baseline_package_version" + assert contract["source_immutable_identity"] == "sha256:e4932ec84dad46d37327ab5aa5a34b95095677b6392bff2f59fd473eb942da52" + assert all( + value == "NOARCH_PACKAGE_WITH_NATIVE_DEPENDENCY_SOLVE" + for value in contract["pinning"]["architecture_availability"].values() + ) + + +@pytest.mark.parametrize("output", ["", "1.2.0\n", "2.0.0\n1.2.0\n", "2.0.0\n2.0.0\n", "prefix 2.0.0\n"]) +def test_airr_parser_rejects_missing_wrong_or_ambiguous_output(output: str) -> None: + contract = load_contract("airr_extra") + assert re.search(contract["version_parser"].removeprefix("regex:"), output) is None + + +def test_alevin_infer_fixture_has_valid_matrix_and_equivalence_class_indices() -> None: + fixture = REPO / "validation-contracts/fixtures/alevin-infer-tiny" + labels = (fixture / "eq_labels.txt").read_text().splitlines() + genes, classes = map(int, labels[:2]) + assert (genes, classes) == (2, 3) + mapping = {} + for line in labels[2:]: + fields = list(map(int, line.split())) + *gene_ids, class_id = fields + assert class_id not in mapping and 0 <= class_id < classes + assert gene_ids and len(set(gene_ids)) == len(gene_ids) + assert all(0 <= gene_id < genes for gene_id in gene_ids) + mapping[class_id] = gene_ids + assert mapping == {0: [0], 1: [1], 2: [0, 1]} + matrix = (fixture / "counts.mtx").read_text().splitlines() + assert matrix[0] == "%%MatrixMarket matrix coordinate real general" + assert list(map(int, matrix[1].split())) == [2, classes, 6] + entries = [line.split() for line in matrix[2:]] + assert len(entries) == 6 + assert len({(row, column) for row, column, _ in entries}) == 6 + counts = {(int(row), int(column)): float(value) for row, column, value in entries} + for row in (1, 2): + assert counts[row, 1] == counts[row, 2] > 0 + assert counts[row, 3] > 0 + expected = json.loads((fixture / "expected.json").read_text()) + for row, column, value in expected["entries"]: + assert value == counts[row, column] + counts[row, 3] / 2 + assert expected["row_totals"] == [sum(counts[row, column] for column in (1, 2, 3)) for row in (1, 2)] + assert (fixture / "quants_mat_rows.txt").read_text().splitlines() == expected["rows"] + assert (fixture / "quants_mat_cols.txt").read_text().splitlines() == expected["columns"] + assert all(len(row) == 16 and set(row) <= set("ACGT") for row in expected["rows"]) + + +def test_alevin_equivalence_class_gzip_has_no_volatile_header() -> None: + fixture = REPO / "validation-contracts/fixtures/alevin-infer-tiny" + encoded = (fixture / "eq_labels.txt.gz").read_bytes() + assert encoded[:4] == bytes([31, 139, 8, 0]) + assert encoded[4:8] == bytes(4) + assert gzip.decompress(encoded) == (fixture / "eq_labels.txt").read_bytes() + + +def test_alevin_smoke_contract_is_bound_to_fixtures_and_not_release_verified() -> None: + contract = load_contract("alevin_fry") + assert contract["smoke_command"][:2] == ["alevin-fry", "infer"] + assert contract["smoke_assertions"]["kind"] == "matrix_market_gene_counts" + assert contract["smoke_assertions"]["expectation_basis"] == "ANALYTICAL_SYMMETRY_NOT_OBSERVED_RUNTIME" + assert contract["smoke_assertions"]["timeout_seconds"] == 60 + assert contract["pinning"]["native_build_validation_status"] == "NOT_RUN" + assert contract["pinning"]["release_status"] == "NOT_PUBLISHED" + assert set(contract["fixture_sha256"]) == set(contract["smoke_inputs"]) + assert all(sha256_file(REPO / path) == digest for path, digest in contract["fixture_sha256"].items()) + + +def test_afterqc_native_package_availability_does_not_waive_legacy_runtime_review() -> None: + contract = load_contract("afterqc_extra") + assert contract["native_amd64_expected"] is True + assert contract["native_arm64_expected"] is True + assert contract["contract_status"] == "CONTRACT_BLOCKED_MULTIPLE_REASONS" + assert contract["pinning"]["new_reproducible_baseline_version"] is None + review = contract["pinning"]["legacy_runtime_review"] + assert review["runtime"] == "Python 2.7.15" + assert review["upstream_support"] == "END_OF_LIFE" + assert review["risk_acceptance"] == "NOT_ESTABLISHED" + assert review["security_scan"] == "NOT_RUN" + assert "LEGACY_RUNTIME_REVIEW_REQUIRED" in contract["remediation"]["remaining_deficiency_flags"] + assert "ARCHITECTURE_EVIDENCE_MISSING" not in contract["remediation"]["remaining_deficiency_flags"] + + +def test_alevinqc_missing_arm64_candidate_cannot_be_replaced_with_old_noarch_release() -> None: + contract = load_contract("alevinqc_extra") + review = contract["pinning"]["architecture_candidate_review"] + assert review["candidate_version"] == "1.26.0" + assert review["native_arm64_artifact_count"] == 0 + assert review["arm64_solver_exit_code"] == 1 + assert review["arm64_solver_exception"] == "PackagesNotFoundError" + assert review["historical_noarch_latest_version"] == "1.10.0" + assert review["historical_noarch_substitution"] == "NOT_AUTHORIZED" + assert contract["pinning"]["new_reproducible_baseline_version"] is None + assert contract["contract_status"] == "CONTRACT_BLOCKED_MULTIPLE_REASONS" + assert contract["native_arm64_expected"] is False diff --git a/tests/test_validation_contracts.py b/tests/test_validation_contracts.py index 9e55cf3..15af164 100644 --- a/tests/test_validation_contracts.py +++ b/tests/test_validation_contracts.py @@ -53,6 +53,38 @@ def test_canonical_serialization_and_hash_are_deterministic(repo: Path) -> None: assert contract_hash(first) == contract_hash(second) +def test_reviewed_canary_override_is_used_only_for_matching_dockerfile(repo: Path) -> None: + baseline = generate_contract(catalog_row(), repo) + baseline["contract_status"] = "CONTRACT_READY" + baseline["blocking_reasons"] = [] + baseline["smoke_command"] = ["example", "--smoke"] + baseline["smoke_success_condition"] = "exit 0" + baseline["source_type"] = "upstream_archive_sha256" + baseline["source_immutable_identity"] = "sha256:" + "a" * 64 + baseline["pinning"] = { + "pinning_canary_schema_version": 1, + "static_contract_status": "STATIC_CONTRACT_READY", + } + baseline["validation_contract_sha256"] = contract_hash(baseline) + path = repo / "validation-contracts" / "schema-v1" + path.mkdir(parents=True) + (path / "example.json").write_text( + json.dumps(baseline, sort_keys=True), encoding="utf-8" + ) + + reviewed = generate_contract(catalog_row(), repo) + assert reviewed["contract_status"] == "CONTRACT_READY" + assert reviewed["pinning"]["static_contract_status"] == "STATIC_CONTRACT_READY" + + (repo / "dockerfiles" / "Dockerfile.example").write_text( + "FROM ubuntu:24.04\nRUN pip install example==9.9.9\n", + encoding="utf-8", + ) + stale = generate_contract(catalog_row(), repo) + assert stale["contract_status"] == "CONTRACT_BLOCKED_MULTIPLE_REASONS" + assert "pinning" not in stale + + def test_generated_contract_is_fail_closed_without_smoke(repo: Path) -> None: contract = generate_contract(catalog_row(), repo) assert contract["expected_executable"] == "example" diff --git a/tests/test_validation_smoke.py b/tests/test_validation_smoke.py new file mode 100644 index 0000000..d526254 --- /dev/null +++ b/tests/test_validation_smoke.py @@ -0,0 +1,286 @@ +"""Synthetic verifier tests, not scientific-tool runtime evidence.""" +from __future__ import annotations + +import copy +import json +from pathlib import Path + +import pytest + +from scripts.validation_contracts import contract_hash, sha256_file +from scripts.validation_smoke import ( + SmokeValidationError, validate_smoke_outputs, validate_version_evidence, +) + + +REPO = Path(__file__).resolve().parents[1] +OUTPUT = "smoke-output/alevin-infer" +MATRIX = "%%MatrixMarket matrix coordinate real general\n2 2 4\n1 1 7\n1 2 7\n2 1 3\n2 2 3\n" + + +@pytest.fixture +def contract(): + return json.loads((REPO / "validation-contracts/schema-v1/alevin_fry.json").read_text()) + + +@pytest.fixture +def workspace(tmp_path): + output = tmp_path / OUTPUT + output.mkdir(parents=True) + (output / "quants_mat.mtx").write_text(MATRIX) + (output / "quants_mat_rows.txt").write_text("AAAAAAAAAAAAAAAC\nAAAAAAAAAAAAAAAG\n") + (output / "quants_mat_cols.txt").write_text("geneA\ngeneB\n") + return tmp_path + + +def check(contract, workspace, **kwargs): + return validate_smoke_outputs( + contract, REPO, workspace, + returncode=kwargs.get("returncode", 0), elapsed_seconds=kwargs.get("elapsed_seconds", 0.1), + ) + + +def test_valid_synthetic_output_returns_content_hashes_only(contract, workspace): + result = check(contract, workspace) + assert result["smoke_outputs_verified"] is True + assert result["shape"] == [2, 2] + assert len(result["output_sha256"]) == 3 + assert result["contract_sha256"] == contract["validation_contract_sha256"] + assert "native_validated" not in result + assert "release_complete" not in result + + +@pytest.mark.parametrize("header", ["real", "integer"]) +def test_coordinate_order_does_not_affect_count_equality(contract, workspace, header): + matrix = MATRIX.splitlines() + matrix[0] = matrix[0].replace("real", header) + (workspace / OUTPUT / "quants_mat.mtx").write_text("\n".join(matrix[:2] + matrix[:1:-1]) + "\n") + assert check(contract, workspace)["smoke_outputs_verified"] + + +@pytest.mark.parametrize("matrix", [ + "", MATRIX.replace("real general", "real symmetric"), + MATRIX.replace("2 2 4", "3 2 4"), MATRIX.replace("2 2 4", "2 2 3"), + MATRIX.replace("1 1 7", "1 1 8"), MATRIX.replace("1 1 7", "1 1 -7"), + MATRIX.replace("1 1 7", "1 1 nan"), MATRIX.replace("1 1 7", "1 1 inf"), + MATRIX.replace("1 1 7", "0 1 7"), MATRIX.replace("1 1 7", "3 1 7"), + MATRIX.replace("1 1 7", "1.5 1 7"), MATRIX.replace("1 1 7", "1 1"), + MATRIX.replace("1 2 7", "1 1 7"), MATRIX + "2 2 3\n", + MATRIX.replace("real general", "integer general").replace("1 1 7", "1 1 7.5"), +]) +def test_corrupt_or_wrong_matrix_fails_closed(contract, workspace, matrix): + (workspace / OUTPUT / "quants_mat.mtx").write_text(matrix) + with pytest.raises(SmokeValidationError): + check(contract, workspace) + + +@pytest.mark.parametrize("name,value", [ + ("quants_mat_rows.txt", "AAAAAAAAAAAAAAAG\nAAAAAAAAAAAAAAAC\n"), + ("quants_mat_rows.txt", "AAAAAAAAAAAAAAAC\n"), + ("quants_mat_cols.txt", "geneB\ngeneA\n"), + ("quants_mat_cols.txt", "geneA\ngeneA\n"), +]) +def test_wrong_labels_fail_even_when_matrix_counts_match(contract, workspace, name, value): + (workspace / OUTPUT / name).write_text(value) + with pytest.raises(SmokeValidationError, match="labels mismatch"): + check(contract, workspace) + + +@pytest.mark.parametrize("name", ["quants_mat.mtx", "quants_mat_rows.txt", "quants_mat_cols.txt"]) +def test_missing_output_is_not_accepted(contract, workspace, name): + (workspace / OUTPUT / name).unlink() + with pytest.raises(SmokeValidationError): + check(contract, workspace) + + +@pytest.mark.parametrize("code,elapsed", [(1, 0.1), (-9, 0.1), (True, 0.1), (0, 61), (0, -1), (0, float("nan")), (0, float("inf")), (0, True)]) +def test_failure_or_invalid_duration_is_not_hidden_by_valid_outputs(contract, workspace, code, elapsed): + with pytest.raises(SmokeValidationError): + check(contract, workspace, returncode=code, elapsed_seconds=elapsed) + + +def test_contract_hash_mismatch_fails_before_outputs(contract, workspace): + contract["smoke_assertions"]["timeout_seconds"] = 1000 + with pytest.raises(SmokeValidationError, match="contract SHA256 mismatch"): + check(contract, workspace) + + +@pytest.mark.parametrize("field,value", [ + ("kind", "ignore_counts"), ("output_directory", "../escape"), + ("output_directory", "/absolute"), ("expected_fixture", "unbound.json"), + ("timeout_seconds", 0), ("timeout_seconds", True), +]) +def test_unsupported_or_unsafe_assertions_fail_closed(contract, workspace, field, value): + changed = copy.deepcopy(contract) + changed["smoke_assertions"][field] = value + changed["validation_contract_sha256"] = contract_hash(changed) + with pytest.raises(SmokeValidationError): + check(changed, workspace) + + +def test_changed_fixture_hash_is_not_ignored(contract, workspace): + name = next(iter(contract["fixture_sha256"])) + contract["fixture_sha256"][name] = "0" * 64 + contract["validation_contract_sha256"] = contract_hash(contract) + with pytest.raises(SmokeValidationError, match="fixture SHA256 mismatch"): + check(contract, workspace) + + +def test_output_symlink_cannot_escape_workspace(contract, workspace, tmp_path): + outside = tmp_path.parent / (tmp_path.name + "-outside.mtx") + outside.write_text(MATRIX) + path = workspace / OUTPUT / "quants_mat.mtx" + path.unlink() + path.symlink_to(outside) + with pytest.raises(SmokeValidationError, match="escapes verification root"): + check(contract, workspace) + + +@pytest.mark.parametrize("field,value", [ + ("tolerance", 1), ("tolerance", 0), ("tolerance", float("nan")), + ("shape", [True, 2]), ("shape", [2, 0]), ("entries", [[1, 1, -7]]), + ("entries", [[1, 1, 7], [1, 1, 7]]), ("rows", ["same", "same"]), + ("row_totals", [15, 6]), ("columns", ["geneA"]), +]) +def test_malformed_expected_results_fail_even_with_updated_hash(contract, workspace, tmp_path, field, value): + repo = tmp_path / "isolated-repo" + for relative in [contract["dockerfile_path"], *contract["fixture_sha256"]]: + destination = repo / relative + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes((REPO / relative).read_bytes()) + relative = contract["smoke_assertions"]["expected_fixture"] + expected_file = repo / relative + expected = json.loads(expected_file.read_text()) + expected[field] = value + expected_file.write_text(json.dumps(expected)) + contract["fixture_sha256"][relative] = sha256_file(expected_file) + contract["validation_contract_sha256"] = contract_hash(contract) + with pytest.raises(SmokeValidationError): + validate_smoke_outputs(contract, repo, workspace, returncode=0, elapsed_seconds=0.1) + + +def test_row_total_tolerance_is_checked_separately(contract, workspace): + # Each entry error is within tolerance, but their summed row error is not. + matrix = MATRIX.replace("1 1 7", "1 1 7.00008").replace("1 2 7", "1 2 7.00008") + (workspace / OUTPUT / "quants_mat.mtx").write_text(matrix) + with pytest.raises(SmokeValidationError, match="row total mismatch"): + check(contract, workspace) + + +def test_verifier_does_not_modify_output_files(contract, workspace): + before = {str(path): path.read_bytes() for path in workspace.rglob("*") if path.is_file()} + check(contract, workspace) + after = {str(path): path.read_bytes() for path in workspace.rglob("*") if path.is_file()} + assert before == after + + +# Synthetic proposed stream metadata only: these tests do not update production +# contracts or establish that a scientific tool emits the supplied text. +VERSION_CASES = [ + ("3ddna_extra", r"(?m)^version: ([^\r\n]+)$", "version: 190716\n"), + ("abricate", r"(?m)^abricate[ \t]+([^\r\n]+)$", "abricate 1.4.0\n"), + ("accelerate", r"(?m)^- `Accelerate` version: ([^\r\n]+)$", "- `Accelerate` version: 1.15.0\n"), + ("agat_extra", r"(?m)^(v[^\r\n]+)$", "v1.7.0\n"), + ("aicsimageio_extra", r"(?m)^([^\r\n]+)$", "4.14.0\n"), + ("airr_extra", r"(?m)^([^\r\n]+)$", "2.0.0\n"), + ("alevin_fry", r"(?m)^alevin-fry[ \t]+([^\r\n]+)$", "alevin-fry 0.18.3\n"), +] + + +@pytest.fixture(params=VERSION_CASES, ids=[case[0] for case in VERSION_CASES]) +def version_case(request): + tool, observation, output = request.param + value = json.loads((REPO / f"validation-contracts/schema-v1/{tool}.json").read_text()) + value["version_observation_parser"] = observation + value["version_output_source"] = "stdout" if tool in ("airr_extra", "aicsimageio_extra") else "either" + value["validation_contract_sha256"] = contract_hash(value) + return value, output + + +def version_check(value, output, **kwargs): + return validate_version_evidence(value, REPO, stdout=output, stderr=kwargs.get("stderr", ""), returncode=kwargs.get("returncode", 0)) + + +def test_version_expected_output(version_case): + value, output = version_case + before = copy.deepcopy(value) + result = version_check(value, output) + assert result["observed_version"] == value["version_expected_value"] + assert result["contract_sha256"] == value["validation_contract_sha256"] + assert value == before + assert "native_validated" not in result + + +@pytest.mark.parametrize("tool,observation,output", VERSION_CASES) +def test_generated_contract_has_exact_version_configuration(tool, observation, output): + value = json.loads((REPO / f"validation-contracts/schema-v1/{tool}.json").read_text()) + assert value["version_observation_parser"] == observation + assert value["version_output_source"] == ("stdout" if tool in ("airr_extra", "aicsimageio_extra") else "either") + # Displayed/internal versions may differ from package release identifiers: + # 3D-DNA release 201008 declares 190716; AGAT displays a leading 'v'. + assert version_check(value, output)["observed_version"] == value["version_expected_value"] + + +@pytest.mark.parametrize("mode", ["missing", "wrong", "duplicate", "conflicting"]) +def test_version_missing_wrong_duplicate_conflicting_fail(version_case, mode): + value, output = version_case + wrong = output.replace(value["version_expected_value"], "v999.999" if value["tool_id"] == "agat_extra" else "999.999") + text = {"missing": "", "wrong": wrong, "duplicate": output * 2, "conflicting": output + wrong}[mode] + with pytest.raises(SmokeValidationError): + version_check(value, text) + + +def test_version_stream_binding(version_case): + value, output = version_case + if value["version_output_source"] == "either": + assert version_check(value, "", stderr=output)["evidence_stream"] == "stderr" + with pytest.raises(SmokeValidationError): + version_check(value, output, stderr=output) + else: + assert version_check(value, output, stderr="runtime warning\n")["evidence_stream"] == "stdout" + with pytest.raises(SmokeValidationError): + version_check(value, "", stderr=output) + + +@pytest.mark.parametrize("returncode", [True, False, 1, -1, "0"]) +def test_version_failed_exit_rejected(version_case, returncode): + value, output = version_case + with pytest.raises(SmokeValidationError): + version_check(value, output, returncode=returncode) + + +@pytest.mark.parametrize("field,replacement", [ + ("version_output_source", "unknown"), + ("version_observation_parser", "["), + ("version_observation_parser", "no-group"), + ("version_observation_parser", "(one)(two)"), + ("version_observation_parser", None), +]) +def test_version_invalid_metadata_rejected(version_case, field, replacement): + value, output = version_case + value[field] = replacement + value["validation_contract_sha256"] = contract_hash(value) + with pytest.raises(SmokeValidationError): + version_check(value, output) + + +def test_version_metadata_hash_mismatch(version_case): + value, output = version_case + value["version_output_source"] = "stderr" + with pytest.raises(SmokeValidationError): + version_check(value, output) + + +def test_unconfigured_contract_is_not_implicitly_accepted(version_case): + value, output = version_case + value.pop("version_observation_parser") + value["validation_contract_sha256"] = contract_hash(value) + with pytest.raises(SmokeValidationError): + version_check(value, output) + + +@pytest.mark.parametrize("output", [None, b"version", "x" * (1024 * 1024 + 1)]) +def test_invalid_or_oversized_version_stream(version_case, output): + value, _ = version_case + with pytest.raises(SmokeValidationError): + version_check(value, output) diff --git a/validation-contracts/contract-ready-tools.txt b/validation-contracts/contract-ready-tools.txt index f367dda..ca7c157 100644 --- a/validation-contracts/contract-ready-tools.txt +++ b/validation-contracts/contract-ready-tools.txt @@ -3,3 +3,5 @@ abricate accelerate agat_extra aicsimageio_extra +airr_extra +alevin_fry diff --git a/validation-contracts/exceptions/blocked-multiple.txt b/validation-contracts/exceptions/blocked-multiple.txt index dddfaa0..e75af10 100644 --- a/validation-contracts/exceptions/blocked-multiple.txt +++ b/validation-contracts/exceptions/blocked-multiple.txt @@ -1,6 +1,5 @@ admixtools no repository-owned bounded EIGENSTRAT fixture establishes a scientifically meaningful qp3Pop execution; the upstream academic/non-commercial license requires distribution-scope review before release eligibility -afterqc_extra no reproducible baseline was selected because native arm64 dependency availability is not established; Dockerfile remains historically unpinned -airr_extra no reproducible baseline was selected because native arm64 dependency availability is not established; Dockerfile remains historically unpinned +afterqc_extra reproducible baseline selection deferred pending explicit unsupported Python 2.7 runtime review; Dockerfile remains historically unpinned alevinqc_extra no reproducible baseline was selected because the current stable package has no linux-aarch64 artifact; Dockerfile remains historically unpinned allhic_extra no canonical scientific executable is explicitly invoked by the Dockerfile; no deterministic scientifically meaningful smoke contract is present in repository evidence; no exact tool-associated scientific version or immutable source pin is present; no explicit tool-specific version command is present altair_extra no canonical scientific executable is explicitly invoked by the Dockerfile; no deterministic scientifically meaningful smoke contract is present in repository evidence; no exact tool-associated scientific version or immutable source pin is present; no explicit tool-specific version command is present diff --git a/validation-contracts/exceptions/blocked-smoke.txt b/validation-contracts/exceptions/blocked-smoke.txt index 1b43355..e69de29 100644 --- a/validation-contracts/exceptions/blocked-smoke.txt +++ b/validation-contracts/exceptions/blocked-smoke.txt @@ -1 +0,0 @@ -alevin_fry scientifically meaningful execution requires a valid RAD and permit-list fixture that is not available in repository evidence diff --git a/validation-contracts/fixtures/admixtools-tiny/README.md b/validation-contracts/fixtures/admixtools-tiny/README.md new file mode 100644 index 0000000..d9c3942 --- /dev/null +++ b/validation-contracts/fixtures/admixtools-tiny/README.md @@ -0,0 +1,32 @@ +# ADMIXTOOLS synthetic smoke candidate + +Status: **STATIC_FIXTURE_ONLY — NOT_RUNTIME_VALIDATED**. +This repository-owned synthetic dataset has 24 biallelic autosomal sites, +six diploid samples, and three artificial populations. It contains no human +or external dataset. Counts represent reference-allele dosage, not phased +haplotypes. Target heterozygotes are present; no missing calls are encoded. +Genetic coordinates span multiple candidate jackknife blocks, but block +retention and numerical behavior require execution evidence. + +Format evidence is pinned to ADMIXTOOLS v8.0.2: + +- [EIGENSTRAT format](https://github.com/DReichLab/AdmixTools/blob/v8.0.2/convertf/README): one genotype row per site, one dosage character per sample; associated SNP and individual tables. +- [qp3Pop input/output](https://github.com/DReichLab/AdmixTools/blob/v8.0.2/README.3PopTest): parameter-file invocation and ordered source/source/target population triple. + +Proposed invocation, with this directory as the working directory: + +```sh +qp3Pop -p qp3pop.par +``` + +No invocation has been performed for this fixture. Before binding it into +the contract, require exit zero, exactly one result for the planned triple, +finite f3/standard-error/Z values, positive standard error and retained SNP +count, and normal completion. Retained counts and numerical expectations +must be established on the exact pinned package, not guessed. The synthetic +result must not be interpreted as evidence about real population ancestry. + +The tool remains blocked pending this execution evidence and distribution +license review. Creating independently authored input data neither approves +software redistribution nor resolves that license gate. No contract, pin, +factory, or readiness status is changed by this fixture. diff --git a/validation-contracts/fixtures/admixtools-tiny/qp3pop.par b/validation-contracts/fixtures/admixtools-tiny/qp3pop.par new file mode 100644 index 0000000..6cc682d --- /dev/null +++ b/validation-contracts/fixtures/admixtools-tiny/qp3pop.par @@ -0,0 +1,4 @@ +genotypename: tiny.geno +snpname: tiny.snp +indivname: tiny.ind +popfilename: tiny.pops diff --git a/validation-contracts/fixtures/admixtools-tiny/tiny.geno b/validation-contracts/fixtures/admixtools-tiny/tiny.geno new file mode 100644 index 0000000..272fc37 --- /dev/null +++ b/validation-contracts/fixtures/admixtools-tiny/tiny.geno @@ -0,0 +1,24 @@ +002211 +012211 +001211 +220011 +210011 +221011 +012201 +001212 +102211 +210012 +221001 +120011 +002201 +012212 +001211 +220012 +210001 +221011 +012211 +001201 +102212 +210011 +221012 +120001 diff --git a/validation-contracts/fixtures/admixtools-tiny/tiny.ind b/validation-contracts/fixtures/admixtools-tiny/tiny.ind new file mode 100644 index 0000000..e72eed2 --- /dev/null +++ b/validation-contracts/fixtures/admixtools-tiny/tiny.ind @@ -0,0 +1,6 @@ +A1 U SourceA +A2 U SourceA +B1 U SourceB +B2 U SourceB +C1 U TargetC +C2 U TargetC diff --git a/validation-contracts/fixtures/admixtools-tiny/tiny.pops b/validation-contracts/fixtures/admixtools-tiny/tiny.pops new file mode 100644 index 0000000..60c4fae --- /dev/null +++ b/validation-contracts/fixtures/admixtools-tiny/tiny.pops @@ -0,0 +1 @@ +SourceA SourceB TargetC diff --git a/validation-contracts/fixtures/admixtools-tiny/tiny.snp b/validation-contracts/fixtures/admixtools-tiny/tiny.snp new file mode 100644 index 0000000..e800725 --- /dev/null +++ b/validation-contracts/fixtures/admixtools-tiny/tiny.snp @@ -0,0 +1,24 @@ +s01 1 0.01 1000 A C +s02 1 0.02 2000 A C +s03 1 0.03 3000 A C +s04 1 0.07 4000 A C +s05 1 0.08 5000 A C +s06 1 0.09 6000 A C +s07 2 0.01 1000 A C +s08 2 0.02 2000 A C +s09 2 0.03 3000 A C +s10 2 0.07 4000 A C +s11 2 0.08 5000 A C +s12 2 0.09 6000 A C +s13 3 0.01 1000 A C +s14 3 0.02 2000 A C +s15 3 0.03 3000 A C +s16 3 0.07 4000 A C +s17 3 0.08 5000 A C +s18 3 0.09 6000 A C +s19 4 0.01 1000 A C +s20 4 0.02 2000 A C +s21 4 0.03 3000 A C +s22 4 0.07 4000 A C +s23 4 0.08 5000 A C +s24 4 0.09 6000 A C diff --git a/validation-contracts/fixtures/alevin-infer-tiny/README.md b/validation-contracts/fixtures/alevin-infer-tiny/README.md new file mode 100644 index 0000000..e570925 --- /dev/null +++ b/validation-contracts/fixtures/alevin-infer-tiny/README.md @@ -0,0 +1,31 @@ +# Alevin-fry inference fixture + +Independently authored synthetic inputs for the pinned v0.18.3 `infer` +subcommand; static fixture and contract only, **not runtime validated**. + +Two cells have counts in three equivalence classes: singleton geneA, +singleton geneB, and an ambiguous geneA/geneB class. Both singleton counts +are equal in each cell. By symmetry the expected EM allocation splits the +ambiguous counts equally: output rows [7, 7] and [3, 3]. This expectation is +analytical, not an observed result. A later native gate must reject any +nonfinite, negative, wrong-shape, wrong-label, wrong-total or mismatched count +(absolute tolerance 0.0001); exit zero alone is insufficient. + +The equivalence-class format uses zero-based gene IDs followed by the class +ID. Its first two lines give gene and class counts. `infer` requires gzip +encoding for these labels; `eq_labels.txt.gz` is generated mechanically by +`gzip -n -k eq_labels.txt`, with no timestamp or source filename in its header. +MatrixMarket coordinates are one-based. Column-name companion data names +the two output genes, not the three input classes. + +Pinned upstream evidence (no source/test data copied): + +- [infer documentation](https://github.com/COMBINE-lab/alevin-fry/blob/ad05742d274230f9141b2eabeebd1c1b31692199/docs/source/infer.rst) +- [CLI input flags](https://github.com/COMBINE-lab/alevin-fry/blob/ad05742d274230f9141b2eabeebd1c1b31692199/src/main.rs) +- [gzip equivalence-class parser](https://github.com/COMBINE-lab/alevin-fry/blob/ad05742d274230f9141b2eabeebd1c1b31692199/src/eq_class.rs) +- [matrix reader, EM invocation and output writer](https://github.com/COMBINE-lab/alevin-fry/blob/ad05742d274230f9141b2eabeebd1c1b31692199/src/infer.rs) + +Run only inside an isolated validation workspace, preserving input directory +layout and binding the fixture hashes. Use two threads and a 60-second +timeout. This smoke does not prove read mapping, RAD parsing, permit-list +generation, UMI resolution, USA mode, or full biological pipeline accuracy. diff --git a/validation-contracts/fixtures/alevin-infer-tiny/counts.mtx b/validation-contracts/fixtures/alevin-infer-tiny/counts.mtx new file mode 100644 index 0000000..e5ca5ea --- /dev/null +++ b/validation-contracts/fixtures/alevin-infer-tiny/counts.mtx @@ -0,0 +1,8 @@ +%%MatrixMarket matrix coordinate real general +2 3 6 +1 1 5 +1 2 5 +1 3 4 +2 1 2 +2 2 2 +2 3 2 diff --git a/validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt b/validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt new file mode 100644 index 0000000..f0f3751 --- /dev/null +++ b/validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt @@ -0,0 +1,5 @@ +2 +3 +0 0 +1 1 +0 1 2 diff --git a/validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt.gz b/validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..4502b920679ef44c622ceebf2bd27cc680eac9ff GIT binary patch literal 36 ncmb2|=3oE=X5&Xj&x`^L9vFofuv}{6a*JV@9Qi~^2q*~vpScM) literal 0 HcmV?d00001 diff --git a/validation-contracts/fixtures/alevin-infer-tiny/expected.json b/validation-contracts/fixtures/alevin-infer-tiny/expected.json new file mode 100644 index 0000000..fc78bb8 --- /dev/null +++ b/validation-contracts/fixtures/alevin-infer-tiny/expected.json @@ -0,0 +1,8 @@ +{ + "columns": ["geneA", "geneB"], + "entries": [[1, 1, 7.0], [1, 2, 7.0], [2, 1, 3.0], [2, 2, 3.0]], + "row_totals": [14.0, 6.0], + "rows": ["AAAAAAAAAAAAAAAC", "AAAAAAAAAAAAAAAG"], + "shape": [2, 2], + "tolerance": 0.0001 +} diff --git a/validation-contracts/fixtures/alevin-infer-tiny/quants_mat_cols.txt b/validation-contracts/fixtures/alevin-infer-tiny/quants_mat_cols.txt new file mode 100644 index 0000000..2aeb21b --- /dev/null +++ b/validation-contracts/fixtures/alevin-infer-tiny/quants_mat_cols.txt @@ -0,0 +1,2 @@ +geneA +geneB diff --git a/validation-contracts/fixtures/alevin-infer-tiny/quants_mat_rows.txt b/validation-contracts/fixtures/alevin-infer-tiny/quants_mat_rows.txt new file mode 100644 index 0000000..050cf58 --- /dev/null +++ b/validation-contracts/fixtures/alevin-infer-tiny/quants_mat_rows.txt @@ -0,0 +1,2 @@ +AAAAAAAAAAAAAAAC +AAAAAAAAAAAAAAAG diff --git a/validation-contracts/inventory.csv b/validation-contracts/inventory.csv index 04934a3..e3a7b84 100644 --- a/validation-contracts/inventory.csv +++ b/validation-contracts/inventory.csv @@ -1,14 +1,14 @@ tool_id,dockerfile_path,classification,package,scientific_version,expected_executable,version_command,smoke_contract,amd64_expected,arm64_expected,runtime_gpu_required,runtime_network_required,runtime_license_required,contract_status,blocking_reason,contract_sha256 -3ddna_extra,dockerfiles/Dockerfile.3ddna_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/3ddna_extra,201008,3d-dna,3d-dna --help,3d-dna --help,true,true,false,false,false,CONTRACT_READY,,393ab6dca7b73c3441ebdbcae79275c84a24edb9924d2783c939f3050eef037e -abricate,dockerfiles/Dockerfile.abricate,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/abricate,1.4.0,abricate,abricate --version,abricate --check,true,true,false,false,false,CONTRACT_READY,,5b95e0bc31e7cfdf102836c73de8a86035371320a13df140e0d922f374931df6 -accelerate,dockerfiles/Dockerfile.accelerate,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/accelerate,1.15.0,accelerate,accelerate env,"python -c 'from accelerate import Accelerator; a=Accelerator(cpu=True); assert a.device.type == '""'""'cpu'""'""''",true,true,false,false,false,CONTRACT_READY,,d4bed397d759fe866bdcd3c5c7197add9ed6ba5660a5e039ac6bf8ea03160a34 -admixtools,dockerfiles/Dockerfile.admixtools,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/admixtools,8.0.2,qp3Pop,qp3Pop -v,,true,true,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,no repository-owned bounded EIGENSTRAT fixture establishes a scientifically meaningful qp3Pop execution; the upstream academic/non-commercial license requires distribution-scope review before release eligibility,e1b39a2629d029a921b550f2e3eb96d250de96c0d9df3b2b0097f6da56e311ca -afterqc_extra,dockerfiles/Dockerfile.afterqc_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/afterqc_extra,,after.py,after.py --version,after.py --qc_only -1 validation-contracts/fixtures/afterqc-tiny.fastq,true,false,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,no reproducible baseline was selected because native arm64 dependency availability is not established; Dockerfile remains historically unpinned,45854a7d2256c367657f9fbcb91b693118d474fedca9eb8faf194abe4abc269f -agat_extra,dockerfiles/Dockerfile.agat_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/agat_extra,1.7.0,agat_convert_sp_gxf2gxf.pl,agat --version,agat_convert_sp_gxf2gxf.pl --gff validation-contracts/fixtures/agat-tiny.gff3 --output smoke.gff3,true,true,false,false,false,CONTRACT_READY,,0fd38af58559f0c55d5cb9959fe97a77adaff740ff7fdaf44afacce6c474289a -aicsimageio_extra,dockerfiles/Dockerfile.aicsimageio_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/aicsimageio_extra,4.14.0,python,python -c 'import aicsimageio; print(aicsimageio.get_module_version())',"python -c 'import numpy as np; from aicsimageio import AICSImage; i=AICSImage(np.zeros((2,3),dtype=np.uint8)); assert i.get_image_data('""'""'YX'""'""').shape == (2,3)'",true,true,false,false,false,CONTRACT_READY,,c07480d0c08a1946b3db71126f4b1bc86af22e68ffbb1b050322b6bd5d6cc07a -airr_extra,dockerfiles/Dockerfile.airr_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/airr_extra,,Rscript,"Rscript -e 'cat(as.character(packageVersion('""'""'airr'""'""')), '""'""'\n'""'""')'","Rscript -e 'library(airr); x=read_rearrangement('""'""'validation-contracts/fixtures/airr-tiny.tsv'""'""'); stopifnot(nrow(x)==1, x$sequence_id[[1]]=='""'""'omnibioai_airr_1'""'""')'",true,false,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,no reproducible baseline was selected because native arm64 dependency availability is not established; Dockerfile remains historically unpinned,afd201499e17c9305cc49259e5d11244050cccc81cb03974f626a8d01c68100d -alevin_fry,dockerfiles/Dockerfile.alevin_fry,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/alevin_fry,0.18.3,alevin-fry,alevin-fry --version,,true,true,false,false,false,CONTRACT_BLOCKED_SMOKE,scientifically meaningful execution requires a valid RAD and permit-list fixture that is not available in repository evidence,8158a93000832f1eff8c791952a749e37672acee9e9a675c3844359c2e45d7fd -alevinqc_extra,dockerfiles/Dockerfile.alevinqc_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/alevinqc_extra,,Rscript,"Rscript -e 'cat(as.character(packageVersion('""'""'alevinQC'""'""')), '""'""'\n'""'""')'","Rscript -e 'library(alevinQC); x=readAlevinQC(system.file('""'""'extdata/alevin_example_v0.14'""'""',package='""'""'alevinQC'""'""')); stopifnot(is.list(x))'",true,false,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,no reproducible baseline was selected because the current stable package has no linux-aarch64 artifact; Dockerfile remains historically unpinned,5e1b2b08a28ae2e104dd7da771c68c4a85933e70123e72fd4c408fa3ad7f2bb2 +3ddna_extra,dockerfiles/Dockerfile.3ddna_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/3ddna_extra,201008,3d-dna,3d-dna --help,3d-dna --help,true,true,false,false,false,CONTRACT_READY,,118d91c766df34effef5cfd70d5f88e263157e6d5f23d225a7ed380fe5e453e8 +abricate,dockerfiles/Dockerfile.abricate,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/abricate,1.4.0,abricate,abricate --version,abricate --check,true,true,false,false,false,CONTRACT_READY,,53be925de09982e9a5875574d1455d319f64bf6e5e93b47fc928c79cdcaef273 +accelerate,dockerfiles/Dockerfile.accelerate,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/accelerate,1.15.0,accelerate,accelerate env,"python -c 'from accelerate import Accelerator; a=Accelerator(cpu=True); assert a.device.type == '""'""'cpu'""'""''",true,true,false,false,false,CONTRACT_READY,,ee015c8dd47bdc8571f390a40e5a0b7dc6da41bbc9be6f292f023660b4941fe0 +admixtools,dockerfiles/Dockerfile.admixtools,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/admixtools,8.0.2,qp3Pop,qp3Pop -v,,true,true,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,no repository-owned bounded EIGENSTRAT fixture establishes a scientifically meaningful qp3Pop execution; the upstream academic/non-commercial license requires distribution-scope review before release eligibility,7e904c83d44c40f68c65f1c8146ae2286cc278bc74641113cb33fc600f8b5c07 +afterqc_extra,dockerfiles/Dockerfile.afterqc_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/afterqc_extra,,after.py,after.py --version,after.py --qc_only -1 validation-contracts/fixtures/afterqc-tiny.fastq,true,true,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,reproducible baseline selection deferred pending explicit unsupported Python 2.7 runtime review; Dockerfile remains historically unpinned,2f3b551d0ce9a2d27585d4ab345f8f6dd81f060139c26647e604fcf679492d82 +agat_extra,dockerfiles/Dockerfile.agat_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/agat_extra,1.7.0,agat_convert_sp_gxf2gxf.pl,agat --version,agat_convert_sp_gxf2gxf.pl --gff validation-contracts/fixtures/agat-tiny.gff3 --output smoke.gff3,true,true,false,false,false,CONTRACT_READY,,254843a87b40693630fd5967c80286f9a82b1ebd429f4090e1bde8c3b2097bab +aicsimageio_extra,dockerfiles/Dockerfile.aicsimageio_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/aicsimageio_extra,4.14.0,python,python -c 'import aicsimageio; print(aicsimageio.get_module_version())',"python -c 'import numpy as np; from aicsimageio import AICSImage; i=AICSImage(np.zeros((2,3),dtype=np.uint8)); assert i.get_image_data('""'""'YX'""'""').shape == (2,3)'",true,true,false,false,false,CONTRACT_READY,,392c12d169b104cfe37f6a07275e7b5fbca6ed6e23b84cf9b0b2cb61eb1d2d21 +airr_extra,dockerfiles/Dockerfile.airr_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/airr_extra,2.0.0,Rscript,"Rscript -e 'cat(as.character(packageVersion('""'""'airr'""'""')), '""'""'\n'""'""')'","Rscript -e 'library(airr); x=read_rearrangement('""'""'validation-contracts/fixtures/airr-tiny.tsv'""'""'); stopifnot(nrow(x)==1, x$sequence_id[[1]]=='""'""'omnibioai_airr_1'""'""')'",true,true,false,false,false,CONTRACT_READY,,a664271f274553502f21bb8375713a8c03d3b88a957bf65f8cc0e7b43ade1b39 +alevin_fry,dockerfiles/Dockerfile.alevin_fry,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/alevin_fry,0.18.3,alevin-fry,alevin-fry --version,alevin-fry infer -c validation-contracts/fixtures/alevin-infer-tiny/counts.mtx -e validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt.gz -o smoke-output/alevin-infer -t 2,true,true,false,false,false,CONTRACT_READY,,89def6237aa466db2002ac35b816ee86cf72dcb98afe7ce2766c10dcfb5554ab +alevinqc_extra,dockerfiles/Dockerfile.alevinqc_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/alevinqc_extra,,Rscript,"Rscript -e 'cat(as.character(packageVersion('""'""'alevinQC'""'""')), '""'""'\n'""'""')'","Rscript -e 'library(alevinQC); x=readAlevinQC(system.file('""'""'extdata/alevin_example_v0.14'""'""',package='""'""'alevinQC'""'""')); stopifnot(is.list(x))'",true,false,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,no reproducible baseline was selected because the current stable package has no linux-aarch64 artifact; Dockerfile remains historically unpinned,ee1219d4554d161772dabdc2662a4c290856e7c451fc156160a0ac686df2bcef allhic_extra,dockerfiles/Dockerfile.allhic_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/allhic_extra,,,,,true,true,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,no canonical scientific executable is explicitly invoked by the Dockerfile; no deterministic scientifically meaningful smoke contract is present in repository evidence; no exact tool-associated scientific version or immutable source pin is present; no explicit tool-specific version command is present,0f3d8ade68e772b9e75a3abfed2a40a0de510d4cf2b81ae9e5917ca89e0a2349 altair_extra,dockerfiles/Dockerfile.altair_extra,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/altair_extra,,,,,true,true,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,no canonical scientific executable is explicitly invoked by the Dockerfile; no deterministic scientifically meaningful smoke contract is present in repository evidence; no exact tool-associated scientific version or immutable source pin is present; no explicit tool-specific version command is present,4f35c864b5cc8c70b461b1334d7d3dbc2138b9cdf241060a33044cd114066f2d ambertools,dockerfiles/Dockerfile.ambertools,MULTIARCH_READY,ghcr.io/omnibioai/omnibioai-sif/ambertools,,,,,true,true,false,false,false,CONTRACT_BLOCKED_MULTIPLE_REASONS,no canonical scientific executable is explicitly invoked by the Dockerfile; no deterministic scientifically meaningful smoke contract is present in repository evidence; no exact tool-associated scientific version or immutable source pin is present; no explicit tool-specific version command is present,5277157e15b22b383947a53e7d2d2ed5b8c2575df0082d1325f23d51eb673762 diff --git a/validation-contracts/inventory.json b/validation-contracts/inventory.json index ce8a8bc..173be1d 100644 --- a/validation-contracts/inventory.json +++ b/validation-contracts/inventory.json @@ -4,7 +4,7 @@ "arm64_expected": "true", "blocking_reason": "", "classification": "MULTIARCH_READY", - "contract_sha256": "393ab6dca7b73c3441ebdbcae79275c84a24edb9924d2783c939f3050eef037e", + "contract_sha256": "118d91c766df34effef5cfd70d5f88e263157e6d5f23d225a7ed380fe5e453e8", "contract_status": "CONTRACT_READY", "dockerfile_path": "dockerfiles/Dockerfile.3ddna_extra", "expected_executable": "3d-dna", @@ -22,7 +22,7 @@ "arm64_expected": "true", "blocking_reason": "", "classification": "MULTIARCH_READY", - "contract_sha256": "5b95e0bc31e7cfdf102836c73de8a86035371320a13df140e0d922f374931df6", + "contract_sha256": "53be925de09982e9a5875574d1455d319f64bf6e5e93b47fc928c79cdcaef273", "contract_status": "CONTRACT_READY", "dockerfile_path": "dockerfiles/Dockerfile.abricate", "expected_executable": "abricate", @@ -40,7 +40,7 @@ "arm64_expected": "true", "blocking_reason": "", "classification": "MULTIARCH_READY", - "contract_sha256": "d4bed397d759fe866bdcd3c5c7197add9ed6ba5660a5e039ac6bf8ea03160a34", + "contract_sha256": "ee015c8dd47bdc8571f390a40e5a0b7dc6da41bbc9be6f292f023660b4941fe0", "contract_status": "CONTRACT_READY", "dockerfile_path": "dockerfiles/Dockerfile.accelerate", "expected_executable": "accelerate", @@ -58,7 +58,7 @@ "arm64_expected": "true", "blocking_reason": "no repository-owned bounded EIGENSTRAT fixture establishes a scientifically meaningful qp3Pop execution; the upstream academic/non-commercial license requires distribution-scope review before release eligibility", "classification": "MULTIARCH_READY", - "contract_sha256": "e1b39a2629d029a921b550f2e3eb96d250de96c0d9df3b2b0097f6da56e311ca", + "contract_sha256": "7e904c83d44c40f68c65f1c8146ae2286cc278bc74641113cb33fc600f8b5c07", "contract_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", "dockerfile_path": "dockerfiles/Dockerfile.admixtools", "expected_executable": "qp3Pop", @@ -73,10 +73,10 @@ }, { "amd64_expected": "true", - "arm64_expected": "false", - "blocking_reason": "no reproducible baseline was selected because native arm64 dependency availability is not established; Dockerfile remains historically unpinned", + "arm64_expected": "true", + "blocking_reason": "reproducible baseline selection deferred pending explicit unsupported Python 2.7 runtime review; Dockerfile remains historically unpinned", "classification": "MULTIARCH_READY", - "contract_sha256": "45854a7d2256c367657f9fbcb91b693118d474fedca9eb8faf194abe4abc269f", + "contract_sha256": "2f3b551d0ce9a2d27585d4ab345f8f6dd81f060139c26647e604fcf679492d82", "contract_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", "dockerfile_path": "dockerfiles/Dockerfile.afterqc_extra", "expected_executable": "after.py", @@ -94,7 +94,7 @@ "arm64_expected": "true", "blocking_reason": "", "classification": "MULTIARCH_READY", - "contract_sha256": "0fd38af58559f0c55d5cb9959fe97a77adaff740ff7fdaf44afacce6c474289a", + "contract_sha256": "254843a87b40693630fd5967c80286f9a82b1ebd429f4090e1bde8c3b2097bab", "contract_status": "CONTRACT_READY", "dockerfile_path": "dockerfiles/Dockerfile.agat_extra", "expected_executable": "agat_convert_sp_gxf2gxf.pl", @@ -112,7 +112,7 @@ "arm64_expected": "true", "blocking_reason": "", "classification": "MULTIARCH_READY", - "contract_sha256": "c07480d0c08a1946b3db71126f4b1bc86af22e68ffbb1b050322b6bd5d6cc07a", + "contract_sha256": "392c12d169b104cfe37f6a07275e7b5fbca6ed6e23b84cf9b0b2cb61eb1d2d21", "contract_status": "CONTRACT_READY", "dockerfile_path": "dockerfiles/Dockerfile.aicsimageio_extra", "expected_executable": "python", @@ -127,18 +127,18 @@ }, { "amd64_expected": "true", - "arm64_expected": "false", - "blocking_reason": "no reproducible baseline was selected because native arm64 dependency availability is not established; Dockerfile remains historically unpinned", + "arm64_expected": "true", + "blocking_reason": "", "classification": "MULTIARCH_READY", - "contract_sha256": "afd201499e17c9305cc49259e5d11244050cccc81cb03974f626a8d01c68100d", - "contract_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", + "contract_sha256": "a664271f274553502f21bb8375713a8c03d3b88a957bf65f8cc0e7b43ade1b39", + "contract_status": "CONTRACT_READY", "dockerfile_path": "dockerfiles/Dockerfile.airr_extra", "expected_executable": "Rscript", "package": "ghcr.io/omnibioai/omnibioai-sif/airr_extra", "runtime_gpu_required": "false", "runtime_license_required": "false", "runtime_network_required": "false", - "scientific_version": "", + "scientific_version": "2.0.0", "smoke_contract": "Rscript -e 'library(airr); x=read_rearrangement('\"'\"'validation-contracts/fixtures/airr-tiny.tsv'\"'\"'); stopifnot(nrow(x)==1, x$sequence_id[[1]]=='\"'\"'omnibioai_airr_1'\"'\"')'", "tool_id": "airr_extra", "version_command": "Rscript -e 'cat(as.character(packageVersion('\"'\"'airr'\"'\"')), '\"'\"'\\n'\"'\"')'" @@ -146,10 +146,10 @@ { "amd64_expected": "true", "arm64_expected": "true", - "blocking_reason": "scientifically meaningful execution requires a valid RAD and permit-list fixture that is not available in repository evidence", + "blocking_reason": "", "classification": "MULTIARCH_READY", - "contract_sha256": "8158a93000832f1eff8c791952a749e37672acee9e9a675c3844359c2e45d7fd", - "contract_status": "CONTRACT_BLOCKED_SMOKE", + "contract_sha256": "89def6237aa466db2002ac35b816ee86cf72dcb98afe7ce2766c10dcfb5554ab", + "contract_status": "CONTRACT_READY", "dockerfile_path": "dockerfiles/Dockerfile.alevin_fry", "expected_executable": "alevin-fry", "package": "ghcr.io/omnibioai/omnibioai-sif/alevin_fry", @@ -157,7 +157,7 @@ "runtime_license_required": "false", "runtime_network_required": "false", "scientific_version": "0.18.3", - "smoke_contract": "", + "smoke_contract": "alevin-fry infer -c validation-contracts/fixtures/alevin-infer-tiny/counts.mtx -e validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt.gz -o smoke-output/alevin-infer -t 2", "tool_id": "alevin_fry", "version_command": "alevin-fry --version" }, @@ -166,7 +166,7 @@ "arm64_expected": "false", "blocking_reason": "no reproducible baseline was selected because the current stable package has no linux-aarch64 artifact; Dockerfile remains historically unpinned", "classification": "MULTIARCH_READY", - "contract_sha256": "5e1b2b08a28ae2e104dd7da771c68c4a85933e70123e72fd4c408fa3ad7f2bb2", + "contract_sha256": "ee1219d4554d161772dabdc2662a4c290856e7c451fc156160a0ac686df2bcef", "contract_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", "dockerfile_path": "dockerfiles/Dockerfile.alevinqc_extra", "expected_executable": "Rscript", diff --git a/validation-contracts/schema-v1/3ddna_extra.json b/validation-contracts/schema-v1/3ddna_extra.json index 4e9fe5f..0cf2f9f 100644 --- a/validation-contracts/schema-v1/3ddna_extra.json +++ b/validation-contracts/schema-v1/3ddna_extra.json @@ -144,11 +144,13 @@ "source_type": "upstream_archive_sha256_and_pinned_conda_version", "tool_id": "3ddna_extra", "validation_contract_schema_version": 1, - "validation_contract_sha256": "393ab6dca7b73c3441ebdbcae79275c84a24edb9924d2783c939f3050eef037e", + "validation_contract_sha256": "118d91c766df34effef5cfd70d5f88e263157e6d5f23d225a7ed380fe5e453e8", "version_command": [ "3d-dna", "--help" ], "version_expected_value": "190716", + "version_observation_parser": "(?m)^version: ([^\\r\\n]+)$", + "version_output_source": "either", "version_parser": "regex:(?m)^version: (190716)$" } diff --git a/validation-contracts/schema-v1/abricate.json b/validation-contracts/schema-v1/abricate.json index 8383353..0c608a3 100644 --- a/validation-contracts/schema-v1/abricate.json +++ b/validation-contracts/schema-v1/abricate.json @@ -142,11 +142,13 @@ "source_type": "upstream_archive_sha256_and_pinned_conda_version", "tool_id": "abricate", "validation_contract_schema_version": 1, - "validation_contract_sha256": "5b95e0bc31e7cfdf102836c73de8a86035371320a13df140e0d922f374931df6", + "validation_contract_sha256": "53be925de09982e9a5875574d1455d319f64bf6e5e93b47fc928c79cdcaef273", "version_command": [ "abricate", "--version" ], "version_expected_value": "1.4.0", + "version_observation_parser": "(?m)^abricate[ \\t]+([^\\r\\n]+)$", + "version_output_source": "either", "version_parser": "regex:(?m)^abricate\\s+(1\\.4\\.0)\\s*$" } diff --git a/validation-contracts/schema-v1/accelerate.json b/validation-contracts/schema-v1/accelerate.json index 0fb41bc..576fdc7 100644 --- a/validation-contracts/schema-v1/accelerate.json +++ b/validation-contracts/schema-v1/accelerate.json @@ -147,11 +147,13 @@ "source_type": "upstream_archive_sha256_and_pinned_conda_version", "tool_id": "accelerate", "validation_contract_schema_version": 1, - "validation_contract_sha256": "d4bed397d759fe866bdcd3c5c7197add9ed6ba5660a5e039ac6bf8ea03160a34", + "validation_contract_sha256": "ee015c8dd47bdc8571f390a40e5a0b7dc6da41bbc9be6f292f023660b4941fe0", "version_command": [ "accelerate", "env" ], "version_expected_value": "1.15.0", + "version_observation_parser": "(?m)^- `Accelerate` version: ([^\\r\\n]+)$", + "version_output_source": "either", "version_parser": "regex:(?m)^- `Accelerate` version: (1\\.15\\.0)\\s*$" } diff --git a/validation-contracts/schema-v1/admixtools.json b/validation-contracts/schema-v1/admixtools.json index 0757a19..216932c 100644 --- a/validation-contracts/schema-v1/admixtools.json +++ b/validation-contracts/schema-v1/admixtools.json @@ -51,6 +51,12 @@ "source": "validation-contracts/evidence/current-package-preflight.csv", "value": "MULTIARCH_READY" }, + { + "field": "distribution_license", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://github.com/DReichLab/AdmixTools/blob/v8.0.2/README#L97-L104", + "value": "pinned release copyright notice permits non-commercial copying with notice retained; intended OmniBioAI distribution scope and any additional permission are not established" + }, { "field": "executable_ambiguity", "quality": "DIRECT_UPSTREAM_EVIDENCE", @@ -84,6 +90,13 @@ "arm64": "NATIVE_PACKAGE_ARTIFACT" }, "distribution_license_review": "REQUIRED_NONCOMMERCIAL_LICENSE", + "distribution_license_review_evidence": { + "copyright_notice_retention_required": true, + "intended_distribution_scope": "UNKNOWN", + "permission_for_intended_distribution": "NOT_ESTABLISHED", + "source": "https://github.com/DReichLab/AdmixTools/blob/v8.0.2/README#L97-L104", + "source_release": "v8.0.2" + }, "evaluated_candidate_version": null, "historical_intended_version": "UNKNOWN", "native_build_validation_status": "NOT_RUN", @@ -124,6 +137,7 @@ "initial_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", "package_mapping_confirmed_read_only": true, "remaining_deficiency_flags": [ + "DISTRIBUTION_LICENSE_REVIEW_REQUIRED", "SMOKE_CONTRACT_MISSING" ], "smoke_network_requirement": "UNKNOWN" @@ -145,7 +159,7 @@ "source_type": "upstream_archive_sha256_and_pinned_conda_version", "tool_id": "admixtools", "validation_contract_schema_version": 1, - "validation_contract_sha256": "e1b39a2629d029a921b550f2e3eb96d250de96c0d9df3b2b0097f6da56e311ca", + "validation_contract_sha256": "7e904c83d44c40f68c65f1c8146ae2286cc278bc74641113cb33fc600f8b5c07", "version_command": [ "qp3Pop", "-v" diff --git a/validation-contracts/schema-v1/afterqc_extra.json b/validation-contracts/schema-v1/afterqc_extra.json index 9e4d77c..6219066 100644 --- a/validation-contracts/schema-v1/afterqc_extra.json +++ b/validation-contracts/schema-v1/afterqc_extra.json @@ -5,7 +5,7 @@ }, "base_image_reference": "mambaorg/micromamba:1.5.8", "blocking_reasons": [ - "no reproducible baseline was selected because native arm64 dependency availability is not established", + "reproducible baseline selection deferred pending explicit unsupported Python 2.7 runtime review", "Dockerfile remains historically unpinned" ], "classification": "MULTIARCH_READY", @@ -30,7 +30,7 @@ "field": "architecture_availability", "quality": "DIRECT_UPSTREAM_EVIDENCE", "source": "https://api.anaconda.org/package/bioconda/afterqc", - "value": "{\"amd64\": \"NOARCH_PACKAGE_REQUIRES_PYTHON_2_7\", \"arm64\": \"NOT_ESTABLISHED\"}" + "value": "{\"amd64\": \"NOARCH_PACKAGE_WITH_NATIVE_PYTHON27_DEPENDENCY_SOLVE\", \"arm64\": \"NOARCH_PACKAGE_WITH_NATIVE_PYTHON27_DEPENDENCY_SOLVE\"}" }, { "field": "base_image_reference", @@ -43,7 +43,7 @@ "field": "baseline_version", "quality": "DIRECT_UPSTREAM_EVIDENCE", "source": "https://api.anaconda.org/package/bioconda/afterqc", - "value": "candidate rejected: latest stable package requires Python 2.7 and no native arm64 dependency solution is established" + "value": "candidate deferred: native dependency solves exist for both architectures, but require unsupported Python 2.7.15; legacy-runtime review not established" }, { "field": "classification", @@ -51,6 +51,12 @@ "source": "validation-contracts/evidence/current-package-preflight.csv", "value": "MULTIARCH_READY" }, + { + "field": "dependency_solution", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://conda.anaconda.org/conda-forge", + "value": "conda create --dry-run --json --override-channels -c bioconda -c conda-forge --no-channel-priority --no-default-packages --subdir afterqc=0.9.7=hdfd78af_4; glibc override 2.17; success true, exit 0; 24 amd64 / 23 arm64 packages, native Python 2.7.15 and OpenSSL 1.1.1w; no installation" + }, { "field": "executable_version_smoke", "quality": "DIRECT_UPSTREAM_EVIDENCE", @@ -69,6 +75,18 @@ "source": "validation-contracts/evidence/current-package-preflight.csv", "value": "ghcr.io/omnibioai/omnibioai-sif/afterqc_extra" }, + { + "field": "package_identity", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://conda.anaconda.org/bioconda/noarch/afterqc-0.9.7-hdfd78af_4.tar.bz2", + "value": "afterqc-0.9.7-hdfd78af_4.tar.bz2;sha256:a0e73790cfd512ecf38f44f11a01812458d1bb7a22df592512e89d27976390f5" + }, + { + "field": "runtime_support", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://www.python.org/doc/sunset-python-2/", + "value": "Python 2 support ended in 2020; artifact availability is not evidence of supported or secure runtime; no risk exception accepted" + }, { "field": "source_identity", "quality": "DIRECT_UPSTREAM_EVIDENCE", @@ -82,19 +100,26 @@ ], "expected_executable": "after.py", "native_amd64_expected": true, - "native_arm64_expected": false, + "native_arm64_expected": true, "package": "ghcr.io/omnibioai/omnibioai-sif/afterqc_extra", "pinning": { "architecture_availability": { - "amd64": "NOARCH_PACKAGE_REQUIRES_PYTHON_2_7", - "arm64": "NOT_ESTABLISHED" + "amd64": "NOARCH_PACKAGE_WITH_NATIVE_PYTHON27_DEPENDENCY_SOLVE", + "arm64": "NOARCH_PACKAGE_WITH_NATIVE_PYTHON27_DEPENDENCY_SOLVE" }, "evaluated_candidate_version": "0.9.7", "historical_intended_version": "UNKNOWN", + "legacy_runtime_review": { + "risk_acceptance": "NOT_ESTABLISHED", + "runtime": "Python 2.7.15", + "security_scan": "NOT_RUN", + "source": "https://www.python.org/doc/sunset-python-2/", + "upstream_support": "END_OF_LIFE" + }, "native_build_validation_status": "NOT_RUN", "new_reproducible_baseline_version": null, "package_artifacts": { - "noarch": "afterqc-0.9.7-hdfd78af_4.tar.bz2;md5:e1097f9eae2bf177048e8816034cc4d0" + "noarch": "afterqc-0.9.7-hdfd78af_4.tar.bz2;sha256:a0e73790cfd512ecf38f44f11a01812458d1bb7a22df592512e89d27976390f5" }, "package_name": "afterqc", "pinning_canary_schema_version": 1, @@ -102,7 +127,7 @@ "source_identity": "sha256:cd4b33f1874bf7588fecdd44a57ca85788022c689922e8186b8dc137c5631bc2", "source_identity_type": "UPSTREAM_ARCHIVE_SHA256", "static_contract_status": "STATIC_CONTRACT_BLOCKED", - "version_selection_basis": "candidate rejected: latest stable package requires Python 2.7 and no native arm64 dependency solution is established" + "version_selection_basis": "candidate deferred: native dependency solves exist for both architectures, but require unsupported Python 2.7.15; legacy-runtime review not established" }, "registry_namespace": "ghcr.io/omnibioai/omnibioai-sif", "remediation": { @@ -129,7 +154,7 @@ "remaining_deficiency_flags": [ "SCIENTIFIC_VERSION_MISSING", "SOURCE_IDENTITY_MISSING", - "ARCHITECTURE_EVIDENCE_MISSING" + "LEGACY_RUNTIME_REVIEW_REQUIRED" ], "smoke_network_requirement": "NETWORK_FREE" }, @@ -160,7 +185,7 @@ "source_type": "unknown_unpinned", "tool_id": "afterqc_extra", "validation_contract_schema_version": 1, - "validation_contract_sha256": "45854a7d2256c367657f9fbcb91b693118d474fedca9eb8faf194abe4abc269f", + "validation_contract_sha256": "2f3b551d0ce9a2d27585d4ab345f8f6dd81f060139c26647e604fcf679492d82", "version_command": [ "after.py", "--version" diff --git a/validation-contracts/schema-v1/agat_extra.json b/validation-contracts/schema-v1/agat_extra.json index 08b7428..7cbf870 100644 --- a/validation-contracts/schema-v1/agat_extra.json +++ b/validation-contracts/schema-v1/agat_extra.json @@ -153,11 +153,13 @@ "source_type": "upstream_archive_sha256_and_pinned_conda_version", "tool_id": "agat_extra", "validation_contract_schema_version": 1, - "validation_contract_sha256": "0fd38af58559f0c55d5cb9959fe97a77adaff740ff7fdaf44afacce6c474289a", + "validation_contract_sha256": "254843a87b40693630fd5967c80286f9a82b1ebd429f4090e1bde8c3b2097bab", "version_command": [ "agat", "--version" ], "version_expected_value": "v1.7.0", + "version_observation_parser": "(?m)^(v[^\\r\\n]+)$", + "version_output_source": "either", "version_parser": "regex:(?m)^(v1\\.7\\.0)\\s*$" } diff --git a/validation-contracts/schema-v1/aicsimageio_extra.json b/validation-contracts/schema-v1/aicsimageio_extra.json index f15ad89..de00a83 100644 --- a/validation-contracts/schema-v1/aicsimageio_extra.json +++ b/validation-contracts/schema-v1/aicsimageio_extra.json @@ -149,12 +149,14 @@ "source_type": "upstream_archive_sha256_and_pinned_conda_version", "tool_id": "aicsimageio_extra", "validation_contract_schema_version": 1, - "validation_contract_sha256": "c07480d0c08a1946b3db71126f4b1bc86af22e68ffbb1b050322b6bd5d6cc07a", + "validation_contract_sha256": "392c12d169b104cfe37f6a07275e7b5fbca6ed6e23b84cf9b0b2cb61eb1d2d21", "version_command": [ "python", "-c", "import aicsimageio; print(aicsimageio.get_module_version())" ], "version_expected_value": "4.14.0", + "version_observation_parser": "(?m)^([^\\r\\n]+)$", + "version_output_source": "stdout", "version_parser": "regex:(?m)^(4\\.14\\.0)\\s*$" } diff --git a/validation-contracts/schema-v1/airr_extra.json b/validation-contracts/schema-v1/airr_extra.json index 3bd8a15..3809f33 100644 --- a/validation-contracts/schema-v1/airr_extra.json +++ b/validation-contracts/schema-v1/airr_extra.json @@ -4,16 +4,13 @@ "statement": "read-only manifest inspection found linux/amd64 and linux/arm64 for mambaorg/micromamba:1.5.8; package architecture evidence is recorded below" }, "base_image_reference": "mambaorg/micromamba:1.5.8", - "blocking_reasons": [ - "no reproducible baseline was selected because native arm64 dependency availability is not established", - "Dockerfile remains historically unpinned" - ], + "blocking_reasons": [], "classification": "MULTIARCH_READY", - "contract_confidence": "MEDIUM", - "contract_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", + "contract_confidence": "HIGH", + "contract_status": "CONTRACT_READY", "display_name": "airr_extra", "dockerfile_path": "dockerfiles/Dockerfile.airr_extra", - "dockerfile_sha256": "9ad8e4e64ed3bbc4cd3e1946c3f67627cba7b830627a60da5366f5782c7023e7", + "dockerfile_sha256": "31f0d3e8f850b39361c7f11f35523d93d2004b5ff40e054d2762aa991c1c0175", "eligibility": { "mapping_status": "EXACT", "package_mapping_confirmed": true, @@ -23,14 +20,14 @@ { "field": "architecture", "quality": "DIRECT_UPSTREAM_EVIDENCE", - "source": "https://api.anaconda.org/package/bioconda/r-airr", - "value": "package is noarch; base manifest provides linux/amd64 and linux/arm64" + "source": "https://api.anaconda.org/package/conda-forge/r-airr", + "value": "noarch r-airr 2.0.0 with native R 4.5.3, readr, stringi, yaml, jsonlite metadata dependency solutions on linux-64 and linux-aarch64" }, { "field": "architecture_availability", "quality": "DIRECT_UPSTREAM_EVIDENCE", - "source": "https://api.anaconda.org/package/bioconda/r-airr", - "value": "{\"amd64\": \"NOARCH_PACKAGE_REQUIRES_R_3_5_1\", \"arm64\": \"NOT_ESTABLISHED\"}" + "source": "https://api.anaconda.org/package/conda-forge/r-airr", + "value": "{\"amd64\": \"NOARCH_PACKAGE_WITH_NATIVE_DEPENDENCY_SOLVE\", \"arm64\": \"NOARCH_PACKAGE_WITH_NATIVE_DEPENDENCY_SOLVE\"}" }, { "field": "base_image_reference", @@ -42,8 +39,8 @@ { "field": "baseline_version", "quality": "DIRECT_UPSTREAM_EVIDENCE", - "source": "https://api.anaconda.org/package/bioconda/r-airr", - "value": "candidate rejected: latest Bioconda package requires R 3.5.1 and no native arm64 dependency solution is established" + "source": "https://api.anaconda.org/package/conda-forge/r-airr", + "value": "latest stable conda-forge r-airr release; exact noarch build with successful metadata-only linux-64 and linux-aarch64 dependency solves" }, { "field": "classification", @@ -51,6 +48,12 @@ "source": "validation-contracts/evidence/current-package-preflight.csv", "value": "MULTIARCH_READY" }, + { + "field": "dependency_solution", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://conda.anaconda.org/conda-forge", + "value": "conda create --dry-run --json --override-channels -c conda-forge --strict-channel-priority --no-default-packages --subdir r-airr=2.0.0=r45hc72bb7e_0; CONDA_OVERRIDE_GLIBC=2.17; both exit 0, success=true, 120 packages; no installation or execution" + }, { "field": "fixture", "quality": "DIRECT_REPOSITORY_EVIDENCE", @@ -63,17 +66,29 @@ "source": "validation-contracts/evidence/current-package-preflight.csv", "value": "ghcr.io/omnibioai/omnibioai-sif/airr_extra" }, + { + "field": "package_identity", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://api.anaconda.org/package/conda-forge/r-airr", + "value": "r-airr-2.0.0-r45hc72bb7e_0.conda;sha256:b8c733fff391dcbc1f13df2a1f46452fa05f9036c01303efb3850d5477f7847c" + }, { "field": "package_runtime", "quality": "DIRECT_REPOSITORY_EVIDENCE", "source": "dockerfiles/Dockerfile.airr_extra", "value": "Dockerfile installs r-airr into a Micromamba environment" }, + { + "field": "smoke_api", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://cran.r-project.org/src/contrib/airr_2.0.0.tar.gz", + "value": "R/Interface.R defines read_rearrangement(file, base=c(\"1\", \"0\"), ...); DESCRIPTION version 2.0.0, NeedsCompilation=no, License=CC BY 4.0; NEWS retains rearrangement API while removing alignment APIs" + }, { "field": "source_identity", "quality": "DIRECT_UPSTREAM_EVIDENCE", - "source": "https://cran.r-project.org/src/contrib/Archive/airr/airr_1.2.0.tar.gz", - "value": "sha256:306cd7084b1ef0617af62e5815df538f2dcd6d6aaabb593df6f579e8ab5c5cef" + "source": "https://cran.r-project.org/src/contrib/airr_2.0.0.tar.gz", + "value": "sha256:e4932ec84dad46d37327ab5aa5a34b95095677b6392bff2f59fd473eb942da52" } ], "expected_architectures": [ @@ -82,27 +97,29 @@ ], "expected_executable": "Rscript", "native_amd64_expected": true, - "native_arm64_expected": false, + "native_arm64_expected": true, "package": "ghcr.io/omnibioai/omnibioai-sif/airr_extra", "pinning": { "architecture_availability": { - "amd64": "NOARCH_PACKAGE_REQUIRES_R_3_5_1", - "arm64": "NOT_ESTABLISHED" + "amd64": "NOARCH_PACKAGE_WITH_NATIVE_DEPENDENCY_SOLVE", + "arm64": "NOARCH_PACKAGE_WITH_NATIVE_DEPENDENCY_SOLVE" }, - "evaluated_candidate_version": "1.2.0", + "baseline_semantics_note": "new baseline 2.0.0 retains read_rearrangement; upstream removes alignment APIs; no historical intended version or runtime compatibility is claimed", + "evaluated_candidate_version": null, "historical_intended_version": "UNKNOWN", "native_build_validation_status": "NOT_RUN", - "new_reproducible_baseline_version": null, + "new_reproducible_baseline_version": "2.0.0", "package_artifacts": { - "noarch": "r-airr-1.2.0-r351h6115d3f_1.tar.bz2;md5:a76851608b2354e9bb3dd457aa21a425" + "noarch": "r-airr-2.0.0-r45hc72bb7e_0.conda;sha256:b8c733fff391dcbc1f13df2a1f46452fa05f9036c01303efb3850d5477f7847c" }, "package_name": "r-airr", "pinning_canary_schema_version": 1, + "previous_rejected_candidate_version": "1.2.0", "release_status": "NOT_PUBLISHED", - "source_identity": "sha256:306cd7084b1ef0617af62e5815df538f2dcd6d6aaabb593df6f579e8ab5c5cef", + "source_identity": "sha256:e4932ec84dad46d37327ab5aa5a34b95095677b6392bff2f59fd473eb942da52", "source_identity_type": "UPSTREAM_ARCHIVE_SHA256", - "static_contract_status": "STATIC_CONTRACT_BLOCKED", - "version_selection_basis": "candidate rejected: latest Bioconda package requires R 3.5.1 and no native arm64 dependency solution is established" + "static_contract_status": "STATIC_CONTRACT_READY", + "version_selection_basis": "latest stable conda-forge r-airr release; exact noarch build with successful metadata-only linux-64 and linux-aarch64 dependency solves" }, "registry_namespace": "ghcr.io/omnibioai/omnibioai-sif", "remediation": { @@ -126,11 +143,7 @@ ], "initial_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", "package_mapping_confirmed_read_only": true, - "remaining_deficiency_flags": [ - "SCIENTIFIC_VERSION_MISSING", - "SOURCE_IDENTITY_MISSING", - "ARCHITECTURE_EVIDENCE_MISSING" - ], + "remaining_deficiency_flags": [], "smoke_network_requirement": "NETWORK_FREE" }, "runtime_database_required": false, @@ -138,9 +151,9 @@ "runtime_license_required": false, "runtime_network_required": false, "runtime_reference_data_required": false, - "scientific_version": null, - "scientific_version_evidence": null, - "scientific_version_source": null, + "scientific_version": "2.0.0", + "scientific_version_evidence": "conda-forge::r-airr=2.0.0=r45hc72bb7e_0", + "scientific_version_source": "new_reproducible_baseline_package_version", "smoke_command": [ "Rscript", "-e", @@ -151,17 +164,19 @@ "validation-contracts/fixtures/airr-tiny.tsv" ], "smoke_success_condition": "exit 0 after parsing exactly one schema-valid AIRR rearrangement with the expected sequence_id", - "source_immutable_identity": null, - "source_reference": null, - "source_type": "unknown_unpinned", + "source_immutable_identity": "sha256:e4932ec84dad46d37327ab5aa5a34b95095677b6392bff2f59fd473eb942da52", + "source_reference": "https://cran.r-project.org/src/contrib/airr_2.0.0.tar.gz", + "source_type": "upstream_archive_sha256_and_pinned_conda_version", "tool_id": "airr_extra", "validation_contract_schema_version": 1, - "validation_contract_sha256": "afd201499e17c9305cc49259e5d11244050cccc81cb03974f626a8d01c68100d", + "validation_contract_sha256": "a664271f274553502f21bb8375713a8c03d3b88a957bf65f8cc0e7b43ade1b39", "version_command": [ "Rscript", "-e", "cat(as.character(packageVersion('airr')), '\\n')" ], - "version_expected_value": null, - "version_parser": "regex:(?m)^([0-9]+(?:\\.[0-9]+)+)\\s*$" + "version_expected_value": "2.0.0", + "version_observation_parser": "(?m)^([^\\r\\n]+)$", + "version_output_source": "stdout", + "version_parser": "regex:\\A(2\\.0\\.0)\\s*\\Z" } diff --git a/validation-contracts/schema-v1/alevin_fry.json b/validation-contracts/schema-v1/alevin_fry.json index c023ee4..db5c17b 100644 --- a/validation-contracts/schema-v1/alevin_fry.json +++ b/validation-contracts/schema-v1/alevin_fry.json @@ -4,12 +4,10 @@ "statement": "read-only manifest inspection found linux/amd64 and linux/arm64 for mambaorg/micromamba:1.5.8; package architecture evidence is recorded below" }, "base_image_reference": "mambaorg/micromamba:1.5.8", - "blocking_reasons": [ - "scientifically meaningful execution requires a valid RAD and permit-list fixture that is not available in repository evidence" - ], + "blocking_reasons": [], "classification": "MULTIARCH_READY", - "contract_confidence": "MEDIUM", - "contract_status": "CONTRACT_BLOCKED_SMOKE", + "contract_confidence": "HIGH", + "contract_status": "CONTRACT_READY", "display_name": "alevin_fry", "dockerfile_path": "dockerfiles/Dockerfile.alevin_fry", "dockerfile_sha256": "b31f4429c6c1212f5c593d32f7d205b3559d393a103645afceb33b627fe55c33", @@ -62,11 +60,23 @@ "source": "validation-contracts/evidence/current-package-preflight.csv", "value": "ghcr.io/omnibioai/omnibioai-sif/alevin_fry" }, + { + "field": "smoke_fixture", + "quality": "DIRECT_REPOSITORY_EVIDENCE", + "source": "validation-contracts/fixtures/alevin-infer-tiny/README.md", + "value": "synthetic two-cell, two-gene, three-class input; symmetric singleton priors yield analytical expected count rows [7,7] and [3,3]; not executed" + }, + { + "field": "smoke_format", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://github.com/COMBINE-lab/alevin-fry/blob/ad05742d274230f9141b2eabeebd1c1b31692199/src/eq_class.rs", + "value": "gzip label file: gene count, class count, then gene IDs followed by class ID; parser init_from_eqc_file" + }, { "field": "smoke_requirement", "quality": "DIRECT_UPSTREAM_EVIDENCE", - "source": "https://github.com/COMBINE-lab/alevin-fry", - "value": "scientific subcommands consume RAD mapping and permit-list inputs" + "source": "https://github.com/COMBINE-lab/alevin-fry/blob/ad05742d274230f9141b2eabeebd1c1b31692199/docs/source/infer.rst", + "value": "documented infer subcommand permits scientifically meaningful bounded equivalence-class gene inference without RAD input; this contract does not validate RAD processing" }, { "field": "source_identity", @@ -80,6 +90,13 @@ "arm64" ], "expected_executable": "alevin-fry", + "fixture_sha256": { + "validation-contracts/fixtures/alevin-infer-tiny/counts.mtx": "d50b0c627f4844a0ceb06d352d5144d2d423be7f1f144547690895a60a6c4702", + "validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt.gz": "6465059c0289de1fd4594bab770ec8ed805978cab428e750a3cd5ea5a60ce37d", + "validation-contracts/fixtures/alevin-infer-tiny/expected.json": "a0bb0086c0482400917682f8e095f305fca34ae5d4a6aa868c3212574b71412f", + "validation-contracts/fixtures/alevin-infer-tiny/quants_mat_cols.txt": "2179b8de40ec954864d12cc3f5f68a58811f156d7ba3b26971eeea35a7aa43d0", + "validation-contracts/fixtures/alevin-infer-tiny/quants_mat_rows.txt": "c8d984910fa921f6f987f258a7fcb6a8ba3f762a32ae1634ea12fe1e4f6a373d" + }, "native_amd64_expected": true, "native_arm64_expected": true, "package": "ghcr.io/omnibioai/omnibioai-sif/alevin_fry", @@ -101,7 +118,7 @@ "release_status": "NOT_PUBLISHED", "source_identity": "sha256:9452d87d9f621d2bf16230cfaf9b2b789407e1a98015361e49ec631f5e46631b", "source_identity_type": "UPSTREAM_ARCHIVE_SHA256", - "static_contract_status": "STATIC_CONTRACT_BLOCKED", + "static_contract_status": "STATIC_CONTRACT_READY", "version_selection_basis": "latest stable Bioconda main-channel release with distinct native linux-64 and linux-aarch64 artifacts" }, "registry_namespace": "ghcr.io/omnibioai/omnibioai-sif", @@ -126,10 +143,8 @@ ], "initial_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", "package_mapping_confirmed_read_only": true, - "remaining_deficiency_flags": [ - "SMOKE_CONTRACT_MISSING" - ], - "smoke_network_requirement": "UNKNOWN" + "remaining_deficiency_flags": [], + "smoke_network_requirement": "NETWORK_FREE" }, "runtime_database_required": false, "runtime_gpu_required": false, @@ -139,20 +154,51 @@ "scientific_version": "0.18.3", "scientific_version_evidence": "alevin-fry=0.18.3", "scientific_version_source": "new_reproducible_baseline_package_version", - "smoke_command": null, - "smoke_expected_outputs": [], - "smoke_inputs": [], - "smoke_success_condition": null, + "smoke_assertions": { + "evidence_scope": "gene inference only; excludes mapping, RAD, permit-list generation and UMI resolution", + "expectation_basis": "ANALYTICAL_SYMMETRY_NOT_OBSERVED_RUNTIME", + "expected_fixture": "validation-contracts/fixtures/alevin-infer-tiny/expected.json", + "kind": "matrix_market_gene_counts", + "output_directory": "smoke-output/alevin-infer", + "timeout_seconds": 60 + }, + "smoke_command": [ + "alevin-fry", + "infer", + "-c", + "validation-contracts/fixtures/alevin-infer-tiny/counts.mtx", + "-e", + "validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt.gz", + "-o", + "smoke-output/alevin-infer", + "-t", + "2" + ], + "smoke_expected_outputs": [ + "smoke-output/alevin-infer/quants_mat.mtx", + "smoke-output/alevin-infer/quants_mat_rows.txt", + "smoke-output/alevin-infer/quants_mat_cols.txt" + ], + "smoke_inputs": [ + "validation-contracts/fixtures/alevin-infer-tiny/counts.mtx", + "validation-contracts/fixtures/alevin-infer-tiny/eq_labels.txt.gz", + "validation-contracts/fixtures/alevin-infer-tiny/quants_mat_rows.txt", + "validation-contracts/fixtures/alevin-infer-tiny/quants_mat_cols.txt", + "validation-contracts/fixtures/alevin-infer-tiny/expected.json" + ], + "smoke_success_condition": "exit 0 within 60 seconds; output labels and 2x2 MatrixMarket counts equal expected.json within absolute tolerance 0.0001; finite nonnegative values and row totals [14,6]; order-independent coordinate comparison, reject duplicate coordinates", "source_immutable_identity": "sha256:9452d87d9f621d2bf16230cfaf9b2b789407e1a98015361e49ec631f5e46631b", "source_reference": "https://github.com/COMBINE-lab/alevin-fry/releases/download/v0.18.3/source.tar.gz", "source_type": "upstream_archive_sha256_and_pinned_conda_version", "tool_id": "alevin_fry", "validation_contract_schema_version": 1, - "validation_contract_sha256": "8158a93000832f1eff8c791952a749e37672acee9e9a675c3844359c2e45d7fd", + "validation_contract_sha256": "89def6237aa466db2002ac35b816ee86cf72dcb98afe7ce2766c10dcfb5554ab", "version_command": [ "alevin-fry", "--version" ], "version_expected_value": "0.18.3", + "version_observation_parser": "(?m)^alevin-fry[ \\t]+([^\\r\\n]+)$", + "version_output_source": "either", "version_parser": "regex:(?m)^alevin-fry\\s+(0\\.18\\.3)\\s*$" } diff --git a/validation-contracts/schema-v1/alevinqc_extra.json b/validation-contracts/schema-v1/alevinqc_extra.json index 0b2362b..314ba04 100644 --- a/validation-contracts/schema-v1/alevinqc_extra.json +++ b/validation-contracts/schema-v1/alevinqc_extra.json @@ -32,6 +32,18 @@ "source": "https://api.anaconda.org/package/bioconda/bioconductor-alevinqc", "value": "{\"amd64\": \"NATIVE_PACKAGE_ARTIFACT\", \"arm64\": \"NO_PACKAGE_ARTIFACT\"}" }, + { + "field": "architecture_candidate_review", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://api.anaconda.org/package/bioconda/bioconductor-alevinqc", + "value": "latest 1.26.0 artifacts only linux-64/osx-64; no native linux-aarch64 artifact; older noarch builds end at 1.10.0, not accepted as a substitute" + }, + { + "field": "architecture_solver_failure", + "quality": "DIRECT_UPSTREAM_EVIDENCE", + "source": "https://conda.anaconda.org/bioconda", + "value": "conda create --dry-run --json --override-channels -c bioconda -c conda-forge --no-channel-priority --no-default-packages --subdir linux-aarch64 bioconductor-alevinqc=1.26.0; CONDA_OVERRIDE_GLIBC=2.17; exit 1 PackagesNotFoundError; no installation" + }, { "field": "base_image_reference", "line": 1, @@ -89,6 +101,23 @@ "amd64": "NATIVE_PACKAGE_ARTIFACT", "arm64": "NO_PACKAGE_ARTIFACT" }, + "architecture_candidate_review": { + "arm64_solver_exception": "PackagesNotFoundError", + "arm64_solver_exit_code": 1, + "candidate_subdirs": [ + "linux-64", + "osx-64" + ], + "candidate_version": "1.26.0", + "channels": [ + "bioconda", + "conda-forge" + ], + "historical_noarch_latest_version": "1.10.0", + "historical_noarch_substitution": "NOT_AUTHORIZED", + "native_arm64_artifact_count": 0, + "native_runtime_validation": "NOT_RUN" + }, "evaluated_candidate_version": "1.26.0", "historical_intended_version": "UNKNOWN", "native_build_validation_status": "NOT_RUN", @@ -156,7 +185,7 @@ "source_type": "unknown_unpinned", "tool_id": "alevinqc_extra", "validation_contract_schema_version": 1, - "validation_contract_sha256": "5e1b2b08a28ae2e104dd7da771c68c4a85933e70123e72fd4c408fa3ad7f2bb2", + "validation_contract_sha256": "ee1219d4554d161772dabdc2662a4c290856e7c451fc156160a0ac686df2bcef", "version_command": [ "Rscript", "-e", diff --git a/validation-contracts/source-pinning-canary-10.json b/validation-contracts/source-pinning-canary-10.json index fae612d..1e3a1f8 100644 --- a/validation-contracts/source-pinning-canary-10.json +++ b/validation-contracts/source-pinning-canary-10.json @@ -82,24 +82,31 @@ }, { "architecture_availability": { - "amd64": "NOARCH_PACKAGE_REQUIRES_PYTHON_2_7", - "arm64": "NOT_ESTABLISHED" + "amd64": "NOARCH_PACKAGE_WITH_NATIVE_PYTHON27_DEPENDENCY_SOLVE", + "arm64": "NOARCH_PACKAGE_WITH_NATIVE_PYTHON27_DEPENDENCY_SOLVE" }, "evaluated_candidate_version": "0.9.7", + "legacy_runtime_review": { + "risk_acceptance": "NOT_ESTABLISHED", + "runtime": "Python 2.7.15", + "security_scan": "NOT_RUN", + "source": "https://www.python.org/doc/sunset-python-2/", + "upstream_support": "END_OF_LIFE" + }, "new_reproducible_baseline_version": null, "package_artifacts": { - "noarch": "afterqc-0.9.7-hdfd78af_4.tar.bz2;md5:e1097f9eae2bf177048e8816034cc4d0" + "noarch": "afterqc-0.9.7-hdfd78af_4.tar.bz2;sha256:a0e73790cfd512ecf38f44f11a01812458d1bb7a22df592512e89d27976390f5" }, "package_name": "afterqc", "remaining_blockers": [ - "no reproducible baseline was selected because native arm64 dependency availability is not established", + "reproducible baseline selection deferred pending explicit unsupported Python 2.7 runtime review", "Dockerfile remains historically unpinned" ], "source_identity": "sha256:cd4b33f1874bf7588fecdd44a57ca85788022c689922e8186b8dc137c5631bc2", "source_reference": "https://github.com/OpenGene/AfterQC/archive/v0.9.7.tar.gz", "static_contract_status": "STATIC_CONTRACT_BLOCKED", "tool_id": "afterqc_extra", - "version_selection_basis": "candidate rejected: latest stable package requires Python 2.7 and no native arm64 dependency solution is established" + "version_selection_basis": "candidate deferred: native dependency solves exist for both architectures, but require unsupported Python 2.7.15; legacy-runtime review not established" }, { "architecture_availability": { @@ -139,24 +146,23 @@ }, { "architecture_availability": { - "amd64": "NOARCH_PACKAGE_REQUIRES_R_3_5_1", - "arm64": "NOT_ESTABLISHED" + "amd64": "NOARCH_PACKAGE_WITH_NATIVE_DEPENDENCY_SOLVE", + "arm64": "NOARCH_PACKAGE_WITH_NATIVE_DEPENDENCY_SOLVE" }, - "evaluated_candidate_version": "1.2.0", - "new_reproducible_baseline_version": null, + "baseline_semantics_note": "new baseline 2.0.0 retains read_rearrangement; upstream removes alignment APIs; no historical intended version or runtime compatibility is claimed", + "evaluated_candidate_version": null, + "new_reproducible_baseline_version": "2.0.0", "package_artifacts": { - "noarch": "r-airr-1.2.0-r351h6115d3f_1.tar.bz2;md5:a76851608b2354e9bb3dd457aa21a425" + "noarch": "r-airr-2.0.0-r45hc72bb7e_0.conda;sha256:b8c733fff391dcbc1f13df2a1f46452fa05f9036c01303efb3850d5477f7847c" }, "package_name": "r-airr", - "remaining_blockers": [ - "no reproducible baseline was selected because native arm64 dependency availability is not established", - "Dockerfile remains historically unpinned" - ], - "source_identity": "sha256:306cd7084b1ef0617af62e5815df538f2dcd6d6aaabb593df6f579e8ab5c5cef", - "source_reference": "https://cran.r-project.org/src/contrib/Archive/airr/airr_1.2.0.tar.gz", - "static_contract_status": "STATIC_CONTRACT_BLOCKED", + "previous_rejected_candidate_version": "1.2.0", + "remaining_blockers": [], + "source_identity": "sha256:e4932ec84dad46d37327ab5aa5a34b95095677b6392bff2f59fd473eb942da52", + "source_reference": "https://cran.r-project.org/src/contrib/airr_2.0.0.tar.gz", + "static_contract_status": "STATIC_CONTRACT_READY", "tool_id": "airr_extra", - "version_selection_basis": "candidate rejected: latest Bioconda package requires R 3.5.1 and no native arm64 dependency solution is established" + "version_selection_basis": "latest stable conda-forge r-airr release; exact noarch build with successful metadata-only linux-64 and linux-aarch64 dependency solves" }, { "architecture_availability": { @@ -170,12 +176,11 @@ "arm64": "alevin-fry-0.18.3-h6789a04_0.conda;sha256:680024b2efcee32468a8287c2e60ca234cdefbafbd515a71bf513a627990e5b2" }, "package_name": "alevin-fry", - "remaining_blockers": [ - "scientifically meaningful execution requires a valid RAD and permit-list fixture that is not available in repository evidence" - ], + "remaining_blockers": [], + "smoke_scope": "bounded infer equivalence-class to gene-count smoke; RAD/permit-list/UMI resolution not exercised", "source_identity": "sha256:9452d87d9f621d2bf16230cfaf9b2b789407e1a98015361e49ec631f5e46631b", "source_reference": "https://github.com/COMBINE-lab/alevin-fry/releases/download/v0.18.3/source.tar.gz", - "static_contract_status": "STATIC_CONTRACT_BLOCKED", + "static_contract_status": "STATIC_CONTRACT_READY", "tool_id": "alevin_fry", "version_selection_basis": "latest stable Bioconda main-channel release with distinct native linux-64 and linux-aarch64 artifacts" }, diff --git a/validation-contracts/summary.json b/validation-contracts/summary.json index 335f925..b79477c 100644 --- a/validation-contracts/summary.json +++ b/validation-contracts/summary.json @@ -7,10 +7,10 @@ "blocked_architecture_count": 0, "blocked_executable_count": 0, "blocked_license_count": 0, - "blocked_multiple_reasons_count": 595, + "blocked_multiple_reasons_count": 594, "blocked_package_mapping_count": 0, "blocked_runtime_dependency_count": 0, - "blocked_smoke_count": 1, + "blocked_smoke_count": 0, "blocked_source_identity_count": 0, "blocked_tools": [ { @@ -23,27 +23,12 @@ }, { "blocking_reasons": [ - "no reproducible baseline was selected because native arm64 dependency availability is not established", + "reproducible baseline selection deferred pending explicit unsupported Python 2.7 runtime review", "Dockerfile remains historically unpinned" ], "contract_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", "tool_id": "afterqc_extra" }, - { - "blocking_reasons": [ - "no reproducible baseline was selected because native arm64 dependency availability is not established", - "Dockerfile remains historically unpinned" - ], - "contract_status": "CONTRACT_BLOCKED_MULTIPLE_REASONS", - "tool_id": "airr_extra" - }, - { - "blocking_reasons": [ - "scientifically meaningful execution requires a valid RAD and permit-list fixture that is not available in repository evidence" - ], - "contract_status": "CONTRACT_BLOCKED_SMOKE", - "tool_id": "alevin_fry" - }, { "blocking_reasons": [ "no reproducible baseline was selected because the current stable package has no linux-aarch64 artifact", @@ -5922,7 +5907,7 @@ } ], "blocked_version_count": 0, - "contract_ready_count": 5, + "contract_ready_count": 7, "contract_target_count": 601, "multiarch_ready_count": 860, "package_intersection_count": 611, @@ -5931,7 +5916,9 @@ "abricate", "accelerate", "agat_extra", - "aicsimageio_extra" + "aicsimageio_extra", + "airr_extra", + "alevin_fry" ], "reference_tools_excluded": 10, "schema_version": 1, From fac440b4d7b2c97a1c9234ff1b7d162e03390a4d Mon Sep 17 00:00:00 2001 From: man4ish Date: Sun, 4 Oct 2026 14:46:43 -0500 Subject: [PATCH 2/2] Retain validated canary payloads for gated release --- .../workflows/contract-native-canary-validation.yml | 12 ++++++++++++ draft/progress.md | 6 ++++++ tests/test_contract_native_validation.py | 5 +++++ 3 files changed, 23 insertions(+) diff --git a/.github/workflows/contract-native-canary-validation.yml b/.github/workflows/contract-native-canary-validation.yml index ab091cb..82877b4 100644 --- a/.github/workflows/contract-native-canary-validation.yml +++ b/.github/workflows/contract-native-canary-validation.yml @@ -98,6 +98,18 @@ jobs: if-no-files-found: error retention-days: 30 + - name: Retain successful SIF candidate for later separately gated release + if: ${{ success() }} + uses: actions/upload-artifact@v4 + with: + name: contract-native-payload-${{ matrix.tool_id }}-${{ matrix.arch }} + path: | + ${{ runner.temp }}/native-evidence/validated.sif + ${{ runner.temp }}/native-evidence/entry.json + if-no-files-found: error + compression-level: 0 + retention-days: 30 + reconcile: if: ${{ always() }} needs: [selection, native-validation] diff --git a/draft/progress.md b/draft/progress.md index 88cd988..deaf29e 100644 --- a/draft/progress.md +++ b/draft/progress.md @@ -191,3 +191,9 @@ - Verified all 591 non-canary contracts byte-identical to remote main and zero diff for frozen workflow/engine/identity/integrity and reference Dockerfiles. Only AIRR's previously reviewed canary Dockerfile pin is included. - Isolated relevant tests **765/765 PASS**; full repository suite **8,307 PASS / 13 SKIP / 0 FAIL**. Ruff, actionlint, static contract validator and diff checks PASS. Skips include absent mounted SIF store; no missing runtime evidence is promoted to PASS. - No image build, workflow dispatch or publication yet. Next in autonomous sequence: exact-scope commit/push/remote verification, then controlled native-only workflow execution before assessing publication eligibility. + +### Remote validation branch and candidate handoff — 2026-10-04 + +- Committed exact 41-file scope as `1f5db1500aeeccdd6b730edb48ce0fe2f034fc52`, pushed non-force branch, and independently verified remote SHA plus workflow/runner/test blobs 3/3. PR #11: https://github.com/OmniBioAI/omnibioai-tool-images/pull/11. Remote CI run `37229412691` passed. +- Before native dispatch, added a success-only Actions artifact handoff retaining the validated SIF and its bound entry record. The former evidence-only retention excluded SIF payloads; without a handoff, future publication would lose the exact validated bytes when runners terminate. No registry write or release step was added. +- Revalidated 69 adapter tests, actionlint and Ruff. Latest change must pass remote CI before merge. No native workflow dispatch, build or publication yet; original worktree preserved. diff --git a/tests/test_contract_native_validation.py b/tests/test_contract_native_validation.py index 33fb907..0aba80c 100644 --- a/tests/test_contract_native_validation.py +++ b/tests/test_contract_native_validation.py @@ -207,6 +207,11 @@ def test_workflow_static_safety(): assert workflow["jobs"]["native-validation"]["needs"] == "selection" assert workflow["jobs"]["native-validation"]["strategy"]["fail-fast"] == "false" assert workflow["jobs"]["reconcile"]["needs"] == ["selection", "native-validation"] + payload = [step for step in workflow["jobs"]["native-validation"]["steps"] if step.get("with", {}).get("name", "").startswith("contract-native-payload-")] + assert len(payload) == 1 + assert payload[0]["if"] == "${{ success() }}" + assert "validated.sif" in payload[0]["with"]["path"] + assert "entry.json" in payload[0]["with"]["path"] for forbidden in ("packages: write", "oras", "docker login", "docker push", "--push", "sif_release", "setup-qemu", "publish=true"): assert forbidden not in path.read_text()