// Typed API helpers + types matching the FastAPI Pydantic models. export const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"; export type Target = "msi" | "tmb" | "none" | "hpv"; export type DatasetId = "coadread" | "hnsc"; export type Metric = "auroc" | "correlation" | "structure"; export type Direction = "neg" | "pos"; export interface ObjectiveSpec { target: Target; metric: Metric; direction?: Direction; } export interface RunParams { generations: number; population: number; genes_per_set: number; max_sets: number; lambda: number; seed: number; /** * ``null`` => no prefilter; the engine searches the full ~20,000-column * pool. Any positive int narrows to the top-N univariate features. */ prefilter_n: number | null; permutations: number; } export interface Candidate { id: string; program_repr: string; fitness: number; parents: string[]; survived: boolean; // v2-only n_nodes?: number; depth?: number; gene_ids?: string[]; // v1-only legacy feature_sets?: string[][]; n_genes?: number; born?: boolean; } export interface GenerationEvent { generation: number; best_fitness: number; median_fitness: number; elitism: number; candidates: Candidate[]; } export interface Winning { id: string; program_repr: string; gene_ids: string[]; cv_fitness: number; holdout_auroc: number; holdout_score: number; permutation_p: number; n_nodes?: number; depth?: number; feature_sets?: string[][]; // v1 legacy // Iterative-unsupervised: full-cohort per-patient scores so a // follow-up run can residualise against this axis. full_scores?: (number | null)[]; full_sample_ids?: string[]; // Held-out per-patient scores, used by the post-hoc alignment step. holdout_scores?: (number | null)[]; holdout_sample_ids?: string[]; } export interface Baseline { id: string; program_repr: string; gene_ids: string[]; holdout_auroc: number; holdout_score: number; feature_sets?: string[][]; // v1 legacy } export interface Posthoc { n_holdout: number; msi_auroc: number | null; tmb_abs_spearman: number | null; hpv_auroc?: number | null; n_msi_held?: number; n_tmb_held?: number; n_hpv_held?: number; } export interface RunResult { engine?: "v1" | "v2"; objective_spec: ObjectiveSpec; fitness_label: string; winning: Winning; baseline?: Baseline; permutation_summary: { n_permutations: number; null_kind?: string; null_score_mean?: number; null_score_p95?: number; null_auroc_mean?: number; null_auroc_p95?: number; }; posthoc?: Posthoc; } export interface RunStatus { id: string; engine: "v1" | "v2"; objective_spec: ObjectiveSpec; params: RunParams; status: "running" | "done" | "error"; error: string | null; n_generations_seen: number; generations_persisted: number; log: Array<{ generation: number; best_fitness: number; median_fitness: number; elitism: number; population_size?: number; }>; } export interface PopulationResponse { run_id: string; engine: "v1" | "v2"; generation: number; best_fitness: number; median_fitness: number; elitism: number; population_size: number; candidates: Candidate[]; } export interface EvaluateRow { id: string; symbol: string; matched: boolean; // Single-gene rank diagnostic — present only for (dataset, target) // pairs where one exists (HNSC/HPV, coadread/TMB). rank?: number | null; total?: number | null; single_gene_metric?: number | null; metric_kind?: "auroc" | "spearman" | null; } export interface EvaluateResponse { revealed: EvaluateRow[]; overlap_count: number; reference_set: string; } async function jsonFetch(path: string, init?: RequestInit): Promise { const res = await fetch(`${API_URL}${path}`, { headers: { "content-type": "application/json" }, ...init, }); if (!res.ok) { const text = await res.text(); throw new Error(`${res.status} ${res.statusText}: ${text}`); } return (await res.json()) as T; } export function postRun(body: { objective_spec: ObjectiveSpec; params: RunParams; engine?: "v1" | "v2"; dataset?: DatasetId; residualize_against?: string[]; coherence?: boolean; diversity?: boolean; /** Per-run DSL injection-rate overrides. Flat dict whose keys are * any of: ``split``, ``effect``, ``fitapply``, ``search``, * ``scalar_share``. Missing keys keep their engine defaults; an * empty / omitted dict reproduces the current behaviour byte-for- * byte. ``search: 0`` is the replacement for the old * ``enable_search: false`` toggle. */ rates_override?: Record; }): Promise<{ run_id: string }> { return jsonFetch("/runs", { method: "POST", body: JSON.stringify(body), }); } export function getRunResult(runId: string): Promise { return jsonFetch(`/runs/${runId}/result`); } export function getRunStatus(runId: string): Promise { return jsonFetch(`/runs/${runId}`); } export function getRunPopulation( runId: string, generation: number, ): Promise { return jsonFetch(`/runs/${runId}/population/${generation}`); } export function postEvaluate(body: { gene_ids: string[]; reference_set: string; dataset?: DatasetId; target?: Target; }): Promise { return jsonFetch("/evaluate", { method: "POST", body: JSON.stringify(body), }); } export interface GeneRankRow { symbol: string; present: boolean; corr: number | null; rank: number | null; percentile: number | null; } export interface TMBRankDiagnostic { cohort: string; n_samples: number; n_genes: number; mmr: GeneRankRow[]; immune: GeneRankRow[]; top_negative: GeneRankRow[]; } export function getTMBRankDiagnostic(): Promise { return jsonFetch("/diagnostic/tmb-rank"); } export interface HPVRankDiagnostic { cohort: string; seed: number; n_samples: number; n_pos: number; n_neg: number; n_genes: number; p16: GeneRankRow[]; cell_cycle: GeneRankRow[]; top_separators: GeneRankRow[]; } export function getHPVRankDiagnostic(): Promise { return jsonFetch("/diagnostic/hpv-rank"); } export interface FullRankRow { opaque_id: string; score: number; rank: number; } export interface ReferenceMark { opaque_id: string; symbol: string; set_name: string; rank: number; score: number; } export interface FullRankDiagnostic { dataset: DatasetId; target: Target; metric_kind: "auroc" | "spearman"; n_samples: number; n_pos: number; n_neg: number; n_genes: number; seed: number; test_size: number; ranks: FullRankRow[]; reference_marks: ReferenceMark[]; } export function getFullRankDiagnostic( dataset: DatasetId, target: Target, ): Promise { const qs = new URLSearchParams({ dataset, target }).toString(); return jsonFetch(`/diagnostic/full-rank?${qs}`); } export interface ModulePerGene { id: string; single_gene_metric: number | null; rank: number | null; total: number | null; } export interface RankedModule { gene_ids: string[]; size: number; combined_holdout: number | null; coherence: number | null; /** The engine's own preference signal: the max GP fitness observed * for any candidate carrying this gene-set in the persisted * population. ``null`` for legacy / pre-Chunk-7 runs. */ gp_fitness?: number | null; /** The actual program_repr of the candidate that earned * ``gp_fitness`` (argmax over the persisted population for this * gene-set). Opaque-safe (built from opaque IDs only). The Lab * feeds this into the shared ProgramGraph renderer on row-expand. */ best_program_repr?: string | null; /** Reference-set names this module's gene_ids intersect (bounded * reveal of a small known set — e.g. ["p16","cell_cycle"]). */ ref_sets: string[]; per_gene: ModulePerGene[]; // Confounder-survival flags. Populated for HNSC/HPV only; null // elsewhere or when the subgroup is too small to score honestly. combined_holdout_oropharynx?: number | null; n_holdout_oropharynx?: number; n_pos_oropharynx?: number; n_neg_oropharynx?: number; survives_site?: boolean | null; combined_holdout_highpurity?: number | null; n_holdout_highpurity?: number; n_pos_highpurity?: number; n_neg_highpurity?: number; survives_purity?: boolean | null; } export interface ModuleSubgroup { kind: string; n: number; tolerance: number; n_proxy_genes?: number; } export interface ModuleRanking { run_id: string; target: Target; dataset: DatasetId; metric_kind: "auroc" | "spearman"; coherence: boolean; n_modules: number; n_train: number; n_test: number; subgroups?: { site?: ModuleSubgroup | null; purity?: ModuleSubgroup | null; }; modules: RankedModule[]; } export function getRunModules(runId: string): Promise { return jsonFetch(`/runs/${runId}/modules`); } export interface OperatorUsageRow { /** Human label for the DSL operator (e.g. "Select", "Fit/Apply"). */ name: string; /** Total occurrences across every candidate in every generation. */ total_uses: number; /** Number of candidate-instances containing this operator ≥ once. */ programs_using: number; } export interface OperatorUsage { run_id: string; n_generations: number; n_candidates: number; operators: OperatorUsageRow[]; } export function getRunOperatorUsage(runId: string): Promise { return jsonFetch(`/runs/${runId}/operator-usage`); } export interface RevealResponse { symbols: string[]; } /** External-cohort transfer test payload (GSE65858 for HNSC/HPV). Gene * NAMES in this payload are ONLY the winner's revealed symbols — same * discipline as /evaluate. */ export interface TransferResult { run_id: string; cohort: string; platform: string; source: string; n_cohort: number; auroc: number | null; p: number | null; n: number; n_pos: number; n_neg: number; n_found: number; n_missing: number; found_symbols: string[]; missing_symbols: string[]; } export function getRunTransfer(runId: string): Promise { return jsonFetch(`/runs/${runId}/transfer`); } export function postReveal(gene_ids: string[]): Promise { return jsonFetch("/reveal", { method: "POST", body: JSON.stringify({ gene_ids }), }); }