# Prompt — "Stability across seeds" panel in the Lab (HNSC/HPV) Add a multi-seed stability panel to the Lab that reruns the HPV search across several seeds and shows (a) how tightly the result holds and (b) which genes recur across independent searches. **Frontend-only** — reuse the existing run, transfer, reveal, and full-rank endpoints. No engine / API / airgap change. `tsc` after. Gated to `dataset === "hnsc" && target === "hpv"` (hidden otherwise). Everything this needs already exists on the wire; do not add a backend endpoint. --- ## What it does For each seed in a list (default `[1, 3, 7, 11, 13, 17, 23, 29]`), fire a normal HPV run with that seed, wait for it to finish, then pull its held-out result and its independent-cohort transfer. Aggregate across seeds into one panel. ## Data flow (all existing endpoints) - Fire a run: `postRun({ objective_spec: OBJECTIVE_PRESETS.hpv, params: { ...params, seed }, engine: "v2", dataset, coherence, diversity, ...ratesDiff })` — i.e. the SAME body `launchRun()` builds, only `params.seed` overridden per seed. Use the user's current Parameters / coherence / diversity / DSL rates so the sweep matches their single-run config. - Wait for completion **without disturbing the main Live view / Result state**. Write a dedicated helper `awaitRunDone(runId): Promise` that opens an `EventSource(${API_URL}/runs/${runId}/stream)`, resolves on the `done` event (parse `RunResult`), rejects on `error`, and closes the stream — it must NOT call `setGenerations` / `setResult` / `setStatus` (those belong to the main single-run UI). (Polling `getRunStatus` until done then `getRunResult` is an acceptable alternative.) - Per seed collect: **`winning.holdout_score`** (this is the field the Result panel's "Held-out AUROC" card and the verdict display — use it, NOT `holdout_auroc`, so the stability dots match the numbers already on screen) and `winning.permutation_p` (from the RunResult), then `getRunTransfer(runId)` → `auroc`, `p`, `n`, `found_symbols`, `missing_symbols`. If transfer fails (e.g. GSE65858 not built), store null for the transfer fields and keep the held-out row — never crash the sweep. - After all seeds: call `getFullRankDiagnostic(dataset, target)` once and build a `symbol → set_name` map from `reference_marks` (p16 / cell_cycle) to tag genes. ## Recurrence (the key output) - A seed's winner symbols come from `postReveal(winning.gene_ids)` — a bounded reveal of just that winner's genes (same discipline `/transfer` uses). Source it from the winner, not the transfer payload, so recurrence still works even if a seed's transfer call failed. (`found_symbols ∪ missing_symbols` from the transfer is an equivalent set when transfer succeeds.) - Tally: for every symbol, count in how many seeds it appears. Sort desc. Tag each with its reference set via the map (else "alternate"/"—"). Genes in only 1 seed collapse into a single "passengers · 1 seed each" row (show the count of such genes). ## UI (new `` — match the approved mockup) Reuse ``, ``, `fmtFit`, and the palette tokens so it looks native. Place it as its own SectionCard in the Result area, after the independent-cohort validation panel. Gated to hnsc/hpv. - Header + its own **"Run stability check"** button (independent of the main Run button) + a seed-list input (comma or space separated, default the 8 above). While sweeping: disable both this button and the main Run button, show "Running seed {k} of {N}…", and offer a **Stop** button that sets a cancel flag checked between seeds (finish the current seed, then halt). - **Section 1 — "Does the result hold across seeds?"** Two horizontal dot-strips on a 0.5 → 1.0 axis (label "0.5 coin-flip" left, "1.0" right): Held-out AUROC (accent-teal dots) and Independent AUROC (amber dots), one dot per completed seed, with a light min–max band behind the dots. Caption with the two ranges (`min–max`, via `fmtFit`) and a one-line plain read ("tight + high = stable, not a lucky seed; the independent cohort costs a little, as expected"). - **Section 2 — "Do the same genes keep coming back?"** Recurrence bars: symbol | bar (width = seeds-appeared / N) | "{k} / {N} · {set}". Colour by tag: cell_cycle amber, p16 gold, alternate teal, passengers grey. Caption: recurring cell-cycle / proliferation genes = real signal; one-off passengers = noise. - **Section 3 — per-seed table**: seed | held-out AUROC | p | independent AUROC | p | genes found/total. Route every number through `fmtFit` ("—" for null). - Panel `?` (InfoTip): what multi-seed proves — that the result isn't one lucky random start (spread), and that the same biology recurs across independent searches (recurrence); note each seed is a full run so the sweep takes a while. ## CONSTRAINTS - Frontend-only. No engine/API/airgap change. Reuse `postRun`, the SSE stream, `getRunResult`, `getRunTransfer`, `getFullRankDiagnostic`. The only gene names shown are the winner's revealed symbols (already bounded via `/transfer`) and the reference marks (bounded known set) — no new reveal surface. - The sweep must not overwrite the main single-run Result / Live view; it keeps its own state (`stabilityRows`, `stabilityStatus`, `stabilityProgress`, `stabilityCancel`). - Runs are sequential (the backend runs one at a time); show progress, allow Stop. Graceful nulls if a transfer fails. ## Checkpoint - On an HNSC/HPV run, a "Stability across seeds" panel appears with a "Run stability check" button; clicking it sweeps the seeds, showing live progress. - When done: two dot-strips (held-out + independent AUROC, one dot per seed with the range), a gene-recurrence bar list tagged by reference set, and a per-seed table. Hidden for colorectal. - Main single-run Result/Live view is untouched while the sweep runs. - `tsc` clean; no behaviour change to existing runs; airgap untouched.