PRS Frontend — requirements sketch

Input for a dedicated planning session. Written 2026-07-21, immediately after Gate B completed, to preserve decisions made while the data was in front of us. Not a design — a brief plus the constraints a design has to satisfy.


1. Why this needs its own session

The PRS pipeline rebuild has produced two substantial data structures that nothing can see:

  • mods_prs_run (Gate A) — dense-callset provenance: caller, callability policy, FASTA checksum, autosomal median depth, four-outcome reconciliation counts.
  • mods_prs_score_manifest (Gate B) — 5,377 rows, one per published score: compatibility class, production-eligibility, reasons, structural feature counts, theoretical bounds, scoring-file checksum.

Grepping both frontend/src/components/PRSVectorBody.jsx and backend/mods_prs_api.py for compat_class, production_eligible, score_manifest, prs_run, dense_callset, validity returns zero matches in either file. There is no API surface and no UI.

Meanwhile Gates C–E will add computed scores with coverage, missingness and percentile semantics that are easy to display wrongly. Deciding the API contract now, before Gate C writes its first score, avoids retrofitting.


2. What exists today (all legacy)

SurfaceFileCovers
Patient Profile PRS slotfrontend/src/components/PRSVectorBody.jsxgenotype summary, per-disease score table, upload, compute, PDR export
Terminal commandfrontend/src/components/Terminal.jsx (~1502)prs status | compute | export | open
Downstream consumersPDRInputInspector.jsx, PatientWeightsPanel.jsxPDR input display
APIbackend/mods_prs_api.py/genotype/upload|summary|concordance, /prs/compute|scores|export/*|preview/pdr-input

Released-only filtering does exist, but in mods_prs_service.py (all_scores_for / latest_score_for, .where(validity == "released")), so the API inherits it. mods_polygenic_score is currently empty, so the UI correctly shows nothing.


3. Hard constraints (things a design must not get wrong)

3.1 Retire the “Cov.” column — do not port it

PRSVectorBody.jsx renders a colour-coded coverage column computing snps_used / snps_in_scoring_file, painting <50% red as “unreliable”.

That ratio is the REF-effect bug’s signature, not a property of the score. Across all 40 legacy outlier scores it clusters at 42–52% — the ~49% of loci the old scorer silently dropped. The UI presents a symptom of our own defect as a data-quality verdict about a published score. It must be removed, not carried forward under a new name.

3.2 Three coverage quantities, never conflated

Each is a distinct number and must be labelled distinctly:

  1. Structural overlap — % of scoring rows matched to target/reference alleles.
  2. Genotype callability — % of matched loci with a confident patient genotype.
  3. Effective score-information coverage — Σ_retained 2p(1-p)w² / Σ_all 2p(1-p)w² (variance-weighted; the one that actually matters).

A single “coverage %” chip is exactly the mistake §3.1 describes.

3.3 Deferred means deferred

A score that is not production_eligible shows “not calculated — ”. Never a partial number, never a greyed-out estimate, never an em-dash that could read as zero. The reasons are already stored per row and are human-readable.

3.4 Percentiles: rank + N + interval, no decorative precision

Show “1 of 930” or “above ~99.9th in this panel”. Never “98.1376th”. A parametric percentile is a secondary field, valid only when the gates pass — never for few-variant, haplotype, skewed or multimodal scores.

3.5 Nothing unreleased may reach a display or an export

validity='released' gates the score readers today. Any new endpoint must inherit that, and the PDR export path must never ingest a non-released row.


4. What the manifest makes newly displayable

Real numbers, already in prod, that have no home yet:

  • Fleet composition — A 1,997 / B 2,950 / C 426 / D 4; 3,863 production-eligible (71.8%); 3.45B variant rows examined.
  • Sole-blocker analysis — the decision-driving view: multiallelic is the only thing deferring 588 scores; unplaceable_rows appears in 399 but blocks alone in 18. Report “appears in” and “sole blocker” as separate columns (they rank oppositely).
  • Per-score detail — class, eligibility, reasons, feature counts (indel, palindromic, multiallelic, chrX, inferred/missing other-allele, unplaceable), theoretical [L, U], scoring-file checksum, trait, weight_type.
  • Gate A provenance — reconciliation counts proving zero residual implicit-zero, callability policy, reference-FASTA checksum, median depth.
  • weight_type distribution — messy catalog-wide (3,250 beta, 1,390 NR, BETA/Beta case variants, 199 log(OR), free-text tail). Worth surfacing, since it affects whether a score’s scale is interpretable.

5. Open questions for the session

  1. Where does this live? A new tab on the Research dashboard, an extension of the Patient Profile PRS slot, or a distinct “PRS pipeline” panel? The manifest is catalog-scoped (not patient-scoped), unlike everything currently in the PRS slot — that may argue for separating “which scores can we compute” from “this patient’s scores”.
  2. Does the manifest need a browsable 5,377-row table, or only aggregates plus lookup by PGS ID? Pagination/filtering cost vs. actual use.
  3. API shape — extend mods_prs_api.py or a new mods_prs_manifest_api.py? Endpoints for fleet summary, per-score detail, sole-blocker analysis, run provenance.
  4. Terminal vs GUI split — the existing prs command is genuinely useful; how much of this is better as prs manifest / prs runs subcommands rather than pixels?
  5. Does the promotion policy get a UI? Promoting multiallelic is a signed-off human decision worth 588 scores; showing its impact before approval may be valuable, but the decision must not become a casual toggle.
  6. Mobile — per CLAUDE.md all frontend work must be mobile-compatible; a 5,377-row table is not, so decide the mobile view deliberately.

6. Sequencing note

Gate C (now built as backend/mods_prs_scorer.py) produces the first correct scores and emits the three coverage quantities as a coverage_v1 metadata payload, which mods_prs_pipeline_service.coverage_block re-validates on read. The frontend needs that API contract to be settled anyway, so either do this planning first and build against an agreed contract, or accept one revision after Gate C. Deciding §3.2’s three quantities up front is the part that matters most, because that is what the legacy UI got wrong.

Reference: docs/prs_validation/compatibility_classification.md (the classifier + fleet results), the rebuild plan at an internal agent path, and GH #237 (classifier cross-validation).