Skip to content

Generalize antigen model: VaccineAntigen abstraction with per-kind tumor-specificity attestation #303

Description

@iskandr

Refined design. Supersedes the original proposal. Companion doc lives in-repo at docs/design/vaccine-antigen.md.
Related: #263 (surface non-SNV antigens), #257 (CTA database), #258 (oncovirus database), #254 (safety scoring).


1. The core reframing

Vaxrank's construct pipeline is built around MutantProteinFragment: a somatic
variant plus a mutant interval (mutant_amino_acid_start_offset
end_offset). The mutant interval is treated as "the part worth targeting."
Only somatic SNV/INDEL antigens become constructs; CTA / ERV / SPLICE / VIRAL
antigens are loaded, scored, and shown in the report, but never reach the
construct writers because they have no variant to place
(external_input.py: "non-coord rows are report-only — no genome placement").

Key observation. Targetability in vaxrank is already not defined by the
mutant interval. VaccinePeptide splits epitopes into target_epitopes /
self_epitopes purely on occurs_in_referencenot on
overlaps_mutation. The mutant interval is only a proxy: for a somatic
mutation, "k-mer absent from the reference proteome" ≈ "k-mer overlaps the
mutation," because the mutation is what makes the k-mer novel. The interval and
the self-screen agree, so we never had to distinguish them.

CTAs break the proxy. A CTA protein is a self protein — its k-mers are all in
the reference proteome — so the screen would reject every CTA k-mer even though
the CTA is a legitimate tumor-restricted target. The fix is not to abandon the
screen but to subtract the CTA's own genes from "self" before screening. The
same predicate then works.

This generalizes to a single definition:

Safely-targetable content of an antigen = the set of k-mers that are
absent from the antigen-appropriate self-reference.

The only per-kind variation is which genes are subtracted from "self"
before building the reference index.

kind genes subtracted from self-reference why
mutation (SNV/INDEL/frameshift) none novelty is intrinsic to the mutation; WT k-mers stay in the reference and are correctly screened out
CTA the full candidate-CTA universe (tsarina.CTA_unfiltered_gene_ids(); see §3.3–§3.4) the CTA protein is self-sequence; tumor-specificity comes from expression, not sequence. A CTA k-mer that also occurs in a confidently-non-CTA gene is genuinely off-target — but uncertain CTA candidates are held out of the subtrahend, not counted as self
ERV the ERV loci same logic as CTA
viral (subtract nothing human; reference is the whole human proteome) a viral k-mer matching any human self-peptide is an autoimmunity risk

Under this model, overlaps_mutation demotes from a gate to a ranking
signal
, and the mutant_amino_acid_* offsets become the mutation kind's
particular way of computing its targetable mask
— one implementation of a
general interface, not a universal field.

This is the precise meaning of "move away from mutant interval to safely
targetable amino-acid content."


2. What is already scaffolded

The rails for this were laid deliberately and are waiting:

  • reference_proteome.py inlines CTA_GENE_IDS (from pirlygenes) and
    ReferenceProteome.from_genome(exclude_cta_genes=True) already builds the
    CTA-excluded index — i.e. "all k-mers occurring in non-CTA genes."
  • CandidateEpitope carries occurs_in_non_CTA_reference, today aliased to
    occurs_in_reference (epitope_logic.py:296) with a comment: "this branch
    will diverge for CTA-matching peptides"
    once the CTA set is populated.
  • VaccinePeptide's target/self split reads occurs_in_reference, with a
    comment promising CTAs move to target_epitopes "without changing this
    code"
    once consumers read the CTA-aware flag (vaccine_peptide.py:166-181).
  • CandidateEpitope.source_class ∈ {mutation, viral, self} already exists.

Three things are unbuilt:

  1. predict_epitopes builds a plain ReferenceProteome(genome) and never the
    antigen-appropriate (e.g. CTA-excluded) index.
  2. Nothing flips the consumer (VaccinePeptide) onto the antigen-aware flag.
  3. CTAs never reach predict_epitopes at all — they are report-only. This
    is the bulk of the work and the reason a VaccineAntigen abstraction is
    needed: a CTA has no varcode.Variant to flow through core_logic's
    variant-keyed pipeline.

3. The VaccineAntigen abstraction

Decision (per design review): introduce VaccineAntigen as a wrapper above
MutantProteinFragment
, with MutantProteinFragment becoming the realization
of the mutation kind. The downstream construct machinery
(VaccinePeptide, ranking, slicing, mRNA/peptide assembly) operates on an
antigen + its targetable mask, not on a variant.

VaccineAntigen
├── kind: "mutation" | "CTA" | "ERV" | "splice" | "viral"
├── gene_name: str
├── amino_acids: str                  # the protein/peptide context window
├── targetable_mask() -> Interval set # which residues are tumor-specific content
│                                      #   mutation: the mutant span
│                                      #   CTA/ERV/viral: the whole ORF
│                                      #   splice: junction-spanning residues
├── self_reference_exclusions() -> set[gene_id]   # genes subtracted from "self"
├── tumor_specificity: TumorSpecificityAttestation   # REQUIRED for construct admission
├── provenance: <per-kind>            # variant | cta_gene_id | erv_orf_id | splice_junction | viral_orf
└── (read-support / VAF fields, where applicable)

3.1 Targetability: a content layer + a two-tier safety screen

A k-mer at offset [s, e) is a candidate iff it overlaps targetable_mask()
(the tumor-specific content layer — mutant span for mutations, whole ORF for
CTA/ERV/viral). Candidates then pass through a two-tier safety screen, each
tier backed by a different index and answering a different question:

Tier 1 — broad self-exclusion (sequence-level, whole non-CTA proteome).
The exact k-mer must be absent from the self-reference built with
self_reference_exclusions() subtracted (for CTA: the CTA-excluded proteome). A
peptide that occurs verbatim in any non-excluded self protein is not
tumor-specific. This is a hard target/self gate — cheap O(1) set membership,
already scaffolded as the exclude_cta_genes index. It catches off-target
self-presentation everywhere.

  • For mutation: self-reference = full proteome. WT k-mers are present and
    correctly excluded; this coincides with "overlaps the mutation," which is why
    today's code gets away with screening on this tier alone.
  • For CTA: a two-level determination. (a) Per-gene — which genes are CTAs
    comes from tsarina (or pirlygenes). (b) Per-peptide — the targetable pool is
    the set difference {k-mers of expressed-CTA proteins} \ {k-mers of confidently-non-CTA proteins}: a CTA peptide is admissible iff it occurs in an
    expressed CTA protein and in no confidently-non-CTA protein. Crucially the
    subtrahend is the complement of the full candidate-CTA universe, not the
    complement of the clean set — uncertain CTA candidates are held out of both
    sets (see §3.4). Peptides shared across CTA family members are kept
    every CTA in the family is tumor-restricted, so intra-family sharing is not an
    off-target risk.
  • occurs_in_non_CTA_reference is the CTA-kind instance of a general
    occurs_in_self_reference(antigen) flag. VaccinePeptide's target/self split
    reads that flag and stays source-agnostic — as the existing comment
    promises.

Tier 2 — vital-tissue MHC-ligand scrutiny (presentation-level, observed,
HLA-restricted).
A finer autoimmunity filter applied to whatever survives
Tier 1. Instead of the whole proteome, it consults observed MS-eluted ligands
from vital organs
, and flags a candidate only when it (or a near-match) is a
peptide actually presented on essential tissue — not merely a substring of a
vital-organ protein — restricted to the patient's HLA alleles. A self-peptide
whose sequence exists in heart but is never eluted there carries little
autoimmunity risk; a peptide eluted from heart on the patient's HLA is a
serious one.

Backed by hitlist (observed
immunopeptidome: peptide × mhc_restriction × source-tissue flags) filtered to
src_healthy_tissue and the vital-organ MS vocabulary from
tsarina (SAFETY_TISSUE_GROUPS =
{brain, heart, lung, liver, pancreas}; _VITAL_TISSUE_MS_NAMES), intersected
with the patient's class-I alleles. The patient-HLA vital-tissue ligand set is
bounded (low thousands of peptides), so vaxrank loads it once and builds a
neighbor index — hitlist itself only does exact lookup, so all near-match logic
lives in vaxrank.

Disposition is a configurable rule ladder. Match strength has two orthogonal
knobs, not a fixed pair of cases:

  • position mask — which residues are compared: all (whole peptide), or
    tcr_facing (MHC-anchor positions excluded — see below), or an explicit
    position list.
  • max distance — Hamming threshold over the masked positions.

Masked-Hamming comparison is per (allele, peptide-length) and only defined
between equal-length peptides. No length is assumed — class-I ligands run
8–11+, class-II 13–25, and lengths needn't match the candidate; the screen
compares within each length band and the mask is computed for whatever length is
in hand. Cross-length similarity, if ever wanted, is a separate length-agnostic
comparator (edit distance, no position mask), kept out of the default ladder.

These are nested, not parallel: TCR-facing differences are a sub-count of total
differences, so for a fixed distance k, {tcr_facing, ≤k}{all, ≤k}.
Hardcoding "full Hamming ≤1 and TCR-facing Hamming ≤1" is therefore redundant —
the second already contains the first. Instead, expose a list of match rules,
each (mask, max_distance, action), evaluated strongest-first, first match
wins
, with the matched rule recorded on the epitope:

vital_tissue_ligand_screen:
  enabled: false
  source: hitlist                     # observed MS-eluted ligands
  tissues: [brain, heart, lung, liver, pancreas]   # tsarina SAFETY_TISSUE_GROUPS
  match_rules:                        # strongest-first; first match wins
    - { mask: all,         max_hamming: 0, action: hard_filter        }
    - { mask: all,         max_hamming: 1, action: penalty, weight: 1.0 }
    - { mask: tcr_facing,  max_hamming: 1, action: penalty, weight: 0.5 }

action ∈ {hard_filter, penalty, flag}; penalty adds weight to the combined
score and sets vital_tissue_autoimmune_risk; flag annotates only. The nesting
makes the ladder well-ordered: an exact hit satisfies every rule and lands on
hard_filter; a whole-peptide near-match that isn't exact falls to the
{all, 1} penalty; an anchors-only difference falls through to the weaker
{tcr_facing, 1} penalty. All matches are reported regardless of action.

Deriving the tcr_facing mask — from the predictor, never a fixed table.
Anchor positions are exactly the residues the MHC groove constrains, which is
precisely what the configured presentation predictor already models. So the mask
is computed per (allele, length) by positional sensitivity: hold a peptide
of that length, substitute each position across residues, and measure the change
in predicted presentation. High-sensitivity positions are anchors (MHC-facing);
low-sensitivity positions are TCR-facing. The result is cached per
(predictor, allele, length).

This is correct by construction for the cases a fixed P2/PΩ table gets wrong:

  • Non-canonical anchors. Alleles that anchor at P1/P3/P5 (or use auxiliary
    anchors) fall out of the sensitivity profile directly — nothing is hardcoded.
  • Non-9-mer / class II. The profile is computed at the actual length; for
    class II it reflects the 9-residue binding core wherever it sits, not a fixed
    index.
  • Non-human MHC. If the configured predictor supports the sample's allele
    (mouse H-2, etc.), the same procedure applies; vaxrank's reference machinery is
    already species-parameterized.

When the model can't be built — disable, don't guess. If the predictor
doesn't support an allele/length, or hitlist has no vital-tissue coverage for the
sample's species/HLA, the tcr_facing rules are skipped and the gap is
annotated
(vital_tissue_coverage), rather than silently falling back to a
human-9-mer assumption. The all-mask rules (which need no anchor model) still
apply where ligand data exists. The comparator stays pluggable for non-Hamming
distances or an externally supplied anchor map.

The two tiers are intentionally asymmetric: Tier 1 is broad sequence presence
(any self protein, exact match, hard gate); Tier 2 is narrow observed
presentation
(vital-organ eluted ligands, HLA-restricted, exact = hard filter /
near = penalty). Tier 1 answers "is this even tumor-specific?"; Tier 2 answers
"if we provoke a response, what does it hit on a vital organ?"

3.2 Tumor-specificity attestation (admission gate)

VaccineAntigen makes tumor specificity a first-class, required property.
An antigen is construct-eligible only if it carries kind-appropriate
evidence; otherwise it stays report-only (today's behavior for non-mutation
kinds).

kind attestation
mutation intrinsic — a somatic mutation absent from normal cells
CTA gene in the POSITIVE set (CTA_gene_names() — tsarina-filtered and expressed). The restriction / restriction_confidence tiers (§3.4) are recorded as ranking + report signals, not a hard gate; hard safety is the per-peptide Tier-2 screen. UNKNOWN (never-expressed) and somatic-leak candidates are not admitted
ERV tumor-specific ERV expression; silent in normal tissue
splice a cause — spliceosome mutation (e.g. SF3B1) or splice-site mutation explaining the junction. Unexplained aberrant splicing is not admissible
viral evidence the virus is tumor-carried, not systemic/diffuse

Principle (carried from PR #302): tumor-specificity evidence comes from the
input/annotation
, never inferred. varcode validates provider annotations; it
does not supply missing data.

3.3 Data sources — consume the pirl-unc stack, don't re-curate

Two sibling packages already curate what this design needs; vaxrank should
consume them rather than maintain its own lists.

  • tsarina (a.k.a. perseus) — curated
    shared-antigen target selection. Provides the partitioned CTA universe
    (CTA_unfiltered_*CTA_filtered_*CTA_gene_names()/CTA_gene_ids(),
    with the gaps CTA_never_expressed_* and CTA_excluded_*; §3.4), the
    HPA-antibody-adaptive evidence tiers (restriction × restriction_confidence
    via CTA_by_axes(), CTA_evidence()), the viral proteomes
    (ONCOGENIC_VIRUSES, viral_peptides(), human_exclusive_viral_peptides()),
    the hotspot mutations (HOTSPOT_MUTATIONS, mutant_peptides()), and the
    vital-organ vocabulary (SAFETY_TISSUE_GROUPS, _VITAL_TISSUE_MS_NAMES).
    It also already computes a per-CTA MS-restriction tier
    (CANCER_ONLYRECURRENT_HEALTHY) by wrapping hitlist — a gene-level
    precursor of this design's per-peptide Tier 2.
  • hitlist — the observed MHC-ligand
    MS dataset tsarina wraps. load_ms_observations(peptide=…, mhc_class=…) (exact
    lookup), observations.parquet carrying peptide, mhc_restriction, and
    source-tissue flags (src_cancer, src_healthy_tissue,
    src_healthy_reproductive, …), plus a peptide→gene mappings sidecar. This is
    the Tier-2 ligand backend.

Supersession. vaxrank currently inlines CTA_GENE_IDS
(reference_proteome.py) and exclude_cta_genes — a single set that
approximates the clean CTA set. This design replaces it with two distinct
tsarina sets: CTA_unfiltered_gene_ids() for the negative subtrahend (what to
hold out of "self") and the tier-gated POSITIVE set
(CTA_gene_ids() × CTA_by_axes(...)) for the targetable pool (§3.4) —
keeping vendored snapshots only as an offline fallback. (The
[reference-pirlygenes] memory named pirlygenes as the CTA source; tsarina is
the newer, HPA-filtered, MS-joined successor and is what the user pointed at
here.)

Boundary. tsarina selects shared targets and scores them with public MS
evidence across population HLA panels; vaxrank designs constructs for one
patient
. vaxrank pulls tsarina's curated sets + hitlist's raw observations and
applies them per-patient (patient HLA, patient tumor expression, patient
construct windows). Curation stays upstream; construct design stays in vaxrank.

3.4 CTA gene partition and expression-evidence tiers

The CTA candidate universe is not a clean binary. tsarina's gene_sets.py
partitions it into nested sets, and vaxrank maps them to three dispositions
crucially including an abstain class that is in neither the positive nor the
negative set.

tsarina's nesting: CTA_unfiltered (full 358-gene universe — any gene called a
CTA by ≥1 source DB) ⊇ CTA_filtered (passes the HPA reproductive-restriction
filter) ⊇ CTA_gene_names() (filtered and expressed, ≥2 nTPM somewhere). The
two gaps are named functions:

disposition tsarina set definition vaxrank treatment
POSITIVE (targetable pool) CTA_gene_names() / CTA_gene_ids() filtered and expressed peptides enter the positive pool; admission graded by the tiers below
NEGATIVE held-out CTA_excluded_* = unfiltered − filtered candidate CTA that fails reproductive restriction (somatic-leak evidence) neither — leaks into healthy tissue (not clean-targetable) but still a CTA candidate (not subtracted from real CTAs). Per tsarina's own docstring
UNKNOWN held-out CTA_never_expressed_* = filtered − expressed passes restriction filter but no protein data and max RNA < 2 nTPM neither — no positive expression evidence (can't attest tumor-restriction → not targetable), but non-detection may be technical (no/poor antibody, low abundance, RNA detection floor), so we do not conclude it is normal self → not subtracted

The negative subtrahend (the proteins whose k-mers cancel candidate CTA
peptides) is therefore proteome \ CTA_unfiltered_gene_ids() — the complement of
the entire candidate universe, so that both held-out classes above are kept out
of the subtrahend. This is a correction to the naive "subtract everything except
the clean CTA set," which would let an unknown/leaky candidate's peptides wrongly
knock out a real target. vaxrank's inlined CTA_GENE_IDS is the wrong set to
subtract
for this reason (it approximates the clean set, not the universe).

Expression-evidence tiers — soft signals, not an admission gate. tsarina
grades evidence quality by HPA antibody reliability, not a flat threshold:
PROTEIN_SUPPORT_ORDER = Enhanced > Supported > Approved > Uncertain > Missing
(hpa.py), with an adaptive RNA-corroboration threshold — a better antibody
needs less RNA backup, a weaker one needs near-pure reproductive RNA:

HPA_ADAPTIVE_PROTEIN_RNA_THRESHOLDS = {  # min deflated reproductive RNA fraction
    Enhanced: 0.80,  Supported: 0.90,  Approved: 0.95,  Uncertain: 0.98,
}   # RNA "deflated" as max(0, nTPM - 1) to zero out basal noise before the ratio

These feed (with the per-modality protein / RNA / MS axes) into restriction ∈ {TESTIS, PLACENTAL, REPRODUCTIVE, SOMATIC} and restriction_confidence ∈ {HIGH, MODERATE, LOW}, queryable via CTA_by_axes(...). restriction_confidence
is a per-source averaged evidence score (synthesize_restriction): each modality
with data scores points — protein IHC +1.0 (+0.5 Enhanced/Supported), RNA +1.0 if
it agrees (+0.5 STRICT), MS +1.0 CANCER_ONLY/EXPECTED_TISSUE (+0.5
SINGLETON_HEALTHY, 0 RECURRENT_HEALTHY) — then score/n_sources: ≥1.2 → HIGH,
≥0.8 → MODERATE, else LOW
. So MODERATE ≈ one solid source, uncorroborated.

Admission = POSITIVE membership; the tiers are not a hard gate. A CTA is
construct-eligible iff its gene is in CTA_gene_names() (already HPA-filtered +
expressed). restriction / restriction_confidence / ms_restriction are
recorded on the VaccineAntigen as ranking signals + report annotations, and
the hard safety work is done per-peptide by the Tier-2 vital-tissue ligand screen.

Why not a hard tier gate — a sanity check on canonical CTAs (2026-05-22) showed
a restriction ∈ {TESTIS,PLACENTAL} × confidence ≥ MODERATE gate would wrongly
reject MAGE-A4 (REPRODUCTIVE — multi-reproductive-tissue expression; yet
afami-cel/Tecelra is FDA-approved), PRAME (LOW — some somatic RNA + only
singleton-healthy MS; yet a leading TCR target), and XAGE1A (reads SOMATIC,
mostly from missing protein data, despite CANCER_ONLY MS). The tiers describe
public-evidence quality, not target validity, so they inform ranking — not
eligibility. The MS axis (ms_restriction: CANCER_ONLY … RECURRENT_HEALTHY) is
the gene-level precursor of the per-peptide Tier 2.


4. What carries over vs. what is new

Carries over (operates on (antigen, epitopes) once the fragment is
generalized): combined-score ranking, allele-coverage-aware selection, peptide +
mRNA construct assembly, the per-(peptide, allele) report.

New work:

  • VaccineAntigen model + per-kind targetable_mask() /
    self_reference_exclusions() / attestation.
  • predict_epitopes and the external-input loaders build the
    antigen-appropriate reference and populate occurs_in_self_reference
    honestly (today's no-op alias diverges).
  • Flip VaccinePeptide onto the antigen-aware flag.
  • core_logic admits non-variant antigens (CTA first) into the
    slice → VaccinePeptide path.
  • Plumb the evidence columns that drive the attestation gate.
  • Tier-2 screen: a VitalTissueLigandScreen backed by hitlist (load the
    patient-HLA × vital-tissue eluted-ligand set once; build a neighbor index), the
    configurable match-rule ladder (mask × max_hamming × action), the
    predictor-derived tcr_facing mask per (allele, length), and the
    exact→hard-filter / near→penalty disposition wired into admission + the
    combined score.
  • Take tsarina + hitlist as (optional) dependencies; replace the inlined
    CTA_GENE_IDS with tsarina.CTA_gene_ids() (vendored snapshot as fallback).

5. Staged delivery

Designed so each stage ships independently and the abstraction lands before the
risky construct-admission change.

  1. Antigen-aware self-reference (Tier 1, no behavior change). Build the
    negative-subtrahend reference in predict_epitopes as proteome \ tsarina.CTA_unfiltered_gene_ids() (vendored universe snapshot as fallback) —
    i.e. exclude the whole candidate universe, not just the clean set, so the
    UNKNOWN/leaky classes (§3.4) are held out. Populate
    occurs_in_non_CTA_reference honestly (diverges from occurs_in_reference
    only for CTA-matching peptides). Note this corrects the current inlined
    CTA_GENE_IDS (clean-set approximation). No CTA antigens flow yet, so no
    output change — but the flag becomes real and testable.
  2. VaccineAntigen type (types-only). Introduce the wrapper; reexpress
    MutantProteinFragment as the mutation kind behind it. Mirrors the
    PeptideContext arc's Phase-1 "types-only" pattern (PeptideContext + CandidateEpitope: multi-axis per-peptide model (Phase 1) #283). No producers
    switched.
  3. CTA admission (the vertical slice). Admit CTA antigens from the POSITIVE
    set (CTA_gene_names()); UNKNOWN/leaky candidates stay report-only. Record
    restriction / restriction_confidence / ms_restriction on the antigen as
    ranking + report signals (not a gate; §3.4). Flip VaccinePeptide onto
    occurs_in_self_reference. First kind to produce constructs. Output changes —
    gated behind config / CLI opt-in, off by default (consistent with Safety scoring of vaccine antigen windows: minimize self-peptide presentation + tissue-aware autoimmunity risk #254's
    enabled: false).
  4. Tier-2 vital-tissue ligand screen. Add the hitlist-backed
    VitalTissueLigandScreen + pluggable similarity comparator; wire exact→hard
    filter and near→penalty into admission and the combined score. Independent of
    stage 3's admission change but most useful once CTAs flow. Opt-in.
  5. Generalize remaining kinds. ERV, splice, viral — each is a
    targetable_mask() + self_reference_exclusions() + attestation triple
    (viral reuses tsarina's human_exclusive_viral_peptides()).
  6. Retire the proxy. Demote overlaps_mutation to a documented ranking
    signal; remove any remaining "interval == targetability" assumptions. Major
    version bump.

6. Open questions

  • Self-reference granularity (Tier 1). Resolved (whole-family, three-set):
    CTA genes are determined per-gene (tsarina/pirlygenes); the targetable pool is
    {k-mers of expressed-CTA proteins} \ {k-mers of confidently-non-CTA proteins},
    where the subtrahend's complement is the full candidate universe
    (CTA_unfiltered), so UNKNOWN (never-expressed) and somatic-leak candidates are
    held out of both sets (§3.4). Family-shared peptides are kept. Requires
    swapping the inlined CTA_GENE_IDS (clean-set approximation) for the universe.
  • UNKNOWN-candidate policy (§3.4). Held out by default (neither targeted nor
    subtracted). Open: should a never-expressed candidate be optionally
    promotable to POSITIVE when the patient's own tumor RNA shows expression —
    turning "no public evidence" into "private evidence" for that patient — without
    ever adding it to the subtrahend?
  • TCR-facing mask derivation (Tier 2). Resolved in favor of
    predictor-derived positional sensitivity per (allele, length) (above), not a
    fixed table. Residual: positional-perturbation cost (bounded, cached) vs.
    reading a predictor's native motif/PWM where exposed (mhcflurry); and the
    sensitivity cutoff that separates anchor from TCR-facing (top-N vs. threshold).
  • hitlist HLA/tissue coverage (Tier 2). When the patient's allele has thin or
    no vital-tissue MS coverage in hitlist, Tier 2 is silently weak. Surface
    coverage as a confidence annotation; decide whether to fall back to predicted
    presentation over vital-tissue proteins (the alternative source we considered)
    when observed coverage is absent.
  • Dependency optionality. tsarina / hitlist pull parquet data and (for
    tsarina) HPA/pyensembl extras. Keep them optional extras
    (vaxrank[shared-antigens]?), with the vendored CTA snapshot covering the
    Tier-1 path when they're absent; Tier 2 simply disables without them.
  • CTA admission policy. Resolved: admission = POSITIVE membership
    (CTA_gene_names()); the HPA-adaptive tiers (§3.4) are soft ranking/report
    signals, not a hard gate (a 2026-05-22 sanity check showed a hard
    {TESTIS,PLACENTAL}×≥MODERATE gate wrongly drops MAGE-A4, PRAME, XAGE1A).
    Hard per-peptide safety is Tier 2. Residual: how the tiers weight ranking, and
    whether to optionally expose a hard tier filter for users who want one. ERV
    expression thresholds still TBD; see Safety scoring of vaccine antigen windows: minimize self-peptide presentation + tissue-aware autoimmunity risk #254.
  • Viral-localization signal source (a LENS column vs. external annotation) —
    TBD; see Oncovirus antigen database for on-target ligand classification (#249 follow-up) #258.
  • CTA target window — whole protein, or a windowed selection? With the
    content layer = whole protein and the two-tier screen doing the trimming,
    "whole protein, let the screen carve it" is the natural default.
  • Relationship to Safety scoring of vaccine antigen windows: minimize self-peptide presentation + tissue-aware autoimmunity risk #254's safety module. This design is the targetability
    half (which residues are eligible) and folds in Safety scoring of vaccine antigen windows: minimize self-peptide presentation + tissue-aware autoimmunity risk #254's tissue-aware
    cross-reactivity axis as Tier 2. Safety scoring of vaccine antigen windows: minimize self-peptide presentation + tissue-aware autoimmunity risk #254's interior self-presentation burden
    (sliding non-mutant k-mers, dose-dilution scoring) remains a complementary
    scoring axis on top. Shared reference/ligand backends; land compatibly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions