Repository navigation
Nineteen literature-derived analysis views (preview), one commit each - #59
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
alisman
force-pushed
the
session/new-features
branch
from
October 23, 2026 03:29
cb0d49e to
0c02064
Compare
alisman
force-pushed
the
session/new-features
branch
from
October 23, 2026 03:37
f80f72d to
9331b24
Compare
alisman
force-pushed
the
session/new-features
branch
from
October 23, 2026 03:38
9331b24 to
30d160f
Compare
alisman
force-pushed
the
session/new-features
branch
from
October 23, 2026 03:42
8b97f80 to
443bcd2
Compare
alisman
force-pushed
the
session/new-features
branch
from
October 23, 2026 05:07
b33f9c8 to
fb2e7e3
Compare
alisman
force-pushed
the
session/new-features
branch
from
October 23, 2026 07:42
fb2e7e3 to
b5614ce
Compare
…k's exemption lib/preview-kinds.ts lists view kinds that are drawn and usable but not yet held to invariant 12 (no method record, no golden test), each with what it still lacks. ViewHead shows such a kind with a Preview badge whose title names what is pending, and scripts/check-method-scripts.ts exempts exactly these kinds, so removing a kind's entry puts it back under both checks at once. Empty here; the analysis views that follow add themselves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…s after a treatment start A new view kind. For each cohort patient, the anchor is their FIRST timeline event of `anchor.eventType` (default Treatment, any; key/value such as SUBTYPE=Hormone or AGENT=…, folded through foldedEqualsSql). A sample's date is its first timeline event of `sampleEvents` (default Sample acquisition / Sequencing / Specimen) naming it in `sampleKey` (default SAMPLE_ID), matched to the study's samples by stable id. The before-sample is the last on or before the anchor day (optionally within `preWindowDays`), the after-sample the first at least `minGapDays` (default 30) after; one sample per side, ties broken by sample id. Per gene, among patients whose before- AND after-sample panels both cover it: acquired (after only), lost (before only), shared, neither; exact McNemar on acquired vs lost (mcnemarExact), BH across the genes tested per cohort. `measure: "driver"` counts only OncoKB oncogenic calls (the pinned store); default any alteration. A gene list (at most 30) or, without one, a scan of every gene altered after treatment in at least `minPost` (default 3) patients, the `top` (default 20, max 40) smallest p drawn. The result carries the funnel (anchored, with a sample before, after, both) and the pairs' spacing (median days before/after, gap quartiles). Refuses past 3 cohorts / 30 genes, bad windows, a driver count without a pin, and a cohort with no pair — naming the funnel so the fix is visible. The per-gene counting is the paired view's, moved into a shared `sideCountsTail` in lib/federation-views.ts that both views put a `ps` (suid, pid, side) CTE in front of. The paired view's SQL is byte-identical: pairedCountsSql's output was compared, old file against new, for named genes and scan, driver and any — all four equal. Figure: diverging bars per gene, lost left, acquired right, one symmetric scale, with shared, pairs and McNemar q columns; caption is the caveat (selected patients — rebiopsy is clinically indicated — acquired ≠ caused; a missed subclone or lower-purity sample reads the same). Verified on lung_msk_pdx (gallery example, SUBTYPE = Systemic therapy, drivers, scan, top 12): 172 patients, 142 with a start, 67 with a sample before, 107 after, 33 with both, samples a median 301 days apart (IQR 154–514); 30 genes tested, CDKN2B 8 acquired / 1 lost (p 0.039, q 0.50), MYC 4/0, KEAP1 5/1. msk_chord_2024 SUBTYPE=Chemo gives 28 pairs, any Treatment 51. Figure lint 0 at 936 and 560; tsc, eslint, npm test (1440 pass) clean. Example pinned (172). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
… and survival by ctDNA detected A new view kind. A sample is plasma when its `attribute` (default SAMPLE_CLASS) is one of `plasma` (default cfDNA, ctDNA) or it sits on one of `plasmaPanels` (e.g. ACCESS129), tissue when the attribute is one of `tissue` (default Tumor). Among patients with both, per gene covered by BOTH kinds' panels (the paired view's counting, through the shared sideCountsTail with a = tissue, b = plasma): altered in both, tissue only, plasma only, neither; positive percent agreement both ÷ altered-in-tissue with a 95% Wilson interval (new wilsonInterval in lib/stats.ts, checked against statsmodels' values), and overall concordance. A gene list (≤ 30) or the `top` (default 15) most altered. ctDNA detected = any alteration (or, with measure "driver", an OncoKB oncogenic one) called in a plasma sample, as a share of plasma patients with a Wilson interval; overall survival by detected vs not, Kaplan–Meier plus log-rank (stratified by study when pooled), from aggregate (detected, months, dead) counts, drawn when each group has ≥ 5 patients, else the reason is shown. `withinDays` keeps only samples the timeline dates (first Sample acquisition/Sequencing/Specimen event naming them) within N days of a sample of the other kind, and the result reports how many samples were dated. The public cfDNA studies carry no sample events, so there it refuses with the counts — noted in the caveat path. Caveat: pairing by patient not date (unless withinDays), clonal haematopoiesis behind plasma- only calls, low shedding behind tissue-only, OS from the study's own origin (possible immortal time when the draw is later). Refusals: limits, bad values, a value on both sides, driver without a pin, no patient with both (listing the attribute's values). Verified on nsclc_ctdx_msk_2022 (gallery example, ten NSCLC genes): 1,127 patients, 1,119 with plasma, 690 tissue, 682 both; PPA EGFR 64% [58–70], KRAS 64%, TP53 68%, STK11 30%, KEAP1 31%, MET 29%; ctDNA detected 759/1,119 (68%, 65–71%); OS median 21.8 vs 41.5 months, log-rank p = 2.2e-12. msk_access_2021 with plasmaPanels ACCESS129 and drivers: 449 pairs, EGFR PPA 79%, CDKN2A 12% (deletions rarely seen in plasma), detected 70%, log-rank p = 0.0002. Figure lint 0 at 936 and 560; tsc, eslint, npm test clean. Example pinned (1,127). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…alteration burden (DISCOVER) A new view kind, after Canisius, Martens & Wessels (Genome Biol 2016). One cohort, 2–20 genes (more is refused with the limit named), over the cohort's samples with a gene panel (every sample of each patient). The database returns only aggregates: samples grouped by panel signature, stratum, burden r (altered genes among those its panels cover) and a bitmask of which listed genes are altered; per stratum and gene the altered-sample counts; and each panel signature's covered genes. Background: p_ij = σ(a_i + b_j) over the genes sample i's panels cover, fitted over the WHOLE altered-gene matrix (e.g. 410 genes in NSCLC) so every sample's expected burden is its observed one and every gene's expected count its observed one — alternating 1-D Newton steps on rows and columns until the largest relative margin residual is below 1e-4 (reported, with a loud caveat when it does not converge); never/always-altered rows and genes sit at ∓∞. Samples sharing (signature, burden) share a row parameter, which is what keeps the data aggregate. Fitted per study and, with `stratifyBy` (e.g. CANCER_TYPE), per stratum. For each pair, over samples profiled for both: the co-altered count against the Poisson-binomial with p_i = p_iA·p_iB — lower tail exclusivity, upper tail co-occurrence, BH per direction. New poissonBinomialTails in lib/stats.ts: exact DP over counts 0…x+1 (the last state "more than x", so both tails keep their precision) while trials·(x+2) ≤ 5e6, else the refined normal approximation, flagged per pair. Beside it the plain two-sided Fisher test on the same samples, BH'd, as the cooccurrence view runs it. `measure: "driver"` counts only OncoKB oncogenic calls. Figure: lower-triangle gene × gene heatmap, hue = direction, shade = DISCOVER q (outlined when q < 0.05, q printed when it fits, a never-co-profiled pair drawn empty), then a table of the 12 most significant pairs, DISCOVER beside Fisher. Unit tests: the Poisson-binomial tails on a hand-checked case (p = ½, ½, ⅕) and against the binomial (scipy binom.cdf(280, 1000, 0.3) = 0.08858) for both the exact and approximate paths; the fit reproducing every margin on a two-panel toy matrix; the figure (lint clean at 560/936). Verified on msk_impact_2017 NSCLC (gallery example: KRAS, EGFR, BRAF, STK11, KEAP1, TP53): 1,567 patients, 1,680 samples, fit converged in 3 rounds. KRAS/EGFR 19 together vs 170 expected (q 9.5e-57; Fisher 5.7e-47); STK11/KEAP1 co-occur (159 vs 66, q 6.9e-25). EGFR/TP53: Fisher co-occurrence q 1.8e-7, DISCOVER 294 vs 300 expected, q 0.45; KEAP1/TP53 Fisher 0.79, DISCOVER exclusive q 0.001; KRAS/STK11 Fisher 2e-17 → DISCOVER 0.02. Stratified by CANCER_TYPE_DETAILED (26 strata) the drivers' exclusivity holds (KRAS/EGFR q 2e-62). tsc, eslint, knip (only tmp files), npm test (1455) clean. Example pinned (1,567). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…atment start
A new view kind, `txendpoints`: the clock starts at an anchored treatment record (timeline
event type Treatment by default, key = value such as SUBTYPE=Immuno or AGENT=PEMBROLIZUMAB,
first occurrence unless LAST) and runs to one of three real-world endpoints (MSK-CHORD,
Nature 2024; GENIE BPC):
- ttnt: the start of the next treatment line after the anchor's own line, lines built only
by lib/treatment-lines.ts (linesCtes; lineGap default 90, lineClasses default systemic);
for an anchor no line holds, the first line starting after it;
- tot: the latest stop date of the anchored records in the anchor's line; a living patient
whose stop lies within ongoingDays (30) of the last follow-up is censored as still on it; a
patient with an anchored record lacking a stop date is left out and counted (a missing stop
cannot be told from an ongoing treatment);
- rwpfs: the first progression event after the anchor day (default Diagnosis / PROGRESSION in
Y, Yes, folded; optional probabilityKey + minProbability floor).
Death (OS_STATUS 1:…) is an event for every endpoint; censoring at the last follow-up,
OS_MONTHS × 30.4375 days on the timeline clock. That clock relation was established on the
data: in msk_chord_2024 no patient (0 of 24,950) has a timeline event more than 31 days after
OS_MONTHS × 30.4375 (99th percentile of last-event-day minus that: −0.07 days). The view
re-checks it on every run (a `late` flag per patient in the SQL) and refuses a study where
more than 5% of anchored patients break it; the caveat states the rule and the count. Left
truncation as in the survival/incidence views (an anchor before day 0 enters at day 0).
SQL: txEndpointCountsSql (lib/federation-views.ts) returns only (kind, entry, t, ev, late, n)
tallies through viewAggregate, so it is aggregate-safe ("local" coverage); added to the
boundary suite (three variants). KM per cohort (≤6) with number at risk, medians with 95%
intervals (new kmMedianCi in lib/stats.ts: Greenwood band on the log scale, R survfit's
default; unit-tested against survfit's 1..10 case: median 5, lower 3, upper NA), k-group
log-rank stratified by study (refused below 10 patients per cohort, as survival does). Refuses
a missing anchor listing the key's values with patient counts, and a bad clock. The KM panel
with its risk table is a new shared figure module, lib/figures/km.ts (added to the
kind-boundary list), so later views reuse it.
Registered in types, runners, figures, coverage, help, result text, MCP description, DSL
reference (row, grammar, gallery example), kind card (endpoint, line gap, anchor), and
PREVIEW_KINDS.
Verified on the gallery example (msk_chord_2024 NSCLC, KRAS mutant 2,194 vs wild-type 5,540,
from first SUBTYPE=Immuno): 981 / 1,651 anchored; rwPFS median 2.6 mo (95% CI 2.2–2.7) vs 2.6
(2.4–2.8), log-rank p = 0.98; TTNT medians 11.6 (10.5–12.7) vs 12.0 (11.2–12.7); TOT 3.2
(2.8–3.7) vs 3.2 (2.8–3.5). Figure linted 0 problems at 936 and 560 px. tsc, eslint, knip,
npm test (1443 pass) and docs:check (all examples) clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
A new view kind, `predictive` (literature: PMIDs 29122777, 32913971, 39499893). One or two cohorts' patients, pooled (baseline hazard stratified by cohort and study when there are several); a treatment exposure defined by an anchor (any timeline event of that type and key = value: exposed; the rest unexposed); a gene list (≤20); endpoint OS by default, with the cox view's monthsAttribute/statusAttribute escape hatch; optional clinical covariates (≤6) coded by lib/views/covariates.ts buildCoxColumns (numbers as they are, categories against their commonest level, as the cox view codes clinical axes); measure any or OncoKB driver. Per gene, Cox (Efron) on gene + exposed + gene×exposed (+ covariates), over the patients whose panel covers the gene — panel-aware exactly as the riskmodel view: the rows come from riskmodelRowsSql (panels, mutated genes, covariates; its anchor flag gives exposure) through PatientLevelResolver.viewRows, the panels' genes from riskmodelPanelGenesSql, and a patient not covered is left out of that gene's fit, never wild-type. The gene's HR among the exposed and among the unexposed are the same model's two contrasts — it is fitted with the interaction coded as gene×exposed and as gene×unexposed (same likelihood), so the gene's own coefficient is each contrast in turn — and the interaction HR, its Wald p and a Benjamini–Hochberg q over the genes fitted are reported. A gene with fewer than minMutated (5) mutated patients in either group, or a fit that runs to infinity, is listed with why, not fitted. Figure: a two-marker forest (new dualForestSvg in the shared lib/figures/forest.ts) with the interaction p · q column. The caveat names immortal-time bias (exposure defined from the whole record, after baseline) and confounding by indication. Coverage "patients". Verified on the gallery example (msk_chord_2024 NSCLC, 7,809 patients, 2,666 with a SUBTYPE=Immuno record, 3,975 deaths; every gene covered for every patient): STK11 HR 1.48 treated / 1.27 untreated (interaction p 0.084), KEAP1 1.41 / 1.66 (p 0.051), KRAS 0.99 / 0.89, TP53 1.02 / 1.72 (interaction HR 0.59, p 4.5e-16, q 2.7e-15), EGFR 1.04 / 0.90, SMARCA4 1.14 / 1.88 (q 1.1e-5). Directions match the checkpoint-inhibitor literature (STK11/KEAP1 adverse on IO). Unit test: on simulated data with a planted interaction the exposed HR equals the unexposed HR × the interaction HR to 1e-6 and the interaction is found (p < 1e-4). Figure linted 0 at 936 and 560 px; tsc, eslint, knip, npm test (1445) and docs:check clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…or the search A new view kind, `cutpoint` (Samstein 2019, PMID 30643254; PMID 38949888); the survival view is untouched. One cohort; a numeric clinical attribute (default TMB_NONSYNONYMOUS) read as the patient's value or, for a sample attribute, the value of the patient's first sample by sample id (argMinIf over sample_unique_id, as riskmodelRowsSql reads covariates); endpoint OS with the monthsAttribute/statusAttribute escape hatch. Method: candidate cuts are the marker's distinct values leaving between 10% and 90% of the patients at or below them, at most 100 (evenly spaced among them). At each, the maximally selected standardized log-rank statistic (Lausen & Schumacher 1992; R maxstat's LogRank): log-rank scores (event minus Nelson–Aalen hazard), summed over the low group, squared over their permutation variance. The maximum is the chosen cut (high = marker > cut). Corrected p: (1 + hits)/(1 + B) over B = 1000 seeded (seed 1, mulberry32) shuffles of the scores over the marker order, each taking its own maximum; the 95th percentile of that maximum is drawn as the significance line. Reported beside it: the ordinary log-rank p at the cut (which ignores the search), the high-vs-low Cox HR, both groups' KM and medians. Rows are sorted canonically before the shuffles, so the seeded p does not depend on row order. Optional stratifyBy: a scan per value (the largest 8 with at least minStratum = 100 patients; the rest counted), as a table. Caveat: optimistic in-sample choice, needs validation; TMB not comparable across panels when studies are pooled. Rows: new cutpointRowsSql (values only, through PatientLevelResolver.viewRows; boundary suite covers it); coverage "patients". Figure: statistic-vs-cut curve with the chosen cut marked and the permutation 95% line, KM at the cut via the shared lib/figures/km.ts, the strata table. Verified on the gallery example (tmb_mskcc_2018, 1,661 ICI-treated patients, 832 deaths, 73 candidate cuts): best cut TMB > 14.4 (322 high), HR 0.45 [0.36–0.55], medians 47 vs 15 mo, corrected p = 0.001 (permutation 95% line χ² 8.25 against a maximum of 67.8), naive p 9.5e-15. Per cancer type: NSCLC 18.6 (p 0.001), melanoma 9.8 (0.001), colorectal 11.1 (0.004), head and neck 8.8 (0.028), bladder 14.7 (0.12), renal 1.1 (0.44), oesophagogastric 10.0 (0.48), glioma 2.9 (0.62); 3 values / 133 patients not scanned. On msk_impact_2017 (not ICI-treated) the direction reverses (HR 1.47), as expected without immunotherapy. Unit tests: a planted threshold at 30 is found with corrected p < 0.01, a null cohort is not significant after correction, candidates respect the window and cap, seeded reruns agree, and under a modest effect the statistic matches the log-rank χ² within 10% (on the real example 82.18 vs 82.50). Figure linted 0 at 936 and 560 px; tsc, eslint, knip, npm test (1452), docs:check clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…andom-effects pooled A pan-cancer forest / meta-analysis view. The exposure is a plot axis: a gene's alteration (exposed = altered; a patient not profiled for the gene is left out by coxRows' own panel-aware rule, never counted as unaltered), a categorical attribute with `exposed` naming the level, or a numeric one with `threshold`. Rows are a categorical attribute (default CANCER_TYPE; patients whose samples disagree, "Multiple", are left out and counted), the study, or the view's cohorts. Within each row a Cox model (Efron ties, lib/stats.ts) is fit for exposed vs not, univariate or adjusted for up to 4 covariates coded by lib/views/covariates.ts as the cox view codes them. Strata under minPatients (30) patients or minEvents (10) events, and past the maxStrata (30) largest, pool into one disclosed "Other" row fit with a baseline hazard per stratum (or, small:"drop", are listed as left out); a row without an exposed and an unexposed event, or whose fit does not converge, is listed with the reason. Rows are pooled by inverse variance on log HR: fixed effect and DerSimonian–Laird random effects (new dersimonianLaird in lib/stats.ts) with tau², I², Cochran's Q and its p. Endpoint OS by default, with the cox view's months/status escape hatch. Per-patient rows come only from patient-level participants (coverage "patients"). Figure: metaForestSvg in the shared lib/figures/forest.ts — n (events), HR [CI], each row's random-effects weight (square sized by it), and the two pooled diamonds. Refuses: no exposure, a numeric exposure without a threshold, a categorical non-alteration exposure without `exposed` (listing its levels), an unknown level, a numeric strata attribute, strata "study" over one study or "cohort" over one cohort, more than 4 covariates, bad bounds. Verified on the gallery example (msk_impact_50k_2026, TP53 vs OS by CANCER_TYPE): 48,179 patients, 43,375 with OS, TP53 status and one cancer type (16,953 deaths); 4,135 without OS, 669 "Multiple" left out; 30 rows (29 cancer types + Other of 34). HRs > 1 almost everywhere (prostate 2.50, endometrial 2.88, thyroid 4.06), < 1 in glioma 0.86 and melanoma 0.78 (as expected: TP53 marks IDH-mutant glioma); random effects 1.63 (1.42–1.87), fixed 1.54 (1.49–1.59), tau² 0.121, I² 93%, Q 431 on 29 df. Figure lints clean at 936 and 560 px. DerSimonian–Laird checked against hand-computed values in test/forest.test.ts, with the row building (pooling, drop, unestimable rows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…ween two cohorts Every gene mutated (called mutations; OncoKB drivers with measure:"driver") in at least minShare (default 5%) of the patients profiled for it and in at least minAltered (10), both cohorts together, gets a logistic regression of mutated on cohort (the first = 1) and the covariates — clinical attributes, a number as recorded, a category against its commonest value, coded by lib/views/covariates.ts as cox/logistic code theirs. Reported per gene: the cohort term's adjusted OR, Wald 95% CI, p, and BH q over the genes fit, beside the raw OR and two-sided Fisher p on the same patients. Patients, not samples, and panel-aware per gene: a patient is in a gene's model only when one of their mutation panels covers it (or WES), never counted as wild type. Rows come from a new adjustedRowsSql (one row per patient: panels, mutated genes, covariate values; no identifier; run only through viewRows, added to the boundary suite) and the panel genes from riskmodelPanelGenesSql. A covariate category in which no patient (or every patient) is mutated would separate; for that gene it is merged into its reference and the caveat counts the genes affected. A gene whose fit still does not converge, or has an empty 2×2 cell, is listed in `failed`, never dropped. Refuses: not exactly two cohorts, >6 covariates, a categorical covariate with >30 values, more than 200 genes past the threshold (naming the count and asking for a higher minShare), no OncoKB pin for drivers, empty cohorts. Figure (lib/figures/adjusted.ts): the shared volcano of adjusted log2 OR against −log10 q (volcano gains an optional pName for its y title; unset, every other volcano is unchanged), genes at q < 0.05 labelled, then the top 15 genes raw beside adjusted. Verified on the gallery example (msk_impact_50k_2026 colorectal, AGE_AT_DX < 50 vs >= 50, adjusted for MSI_TYPE and TMB_SCORE): 1,699 vs 3,149 patients (1,693 / 3,146 with both covariates); 56 genes tested, all fit (9 with a separating MSI category merged), 4 at q < 0.05 — BRAF 7.4% vs 12.9% (raw OR 0.54, adjusted 0.52, q 4e-7), TCF7L2 1.40, TP53 77% vs 72% (1.31), RNF43 0.66 — matching the early-onset CRC literature; APC and SOX9 are nominal raw and not after adjustment. Lints clean at 936 and 560 px. Unit tests: a confounded gene's adjusted OR returns to 1 while the raw one does not; unprofiled patients are left out of the gene; an unfittable gene is listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
… and losses from segments A new view kind. From `copy_number_seg` (hg19 only: a study on another build is refused naming each study's build, as the genome view does), computed per sample inside the database: - FGA: the share of the sample's segmented length (autosomes; X with includeX) whose |segment mean| is at or past `threshold` (default 0.2, log2); - arm calls: segments split at hg19's centromeres (new HG19_CENTROMERES in lib/federation-views.ts, the p11.1/q11 boundary of UCSC's hg19 cytoBand, source cited), the acrocentric p arms of 13/14/15/21/22 left out (39 arms, as Taylor et al. 2018); an arm is gained (lost) when more than `armShare` (default 0.5) of its length is past ±threshold, and measured when segments cover that share; - aneuploidy score: arms gained or lost. Only a joint histogram (FGA in 1-point bins × score × arms measured) and per-arm measured / gain / loss counts leave the database. Per cohort (≤ 6): quartiles and 5th/95th percentiles from the histograms, Mann–Whitney (2 cohorts) or Kruskal–Wallis (3+) on each (lib/stats.ts, from histograms), and each arm's gain/loss share of the samples measuring it. Figure: aneuploidy score and FGA as horizontal boxes per cohort (side by side when wide, stacked when narrow), the rank test, then an arm × cohort heatmap (gain strip above loss strip, shade relative to the most frequent call, named in the title). Caveat: every sample of each patient counts, uncovered arms are not called (lowering the score; counted per cohort), panel segments are noisy and purity is not corrected. Verified on msk_impact_50k_2026 (gallery example): colorectal 5,390 samples scored, median score 7 [2–12], FGA 15% [4–28], 20q gained 60%, 13q 42%, 18q lost 53%, 17p 36%; breast 6,906, 1q 45% and 8q 41% gained, 16q 39% lost; pancreatic 2,910, median 2 arms (low purity). Kruskal–Wallis score p = 2e-163, FGA p = 1.2e-222. msk_impact_2017 colorectal alone gives the same arms (20q 55%, 18q 56%). Mixing hnsc_a5consortium_2025 (hg38) with msk_impact_2017 is refused, naming both builds. Figure lint 0 at 560/936 (unit test also 1352); tsc, eslint, npm test (1459) clean. Example pinned (4,993 / 5,803 / 2,729). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
… cohort
`validateOn` names a second cohort of the document (literature: PMIDs 37477937, 38488807). The
ensemble is fitted on the view's cohort exactly as before (the fit, its cache key and every
reported number are unchanged; validateOn is a presentation key, like describe, so it never
refits). Then every patient of the validation cohort — one patient-level study, any study —
is read with the fit's own row query (riskViewRows: same endpoint, anchor, strata, measure),
kept by the same keepRiskRows rule, and scored with the ensemble's mean coefficients: each
gene's coefficient averaged over every run, zero where a run dropped it (selected × mean
where kept) — the average the existing `validate` half is scored with. Coverage rule: a
validation patient whose panels do not cover every gene the model weighs (non-zero ensemble
coefficient) is left out and counted, never scored as wild-type. Patients are cut at the
training risk-group thresholds (the groups' `from` raw scores) and reported with Harrell's
C-index (lib/coxnet.ts concordance, within strata) and a seeded bootstrap 95% interval (200
resamples), KM per group, the k-group log-rank (stratified by the model's strata) and each
group's Cox HR against the lowest. Refused: a cohort not in the document, the fitted cohort
itself, a multi-study or aggregate-only cohort, and a model with covariates (their coding is
not carried to another cohort yet). The figure draws the validation's KM panel (shared
lib/figures/km.ts) and its C-index / log-rank line below the existing figure; the caveat
names the thresholds, the coverage exclusions, and that patients of two studies cannot be
de-duplicated. New module lib/views/riskmodel-validate.ts; riskViewRows and RiskResolver
exported from lib/views/riskmodel.ts.
Byte-identical without validateOn: the existing riskmodel gallery example ("A survival risk
model from mutated genes in lung cancer") was run before the change and after it, with
`ms`/`renderMs` stripped from the JSON — identical (113,978 characters; a second run before
the change was identical too, so the comparison is deterministic), and its SVG at 936 and
560 px is identical as well.
Verified on the gallery example (fit on msk_impact_2017 NSCLC, 1,440 patients, 468 events,
10 runs, held-out C-index 0.613; validate on msk_chord_2024 NSCLC): 7,809 scored, 0 not
covered (21 weighted genes), C-index 0.607 (95% bootstrap CI 0.597–0.616); low / intermediate
/ high risk n 2,790 / 3,237 / 1,782, medians 66.4 / 33.3 / 20.7 months, HR 1.63 [1.51–1.76]
and 2.12 [1.95–2.31], log-rank p 8.1e-73. Unit test with a stub resolver: scores by
selected × mean coefficient, the training cut, an uncovered patient left out. Figure linted 0
at 936 and 560 px; tsc, eslint, knip, npm test (1453) and docs:check clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…rimary vs metastasis concordance
Each patient with a sample sequenced for mutations, at their highest sensitive therapeutic
level (1, 2, 3A, 3B, 4, or none) over the called mutations and high-level copy-number calls
in any of their samples, read from the version-pinned OncoKB store (oncokb_annotation.
variant_annotation at the run's pin, echoed in the result and caveat), and separately whether
any alteration carries a resistance level (R1, R2). Structural variants are matched as
driverJoin matches them, but the store holds no SV level at v7.5; the caveat names the types
the store does hold (actionStoreTypesSql). Per cohort (≤4) and, with groupBy, per group of a
categorical attribute per patient ("Multiple" when samples disagree, the `top` (8) largest
drawn and the rest pooled into a named Other). For patients with a sequenced sample on each
side of pairBy (default SAMPLE_TYPE Primary / Metastasis, case-insensitive; null turns it
off), the 6x6 cross-table of their highest level per side and the discordant share.
All counts in SQL (actionLevelsSql, actionConcordanceSql; boundary suite covers both),
through viewAggregate, so coverage is "local". The store is pre-filtered to rows with a level
(a few thousand of its millions) before the join: 55 s -> 5 s on msk_impact_50k_2026.
The store's level is its highest for the alteration in ANY cancer type, not the patient's
own, so the shares are an upper bound; the caveat, the axis title and the gallery note say so.
"No level" means none on the genes sequenced; an unprofiled gene is unknown.
Verified on the gallery example (msk_impact_50k_2026 by CANCER_TYPE): 48,179 patients, all
sequenced; Level 1 27,154 (56%), 2 1,527, 3A 3,244, 3B 0 (no 3B in the store), 4 3,061,
none 13,193; R1 9,698. Pancreatic 87% level 1 and 83% R1 (KRAS, levels not tumour-specific —
the upper-bound effect), prostate 38%. 2,278 patients with both a primary and a metastasis:
581 (25.5%) discordant, diagonal 1,108 / 32 / 84 / 85 / 388. Lints clean at 936 and 560 px.
Unit test of the top-N pooling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…anonical driver A new kind. Within one cohort, each patient sequenced for mutations is driver-positive with an OncoKB driver (oncogenic or likely oncogenic at the pin, via driverJoin: a called mutation, a high-level copy-number call or a structural variant) in a canonical driver list (default the NSCLC set EGFR, KRAS, ALK, ROS1, BRAF, MET, RET, ERBB2, NTRK1-3; user-editable, up to 40 genes); else driver-negative when a mutation panel of theirs covers every listed gene (or WES); else "not fully profiled" — counted and kept out of both groups, never driver-negative. Then every other gene's alteration (mutation by default; sv counts either fusion partner; cna) is compared between driver-negative and driver-positive patients, in patients over those profiled for the gene (the enrichment view's panel-aware construction: panel samples per panel joined to their genes plus whole-exome, an off-panel call counting as measured), two-sided Fisher, BH q, log2 of the negative share over the positive one. All counts in SQL (dnegGroupsSql, dnegAlteredSql, dnegProfiledSql over one shared CTE chain; boundary suite covers each), through viewAggregate, coverage "local". Figure (lib/figures/drivernegative.ts): the group split as one bar, the shared volcano (driver-negative to the right), and the genes most enriched without a driver. Refuses: not exactly one cohort, no OncoKB pin, >40 drivers, a bad symbol, an unknown alteration, a cohort where one group is empty (naming the three counts). Verified on the gallery example (msk_chord_2024 NSCLC): 7,809 patients, 4,911 (62.9%) driver-positive, 2,898 (37.1%) driver-negative, 0 not fully profiled (IMPACT panels cover all eleven). 487 genes tested, 95 at q < 0.05: NF1 12.4% vs 5.0% (q 3e-28), KMT2D, TP53 59% vs 47%, NFE2L2 5.6% vs 1.6%, CDKN2A, RASA1, PTEN, KEAP1, MAP2K1 1.8% vs 0.5%, NRAS — the alternative MAPK and NRF2 routes expected without a canonical driver; RBM10 goes with the drivers. With alteration "sv": 114 genes, NFE2L2 rearrangements 17 vs 2 (q 4e-4). 2.3 s. Lints clean at 936 and 560 px. Unit test of the per-gene counting (WES denominators, off-panel calls, untested genes, Fisher, BH order). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
The merged-row tooltip check hovers the label gutter at fixed offsets from the canvas, and the drag-to-cohort check presses 40px into it; a pointer past the bottom of the viewport never lands. The oncoprint sits below the add-view panel, which grows by a row with every few view kinds; with the preview kinds it pushed the canvas to the window's edge (top at 828 of 1000px) and these checks failed while master passed. Scrolling the canvas to the top first makes them independent of what sits above it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
lib/new-in-lab.ts lists the view kinds the portal does not draw in any form: the regressions (cox, logistic, the risk model and the new outcome views), the timeline views (course, sequence, incidence, labs, attrition), the new association views, and the sample- and genome-level views (paired, acquired, ctdna, clonality, genome, instability, composition). Kinds the portal has in some form — its Comparison, Plots, Mutations, Pathways and patient-view tabs, the oncoprint, survival, co-occurrence, co-expression, cancer types summary, generic assays, differential expression — are left out. The add-view buttons and the Examples tab's cards (any example drawing such a kind) show a green NEW after the name, titled "not in cBioPortal": 23 of 42 buttons and 29 of 82 examples on this branch, checked in a browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
The mark's text takes its own colour token (--new: orange, a lighter orange in dark mode), so it stands apart from the page's teal and grey without a fill. The Examples tab's filter chips carry it too: a view chip for a NEW kind, and a topic chip when at least one example under it draws a NEW view (23 of 42 view chips and 55 of 101 topic chips on this branch, checked in a browser). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
The view and topic filters were rows of outlined pills, which with a NEW mark beside many of them read as a wall of boxes. Each is now a list of plain links flowing down columns (11rem wide, as many as the card fits): the name with its first letter capitalised (by CSS, so the text and what e2e matches are unchanged), its example count, and NEW where it applies; the chosen one is bold and underlined, and clicking it again clears the filter. The two lists' headings are bold ink over a rule. The "all" chip beside the search stays as it was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
The add-view pills were white outlines on the white card; with 42 of them, many carrying NEW, they ran together. A kind not yet in the document now sits on the border grey with ink text and no outline, so each pill reads apart from its neighbours; one already in the document keeps its teal fill, and one this study cannot draw stays faded with its reason in the title. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…ion to death
A new view kind, `multistate` (Rueda et al., Nature 2019, PMID 30867590): three states per
patient on the timeline's clock, ≤4 cohorts.
- Start is the timeline's day 0. In msk_chord_2024 day 0 is first sequencing: every one of
24,950 patients has a Sequencing event on day 0 and none earlier (Sample acquisition and
Diagnosis SUBTYPE=Primary sit mostly before it). The view counts patients with a Sequencing
event on day 0 on every run and names the origin "first sequencing" only when ≥90% do;
otherwise the caveat says day 0 is the study's own and that later-sequenced patients carry
immortal time.
- Progression is the first event of type Diagnosis with PROGRESSION in Y / Yes (folded; an
optional probabilityKey + minProbability floor), the same settings and defaults as
txendpoints' rwPFS. A progression on or before day 0 starts the patient in the progression
state (9,111 of 24,950 in msk_chord_2024), or, with prevalent: "exclude", leaves them out.
A progression on or after the end of follow-up is not counted (and counted in the caption).
- Death / end of follow-up: OS_STATUS / OS_MONTHS, OS_MONTHS × 30.4375 days on the timeline's
clock, with txendpoints' clock check (a timeline event > 31 days past it; a study over 5% is
refused).
Markov, clock-forward: every hazard runs on time since day 0, progression → death included,
with delayed entry at the progression. Per cohort: events, person-months and rates per 100
person-months (Poisson 95% interval); Nelson–Aalen cumulative hazards per transition (Aalen
variance, log-scale interval); Aalen–Johansen state occupation from the initial distribution,
drawn as stacked areas to where ≥10 of the cohort remain followed and tabled at horizons
(12/24/60 mo). Per transition a Cox model (Efron) of each cohort against the first, baseline
per study, no ratio for a cohort with < 5 events. Design chosen: cohort vs cohort, so the view
stays aggregate-only ("local" coverage).
Shared code: the txendpoints SQL pieces (OS, last timeline day, progression events, the event
FROM/attribute helpers) are now functions in lib/federation-views.ts that both kinds call, and
the progression defaults/checks/words and the clock tolerance live in lib/timeline-endpoints.ts.
txendpoints is unchanged: its SQL for four variants is byte-identical before/after, and its
gallery example's result JSON and SVGs (936, 560) are byte-identical. lib/stats.ts: CoxRow takes
an optional `entry` (counting-process risk sets); rows without one take the same path as before.
New illnessDeath() estimator. multistateCountsSql returns only (prevalent, tp, te, dead, flags,
n) tallies through viewAggregate; added to the boundary suite.
Refuses: >4 cohorts, bad prevalent/horizons/cutoff, a failed clock check, no follow-up, and no
progression event at all (listing the values the key takes).
Verified on the gallery example (msk_chord_2024 colorectal, stage 1-3 n=3,171 vs stage 4
n=2,372; 937 and 1,004 prevalent; 41 never assessed): start→progression 1.2 vs 6.5 per 100
person-months, HR 4.41 (4.01–4.86); start→death 0.11 vs 0.32, HR 2.63 (1.82–3.82);
progression→death 1.6 vs 2.8, HR 1.70 (1.55–1.86). At 24 months not progressed / progressed /
dead: 47/36/17% vs 9/49/43%. Unit tests: illness-death estimators against a hand computation,
the delayed-entry Cox partial likelihood against brute-force risk sets, the figure linted at
560 and 936. Real figure linted 0 problems at 936 and 560. tsc, eslint, knip, npm test (1510)
and docs:check clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
A new view kind, `rmst`, for when hazards are not proportional (immunotherapy curves). The survival view is unchanged. Per cohort (≤6): the area under the Kaplan–Meier curve from 0 to τ — the mean months lived in the first τ — with the Greenwood-type standard error of that area (Klein & Moeschberger; R survRM2's rmst1: Σ A_j² d_j / (n_j (n_j − d_j)), A_j the area from t_j to τ, 0 where n_j = d_j) and a 95% interval; against the first cohort the RMST difference and ratio (log scale), each with a Wald interval and two-sided p (each vs the first for >2). τ defaults to the largest time at which every cohort still has 10 at risk (floored to 0.1 month, stated per panel); a set τ past a cohort's last follow-up is refused, naming it. landmarkMonths (≤4, increasing, default [0]): for each L the patients alive and uncensored at L (t > L), clock reset there, KM and RMST from it; drawn as small multiples. Endpoint: OS by default via the resolvers' survivalCounts (the cox/survival monthsAttribute / statusAttribute escape hatch works too). anchor: OS from a treatment start through txendpoints' own SQL — txEndpointCountsSql gains endpoint "os" (death alone; the other endpoints' SQL is byte-identical for all four snapshot variants) — with its clock check and left truncation; the anchor parsing and the "no patient has the anchor" refusal moved into lib/timeline-endpoints.ts, which txendpoints now calls (its gallery result JSON and SVGs at 936 and 560 are byte-identical). lib/figures/km.ts takes an optional shadeTo (area under each curve to τ, a rule at τ); unset it draws exactly as before (txendpoints' SVGs byte-identical). New rmst() and atRiskAtTimes() in lib/stats.ts. Only (entry, t, event) counts leave the database; "local" coverage; the "os" variant is in the boundary suite. Unit tests: RMST and its SE against a hand computation on a toy KM (events 1, 3, 4; censored 2, 5; τ 4.5: area 3.2667, SE 0.5830); no censoring equals mean(min(t, τ)); delayed entry; contrast intervals; landmark reset; figure lint at 560 and 936. survRM2's own example data (PBC) is not available offline, so it is not reproduced. Verified on the gallery example (tmb_mskcc_2018, TMB < 10 n=1,201 vs ≥ 10 n=460, τ = 24): RMST 14.5 (13.9–15.0) vs 17.7 (16.9–18.6) months, difference +3.3 (2.3–4.2), p = 1e-10, ratio 1.23 (1.15–1.30); from 6 months (758 vs 321) 16.0 vs 19.6, +3.6; from 12 months (503 vs 241) 17.4 vs 20.6, +3.2. Anchored run (msk_chord NSCLC from first SUBTYPE=Immuno, KRAS mutant 981 vs wild-type 1,651 anchored, as txendpoints): default τ 81.5 months, RMST 27.9 vs 25.5. Refusals checked: τ = 500 (names both cohorts' last follow-up), an unknown anchor value (lists SUBTYPE's values). Figures lint 0 at 936 and 560. tsc, eslint, knip, npm test (1520) and docs:check clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…l by each A new view kind (Chowell et al. Science 2018; McGranahan et al. Cell 2017). Per patient, from one viewRows query (no identifiers; patient-level studies only, view-coverage "patients"): whether each of HLA-A, -B, -C carries two alleles that match at `resolution` fields of the allele name (default 2, protein level; 1 = allele group, "full" = every field). Allele names are normalised in SQL so hla_a_02_01_01_01 (MSK-IMPACT), A*02:01 (nsclc_mskcc_2018) and A0201 (the melanoma studies' "A0201/A2402" pairs) all read A*02:01. The default attribute layouts cover all three (HLA-A1/HLA-A2, HLA_A1_ALLELE(S), HLA_A); `alleles` names others. HLA LOH: any of `lohAttributes` (default HLA_A1_LOH … HLA_C2_LOH, HLA_LOH) equal to one of `lohValues` (Loss/Yes/LOH/True) in any sample; a patient with no call anywhere is left out of that split. Per cohort: patients typed at all three loci homozygous at 0/1/2/3 loci, per-locus homozygosity, patients typed at fewer fields than asked, LOH share among patients with a call. Then the endpoint (default OS; monthsAttribute/statusAttribute) by homozygous at >=1 locus vs heterozygous at all, and by HLA LOH vs none: KM, log-rank and Cox HR (Efron), stratified by study when pooled, drawn when each group has >= 5 patients, else the reason. `covariates` (<= 4) add an adjusted HR (numbers as they are, categories as indicators against the commonest, past 10 levels pooled). Supertypes and HED are not computed (said in the caption). msk_impact_50k_2026 has no treatment timeline, so there is no immunotherapy anchor: the caption says OS runs from each study's origin, and that outside treated patients homozygosity reads prognosis, not response; and that LOHHLA calls only heterozygous loci, so the LOH split is not independent of the genotype. A study without alleles is refused listing its HLA-named attributes (msk_chord_2024 has none — the internal studies will refuse the same way). Verified, gallery example 1 (msk_impact_50k_2026 NSCLC, adjusted for TMB_SCORE): 6,633 patients, 5,901 typed; homozygous at 0/1/2+ loci 78.2/16.1/5.8% (A 12.0%, B 6.6%, C 10.3%); OS hom vs not HR 1.02 [0.92–1.12], log-rank p 0.76, adjusted 1.01. HLA LOH 1,239 of 4,158 with a call (29.8%); OS median 32.0 vs 47.5 months, HR 1.35 [1.22–1.50], p 7.6e-9, adjusted 1.31. Example 2 (skcm_dfci_2015 + skcm_mskcc_2014 + mel_ucla_2016, all on checkpoint blockade): 212 typed, 22.2% homozygous; OS median 12.1 vs 21.7 months, HR 1.22 [0.82–1.82], stratified by study — Chowell's direction; no LOH calls, so that split says why. Figures lint 0 at 936 and 560; unit test of the counts, split, covariate coding and figure; tsc, eslint, npm test (1507) and docs:check clean; both examples pinned (6,633; 212). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…alls, with before/after
An option on the ctdna view (Razavi et al. Nat Med 2019; MSK-ACCESS with matched normal, Brannon
et al. 2021). With `chFilter: true` (or {genes, normal, normalMinAlt, normalMinRatio}), plasma
mutation calls likely from clonal haematopoiesis are left out of every count — agreement per gene,
ctDNA detected and survival by detection:
- by gene: DNMT3A, TET2, ASXL1, PPM1D, JAK2, SF3B1, SRSF2, U2AF1, CBL, GNB1, GNAS, IDH2 (the CH
genes of Razavi 2019 / Bolton et al. Nat Genet 2020). TP53, ATM and CHEK2 are CH genes too but
left off by default because their plasma calls are as often the tumour's; `genes: []` for none.
- by the matched normal (default on): mutation_derived's normal counts, where loaded — at least
normalMinAlt (2) alternate reads in the white-cell sample at an allele fraction at least
normalMinRatio (0.3) x the plasma's. The ratio is from the data: of ACCESS plasma calls with
>= 2 normal reads, 126 of 210 non-CH-gene calls have a normal VAF under a tenth of the plasma's —
tumour DNA in the buffy coat (EGFR, KRAS), which `normalAltCount > 0` alone would have removed.
Copy-number and structural calls and tissue are never filtered. Implemented as a `chx` CTE of the
plasma samples' removed calls (genomic_event_derived joined to mutation_derived on sample, gene and
protein change — all 7,283 ACCESS calls match) and an exclusion condition passed to the shared
sideCountsTail and to the detected CTE; both take it as an optional argument that adds nothing when
absent. The result adds per panel `ch` (calls, with normal counts, removed by gene / by normal /
either, patients, removed calls per gene) and `before` (the unfiltered detected share, plasma-only
and both per gene, the OS curves' medians and log-rank p, from a second run of the same queries
without the rule on the same genes); the figure adds a line and a "plasma-only unfiltered" column,
the caption the rule, the counts and its biases. preview-kinds' ctdna entry names the filter.
Unset is byte-identical: the existing gallery example (nsclc_ctdx_msk_2022) and msk_access_2021
with plasmaPanels, run before and after this change — result JSON (queries and timing aside), the
SVG at 936 and 560 px, and the sorted SQL of every query — compare equal with cmp.
Verified, new gallery example (msk_access_2021, plasmaPanels ACCESS129, chFilter true): 83 of 2,024
plasma mutation calls removed in 51 patients (41 by gene: SF3B1 14, DNMT3A 11, GNAS 8, IDH2 4; 44 by
the normal, TP53 14); ctDNA detected 453/617 (73.4%) vs 457 (74.1%) unfiltered; OS log-rank p 3.4e-4
vs 3.5e-4; removed calls are plasma-only (AR 12 -> 11, RB1 10 -> 8, ATM 17 -> 16) while every both
call stays. nsclc_ctdx_msk_2022: 842 of 2,377 plasma calls have normal counts (the ctDx panel has
none); 22 removed in 20 patients, detection 759 unchanged, MET both 25 -> 24. Figures lint 0 at 936
and 560; tsc, eslint, npm test (1512) and docs:check clean; example pinned (617).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
… organ
A new kind (MSK-MET, Nguyen et al. Cell 2022; LUAD organotropism, PMID 37084736). Within one
cohort, metastatic sites come from whichever source it carries (source auto, or chosen):
- flags: per-organ Yes/No attributes with a prefix (default DMETS_DX_, MSK-MET's); a patient is
evaluable for a site when its flag is recorded Yes or No;
- timeline: event type / key naming a site (default Diagnosis / TUMOR_SITE, folded through
lib/event-names.ts), at any date; a patient with any site recorded is evaluable for all;
- sample: a biopsy-site attribute (default METASTATIC_SITE), free text mapped to organs by the
view's own regex rules (ORG_SITE_MAP); unmatched values become "Other" and are listed in the
caveat with their patient counts. Every patient is evaluable ("site sampled, not every site").
With none, the view refuses naming what the cohort has under each source.
Per site × gene, over patients evaluable for the site AND profiled for the gene (panel-aware:
a mutation/CNA/SV panel listing it, or WES; off-panel calls in neither side), the 2×2 of
metastasis there × altered (default OncoKB-driver mutations at the pin; alteration and measure
settable) per stratum of stratifyBy (default CANCER_TYPE per patient, "Multiple" when samples
disagree; null for none). Test: Cochran–Mantel–Haenszel (lib/stats.ts, continuity-corrected)
with the Mantel–Haenszel common OR; BH over the grid; a cell needs minPatients (5) altered, with
and without the site. Genes: a list (≤20) or the top (12, ≤20) most altered. Sites ≤30, refused
past that. With the timeline source, also the cumulative incidence of the first event at one
site after day 0 by one gene's status (altered vs profiled-not-altered), death competing:
incidenceCountsSql over each half of the cohort, Aalen–Johansen, Gray's permutation test; patients
with the site by day 0 are excluded and counted; the panel is withheld (noIncidence) when >5% of
records have the site after their last follow-up (the clock check). Default site/gene: the
grid's most significant cell.
All SQL in lib/federation-views.ts returns counts only (orgSourcesSql, orgSitesSql,
orgOtherValuesSql, orgTopGenesSql, orgCountsSql, orgIncidenceSql; added to the boundary suite);
coverage "local". Figure: gene × site heatmap of the log2 MH OR (diverging, clamped at OR 2,
outlined at q<0.05), the ten strongest cells, and the incidence curves through the shared km
panel, which gains an `origin` option (default 1, so every existing KM figure is unchanged).
Verified on the gallery examples:
- msk_met_2021 pan-cancer (25,775 patients, flags, 27 cancer-type strata): 21 sites × 12 genes,
252 cells tested, 66 at q<0.05. TP53 with liver (MH OR 1.45, q 1e-30), distant LN, CNS/brain
(1.42), lung, bone, adrenal; EGFR with CNS/brain (1.63, q 2e-10), bone and PNS; APC with liver
(1.58) and lung; PTEN and ARID1A with less intra-abdominal spread (0.62, 0.67) — the paper's
map. 13 s.
- msk_chord_2024 NSCLC (7,809), timeline: TP53 × lymph nodes OR 2.0, EGFR × CNS/brain 1.68,
SMARCA4/STK11/KEAP1 × adrenal. Incidence of a first CNS/brain site, EGFR-mutant 1,350 vs
5,107 (411 / 941 excluded with brain sites by day 0): 14.2% vs 12.8% at 12 mo, 32.0% vs 22.7%
at 60 mo, Gray p 5e-4; 0 of 6,457 records past follow-up. 9 s.
- sample source on the same cohort: 14 organs, TP53 × CNS/Brain OR 2.1, EGFR × pleura 1.9.
Figures lint 0 problems at 936 and 560 px; tsc, eslint, knip, npm test (1,511 pass) and
docs:check (all examples, counts pinned for the two new ones) clean. Unit test of the per-cell
tables and CMH.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…, by an attribute, and OS by subtype A new kind applying a published molecular classifier per patient as derived subtypes. One classifier so far, `endometrial` (TCGA 2013 / ProMisE / WHO 2020); the `classifier` setting refuses any other, naming those that exist, and a new one is a CLASSIFIERS entry plus its SQL. Hierarchy per patient over all their samples, first match wins: - POLE-ultramutated: a somatic POLE exonuclease-domain hotspot (León-Castillo et al., J Pathol 2020, PMID 31829442: P286R, S297F, F367S/V, V411L, L424I/V, P436R, M444K, A456P, S459F, M295R, D368Y), off-panel and germline calls excluded; - else MMR-deficient: MSI-high in any sample. msi setting: a categorical call (default MSI_TYPE, Instable / Stable) or a score (MSI_SCORE, else MSI_SENSOR_SCORE: MSIsensor ≥ 10 high, < 3 stable, in between indeterminate); the default is the first the cohorts carry, refused naming their MSI attributes and example values when none is. Not MMR IHC (not in the warehouse); - else p53-abnormal: an OncoKB-driver TP53 mutation at the pin (p53 "nonsynonymous": any protein-changing TP53 call); - else NSMP. "Not classifiable" (counted with the reason, kept out of shares, tests and curves, never guessed): not sequenced, not profiled for POLE or TP53 (panel-aware), or no MSI-high/stable call when the hierarchy reaches MSI. Per cohort (≤6): subtype shares; with `by`, per group of a patient attribute (top 8, rest pooled into a named Other) with chi-squared; chi-squared between cohorts; OS by subtype (KM, k-group log-rank, stratified by study) for the first three cohorts. SQL (lib/federation-views.ts: molCountsSql, molSurvivalSql, molMsiAttributesSql) returns counts only — classes stay in CTEs; boundary suite covers each; coverage "local". Figure: stacked bars per cohort and group, and the shared KM panel, which gains a `series` option so a subtype keeps its colour when another is empty (default its place in the list: existing figures unchanged). Verified: - msk_impact_50k_2026 endometrial (gallery): 2,373 patients, 196 not classifiable (no MSI call); of 2,177 classified POLEmut 119 (5.5%), MMRd 353 (16%), p53abn 895 (41%), NSMP 810 (37%). By ANCESTRY_LABEL p53abn is 71% (137/194) in AFR vs 34% in non-ASJ EUR, χ² 116 (21 df) p 4e-15 — the Black/White difference of PMID 37651310. OS: POLEmut 1 death of 112, p53abn median 29.6 mo, NSMP 78.2, log-rank χ² 199 (3 df). - ucec_tcga_pan_can_atlas_2018 against TCGA's own SUBTYPE (second gallery example, MSI from MSI_SENSOR_SCORE): UCEC_POLE 48/49 -> POLEmut, UCEC_MSI 122/126 -> MMRd, CN_HIGH 139/161 -> p53abn, CN_LOW 135/145 -> NSMP. Figures lint 0 at 936 and 560 px; tsc, eslint, knip, npm test (1,517 pass) and docs:check (counts pinned for both examples) clean. Unit test of the group pooling and the hotspot list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…' somatic second hits
A new kind (PMIDs 34635660, 36593350, 28825054, 26000489; Jonsson et al. Nature 2019). Per gene
and cohort (≤4), patient-level and panel-aware:
- carriers: patients with a mutation the study marks GERMLINE (mutation_status, case-insensitive)
in the gene, among the patients sequenced for it (a tumour mutation panel listing it, or WES).
A carrier whose panel omits the gene is counted apart (outsidePanel) and a patient not on a
covering panel is never a non-carrier. The share is of patients sequenced, not germline-tested
— the warehouse cannot tell a negative germline test from none; the caveat says so;
- second hit among carriers: another called mutation in the gene not marked germline (uncalled and
off-panel excluded), or a deep deletion (−2) among patients profiled for copy number; reported
separately and as "either". LOH is not assessed (no allele-specific copy number is read; the
result names the view's studies that hold FACETS), so the rate is a floor;
- non-carriers: the same somatic rate; Fisher's exact test carriers vs non-carriers per gene, BH
over the cohort's genes.
Genes: a list (≤60) or every gene with ≥ minCarriers (3) carriers in a cohort (refused past 60,
naming the count). Optional survival: {gene}: OS (OS_MONTHS/OS_STATUS) of that gene's carriers
against non-carriers, KM and log-rank stratified by study. Refuses a cohort with no germline call
at all, and a minCarriers no gene reaches (naming the top genes and their carriers).
SQL (germCarriersSql, germCountsSql, germSurvivalSql) returns counts only — per-patient flags stay
in CTEs; boundary suite covers each; coverage "local". Figure: per cohort one row per gene
(carriers / sequenced, then paired bars of the carriers' second-hit rate and the non-carriers'
somatic rate, q), and the shared KM panel.
Public germline calls are few (lung_nci_2022 100 calls / 72 samples, coadread_tcga_pub 101 /
59, pog570 74 / 68, brca/ov_tcga_pub, prad_su2c_2019 40 / 39). The two with the most calls
spread them over 74–86 genes with 1–3 carriers each and no second hits, so the gallery example is
prad_su2c_2019, the study where the question has an answer: 429 sequenced, BRCA2 17 carriers
(4.0%), 3 with a second somatic BRCA2 mutation (17.6%) vs 7.3% of non-carriers (q 0.34); BRCA1 2
of 6 vs 0.5% (q 0.005); ATM 1 of 7; no deep deletions in carriers. OS: only 128 patients have OS;
BRCA2 carriers 5 (median 12.3 mo) vs 123 (28.1), log-rank p 0.33. Also run on ov_tcga_pub (BRCA1
26 / BRCA2 25 carriers of 316 sequenced, 0 and 1 second hits), brca_tcga_pub, pog570, lung_nci,
coadread_tcga_pub. Figures lint 0 at 936 and 560 px; tsc, eslint, knip, npm test (1,522 pass)
and docs:check (count pinned) clean. Unit test of the per-gene counting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
Master's d7fa8ad made lib/sql-number.ts's finiteSql() the one numeric read of a stored value ("nan"/"inf" are missing, not NaN), with a unit test that fails on any bare toFloat64OrNull in lib. The preview views' SQL in lib/federation-views.ts, written before it, still read survival months, the cut-point marker, the MSI score, OS for the KM panels and the progression probability floor with toFloat64OrNull; all twelve reads now go through finiteSql. No pinned example count moved (docs:check: every example passes), and npm test is green (1,556). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
…ribute Most view settings that name an attribute were checked against a pattern without the `custom:<dataset>/<COLUMN>` form, so an uploaded column could define a cohort but not, say, stratify a cut-point or supply survival months. lib/custom-data-names.ts gains VIEW_ATTR_RE: ATTR_NAME_RE's two forms, a built-in name also taking "." and "-" (HLA-A1). The views' shared ATTR_NAME is now that pattern, and the survival-column checks of survival, cox, cutpoint, forest, rmst, predictive and hla, the ad hoc custom patterns of cutpoint and adjusted, and organotropism's SQL-side check use it. Every setting it guards already reaches its query through clinicalTable(), which merges the uploads in, so no SQL changed. Checked with uploads on tmb_mskcc_2018 (a patient-level copy of TMB, a lung/other grouping and OS columns; a sample-level grouping): survival and cox on the custom OS columns give the same answer as on OS_MONTHS/OS_STATUS (1,661 patients, 832 events; HR 0.9822 both); cutpoint and forest on them run, as do cutpoint's stratifyBy, forest's strata, rmst's endpoint, actionability's groupBy, exclusivity's stratifyBy and cancertypes' groupBy on the custom groupings. The hla, molsubtype, ctdna, organotropism and paired settings pass their names to clinicalTable the same way but were not run on an upload here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC
alisman
force-pushed
the
session/new-features
branch
from
October 23, 2026 13:58
b5614ce to
27b049b
Compare
alisman
marked this pull request as ready for review
October 23, 2026 13:58
This was referenced Oct 23, 2026
alisman
added a commit
that referenced
this pull request
Oct 23, 2026
Snapshots for the 19 view kinds and options #59 added, in four parallel shards
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Twelve analysis views taken from the cancer genomics literature, as previews: each is drawn, runs on the public warehouse, and says in its caveat what it computed and what it leaves out. None has a method record or golden test yet. One commit per view, on top of a setup commit.
Why they're previews.
lib/preview-kinds.tslists each new kind with what it still lacks. Each listed view gets a Preview badge in its header, whose title names what is pending. The same list exempts the kind incheck:methods. Removing a kind's entry puts it back under the full checks.The candidates came from searching the abstracts of the 400 PubMed papers linked to cBioPortal's 551 public studies for analysis methods, keeping the ones no existing view covers (survey).
txendpointslib/treatment-lines.ts), time on treatment, real-world PFS (NLP progression events). KM + log-rank, median CIs.predictivecutpointriskmodelvalidateOnforestadjustedactionabilitydrivernegativeacquiredctdnaexclusivityinstabilityRound two adds seven more, the same way:
multistatermstorganotropismMETASTATIC_SITE; cumulative incidence of one site by a gene, death competing.molsubtypegermlinehlactdnachFilterShared code these added:
lib/figures/km.ts, a KM panel with a number-at-risk table. Added to the kind-boundary shared list.dualForestSvgandmetaForestSvginlib/figures/forest.ts.pNameon the volcano. Unset, every other volcano draws as before.lib/stats.ts:kmMedianCi,wilsonIntervalandpoissonBinomialTails.sideCountsTail. The paired view's SQL is byte-identical in all four configurations.Checked:
tsc,eslint, andnpm test(1,554 pass).docs:check: every gallery example runs and its counts are pinned.scripts/gate.sh: every lane green. All 172 e2e checks pass, after one test fix: two oncoprint checks (the merged-row tooltip, drag-to-cohort) now scroll the canvas into view first, since the larger add-view panel pushed it below the window.Not done:
/verificationrows for the new statistics; these are what removing a kind fromPREVIEW_KINDSwill need.mskimpactdata. Demo documents for it come separately.🤖 Generated with Claude Code
https://claude.ai/code/session_01HzVFGjhH59eexZVvbqGWHC