Skip to content

v0.3 membership-store contract

The scalable membership store is the production-size counterpart to the fixture-oriented in-memory MembershipArtifact. It is designed for the full autosomal gnomAD membership plus the pinned ClinVar source without constructing a Python tuple or set containing every row or variant key.

This repository ships the contract, producer, verifier, and synthetic tests. The first real store candidate is published and independently verified at exact Hub commit 96e97a7ff..., with its placed-window and held-role split evidence separately verified at exact Hub commit 6d2ec7dd.... These are deterministic unphased variant-membership artifacts, not the phased haplotype holdout described by historical RFC-0006 or a released v0.3 snapshot. A proximity window over unphased variants is not a substitute for phased haplotype membership.

Artifact layout

One immutable store directory contains exactly:

File Purpose
manifest.json Closed semantic contract, exact source lineage, counts, row digest, and file identities
memberships.parquet Portable canonically ordered membership rows
lookup.sqlite Read-only point index, integer R-tree interval index, and role iterator
snapshot-lineage.json Exact lineage bytes consumed by the builder; self-contained source provenance
build-receipt.json Closed builder commit, digest-pinned container, package, runtime, and creation record

The manifest schema is configs/data_v03/membership-store.schema.json. Build inputs use configs/data_v03/membership-build-spec.schema.json; the physical build receipt uses configs/data_v03/membership-build-receipt.schema.json. All three reject unknown fields.

The manifest binds a closed lineage evidence profile. Real artifacts report official; reduced test-only artifacts report synthetic_fixture. Both remain fully verifiable, but verification summaries expose the profile so downstream automation cannot mistake fixture mechanics for production evidence.

Build flow

The builder accepts no arbitrary row stream. Its source specification must name all 22 local gnomAD Parquet shards and the local ClinVar Parquet shard. Each file is matched to the immutable repository, revision, namespace, artifact path, SHA-256, size, row count, schema, and remote-postflight evidence already recorded in snapshot-lineage.json. For a non-fixture build, the official snapshot-lineage verifier captures that file once, verifies its closed semantic contract (including every gnomAD and ClinVar postflight Parquet audit), and returns the exact captured payload and identity with deeply immutable parsed semantics. Those postflight summaries include normalized identities for every verified remote file and reconcile the receipt/audit and Parquet identities. The membership builder derives source bindings only from that verified return value and persists those same captured bytes; it never reopens the lineage path. The reduced synthetic-fixture path exists only for contract tests and is not publication evidence.

The read-only lineage capture and semantic verifier are part of the installed package and have no dependency on the repository's operational tools tree. The lineage assembly CLI delegates to the same implementation. Default MembershipStore.open(..., verify=True) behavior therefore remains available from a wheel install while preserving duplicate-key rejection, single-read byte identity, deep immutability, and the complete non-fixture semantic contract.

exact snapshot lineage
        +
23 local staged Parquet files
        ├── single-descriptor private source capture + exact byte/schema/count checks
        ├── source-specific canonical row derivation
        ├── disk-backed duplicate and cross-role rejection
        ├── numeric-chromosome canonical ordering
        ├── independent single-read private-snapshot verification
        └── file/directory fsync + atomic no-clobber publication

Normalized ClinVar B/LB/LP/P rows are labeled memberships. They follow exactly the same chromosome split as gnomAD: train is chromosomes 1–19 and 22, validation is chromosome 20, and evaluation is chromosome 21. In particular, train-chromosome P/LP rows remain usable training anchors; being a ClinVar membership does not make a train row a holdout. VUS/OTHER, non-primary contigs, X, Y, and MT are excluded from labeled membership while their raw artifact identity and filtered counts remain bound in source provenance.

Raw ClinVar assertion rows remain in Parquet and SQLite. A canonical variant with both benign-family and pathogenic-family assertions is rejected. Duplicate same-target assertions are retained for provenance but collapse to one benchmark variant, deterministically preferring P over LP and B over LB, then the smallest source-row identity. clinvar_class_role_counts therefore reports unique labeled variants; source_role_counts reports each exact source ID by role, and source_kind_role_counts["clinvar"] reports the aggregate raw retained assertions. The row reason bits are 1 for gnomAD source membership and 2 for ClinVar labeled membership. Every output role is non-empty and every row role must match its chromosome assignment.

These are variant memberships, not phased haplotypes. The pinned inputs contain no phased haplotype membership, so the store records that capability as unavailable rather than synthesizing proximity-based or pseudo-phased labels.

An abbreviated build spec looks like this; a real spec must contain all 23 entries and no ellipsis:

{
  "$schema": "./membership-build-spec.schema.json",
  "schema_version": "geno-lewm.membership-build-spec.v1",
  "artifact_id": "geno-lewm-data-v0.3.0-membership-r1",
  "snapshot_lineage": "snapshot-lineage.json",
  "snapshot_lineage_sha256": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "builder": {
    "git_commit": "0123456789abcdef0123456789abcdef01234567",
    "container_image": "ghcr.io/abdelstark/geno-lewm@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  },
  "sources": [
    {
      "kind": "gnomad",
      "chromosome": "1",
      "path": "sources/gnomad-chr1.parquet"
    },
    {
      "kind": "clinvar",
      "path": "sources/clinvar.parquet"
    }
  ]
}

Build and independently verify it with:

export GENO_LEWM_VERIFIED_BUILD_CONTAINER_IMAGE='ghcr.io/abdelstark/geno-lewm@sha256:<64-hex-digest>'

uv run python -m tools.data.v03_membership_store build \
  --spec-json /path/to/membership-build.json \
  --output-dir /path/to/geno-lewm-data-v0.3.0-membership-r1

uv run python -m tools.data.v03_membership_store verify \
  --store-dir /path/to/geno-lewm-data-v0.3.0-membership-r1

The declared builder commit must equal git rev-parse HEAD, and the checkout must remain clean at both provenance gates. The container value must equal GENO_LEWM_VERIFIED_BUILD_CONTAINER_IMAGE, injected by the trusted container launcher; an installed checkout without verifiable Git state or a missing/mismatched container binding fails before ingestion. The receipt records both verification mechanisms. This is an invocation binding, not a claim that an environment variable is a cryptographic container attestation.

Build and full verification require PyArrow from the dev or train extra. Runtime lookup uses only SQLite from the Python standard library.

Identity and verification

The semantic content_identity covers:

  • assembly and chromosome roles;
  • exact snapshot-lineage bytes and lineage identity;
  • every source revision, namespace, artifact and verification binding;
  • source, split, row, distinct-variant, filtered, source×role, and unique ClinVar-class×role counts;
  • a SHA-256 over length-framed canonical rows in numeric chromosome order.

The semantic identity deliberately excludes container-format and build-runtime bytes so the same row contract is not redefined by a PyArrow or SQLite encoding detail. The separate physical_identity covers the semantic identity plus the SHA-256 and size of Parquet, SQLite, bundled lineage, and build-receipt bytes. The verifier opens each published file once, copies it through that checked descriptor into private read-only storage, and hashes and parses only that snapshot. It rejects extra files, directories, symlinks, and duplicate file bindings; validates bundled lineage and receipt provenance; scans Parquet and SQLite independently; reconciles every R-tree entry and both cross-tabs; and requires both semantic scans to match the manifest. Scalar SQLite tables are STRICT with closed type, range, role, source, and ClinVar-label checks.

Consumption

MembershipStore.open(path) verifies once and opens lookup.sqlite read-only. Point queries use a composite SQLite index. Interval queries constrain the integer R-tree first, then join membership rows by integer primary key while applying role/source filters. iter_role(...) streams raw membership assertions; iter_labeled_clinvar(...) streams the deterministic unique labeled-variant benchmark unit in bounded batches. The MembershipStoreHoldoutPolicy adapter can be passed where the existing tuple builder accepts HoldoutPolicy; it excludes validation/evaluation chromosomes only. Train-role ClinVar B/LB/LP/P memberships remain data memberships and never become holdouts. Source-filtered lookups remain indexed and never construct a million-key Python set. Unplaced windows and X, Y, or MT windows fail closed because they are outside the autosomal role contract.

Store and holdout-policy equality/hash are based on content_identity. Pickle contains no live SQLite connection; each process and thread lazily opens its own immutable read-only handle, and a post-fork PID guard discards inherited handle state before lookup. Connections owned by short-lived threads are weakly reclaimed when those thread objects expire; close() deterministically closes all remaining live-thread connections. A verified open retains a private content-bound SQLite snapshot, so replacement of the published path after open cannot change later thread/process queries.

Opening with verify=False skips the expensive private capture and full file scan, checks the exact layout and manifest immediately, then checks SQLite schema/metadata on first lookup. It does not defend against later path replacement. Use that mode only inside an externally enforced immutable-storage boundary after retaining a trusted verification result for the exact identity.

Validation

uv run pytest tests/unit/test_data_membership_store.py -q
uv run ruff check geno_lewm/data/_membership_store_*.py \
  geno_lewm/data/membership_store.py \
  tools/data/v03_membership_store.py tests/unit/test_data_membership_store.py
uv run mypy --strict geno_lewm/data/_membership_store_*.py \
  geno_lewm/data/membership_store.py \
  tools/data/v03_membership_store.py
uv run python tools/api/snapshot.py check
uv run --extra docs mkdocs build --strict